客户端在发送消息后关闭,为什么gen_tcp带有opts{active,false}接受两次



我只是用gen_tcp做了一个测试。一个简单的echo服务器和一个客户端。

但客户端启动和关闭,服务器接受两个连接,一个正常,另一个坏。

我的演示脚本有什么问题,如何解释?

服务器

-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
  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. 进程正常退出

因此,实际上,您的echo服务器运行良好,但在@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

在客户端等待echo服务器发回数据,然后再关闭套接字。

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或gen_listener_tcp之类的东西来侦听和接受服务器端的连接。这两个项目都有echo服务器的示例:tcp_echo(ranch)和echo_server.ell(gen_listener_tcp)。

最新更新