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 around gen_tcp.
SSL transport
The SSL transport is a thin wrapper around ssl.

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 call ranch: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
calling ranch:accept_ack/1. You can use Transport: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 or gen_fsm
behaviours have the particularity of having their start_link call not
return until the init function returns. This is problematic, because
you won’t be able to call ranch:accept_ack/1 from the init 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 with proc_lib like all special
processes), then perform any needed operations before falling back into
the normal gen_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 of 0 then the gen_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 the init/1 function
of one of your application supervisors.

Ranch requires at the minimum two kinds of child specs for embedding.
First, you need to add ranch_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集成到自己的項目中

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章