1

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>

```

linbo
  • 2,393
  • 3
  • 22
  • 45
  • 2
    Your socket is never listening, so you don't receive your message. You also instantly close the connection from the client side before anything can happen. Control is also not being passed to the new process receiving on the socket. Here is a different approach: https://github.com/zxq9/erlmud/blob/master/erlmud-0.1/tcplistener.erl Control in this case is being handed off eventually to this: https://github.com/zxq9/erlmud/blob/master/erlmud-0.1/teltalker.erl Note this is *not* OTP style code, just raw Erlang. – zxq9 Dec 03 '15 at 14:12

1 Answers1

3

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

potatosalad
  • 4,819
  • 2
  • 19
  • 21
  • Thanks for your help. I have one question, loop sleep 10s after client close socket, why peername still works first time? – linbo Dec 04 '15 at 02:55
  • I think it's dependent on the `inet_descriptor->state` flag as well as the socket address stored by `inet_descriptor->peer_ptr` (see [lines 8639-8657 of inet_drv.c](https://github.com/erlang/otp/blob/OTP-18.1.5/erts/emulator/drivers/common/inet_drv.c#L8639-L8657)). If there is data sitting in the buffer, even if the client has already closed the connection, the socket appears to still be considered active. Once that data has been read, the socket is soon after marked as inactive and closed. – potatosalad Dec 04 '15 at 05:45
  • Thanks, I will take a look. – linbo Dec 04 '15 at 07:31