Ranch嚐鮮
簡介
ranch是一個用erlang實現的tcp鏈接的管理池,他有如下特點(摘抄自git庫):
Ranch aims to provide everything you need to accept TCP connections with a small code base and low latency while being easy to use directly as an application or to embed into your own.
Ranch provides a modular design, letting you choose which transport and protocol are going to be used for a particular listener. Listeners accept and manage connections on one port, and include facilities to limit the number of concurrent connections. Connections are sorted into pools, each pool having a different configurable limit.
Ranch also allows you to upgrade the acceptor pool without having to close any of the currently opened sockets.
簡單來說就是
- Ranch很輕量,且內斂,集成到項目中很容易。
- 通過使用模塊化的設計,可以實現同時存在多個tcp連接池且允許他們有不同的配置,不同的處理邏輯,可以做到完全隔離。
- 同時支持在線更新連接池的配置而不用關閉當前已經打開的socket。
Git庫地址
簡單實例
項目用一個簡單的echo服務介紹了一個簡單的ranch使用模型。
tcp_echo_app.erl
start(_Type, _Args) ->
{ok, _} = ranch:start_listener(tcp_echo, 1,
ranch_tcp, [{port, 5555}], echo_protocol, []),
tcp_echo_sup:start_link().
echo_protocol.erl
-module(echo_protocol).
-behaviour(ranch_protocol).
-export([start_link/4]).
-export([init/4]).
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).
loop(Socket, Transport) ->
case Transport:recv(Socket, 0, 5000) of
{ok, Data} ->
Transport:send(Socket, Data),
loop(Socket, Transport);
_ ->
ok = Transport:close(Socket)
end.
當然要使用這些邏輯的基礎前提是首先application:ensure_all_started(ranch).
直接看的話還是有點難懂,主要體現在:
- 爲什麼首先要啓動一個ranch的application才能開始監聽端口
- ranch:start_listener 實現了什麼邏輯,每個指定的參數內容是什麼
- echo_protocol模塊的ranch_protocol是一個怎樣的定義,它start_link函數的入參從何而來
- tcp_echo_sup的作用是什麼
現在我們帶着這些問題開始學習ranch的邏輯
1. 爲什麼首先要啓動一個ranch的application才能開始監聽端口
要解答這個問題,我們得先知道ranch的application啓動後都做了哪些事情
ranch_app.erl
-module(ranch_app).
-behaviour(application).
start(_, _) ->
_ = consider_profiling(),
ranch_sup:start_link().
我們暫時忽略consider_profiling(實際上這部分是用來做prof分析的根據app配置決定是否啓動,和功能邏輯無關),也就是啓動了ranch_sup一個貌似supervisor的管理者
ranch_sup.erl
-module(ranch_sup).
-behaviour(supervisor).
-spec start_link() -> {ok, pid()}.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
ranch_server = ets:new(ranch_server, [
ordered_set, public, named_table]),
Procs = [
{ranch_server, {ranch_server, start_link, []},
permanent, 5000, worker, [ranch_server]}
],
{ok, {{one_for_one, 10, 10}, Procs}}.
看來並沒有猜錯,ranch_sup就是一個supervisor,他建立了一張ets表,然後定義了自己的child的描述。再回到例子的ranch:start_listener 就可以大膽猜測下,其實就是啓動一個supervisor用來管理之前特性說明裏的,同時支持多個tcp端口監聽實例,而這些實例統一由ranch_sup來管理,當然這還是我們的猜測,還要看下ranch_server的具體實現才能確定。
ranch_server.erl
-module(ranch_server).
-behaviour(gen_server).
-define(TAB, ?MODULE).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
Monitors = [{{erlang:monitor(process, Pid), Pid}, Ref} ||
[Ref, Pid] <- ets:match(?TAB, {{conns_sup, '$1'}, '$2'})],
{ok, #state{monitors=Monitors}}.
看起來並不是我們猜測的那樣,ranch_server並沒有要和某一個端口進行關聯之類的邏輯,再看看的handle_call的回調內容
handle_call({set_new_listener_opts, Ref, MaxConns, Opts}, _, State) ->
ets:insert(?TAB, {{max_conns, Ref}, MaxConns}),
ets:insert(?TAB, {{opts, Ref}, Opts}),
{reply, ok, State};
handle_call({set_connections_sup, Ref, Pid}, _,
State=#state{monitors=Monitors}) ->
case ets:insert_new(?TAB, {{conns_sup, Ref}, Pid}) of
true ->
MonitorRef = erlang:monitor(process, Pid),
{reply, true,
State#state{monitors=[{{MonitorRef, Pid}, Ref}|Monitors]}};
false ->
{reply, false, State}
end;
handle_call({set_port, Ref, Port}, _, State) ->
true = ets:insert(?TAB, {{port, Ref}, Port}),
{reply, ok, State};
handle_call({set_max_conns, Ref, MaxConns}, _, State) ->
ets:insert(?TAB, {{max_conns, Ref}, MaxConns}),
ConnsSup = get_connections_sup(Ref),
ConnsSup ! {set_max_conns, MaxConns},
{reply, ok, State};
handle_call({set_opts, Ref, Opts}, _, State) ->
ets:insert(?TAB, {{opts, Ref}, Opts}),
ConnsSup = get_connections_sup(Ref),
ConnsSup ! {set_opts, Opts},
{reply, ok, State};
原來ranch_server起得是一個管理者的角色,他管理着所有當前的實例,相當與ranch_server這張ets表的代理者
總結一下就是ranch的application使用ranch_sup管理了ranch_server進程,ranch_server進程作爲ranch_server這張ets表的代理者保存記錄着當前所有的實例的一些信息。有了這些基礎工作才能實現多實例的管理。
2.ranch:start_listener 實現了什麼邏輯,每個指定的參數內容是什麼
ranch.erl
-spec start_listener(ref(), non_neg_integer(), module(), any(), module(), any())
-> {ok, pid()} | {error, badarg}.
start_listener(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts)
when is_integer(NbAcceptors) andalso is_atom(Transport)
andalso is_atom(Protocol) ->
_ = code:ensure_loaded(Transport),
case erlang:function_exported(Transport, name, 0) of
false ->
{error, badarg};
true ->
Res = supervisor:start_child(ranch_sup, child_spec(Ref, NbAcceptors,
Transport, TransOpts, Protocol, ProtoOpts)),
Socket = proplists:get_value(socket, TransOpts),
case Res of
{ok, Pid} when Socket =/= undefined ->
%% Give ownership of the socket to ranch_acceptors_sup
%% to make sure the socket stays open as long as the
%% listener is alive. If the socket closes however there
%% will be no way to recover because we don't know how
%% to open it again.
Children = supervisor:which_children(Pid),
{_, AcceptorsSup, _, _}
= lists:keyfind(ranch_acceptors_sup, 1, Children),
%%% Note: the catch is here because SSL crashes when you change
%%% the controlling process of a listen socket because of a bug.
%%% The bug will be fixed in R16.
catch Transport:controlling_process(Socket, AcceptorsSup);
_ ->
ok
end,
Res
end.
-spec child_spec(ref(), non_neg_integer(), module(), any(), module(), any())
-> supervisor:child_spec().
child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts)
when is_integer(NbAcceptors) andalso is_atom(Transport)
andalso is_atom(Protocol) ->
{{ranch_listener_sup, Ref}, {ranch_listener_sup, start_link, [
Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
]}, permanent, infinity, supervisor, [ranch_listener_sup]}.
start_listener函數的所有參數都被傳給ranch_listener_sup了,所以我們只能再去ranch_listener_sup裏看看了
ranch_listener_sup.erl
-module(ranch_listener_sup).
-behaviour(supervisor).
start_link(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) ->
MaxConns = proplists:get_value(max_connections, TransOpts, 1024),
ranch_server:set_new_listener_opts(Ref, MaxConns, ProtoOpts),
supervisor:start_link(?MODULE, {
Ref, NbAcceptors, Transport, TransOpts, Protocol
}).
init({Ref, NbAcceptors, Transport, TransOpts, Protocol}) ->
AckTimeout = proplists:get_value(ack_timeout, TransOpts, 5000),
ConnType = proplists:get_value(connection_type, TransOpts, worker),
Shutdown = proplists:get_value(shutdown, TransOpts, 5000),
ChildSpecs = [
{ranch_conns_sup, {ranch_conns_sup, start_link,
[Ref, ConnType, Shutdown, Transport, AckTimeout, Protocol]},
permanent, infinity, supervisor, [ranch_conns_sup]},
{ranch_acceptors_sup, {ranch_acceptors_sup, start_link,
[Ref, NbAcceptors, Transport, TransOpts]},
permanent, infinity, supervisor, [ranch_acceptors_sup]}
],
{ok, {{rest_for_one, 10, 10}, ChildSpecs}}.
好吧,ProtoOpts倒是被set後就沒再繼續進行傳遞了,TransOpts被get出來幾個值,之後有和其他的參數原模原樣的傳給了ranch_conns_sup和ranch_acceptors_sup,在這裏嘗試看下ProtoOpts的用法
ranch_server.erl
set_new_listener_opts(Ref, MaxConns, Opts) ->
gen_server:call(?MODULE, {set_new_listener_opts, Ref, MaxConns, Opts}).
handle_call({set_new_listener_opts, Ref, MaxConns, Opts}, _, State) ->
ets:insert(?TAB, {{max_conns, Ref}, MaxConns}),
ets:insert(?TAB, {{opts, Ref}, Opts}),
{reply, ok, State};
get_protocol_options(Ref) ->
ets:lookup_element(?TAB, {opts, Ref}, 2).
ranch_server對opts提供了基於ets的 get和set接口
ranch_conns_sup.erl
init(Parent, Ref, ConnType, Shutdown, Transport, AckTimeout, Protocol) ->
process_flag(trap_exit, true),
ok = ranch_server:set_connections_sup(Ref, self()),
MaxConns = ranch_server:get_max_connections(Ref),
Opts = ranch_server:get_protocol_options(Ref), %%這裏被調用
ok = proc_lib:init_ack(Parent, {ok, self()}),
loop(#state{parent=Parent, ref=Ref, conn_type=ConnType,
shutdown=Shutdown, transport=Transport, protocol=Protocol,
opts=Opts, ack_timeout=AckTimeout, max_conns=MaxConns}, 0, 0, []).
loop(State=#state{parent=Parent, ref=Ref, conn_type=ConnType,
transport=Transport, protocol=Protocol, opts=Opts,
ack_timeout=AckTimeout, max_conns=MaxConns},
CurConns, NbChildren, Sleepers) ->
receive
{?MODULE, start_protocol, To, Socket} ->
case Protocol:start_link(Ref, Socket, Transport, Opts) of
Transport:controlling_process(Socket, Pid),
...
Ret ->
To ! self(),
error_logger:error_msg(
"Ranch listener ~p connection process start failure; "
"~p:start_link/4 returned: ~999999p~n",
[Ref, Protocol, Ret]),
Transport:close(Socket),
也就是被原模原樣的傳給了剛開始的Protocol指定的模塊,ranch本身不需要這部分,Opts是提供給使用者的,針對單獨實例的一個dict,可以在自己的Protocol啓動邏輯裏使用,而Transport這裏用了Transport:controlling_process(Socket, Pid),Transport:close(Socket),看起來很像gen_tcp模塊這種東西,結合echo例子用的ranch_tcp,這時候就沒法猜只能看文檔了
A transport defines the interface to interact with a socket.
TCP transport
The TCP transport is a thin wrapper aroundgen_tcp
.
SSL transport
The SSL transport is a thin wrapper aroundssl
.
ranch庫本身提供了基於gen_tcp實現的ranch_tcp和基於ssl實現的ranch_ssl,當然也可以自定義transport,只需要實現一個符合ranch_transport behavior 的模塊即可。
現在我們解決了參數中的四個
Ref , NbAcceptors, Transport , TransOpts, Protocol , ProtoOpts
Ref是用來標記這個實例的名字,用atom來描述
Transport 指定當前數據傳輸的方式,例子裏用的ranch_tcp
Protocol 指定tcp消息的具體處理模塊,一般來說是一個gen_server描述,這裏具體的邏輯還要在細看
ProtoOpts 則是傳給這個模塊的配置,供用戶模塊自己使用,ranch會根據Ref標記來存儲這些值
接着看代碼
ranch_acceptors_sup
-module(ranch_acceptors_sup).
-behaviour(supervisor).
-spec start_link(ranch:ref(), non_neg_integer(), module(), any())
-> {ok, pid()}.
start_link(Ref, NbAcceptors, Transport, TransOpts) ->
supervisor:start_link(?MODULE, [Ref, NbAcceptors, Transport, TransOpts]).
init([Ref, NbAcceptors, Transport, TransOpts]) ->
ConnsSup = ranch_server:get_connections_sup(Ref),
LSocket = case proplists:get_value(socket, TransOpts) of
undefined ->
{ok, Socket} = Transport:listen(TransOpts),
Socket;
Socket ->
Socket
end,
{ok, {_, Port}} = Transport:sockname(LSocket),
ranch_server:set_port(Ref, Port),
Procs = [
{{acceptor, self(), N}, {ranch_acceptor, start_link, [
LSocket, Transport, ConnsSup
]}, permanent, brutal_kill, worker, []}
|| N <- lists:seq(1, NbAcceptors)],
{ok, {{one_for_one, 10, 10}, Procs}}.
ranch_tcp.erl
listen(Opts) ->
Opts2 = ranch:set_option_default(Opts, backlog, 1024),
Opts3 = ranch:set_option_default(Opts2, send_timeout, 30000),
Opts4 = ranch:set_option_default(Opts3, send_timeout_close, true),
%% We set the port to 0 because it is given in the Opts directly.
%% The port in the options takes precedence over the one in the
%% first argument.
gen_tcp:listen(0, ranch:filter_options(Opts4,
[backlog, ip, linger, nodelay, port, raw,
send_timeout, send_timeout_close],
[binary, {active, false}, {packet, raw},
{reuseaddr, true}, {nodelay, true}])).
NbAcceptors比較容易理解,這是一個int值指定了有多少個accepter用來處理對LSocket發起的連接請求。
TransOpts的用法就相對複雜,首先使用的socket字段的值,如果就直接把這個值當做LSocket進入邏輯,如果沒有就調用Transport:listen來監聽端口來生成LSocket
對於gen_tcp他允許用戶指定ip,port,linger,nodelay,raw這五個字段,其他的由ranch_tcp指定,不允許用戶設定。
這5個參數都是gen_tcp模塊的原生邏輯,參考gen_tcp模塊即可這就不再做解釋
也就是說TransOpts 描述的LSocket,可以直接通過socket字段來指定一個LSocket完成後續邏輯,或者指定要監聽端口的參數包括且限於上面說的5個參數來指定tcp連接的屬性。
至此,我們探究完了ranch:start_listener的所有參數。
3.echo_protocol模塊的ranch_protocol是一個怎樣的定義,它start_link函數的入參從何而來
現在我們已經可以直接回答這個問題的第二部分,echo_protocol就是上面我們談到的Protocol,他是用來指定Socket的具體處理邏輯模塊的,他的參數全部由ranch:start_listener 提供,這裏我們看下echo_protocol的具體定義
echo_protocol.erl
-module(echo_protocol).
-behaviour(ranch_protocol).
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).
loop(Socket, Transport) ->
case Transport:recv(Socket, 0, 5000) of
{ok, Data} ->
Transport:send(Socket, Data),
loop(Socket, Transport);
_ ->
ok = Transport:close(Socket)
end.
ranch_protocol.erl
-module(ranch_protocol).
%% Start a new connection process for the given socket.
-callback start_link(
Ref::ranch:ref(),
Socket::any(),
Transport::module(),
ProtocolOptions::any())
-> {ok, ConnectionPid::pid()}.
ranch_protocol只需要完成一個回調 start_link/4的定義即可,入參分別爲監聽標識,accept之後生成的Socket,傳輸方式Transport,還是有ProtocolOptions
例子中的內容也很清楚,啓動一個進程,進入loop循環,每次將從socket收到的內容原模原樣返回回去,需要注意的是ranch:accept_ack(Ref)這步,原因的話看下官方文檔:
The newly started process can then freely initialize itself. However,
it must callranch:accept_ack/1
before doing any socket operation.
This will ensure the connection process is the owner of the socket.
It expects the listener’s name as argument.
要理解這部分就必須完整的看完accept到具體的邏輯處理的過程,我們從accept找起
ranch_acceptor.erl
loop(LSocket, Transport, ConnsSup) ->
_ = case Transport:accept(LSocket, infinity) of
{ok, CSocket} ->
Transport:controlling_process(CSocket, ConnsSup),
%% This call will not return until process has been started
%% AND we are below the maximum number of connections.
ranch_conns_sup:start_protocol(ConnsSup, CSocket);
之前我們啓動了NbAcceptors個ranch_acceptor用來處理LSocket的accept請求,他們每個都阻塞在Transport:accept/2 這裏,當有連接請求被處理後,將Socket控制權交給connsSup,然後給sup進行通知
ranch_conns_sup.erl
-spec start_protocol(pid(), inet:socket()) -> ok.
start_protocol(SupPid, Socket) ->
SupPid ! {?MODULE, start_protocol, self(), Socket},
receive SupPid -> ok end.
loop(State=#state{parent=Parent, ref=Ref, conn_type=ConnType,
transport=Transport, protocol=Protocol, opts=Opts,
ack_timeout=AckTimeout, max_conns=MaxConns},
CurConns, NbChildren, Sleepers) ->
receive
{?MODULE, start_protocol, To, Socket} ->
case Protocol:start_link(Ref, Socket, Transport, Opts) of
{ok, Pid} ->
Transport:controlling_process(Socket, Pid),
Pid ! {shoot, Ref, Transport, Socket, AckTimeout},
put(Pid, true),
CurConns2 = CurConns + 1,
if CurConns2 < MaxConns ->
To ! self(),
loop(State, CurConns2, NbChildren + 1,
Sleepers);
true ->
loop(State, CurConns2, NbChildren + 1,
[To|Sleepers])
end;
...
可以看到是先調用Protocol:start_link/4 啓動了Protocol模塊指定的進程,然後將Socket的控制權交給這個新啓動的進程,之後再發送一條消息知會處理進程控制權已經交接完畢,這個Socket可以使用了,反過來說就是在收到這條shot消息之前,我們並不能確定Socket的控制權已經交接完畢,所以在echo_protocol裏我們首先調用了ranch:accept_ack/1 確保進程init完成後就是可用的。之前看ranch_tcp:listen/1的時候我們也看到了強制設置的socket爲{activie,false},因此也不用擔心消息丟失。在拿到Socket控制權之後就可以隨意更改Socket設置了
If your protocol code requires specific socket options, you should
set them while initializing your connection process, after
callingranch:accept_ack/1
. You can useTransport:setopts/2
for that purpose.
這裏還有一點要注意就是,如果Protocol是一個gen_server描述,那麼gen_server:start_link是一個阻塞調用,意味着Protocol:start_link(Ref, Socket, Transport, Opts) 在你的init函數裏有ranch:accept_ack/1的情況下是永遠無法返回的,所以得專門處理下,官方文檔裏提供了兩種解決方案
Special processes like the ones that use the
gen_server
orgen_fsm
behaviours have the particularity of having theirstart_link
call not
return until theinit
function returns. This is problematic, because
you won’t be able to callranch:accept_ack/1
from theinit
callback
as this would cause a deadlock to happen.
There are two ways of solving this problem.
The first, and probably the most elegant one, is to make use of the
gen_server:enter_loop/3
function. It allows you to start your process
normally (although it must be started withproc_lib
like all special
processes), then perform any needed operations before falling back into
the normalgen_server
execution loop.
-module(my_protocol).
-behaviour(gen_server).
-behaviour(ranch_protocol).
-export([start_link/4]).
-export([init/4]).
%% Exports of other gen_server callbacks here.
start_link(Ref, Socket, Transport, Opts) ->
proc_lib:start_link(?MODULE, init, [Ref, Socket, Transport, Opts]).
init(Ref, Socket, Transport, _Opts = []) ->
ok = proc_lib:init_ack({ok, self()}),
%% Perform any required state initialization here.
ok = ranch:accept_ack(Ref),
ok = Transport:setopts(Socket, [{active, once}]),
gen_server:enter_loop(?MODULE, [], {state, Socket, Transport}).
%% Other gen_server callbacks here.
The second method involves triggering a timeout just after
gen_server:init
ends. If you return a timeout value of0
then thegen_server
will call
handle_info(timeout, _, _)
right away.
-module(my_protocol).
-behaviour(gen_server).
-behaviour(ranch_protocol).
%% Exports go here.
init([Ref, Socket, Transport]) ->
{ok, {state, Ref, Socket, Transport}, 0}.
handle_info(timeout, State={state, Ref, Socket, Transport}) ->
ok = ranch:accept_ack(Ref),
ok = Transport:setopts(Socket, [{active, once}]),
{noreply, State};
%% ...
就是要麼通過使用gen_server:enter_loop/3
,和proc_lib
的配合,強制讓啓動gen_server
的時候直接調用Mod:init
而不是gen_server:init_it/6
來避免進程啓動的阻塞。
或者就是確保進程啓動後執行的第一個動作一定是ranch:accept_ack/1
,通過gen_server
的啓動超時機制,在超時回調里加入ranch:accept_ack/1
來確保ack的執行
4.tcp_echo_sup的作用是什麼
乍一看tcp_echo_sup好像確實沒用到啊。事實在這個例子來說他確實沒用到,不過例子這麼寫也是有原因的
tcp_echo.app.src
{application, tcp_echo, [
{description, "Ranch TCP echo example."},
{vsn, "1"},
{modules, []},
{registered, [tcp_echo_sup]},
{applications, [
kernel,
stdlib,
ranch
]},
{mod, {tcp_echo_app, []}},
{env, []}
]}.
其實這個代表的就是你自己項目的主sup,也就是下面的your application supervisors,他就是用來掛載啓動ranch實例的
To embed Ranch in your application you can simply add the child specs
to your supervision tree. This can all be done in theinit/1
function
of one of your application supervisors.
Ranch requires at the minimum two kinds of child specs for embedding.
First, you need to addranch_sup
to your supervision tree, only once,
regardless of the number of listeners you will use. Then you need to
add the child specs for each listener.
我們以CowBoy爲例看下ranch啓動後的狀態
ranch_listener_sup是一個實例的主持者,他會根據入參初始化ranch_acceptors_sup,決定啓動多少個ranch_accepter來進行accept工作,同時啓動ranch_conns_sup等待連接到來,以後沒有一個連接到來,都會啓動一個用戶定義的Protocol模塊對應的進程來處理Socket信息,這個進程掛在在ranch_conns_sup下被統一管理
而ranch_server管理記錄着這個實例的絕大多數基礎信息
篇幅原因下一篇再來談如何將ranch集成到自己的項目中