Reputation: 18473
I am implementing simple tcp server with the following sequence:
{ok, LS} = gen_tcp:listen(Port,[{active, true}, {reuseaddr, true}, {mode, list}]),
{ok, Socket} = gen_tcp:accept(LS),
Pid = spawn_link(M, F, [Socket]),
gen_tcp:controlling_process(Socket, Pid)
Using the option {active, true} might cause a race condition where a new packet arrives on the socket process before the "controlling_process" get called , which would result in {tcp,Socket,Data} message arriving to the father proccess instead of the child.
How this could be avoided ?
Upvotes: 10
Views: 1430
Reputation: 71
There absolutely is a race condition. I just encountered it, today, in OTP 21.2, and that's why I'm here. A packet can arrive between the time that accept
returns and the time that inet:tcp_controlling_process
sets the socket to passive.
I just wanted to point out tiny simplification to @Keynslug's answer above. The socket can be set to active from a non-owning process, so the ack
messaging and enter_loop
are unnecessary
-define(TCP_OPTIONS, [binary, {active, false}, ...]).
...
start(Port) ->
{ok, Socket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept(Socket).
accept(ListenSocket) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
Pid = spawn(fun() ->
io:format("Connection accepted ~n", []),
loop(Socket)
end),
gen_tcp:controlling_process(Socket, Pid),
inet:setopts(Socket, [{active, once}]),
accept(ListenSocket);
Error ->
exit(Error)
end.
loop(Sock) ->
%% set soscket options to receive messages directly into itself
inet:setopts(Sock, [{active, once}]),
receive
{tcp, Socket, Data} ->
io:format("Got packet: ~p~n", [Data]),
...,
loop(Socket);
{tcp_closed, Socket} ->
io:format("Socket ~p closed~n", [Socket]);
{tcp_error, Socket, Reason} ->
io:format("Error on socket ~p reason: ~p~n", [Socket, Reason])
end.
Upvotes: 2
Reputation: 377
If the socket is active, inet:tcp_controlling_process
(called by gen_tcp:controlling_process
) sets the socket to passive, then selectively receives all messages related to that socket and sends them to the new owner, effectively moving them to the new owner's message queue. Then it restores the socket to active.
So there's no race condition: they have already thought of that and fixed it in the library.
Upvotes: 3
Reputation: 2766
You are right. In such cases you surely need {active, false}
passed among listening socket options. Consider this snippet of code:
-define(TCP_OPTIONS, [binary, {active, false}, ...]).
...
start(Port) ->
{ok, Socket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept(Socket).
accept(ListenSocket) ->
case gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
Pid = spawn(fun() ->
io:format("Connection accepted ~n", []),
enter_loop(Socket)
end),
gen_tcp:controlling_process(Socket, Pid),
Pid ! ack,
accept(ListenSocket);
Error ->
exit(Error)
end.
enter_loop(Sock) ->
%% make sure to acknowledge owner rights transmission finished
receive ack -> ok end,
loop(Sock).
loop(Sock) ->
%% set soscket options to receive messages directly into itself
inet:setopts(Sock, [{active, once}]),
receive
{tcp, Socket, Data} ->
io:format("Got packet: ~p~n", [Data]),
...,
loop(Socket);
{tcp_closed, Socket} ->
io:format("Socket ~p closed~n", [Socket]);
{tcp_error, Socket, Reason} ->
io:format("Error on socket ~p reason: ~p~n", [Socket, Reason])
end.
Thus you will not lost anything until controlling_process
succeeds. It is known problem been discussed a lot over internets.
If you wish to use ready to go solution you surely need to take a look at Ranch project.
Upvotes: 17