使用ranch tcp開發服務端

來源:互聯網
上載者:User

標籤:des   http   java   使用   os   io   for   art   

Ranch:

簡單來說,Ranch就是一個tcp acceptor pool,用於高並發下的tcp串連建立與管理。可以設定並發連結最大數量,在不關閉socket串連的情況下可以動態升級串連池。Cowboy就是使用的ranch。

https://github.com/ninenines/ranch


下面通過改造ranch內建的reverse example實現簡易的服務端。


game_server.app.src

{application, game_server, [{description, "Ranch TCP reverse example."},{vsn, "1"},{modules, []},{registered, []}, {applications, [kernel,stdlib,ranch]},{mod, {game_server_app, []}},{env, []}]}.

game_server_app.erl

-module(game_server_app).-behaviour(application).-export([start/2, stop/1]).%% start/2start(_Type, _StartArgs) ->    {ok, _Pid} = ranch:start_listener(tcp_reverse, 1,        ranch_tcp, [{port, 5555},{max_connections, 10240}], game_protocol, []),    game_server_sup:start_link().%% stop/1stop(State) ->    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

-module(game_server_sup).-behaviour(supervisor).-export([start_link/0, init/1]).-spec start_link() -> {ok, pid()}.start_link() ->supervisor:start_link({local, ?MODULE}, ?MODULE, []).%% init/1init([]) ->{ok, {{one_for_one, 10, 10}, []}}.

game_protocol.erl

-module(game_protocol).-behaviour(gen_server).-behaviour(ranch_protocol).%% API.-export([start_link/4]).%% gen_server.-export([init/4]).-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).-define(TIMEOUT, 50000).-record(state, {socket, transport}).%% API.start_link(Ref, Socket, Transport, Opts) ->    proc_lib:start_link(?MODULE, init, [Ref, Socket, Transport, Opts]).%% gen_server.%% This function is never called. We only define it so that%% we can use the -behaviour(gen_server) attribute.init([]) -> {ok, undefined}.init(Ref, Socket, Transport, _Opts = []) ->    ok = proc_lib:init_ack({ok, self()}),    ok = ranch:accept_ack(Ref),    ok = Transport:setopts(Socket, [{active, once}, {packet, 2}]),    gen_server:enter_loop(?MODULE, [],        #state{socket=Socket, transport=Transport},        ?TIMEOUT).handle_info({tcp, Socket, Data}, State=#state{        socket=Socket, transport=Transport}) ->    io:format("Data:~p~n", [Data]),    Transport:setopts(Socket, [{active, once}]),    Transport:send(Socket, reverse_binary(Data)),    {noreply, State, ?TIMEOUT};handle_info({tcp_closed, _Socket}, State) ->    {stop, normal, State};handle_info({tcp_error, _, Reason}, State) ->    {stop, Reason, State};handle_info(timeout, State) ->    {stop, normal, State};handle_info(_Info, State) ->    {stop, normal, State}.handle_call(_Request, _From, State) ->    {reply, ok, State}.handle_cast(_Msg, State) ->    {noreply, State}.terminate(_Reason, _State) ->    ok.code_change(_OldVsn, State, _Extra) ->    {ok, State}.%% Internal.reverse_binary(B) when is_binary(B) ->    list_to_binary(lists:reverse(binary_to_list(B))).

這裡init的實現與常規的gen_server不一樣。首先來說為什麼不能用常規的gen_server寫法。常規寫法如下:

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

gen_server的start_link只有在init/1執行完畢後才會返回,但我們來看ranch:accept_ack(Ref):

-spec accept_ack(ref()) -> ok.accept_ack(Ref) ->    receive {shoot, Ref, Transport, Socket, AckTimeout} ->        Transport:accept_ack(Socket, AckTimeout)    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

erl -pa ebin deps/*/ebin +K true +P 199999     -sname game_server     -s game 

-s game表示啟動時預設調用game:start/0方法。

game.erl

-module(game).%% ====================================================================%% API functions%% ====================================================================-export([start/0, stop/0]).start() ->    ok = application:start(ranch),    ok = application:start(game_server).stop() ->    application:stop(ranch),    application:stop(game_server).


如果設定{packet, raw}的話,直接開啟一個Terminal $ telnet localhost 5555 就可以進行測試了。

不過這裡設定的{packet,2}, 所以寫了個測試client發送訊息,建立串連->發送訊息->接收返回訊息->關閉串連:

-module(client).-export([send/1]).send(BinMsg) ->    SomeHostInNet = "localhost",     {ok, Sock} = gen_tcp:connect(SomeHostInNet, 5555,                                 [binary, {packet, 2}]),    ok = gen_tcp:send(Sock, BinMsg),    receive        {tcp,Socket,String} ->            io:format("Client received = ~p~n",[String]),                   gen_tcp:close(Socket)        after 60000 ->            exit            end,    ok = gen_tcp:close(Sock).

handler_info中加入不同訊息的處理,就可以時間一個簡單的遊戲伺服器了。R17後可以使用{active, N}, 程式效率應該會更高。


















聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.