ErlangでTCP通信を処理する練習として、簡単な"echo"サーバ/クライアントを書いてみた。
複数の接続を同時に受け付けられるようにしてみた。そのため、accept()の直後にspawn()させている。
-module(echo_server).
-export([start/1, stop/1]).
start(Port) when is_integer(Port) ->
spawn(fun() -> init(Port) end).
stop(Pid) -> Pid ! {self(), stop}.
init(Port) ->
{ok, ListenSocket} = gen_tcp:listen(Port,
[binary, {active, true}, {reuseaddr, true}, {packet, line}]),
Pid = spawn(fun() -> accept_spawn(ListenSocket) end),
io:format("new accept_spawn : ~p~n", [Pid]),
loop(ListenSocket).
loop(ListenSocket) ->
receive
{_From, stop} ->
gen_tcp:close(ListenSocket),
io:format("closed: ~p~n", [ListenSocket]);
{_From, Other} ->
io:format("unknown message: ~p~n", [Other]),
loop(ListenSocket)
end.
accept_spawn(ListenSocket) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
{ok, {Host, Port}} = inet:peername(Socket),
io:format("~p:connected from ~p, port ~p~n", [self(), Host, Port]),
%% このプロセス自身は引き続きクライアントソケットとのデータのやり取りに進むため、
%% accept()を行うプロセスを新しくspawn()させている。これにより複数の接続を
%% 処理できるようになる。
Pid = spawn(fun() -> accept_spawn(ListenSocket) end),
io:format("new accept_spawn : ~p~n", [Pid]),
echo_proc(Socket);
{error, Why} ->
io:format("accept() error: ~p~n", [Why])
end.
echo_proc(Socket) ->
receive
{tcp, Socket, Bin} ->
io:format("~p:read and echo [~s]~n", [self(), Bin]),
gen_tcp:send(Socket, Bin),
echo_proc(Socket);
{tcp_closed, Socket} ->
io:format("~p:closed from client.~n", [self()])
after 4000 ->
io:format("~p:read timeout~n", [self()]),
gen_tcp:close(Socket)
end.
-module(echo_client).
-export([connect/2]).
connect(Host, Port) when is_list(Host), is_integer(Port) ->
{ok, Socket} = gen_tcp:connect(Host, Port, [binary, {active, false}]),
io:format("connected: ~p~n", [Socket]),
io:format("input 'quit' for disconnection.~n", []),
loop(Socket).
loop(Socket) ->
case io:get_line("echo> ") of
eof -> disconnect(Socket);
Str when Str =:= "quit\n" -> disconnect(Socket);
Str ->
case gen_tcp:send(Socket, Str) of
ok -> pass;
{error, Why} ->
io:format("send() error: ~p~n", [Why]),
disconnect(Socket)
end,
case gen_tcp:recv(Socket, 0) of
{ok, Reply} ->
io:format("Reply = ~s", [binary_to_list(Reply)]),
loop(Socket);
{error, Why2} ->
io:format("recv() error: ~p~n", [Why2]),
disconnect(Socket)
end
end.
disconnect(Socket) ->
io:format("disconnect from ~p~n", [Socket]),
gen_tcp:close(Socket).
DOS> erl Eshell V5.6.3 (abort with ^G) 1> Pid = echo_server:start(1234). <0.31.0> 2> new accept_spawn : <0.33.0>
ここで適当にtelnetクライアントを立ち上げ、localhostのポート1234宛に接続し、"foo\n", "bar\n"と続けて送信し、クライアント側から切断してみる。その時のサーバ側の出力は次のようになる。
2> <0.33.0>:connected from {127,0,0,1}, port 4084
2> new accept_spawn : <0.34.0>
2> <0.33.0>:read and echo [foo
]
2> <0.33.0>:read and echo [bar
]
2> <0.33.0>:closed from client.
今回作ってみたecho_serverは、4000msの受信タイムアウトを設定している。実際にtelnetで接続し、数バイト送信後に4秒以上放置してみる。
2> <0.34.0>:connected from {127,0,0,1}, port 4102
2> new accept_spawn : <0.35.0>
2> <0.34.0>:read and echo [foo
]
2> <0.34.0>:read timeout
echo_serverを停止する。
2> echo_server:stop(Pid).
{<0.29.0>,stop}
accept() error: closed
closed: #Port<0.97>
最後にaccept()エラーが出ているのは、gen_tcp:accept()で待っているプロセスがあるため。
エラーを出さずに終わらせたいが、上手な方法が分からない・・・。
別の端末なりプロンプトでerlを立ち上げ、echo_client:connect/2を実行する。
DOS> erl
Eshell V5.6.3 (abort with ^G)
1> echo_client:connect("localhost", 1234).
connected: #Port<0.205>
input 'quit' for disconnection.
echo> foo
Reply = foo
echo> bar
Reply = bar
echo> quit
disconnect from #Port<0.205>
ok
gen_tcpモジュールを調べてみると、タイムアウトオプションが豊富に揃っている印象を受ける。
gen_tcp:connect/4 を使うと、4番目の引数で接続タイムアウトをミリ秒で指定できる。
connect()の接続タイムアウトは、以前も調べたが存在しないホストを指定する事で発生させる事が出来る。
C:\in_vitro\erlang>erl
Eshell V5.6.3 (abort with ^G)
1> gen_tcp:connect("192.168.250.61", 1234, [binary], 3000).
{error,timeout}
ちなみに、ホストはあるがListenされていないポート宛の場合はECONNREFUSEDになる。
3> gen_tcp:connect("localhost", 1234, [binary]).
{error,econnrefused}
gen_tcp:accept/2を使うと、accept()にもタイムアウトを設定できる。
accept(ListenSocket, Timeout) -> {ok, Socket} | {error, Reason}
まず {active, true} を gen_tcp:listen/2 や gen_tcp:connect/3,4 のOptionsで指定してアクティブモードでソケットを作成した場合、受信データはプロセスへのメッセージとして送られる。従って、受信タイムアウトを実現するには単純にreceiveとafterを組み合わせる。
今回の例だとこの部分になる。
echo_proc(Socket) ->
receive
{tcp, Socket, Bin} ->
io:format("~p:read and echo [~s]~n", [self(), Bin]),
gen_tcp:send(Socket, Bin),
echo_proc(Socket);
{tcp_closed, Socket} ->
io:format("~p:closed from client.~n", [self()])
after 4000 ->
% 4000ミリ秒経ってもメッセージが来ない時はタイムアウト処理
io:format("~p:read timeout~n", [self()]),
gen_tcp:close(Socket)
end.
パッシブモード ({active, false}) の場合は gen_tcp:recv/3 の最後の引数で受信タイムアウトをミリ秒で指定できる。
recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason}
送信タイムアウトはinet:setopts/2を使って {send_timeout, Integer} タプルをソケットに設定する。
gen_tcp:send()にはタイムアウトオプションは無い。
gen_tcpのリファレンスマニュアルより:
{ok,Sock} = gen_tcp:connect(HostAddress, Port, [{active,false}, {send_timeout, 5000},...
UNIX-CでPOSIXのソケット関数を操る場合、これらのタイムアウトはシグナルを用いたり、一時的に非同期モードでselect()を用いたりと煩雑な操作が必要になる。
Erlangの場合は、通信系が出自であるせいか、接続・送信・受信という3大タイムアウトを簡単に設定できるようになっているのが魅力。