使用erlang ranch tcp开发服务端

398 阅读4分钟

blog.csdn.net/huang1196/a…

Ranch:

简单来说,Ranch就是一个tcp acceptor pool,用于高并发下的tcp连接建立与管理。可以设置并发链接最大数量,在不关闭socket连接的情况下可以动态升级连接池。Cowboy就是使用的ranch。\

github.com/ninenines/r…

\

下面通过改造ranch自带的reverse example实现简易的服务端。

\

game_server.app.src\

[plain]  view plain  copy

  1. {application, game_server, [  
  2.     {description, "Ranch TCP reverse example."},  
  3.     {vsn, "1"},  
  4.     {modules, []},  
  5.     {registered, []},   
  6.     {applications, [  
  7.         kernel,  
  8.         stdlib,  
  9.         ranch  
  10.     ]},  
  11.     {mod, {game_server_app, []}},  
  12.     {env, []}  
  13. ]}.  

\

game_server_app.erl

[plain]  view plain  copy

  1. -module(game_server_app).  
  2. -behaviour(application).  
  3. -export([start/2, stop/1]).  
  4.   
  5. %% start/2  
  6. start(_Type, _StartArgs) ->  
  7.     {ok, _Pid} = ranch:start_listener(tcp_reverse, 1,  
  8.         ranch_tcp, [{port, 5555},{max_connections, 10240}], game_protocol, []),  
  9.     game_server_sup:start_link().  
  10.   
  11.   
  12. %% stop/1  
  13. stop(State) ->  
  14.     ok.  

\

这里注意ranch:start_listener(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) -> {ok, pid()} | {error, badarg}.

最大连接数max_connections就是在这里进行设定, 默认值1024. NbAcceptors, Acceptor的数量,具体数值要根据实际并发设置。

Ranch接受请求并建立连接,然后就会将具体的处理交给实现了ranch_protocol行为的game_protocol,erlang中的behaviour跟java中的接口差不多。

\

game_server_sup.erl\

[plain]  view plain  copy

  1. -module(game_server_sup).  

  2. -behaviour(supervisor).  

  3. -export([start_link/0, init/1]).  

  4.   

  5. -spec start_link() -> {ok, pid()}.  

  6. start_link() ->  

  7.     supervisor:start_link({local, ?MODULE}, ?MODULE, []).  

  8.   

  9.   

  10. %% init/1  

  11. init([]) ->  

  12.     {ok, {

    {one_for_one, 10, 10}, []}}.  


game_protocol.erl

[plain]  view plain  copy

  1. -module(game_protocol).  
  2. -behaviour(gen_server).  
  3. -behaviour(ranch_protocol).  
  4.   
  5. %% API.  
  6. -export([start_link/4]).  
  7.   
  8. %% gen_server.  
  9. -export([init/4]).  
  10. -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).  
  11.   
  12. -define(TIMEOUT, 50000).  
  13.   
  14. -record(state, {socket, transport}).  
  15.   
  16. %% API.  
  17.   
  18. start_link(Ref, Socket, Transport, Opts) ->  
  19.     proc_lib:start_link(?MODULE, init, [Ref, Socket, Transport, Opts]).  
  20.   
  21. %% gen_server.  
  22.   
  23. %% This function is never called. We only define it so that  
  24. %% we can use the -behaviour(gen_server) attribute.  
  25. init([]) -> {ok, undefined}.  
  26.   
  27. init(Ref, Socket, Transport, _Opts = []) ->  
  28.     ok = proc_lib:init_ack({ok, self()}),  
  29.     ok = ranch:accept_ack(Ref),  
  30.     ok = Transport:setopts(Socket, [{active, once}, {packet, 2}]),  
  31.     gen_server:enter_loop(?MODULE, [],  
  32.         #state{socket=Socket, transport=Transport},  
  33.         ?TIMEOUT).  
  34.   
  35. handle_info({tcp, Socket, Data}, State=#state{  
  36.         socket=Socket, transport=Transport}) ->  
  37.     io:format("Data:~p~n", [Data]),  
  38.     Transport:setopts(Socket, [{active, once}]),  
  39.     Transport:send(Socket, reverse_binary(Data)),  
  40.     {noreply, State, ?TIMEOUT};  
  41. handle_info({tcp_closed, _Socket}, State) ->  
  42.     {stop, normal, State};  
  43. handle_info({tcp_error, _, Reason}, State) ->  
  44.     {stop, Reason, State};  
  45. handle_info(timeout, State) ->  
  46.     {stop, normal, State};  
  47. handle_info(_Info, State) ->  
  48.     {stop, normal, State}.  
  49.   
  50. handle_call(_Request, _From, State) ->  
  51.     {reply, ok, State}.  
  52.   
  53. handle_cast(_Msg, State) ->  
  54.     {noreply, State}.  
  55.   
  56. terminate(_Reason, _State) ->  
  57.     ok.  
  58.   
  59. code_change(_OldVsn, State, _Extra) ->  
  60.     {ok, State}.  
  61.   
  62. %% Internal.  
  63.   
  64. reverse_binary(B) when is_binary(B) ->  
  65.     list_to_binary(lists:reverse(binary_to_list(B))).  

\

这里init的实现与常规的gen_server不一样。首先来说为什么不能用常规的gen_server写法。常规写法如下:

[plain]  view plain  copy

  1. init([Ref, Socket, Transport, Opts]) ->  
  2.     ok = ranch:accept_ack(Ref),  
  3.     ok = Transport:setopts(Socket, [{active, once}, {packet, 2}]),  
  4.     {ok, #state{socket=Socket, transport=Transport}}.  


gen_server的start_link只有在init/1执行完毕后才会返回,但我们来看ranch:accept_ack(Ref):

[plain]  view plain  copy

  1. -spec accept_ack(ref()) -> ok.  
  2. accept_ack(Ref) ->  
  3.     receive {shoot, Ref, Transport, Socket, AckTimeout} ->  
  4.         Transport:accept_ack(Socket, AckTimeout)  
  5.     end.  

运行ranch:accept_ack/1时,进程会阻塞,等待{shoot, ...}这条消息,直到接收到此消息才会继续执行,接着才会完成init。但是{shoot, ...}这条消息从哪里来?查下ranch源码不难发现,ranch在建立了与新的gen_server进程的连接后,会向gen_server进程发送该消息(参考ranch_conns_sup:loop/4). 显然,gen_server进程在等待ranch:accept_ack接收到{shoot,...}消息迟迟不能返回,而ranch又无法与gen_server进程连接发送不了{shoot, ...}消息,造成死锁。故使用proc_lib:start_link/3优雅地解决了此问题。

\

下面copy一下文档的一个说明:

By default the socket will be set to return `binary` data, with the
options `{active, false}`, `{packet, raw}`, `{reuseaddr, true}` set.
These values can't be overriden when starting the listener, but
they can be overriden using `Transport:setopts/2` in the protocol.

It will also set `{backlog, 1024}` and `{nodelay, true}`, which
can be overriden at listener startup.\

这也就是为什么{active, once}, {packet, 2}只能在procotol里重写\

\

这样就实现了一个基本的服务端,make后编写脚本启动:

start.sh\

[plain]  view plain  copy

  1. erl -pa ebin deps/*/ebin +K true +P 199999 \  
  2.     -sname game_server \  
  3.     -s game   


-s game表示启动时默认调用game:start/0方法。

game.erl\

[plain]  view plain  copy

  1. -module(game).  
  2.   
  3. %% ====================================================================  
  4. %% API functions  
  5. %% ====================================================================  
  6. -export([start/0, stop/0]).  
  7.   
  8. start() ->  
  9.     ok = application:start(ranch),  
  10.     ok = application:start(game_server).  
  11.   
  12. stop() ->  
  13.     application:stop(ranch),  
  14.     application:stop(game_server).  


\

如果设置{packet, raw}的话,直接打开一个Terminal $ telnet localhost 5555 就可以进行测试了。

不过这里设置的{packet,2}, 所以写了个测试client发送消息,建立连接->发送消息->接收返回消息->关闭连接:

[plain]  view plain  copy

  1. -module(client).  
  2.   
  3. -export([send/1]).  
  4.   
  5. send(BinMsg) ->  
  6.     SomeHostInNet = "localhost",   
  7.     {ok, Sock} = gen_tcp:connect(SomeHostInNet, 5555,  
  8.                                  [binary, {packet, 2}]),  
  9.     ok = gen_tcp:send(Sock, BinMsg),  
  10.     receive  
  11.         {tcp,Socket,String} ->  
  12.             io:format("Client received = ~p~n",[String]),         
  13.             gen_tcp:close(Socket)  
  14.         after 60000 ->  
  15.             exit          
  16.     end,  
  17.     ok = gen_tcp:close(Sock).  

\

handler_info中加入不同消息的处理,就可以时间一个简单的游戏服务器了。R17后可以使用{active, N}, 程序效率应该会更高。

\

release和项目下载见: blog.csdn.net/huang1196/a…

\