gen_udp を Supervisor 配下に置いて UDP Server 作ってみた

Kademliaerlang 実装で使おうと思って書いてみた。
いろいろ修正した。詳細は下記。

-module(udp_server).
-author('cooldaemon@gmail.com').
-behaviour(supervisor).

-record(udp_server_option, {
  option       = [binary],
  port         = 4000,
  max_restarts = 3,
  time         = 60,
  shutdown     = 2000,
  recv_length  = 0,
  recv_timeout = infinity
}).

-export([behaviour_info/1]).

-export([start_link/1, start_link/2, start_link/3]).
-export([stop/0, stop/1]).
-export([init/1]). 

-export([receiver_start_link/3]).
-export([receiver_init/3]).

% Behaviour Callbacks
behaviour_info(callbacks) -> [{handle_call, 4}, {handle_info, 1}];
behaviour_info(_Other)    -> undefined.

% Supervisor
%% External APIs
start_link(Module) ->
  start_link(Module, #udp_server_option{}).

start_link(RegistName, Module) when is_atom(Module) ->
  start_link(RegistName, Module, #udp_server_option{});
start_link(Module, Option) ->
  start_link({local, ?MODULE}, Module, Option).

start_link({Dest, Name}=RegistName, Module, Option) ->
  supervisor:start_link(
    {Dest, supervisor_name(Name)},
    ?MODULE,
    [RegistName, Module, Option]
  ).

stop() ->
  stop(?MODULE).

stop(Name) ->
  case whereis(supervisor_name(Name)) of
    Pid when is_pid(Pid) ->
      exit(Pid, normal),
      ok;
    _ -> not_started
  end.

supervisor_name(Name) ->
  list_to_atom(atom_to_list(Name) ++ "_sup").

%% Callbacks
init([{_Dest, Name}=RegistName, Module, Option]) ->
  #udp_server_option{
    max_restarts = MaxRestarts,
    time         = Time,
    shutdown     = Shutdown
  } = Option,
  {ok, {{one_for_one, MaxRestarts, Time}, [
    {
      Name,
      {?MODULE, receiver_start_link, [RegistName, Module, Option]},
      permanent,
      Shutdown,
      worker,
      []
    }
  ]}}.

% ProcLib - udp_server_receiver
%% External APIs
receiver_start_link({Dest, Name}, Module, Option) ->
  {ok, Pid}
    = proc_lib:start_link(?MODULE, receiver_init, [self(), Module, Option]),
  case Dest of
    local   -> register(Name, Pid);
    _Global -> global:register_name(Name, Pid)
  end,
  {ok, Pid}.

%% Callbacks
receiver_init(Parent, Module, Option) ->
  case gen_udp:open(
    Option#udp_server_option.port,
    Option#udp_server_option.option
  ) of
    {ok, Socket} ->
      proc_lib:init_ack(Parent, {ok, self()}),
      recv(
        proplists:get_value(active, Option#udp_server_option.option),
        Socket, Module, Option
      );
    {error, Reason} ->
      exit({error, Reason})
  end.

recv(false, Socket, Module, Option) ->
  case gen_udp:recv(
    Socket,
    Option#udp_server_option.recv_length,
    Option#udp_server_option.recv_timeout
  ) of
    {ok, {Address, Port, Packet}} ->
      Module:handle_call(Socket, Address, Port, Packet),
      recv(false, Socket, Module, Option);
    {error, Reason} ->
      exit({error, Reason})
  end;

recv(_Active, Socket, Module, Option) ->
  receive
    {udp, Socket, Address, Port, Packet} ->
      Module:handle_call(Socket, Address, Port, Packet),
      recv(true, Socket, Module, Option);
    OtherMessage ->
      Module:handle_info(OtherMessage),
      recv(true, Socket, Module, Option)
  after Option#udp_server_option.recv_timeout ->
    exit({error, udp_timeout})
  end.

初めは、receive する度に start_child を使って Supervisor 配下に子プロセスを追加する仕組みにしていたが、別に子プロセスが死のうと生きようと関係ないやーと思い直して spawn にしてみた。
spawn する・しないは、呼び先に任せてるべきだと思ったので、spawn する事を止めた。

んー、active モード時の為に他のメッセージも受け付けるようにしてみたけど、使い道が無かった。
メッセージを送りやすいよう、receiver に名前を付けた。

global で名前を付けると、stop は動かない。

udp_server を使った echo server は下記の通り。

-module(udp_echo).
-author('cooldaemon@gmail.com').
-behaviour(udp_server).

-export([start/0, stop/0]).
-export([handle_call/4, handle_info/1]).
-export([test/0]).

% External APIs
start() -> udp_server:start_link(?MODULE).
stop()  -> udp_server:stop().

% Callbacks
handle_call(Socket, Address, Port, Packet) ->
  io:fwrite("receive:~p~n", [binary_to_term(Packet)]),
  gen_udp:send(Socket, Address, Port, Packet),
  ok.

handle_info(_Message) -> ok.

% Test
test() ->
  {ok, Socket} = gen_udp:open(0, [binary]),
  gen_udp:send(Socket, "localhost", 4000, term_to_binary({foo, bar})),
  gen_udp:close(Socket).

突っ込み、添削、大歓迎。

はてブ id:teahut さんへの返信。

tcp_server とペアで広まるとよさそう.ただ,TCP は accept して子ソケットが作られるので処理が複雑になるけど (だから tcp_server が嬉しいけど),UDP は単純なので普通に書いてもいいのかも

tcp_server の EDOC 書きとか、Bug Fix が終わってないのに浮気してて、ごめんなさい orz
えーっと、ポートを開けて待っているプロセスは、落ちられると困るので Supervisor 配下が良いかと思います。
こうしておくと、udp_server の実装側で例外発生時にバシバシ落とせて、Erlang っぽいかなーと(w;
後は、init を呼んだり、State を持ち回るようにしたら、更にそれっぽいかも。