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=[{