《Erlang/OTP併發編程實戰》第三章 開發基於 TCP 的 RPC 服務

  1. 進程間的消息傳遞是異步的。
  2. 信箱的大小是沒有上限的。
  3. gen_server:call/2 的默認應答等待超時爲 5 秒。
  4. gen_server:
    -module(tcp_rpc_server).
    
    -behaviour(gen_server).
    
    %% API
    -export([
        start_link/1,
        start_link/0,
        get_count/0,
        stop/0
    ]).
    
    -export([
        init/1,
        handle_call/3,
        handle_cast/2,
        handle_info/2,
        terminate/2,
        code_change/3
    ]).
    
    -define(SERVER, ?MODULE).
    -define(DEFAULT_PORT, 1055).
    
    -record(state, {port, lsock, request_count = 0}).
    
    start_link() ->
        start_link(?DEFAULT_PORT).
    
    start_link(Port) ->
        gen_server:start_link({local, ?SERVER}, ?MODULE, [Port], []).
    
    get_count() ->
        gen_server:call(?SERVER, get_count).
    
    stop() ->
        gen_server:call(?SERVER, stop).
    
    init([Port]) ->
        {ok, LSock} = gen_tcp:listen(Port, [{active, true}]),
        {ok, #state{port = Port, lsock = LSock}, 0}.
    
    %% 超時值:將超時值置爲0就是讓gen_server容器在init/1結束後立即觸發一次超時,從而迫使進程在完成初始化之後第一時間處理超時消息。
    
    handle_call(get_count, _From, State) ->
        {reply, {ok, State#state.request_count}, State}.
    
    handle_cast(stop, State) ->
        {stop, normal, State}.
    
    handle_info({tcp, Socket, RawData}, State) ->
        do_rpc(Socket, RawData),
        RequestCount = State#state.request_count,
        {noreply, State#state{request_count = RequestCount + 1}};
    
    %% 一種延遲的初始化操作
    handle_info(timeout, #state{lsock = LSock} = State) ->
        {ok, _Sock} = gen_tcp:accept(LSock),
        {noreply, State}.
    
    terminate(_Reason, _State) ->
        ok.
    
    code_change(_OldVsn, State, _Extra) ->
        {ok, State}.
    
    do_rpc(Socket, RawData) ->
        try
            {M, F, A} = split_out_mfa(RawData),
            Result = apply(M, F, A),
            gen_tcp:send(Socket, io_lib:fwrite("~p~n", [Result]))
        catch
            _Class:Err ->
                gen_tcp:send(Socket, io_lib:fwrite("~p~n", [Err]))
        end.
    
    % I/O列表
    % io_lib:fwrite/2 的結果不一定是普通字符串(即扁平字符列表)。
    % 即便如此,仍然可以直接將結果傳給套接字,這個結果被稱爲 I/O列表:它是一個可以深層嵌套的列表,
    % 既可以包含字符編碼也可以包含二進制數據塊。
    % 通過這種方式,在依次輸出多個I/O列表時,就不用再爲了拼接所有數據而專門創建一箇中間列表了。
    
    split_out_mfa(RawData) ->
        MFA = re:replace(RawData, "\r\n$", "", [{return, list}]),
        {match, [M, F, A]} =
            re:run(MFA,
                "(.*):(.*)\s*\\((.*)\s\\)\s*.\s*.\s*$", [{capture, [1, 2, 3], list}, ungreedy]),
        {list_to_atom(M), list_to_atom(F), args_to_terms(A)}.
    
    args_to_terms(RawArgs) ->
        {ok, Toks, _Line} = erl_scan:string("[" ++ RawArgs ++ "]. ", 1),
        {ok, Args} = erl_parse:parse_term(Toks),
        Args.
    
    % 帶外消息:當服務器需要與第三方模塊通信,而第三方模塊又依賴於直接消息通信而非OTP庫調用時,需要用handle_info
    
    % gen_server超時事件
    % gen_server設置了超時之後,一旦觸發超時,就會產生一條由原子timeout構成的帶外消息,這條消息由handle_info/2回調處理,
    % 該機制常用於處理服務器在超時時間內未收到任何請求的情況,此時可以用它來喚醒服務器並執行一些指定操作。

     

  5. 測試框架
    1. EUnit:
      1. 主要用於單元測試
      2. 使用方法:
        -include_lib("eunit/include/eunit.hrl").
        
        start_test() ->
            {ok, _} = tcp_rpc_server:start_link().

        ps:該函數必須沒有任何參數且函數名要以 _test 結尾。

      3. 測試命令:
        1. eunit:test(tcp_rpc_server).
        2. tcp_rpc_server:test().

           

    2. Common Test
      基於所謂的 OTP Test Server
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章