《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
发布了42 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sanmao123456_/article/details/103419388