Клиент закрыл после отправки сообщения, почему 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 ответ

Решение

Но клиент запущен и закрыт, сервер принимает два соединения, и одно в порядке, другое плохо.

На самом деле, ваш сервер принял только одно соединение:

  1. Войти loop/1 после принятия соединения от клиента
  2. inet:peername/1 возвращается {ok,{{192,168,2,184},51608}} потому что розетка все еще открыта
  3. gen_tcp:recv/2 возвращается <<"aa">> который был отправлен клиентом
  4. gen_tcp:send/2 отправляет данные от 3 клиенту
  5. Войти loop/1 снова
  6. inet:peername/1 возвращается {error,enotconn} потому что сокет был закрыт клиентом
  7. gen_tcp:recv/2 возвращается {error,closed}
  8. Процесс завершается нормально

Таким образом, на самом деле ваш эхо-сервер функционирует просто отлично, однако есть некоторые улучшения, которые могут быть упомянуты в комментарии, сделанном @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).

Другие вопросы по тегам