supervisor の simple_one_for_one を使って echoserver を書き直した
gen_tcp の練習で書いた id:cooldaemon:20070614:1181827975 を gen_server 化しようと考えたが、gen_tcp モジュールの accept や recv は処理をブロックするので、gen_server 化を諦めて id:cooldaemon:20070717:1184676090 のように proc_lib を使ってお茶を濁していた。
しかし、つい最近、Erlang Community - Building a Non-blocking TCP server using OTP principles - Trapexit を読んで、prim_inet モジュールを使う手がある事を知る。
残念な事に prim_inet は、直接使う事が推奨されていない為、仕事で erlang を使いたい私は、全く使う気になれなかったが、simple_one_for_one の使い方等が非常に参考になったので、echoserver を書き直してみる事にした。
Server Design
+----------------+ | tcp_server_sup | +--------+-------+ | (one_for_one) +----------------+---------+ | | +-------+------+ +-------+--------+ | tcp_acceptor | + tcp_client_sup | +--------------+ +-------+--------+ | (simple_one_for_one) +-----|---------+ +-------|--------+| +--------+-------+|+ | tcp_echo |+ +----------------+
ほぼ、Erlang Community - Building a Non-blocking TCP server using OTP principles - Trapexit のパクリ。
Srouce
tcp_server.hrl
-author('cooldaemon@gmail.com'). -define(MAX_RESTART, 5). -define(MAX_TIME, 60). -define(SHUTDOWN_WAITING_TIME, 2000).
各 supervisor の init の戻り値に使う定数を宣言。
tcp_server_sup.erl
-module(tcp_server_sup). -author('cooldaemon@gmail.com'). -behaviour(supervisor). -include("tcp_server.hrl"). % External API -export([start_link/2, stop/0]). % Callbacks -export([init/1]). % External API start_link(Port, Module) -> supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, Module]). stop() -> case whereis(?MODULE) of Pid when pid(Pid) -> exit(Pid, shutdown), ok; _ -> not_started end. % Callbacks init([Port, Module]) -> {ok, {{one_for_one, ?MAX_RESTART, ?MAX_TIME}, [ { tcp_acceptor, {tcp_acceptor, start_link, [Port]}, permanent, ?SHUTDOWN_WAITING_TIME, worker, [tcp_acceptor] }, { tcp_client_sup, {tcp_client_sup, start_link, [Module]}, permanent, infinity, supervisor, [] } ]}}.
start_link を実行すると tcp_acceptor と tcp_client_sup を起動する。
start_link の引数 Port は listen するポート番号。Module は、accept した Socket を処理するモジュールの名前。
今回、Module には echoserver として機能する tcp_echo を指定するが、用途によって切り替える事ができる。
tcp_acceptor.erl
-module(tcp_acceptor). -author('cooldaemon@gmail.com'). % External API -export([start_link/1]). % Callbacks -export([init/2, accept/1]). % External API start_link(Port) -> proc_lib:start_link(?MODULE, init, [self(), Port]). % Callbacks init(Parent, Port) -> case gen_tcp:listen( Port, [{active, false}, binary, {packet, line}, {reuseaddr, true}] ) of {ok, ListenSocket} -> proc_lib:init_ack(Parent, {ok, self()}), accept(ListenSocket); {error, Reason} -> proc_lib:init_ack(Parent, {error, Reason}), error end. accept(ListenSocket) -> {ok, Socket} = gen_tcp:accept(ListenSocket), tcp_client_sup:start_child(Socket), accept(ListenSocket).
listen と accept を担当。
accept した Socket は tcp_client_sup:start_child 経由で tcp_echo に渡す。
tcp_client_sup.erl
-module(tcp_client_sup). -aauthor('cooldaemon@gmail.com'). -behaviour(supervisor). -include("tcp_server.hrl"). % External API -export([start_link/1, start_child/1]). % Callbacks -export([init/1]). % External API start_link(Module) -> supervisor:start_link({local, ?MODULE}, ?MODULE, [Module]). start_child(Socket) -> supervisor:start_child(?MODULE, [Socket]). % Callbacks init([Module]) -> {ok, {{simple_one_for_one, ?MAX_RESTART, ?MAX_TIME}, [{ undefined, {Module, start_link, []}, temporary, ?SHUTDOWN_WAITING_TIME, worker, [] }]}}.
simple_one_for_one を使っている為、Module:start_link (今回は、tcp_echo:start_link) は init 終了時に実行されず、start_child 実行時に実行される。
ちなみに、erlang 付属の httpd モジュールでは、似たような処理を one_for_one で行っている。
tcp_echo.erl
-module(tcp_echo). -author('cooldaemon@gmail.com'). % External API -export([start_link/1]). % Callbacks -export([init/2, recv/1]). % External API start_link(Socket) -> proc_lib:start_link(?MODULE, init, [self(), Socket]). % Callbacks init(Parent, Socket) -> proc_lib:init_ack(Parent, {ok, self()}), recv(Socket). recv(Socket) -> case gen_tcp:recv(Socket, 0) of {ok, B} -> case B of <<"bye\r\n">> -> gen_tcp:send(Socket, <<"cya\r\n">>), gen_tcp:close(Socket); Other -> gen_tcp:send(Socket, Other), recv(Socket) end; {error, closed} -> ok end.
渡された Socket を使って recv したり send したり・・・。
Running
% erl 1> tcp_server_sup:start_link(11211, tcp_echo). {ok,<0.35.0>}
% telnet 127.0.0.1 11211 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. test test hogehoge hogehoge bye cya Connection closed by foreign host.
2> tcp_server_sup:stop(). ok ** exited: shutdown **
添削大歓迎