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 **


添削大歓迎