Клиент закрыл после отправки сообщения, почему gen_tcp с опциями {active, false} принимают дважды
Я просто делаю тестирование с помощью gen_tcp. Один простой эхо-сервер и один клиент.
Но клиент запущен и закрыт, сервер принимает два соединения, и одно в порядке, другое плохо.
Любая проблема моего демо-сценария, и как это объяснить?
сервер
-module(echo).
-export([listen/1]).
-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
listen(Port) ->
{ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept(LSocket).
accept(LSocket) ->
{ok, Socket} = gen_tcp:accept(LSocket),
spawn(fun() -> loop(Socket) end),
accept(LSocket).
loop(Socket) ->
timer:sleep(10000),
P = inet:peername(Socket),
io:format("ok ~p~n", [P]),
case gen_tcp:recv(Socket, 0) of
{ok, Data} ->
gen_tcp:send(Socket, Data),
loop(Socket);
{error, closed} ->
ok;
E ->
io:format("bad ~p~n", [E])
end.
Демо-сервер
1> c(echo).
{ok,echo}
2> echo:listen(1111).
ok {ok,{{192,168,2,184},51608}}
ok {error,enotconn}
клиент
> spawn(fun() -> {ok, P} = gen_tcp:connect("192.168.2.173", 1111, []), gen_tcp:send(P, "aa"), gen_tcp:close(P) end).
<0.64.0>
```
1 ответ
Но клиент запущен и закрыт, сервер принимает два соединения, и одно в порядке, другое плохо.
На самом деле, ваш сервер принял только одно соединение:
- Войти
loop/1
после принятия соединения от клиента inet:peername/1
возвращается{ok,{{192,168,2,184},51608}}
потому что розетка все еще открытаgen_tcp:recv/2
возвращается<<"aa">>
который был отправлен клиентомgen_tcp:send/2
отправляет данные от 3 клиенту- Войти
loop/1
снова inet:peername/1
возвращается{error,enotconn}
потому что сокет был закрыт клиентомgen_tcp:recv/2
возвращается{error,closed}
- Процесс завершается нормально
Таким образом, на самом деле ваш эхо-сервер функционирует просто отлично, однако есть некоторые улучшения, которые могут быть упомянуты в комментарии, сделанном @zxq9.
Улучшение 1
Передайте управление принятым сокетом вновь порожденному процессу.
-module(echo).
-export([listen/1]).
-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
listen(Port) ->
{ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept(LSocket).
accept(LSocket) ->
{ok, CSocket} = gen_tcp:accept(LSocket),
Ref = make_ref(),
To = spawn(fun() -> init(Ref, CSocket) end),
gen_tcp:controlling_process(CSocket, To),
To ! {handoff, Ref, CSocket},
accept(LSocket).
init(Ref, Socket) ->
receive
{handoff, Ref, Socket} ->
{ok, Peername} = inet:peername(Socket),
io:format("[S] peername ~p~n", [Peername]),
loop(Socket)
end.
loop(Socket) ->
case gen_tcp:recv(Socket, 0) of
{ok, Data} ->
io:format("[S] got ~p~n", [Data]),
gen_tcp:send(Socket, Data),
loop(Socket);
{error, closed} ->
io:format("[S] closed~n", []);
E ->
io:format("[S] error ~p~n", [E])
end.
Улучшение 2
Подождите на стороне клиента, чтобы эхо-сервер отправил обратно данные, прежде чем закрывать сокет.
spawn(fun () ->
{ok, Socket} = gen_tcp:connect("127.0.0.1", 1111, [binary, {packet, 0}, {active, false}]),
{ok, Peername} = inet:peername(Socket),
io:format("[C] peername ~p~n", [Peername]),
gen_tcp:send(Socket, <<"aa">>),
case gen_tcp:recv(Socket, 0) of
{ok, Data} ->
io:format("[C] got ~p~n", [Data]),
gen_tcp:close(Socket);
{error, closed} ->
io:format("[C] closed~n", []);
E ->
io:format("[C] error ~p~n", [E])
end
end).
пример
Сервер должен выглядеть примерно так:
1> c(echo).
{ok,echo}
2> echo:listen(1111).
[S] peername {{127,0,0,1},57586}
[S] got <<"aa">>
[S] closed
Клиент должен выглядеть примерно так:
1> % paste in the code from Improvement 2
<0.34.0>
[C] peername {{127,0,0,1},1111}
[C] got <<"aa">>
рекомендации
Как упоминалось @ zxq9, это не код в стиле OTP, и, вероятно, его не следует использовать ни для чего, кроме образовательных целей.
Лучшим подходом может быть использование чего-то вроде ранчо или gen_listener_tcp для прослушивания и приема соединений на стороне сервера. Оба проекта имеют примеры эхо-серверов: tcp_echo (ранчо) и echo_server.erl (gen_listener_tcp).