tcpnetwork-programmingerlanggen-tcp

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


I just do a testing with gen_tcp. One simple echo server, and one client.

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

Any issue of my demo script, and how to explain it?

server

-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.

Demo Server

1> c(echo).
{ok,echo}
2> echo:listen(1111).
ok {ok,{{192,168,2,184},51608}}
ok {error,enotconn}

Client

> 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>

```


Solution

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

    Actually, your server only accepted one connection:

    1. Enter loop/1 upon accepting the connection from the client
    2. inet:peername/1 returns {ok,{{192,168,2,184},51608}} because the socket is still open
    3. gen_tcp:recv/2 returns <<"aa">> which was sent by the client
    4. gen_tcp:send/2 sends the data from 3 to the client
    5. Enter loop/1 again
    6. inet:peername/1 returns {error,enotconn} because the socket was closed by the client
    7. gen_tcp:recv/2 returns {error,closed}
    8. The process exits normally

    So in reality, your echo server is functioning just fine, however there are some improvements that can be made that are mentioned in the comment made by @zxq9.

    Improvement 1

    Hand off control of the accepted socket to the newly spawned process.

    -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.
    

    Improvement 2

    Wait on the client side for the echo server to send back the data before closing the socket.

    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).
    

    Example

    The server should look something like this:

    1> c(echo).
    {ok,echo}
    2> echo:listen(1111).
    [S] peername {{127,0,0,1},57586}
    [S] got <<"aa">>
    [S] closed
    

    The client should look something like this:

    1> % paste in the code from Improvement 2
    <0.34.0>
    [C] peername {{127,0,0,1},1111}
    [C] got <<"aa">>
    

    Recommendations

    As @zxq9 mentioned, this is not OTP style code and probably shouldn't be used for anything beyond educational purposes.

    A better approach might be to use something like 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).