使用Ranch搭建自己的TCP連接池
上一篇我們講了ranch的基礎邏輯還看了一個簡單的echo例子,這次我們嘗試真正使用ranch
將Ranch集成到自己的項目中
集成部分的話參照cowboy使用rebar將ranch配置到自己項目的deps目錄中即可,erlang.mk應該也可以,不過我自己沒嘗試過。。有興趣的可以自己探索下應該都是共通的。這裏就不再贅述集成部分了,ranch本身也說過是包裝成一個易集成的結構的。
使用ranch
這裏我們假設項目中已經有老的tcp接口了,我們現在換成ranch架構(沒有也沒關係,差距並不大,比較麻煩的都是消息的解包和登陸邏輯的處理)
這裏介紹兩種使用方式,一種專門起一個進程讓進程阻塞在recv部分,有消息就處理,沒就保持阻塞,另外一種就是每次讓進程設置socket爲{active, once}
,然後使用{tcp,S, Data}
回調來處理的邏輯,這樣可以保持進程不阻塞,這種模式可以適用於gen_server模型,你可以把業務邏輯也放到這個接收進程裏,雖然不推薦這樣做,下面的兩個具體實現都會是將消息轉發給業務處理進程而不是由接收進程來處理,所以不會體現業務邏輯部分
定義ProtoCol
阻塞模式
cowboy本身就是一個典型的阻塞模型的例子,Cowboy Git庫地址,但是要看懂cowboy的處理邏輯還需要了解一些http底層處理,這裏我們用最簡單的模型來展示ranch的使用
首先,定義我們的類似echo_protocol的tcp消息處理模塊,當然他肯定是一個ranch_protocol描述的模塊,也就是這個模塊必然提供start_link/4
並返回{ok, Pid}
這樣就很簡單了,我們可以根據上一篇中的echo的例子稍微進行改寫,加入消息的解析,解析成自己系統可以識別的就可以了
-module(my_protocol).
-behaviour(ranch_protocol).
-export([start_link/4]).
-export([init/4]).
-record(state, {accid = 0,
buffer = <<>>,
pid
}).
start_link(Ref, Socket, Transport, Opts) ->
Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]),
{ok, Pid}.
init(Ref, Socket, Transport, _Opts = []) ->
ok = ranch:accept_ack(Ref),
loop(Socket, Transport, #state{}).
loop(Socket, Transport, #state{buffer = Buffer} = State) ->
case Transport:recv(Socket, 0, 5000) of
{ok, Data} ->
%%改動部分
{MsgList, NewBuffer} = prase_body(<<Buffer/binary, Data/binary>>),
NewState = handle_msg(MsgList, State#state{buffer = NewBuffer}),
loop(Socket, Transport, NewState);
_ ->
ok = Transport:close(Socket)
end.
%%新加邏輯
prase_body(MsgBody) ->
prase_body2(MsgBody, []).
prase_body2(<<Len:16, MsgBody:Len/binary, Left/binary>>, MsgList) ->
prase_body2(Left, [decode_msg(MsgBody)|MsgList]);
prase_body2(MsgBody, MsgList) ->
{lists:reverse(MsgList), MsgBody}.
handle_msg([{login, accid, AccID}|L], State) ->
{ok, Pid} = start_server(AccID),
handle_msg(L, State#state{accid = AccID, pid = Pid});
handle_msg([Msg|L], #state{pid = Pid} = State) ->
Pid ! Msg,
handle_msg(L, State).
基礎的這個版本沒有解包部分,我們先假設我們的對包協議是用2字節來表示包體的長度Len,然後接下來的長度Len就是具體的打包過的包體,至於包體的打包方式,就多種多樣了,可以是json或者protobuf或者其他的,保持自己項目在用的就可以,也就是上面沒實現的decode_msg/1
,
而start_server的賬號登陸部分寫的很簡陋,根據自己項目進行填充即可。
gen_server模型下的非阻塞模式
-module(my_protocol).
-behaviour(gen_server).
-behaviour(ranch_protocol).
%% API.
-export([start_link/4]).
%% gen_server.
-export([init/1]).
-export([init/4]).
-export([handle_call/3]).
-export([handle_cast/2]).
-export([handle_info/2]).
-export([terminate/2]).
-export([code_change/3]).
-define(TIMEOUT, 5000).
-record(state, {socket, transport, buffer, accid, pid}).
%% 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}]),
gen_server:enter_loop(?MODULE, [],
#state{socket=Socket, transport=Transport, buffer = <<>>},
?TIMEOUT).
handle_info({tcp, Socket, Data}, State=#state{
socket=Socket, transport=Transport, buffer = Buffer}) ->
Transport:setopts(Socket, [{active, once}]),
%% 改動部分
{MsgList, NewBuffer} = prase_body(<<Buffer/binary, Data/binary>>),
NewState = handle_msg(MsgList, State#state{buffer = NewBuffer}),
{noreply, NewState, ?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.
%% 新加邏輯
prase_body(MsgBody) ->
prase_body(MsgBody, []).
prase_body(<<Len:16, MsgBody:Len/binary, Left/binary>>, MsgList) ->
prase_body(Left, [decode_msg(MsgBody)|MsgList]);
prase_body(MsgBody, MsgList) ->
{lists:reverse(MsgList), MsgBody}.
handle_msg([{login, accid, AccID}|L], State) ->
{ok, Pid} = start_server(AccID),
handle_msg(L, State#state{accid = AccID, pid = Pid});
handle_msg([Msg|L], #state{pid = Pid} = State) ->
Pid ! Msg,
handle_msg(L, State).
emm是的這個例子是抄的例子reverse_protocol,只需要稍微改動下消息體的處理部分和state就可以完成,同樣記得實現自己的start_server/1
和decode_msg/1
調用自己定義的ProtoCol
{ok, _} = ranch:start_listener(my_echo, 10,
ranch_tcp, [{port, 5555}], my_protocol, []),
將這句代碼加入到項目啓動邏輯中即可,同時記得把ranch加入到項目的app.src依賴中,這樣就不用主動的調用ranch的啓動邏輯了。
這裏加入一點雜談,就是爲什麼要在已經有“穩定”的tcp框架的情況下,再去費勁改成ranch框架呢,我只談自己的想法,不一定適用於所有人,大家看看就好
- 首先呢,我們的另外一個deps庫間接調用了ranch庫,所以我不得不讓ranch存在於我的deps目錄,所以既然都沒辦法裁剪了,乾脆就用了。而且ranch的使用項目也非常的多,也就意味着他經過了多個項目多個應用場景的考驗,比起自己寫的單項目的穩定性和各方面的考慮都是無法比擬的。
- 另外一個在與ranch的特性,ranch將每一個監聽實例用一個ranch_listener_sup來管理,也就意味着一旦你接入了ranch,後續你想再使用tcp接口就相當的廉價了,你只需要像上面一樣定義你的消息處理邏輯,之後一句start_litener就可以完成所有的處理,不用再操心斷開,重連,數量限制這些各種邏輯,如果以後需要擴展其他的tcp接口,無疑帶來了巨大的便利,我們只用重新定義一個模塊,而不是重構或者直接copy代碼重寫一份。
- 在一個就是將來的新項目架構,你只需要更新ranch到最新版本,就可以支持最新版本的erlang特性和語法,而且是經過檢驗的何樂而不爲呢