发送消息后客户端关闭,为什么 gen_tcp with opts {active, false} accept 两次

Client closed after sending a message, why gen_tcp with opts {active, false} accept twice

我只是用 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>

```

But client started and closed, server accept two connection, and one is ok, the other is bad.

实际上,您的服务器只接受了一个连接:

  1. 接受客户端连接后输入loop/1
  2. inet:peername/1 returns {ok,{{192,168,2,184},51608}} 因为套接字仍然打开
  3. gen_tcp:recv/2 returns <<"aa">> 客户端发送
  4. gen_tcp:send/2将3的数据发送给客户端
  5. 再次输入loop/1
  6. inet:peername/1 returns {error,enotconn} 因为套接字被客户端关闭了
  7. gen_tcp:recv/2 returns {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 风格的代码,可能不应该用于教育以外的任何目的。

更好的方法可能是使用 ranch or gen_listener_tcp for the server side listening and accepting of connections. Both projects have examples of echo servers: tcp_echo (ranch) and echo_server.erl (gen_listener_tcp).