[erlang-questions] OTP compliant gen_tcp client

Vance Shipley <>
Fri Apr 17 04:07:04 CEST 2009


In order to have well behaved OTP compliant applications
we must be sure to process all of the system messages 
received.  Using behaviours such as gen_server and gen_fsm
will ensure that the proper handlers are in place for those
system messages.  Where you will run into trouble however
is when your process is blocking as it does in a selective
receive.  Many library functions will put your process into
a selective receive, and obvious example being gen_server:call/3.

A lot has been written about gen_tcp server side implementation
strategies and I see that there are a lot of subtle issues 
to be considered to get it just right.  I haven't been able to
find anything written on the client side.  I have attached an
example of a client side finite state machine and vite you
to punch holes in it.

The call to gen_tcp:connect/3 may take several minutes to 
return in the case of a time out so calling it in the init/1
function is a very bad idea.  If you create a connecting state
and call gen_tcp:connect/3 from that state handler it solves
the problem of hanging the supervisor however the process will
still not respond to system messages until after it returns.
This breaks lots of OTP goodness.

So the solution would seem to be to spawn a process to wait
on the connection.   Once connected it sets the controlling
process to be the gen_fsm pid and sends the socket back.

-- 
	-Vance
-------------- next part --------------
-module(client_fsm).

-behaviour(gen_fsm).

-export([init/1, handle_event/3, handle_sync_event/4, handle_info/3,
			terminate/3, code_change/4]).
-export([connecting/2, connected/3]).
-export([connect/3]).

-record(statedata, {connecter, socket, address, port}).

-define(RETRY_TIMER, 5000).

init([Address, Port]) ->
	process_flag(trap_exit, true),
	StateData = #statedata{address = Address, port = Port},
	{ok, connecting, StateData, 0}.

connecting(timeout, #statedata{address = Address, port = Port} = StateData) ->
	Pid = proc_lib:spawn_link(?MODULE, connect, [self(), Address, Port]),
	{next_state, connecting, StateData#statedata{connecter = Pid}};
connecting({Connecter, Socket},
		#statedata{connecter = Connecter} = StateData) when is_port(Socket) ->
	{next_state, connected, StateData#statedata{socket = Socket}};
connecting({Connecter, {error, econnrefused}},
		#statedata{connecter = Connecter} = StateData) ->
	io:fwrite("Connection refused by remote host~n"),
	{next_state, connecting, StateData};
connecting({Connecter, {error, etimedout}},
		#statedata{connecter = Connecter} = StateData) ->
	io:fwrite("No response from remote host~n"),
	{next_state, connecting, StateData};
connecting({Connecter, {error, enetunreach}},
		#statedata{connecter = Connecter} = StateData) ->
	io:fwrite("Remote network unreachable~n"),
	{next_state, connecting, StateData};
connecting({Connecter, {error, ehostunreach}},
		#statedata{connecter = Connecter} = StateData) ->
	io:fwrite("Remote host unreachable~n"),
	{next_state, connecting, StateData};
connecting({Connecter, {error, Reason}},
		#statedata{connecter = Connecter} = StateData) ->
	io:fwrite("Connection failed~n"),
	{stop, Reason, StateData}.

connected({tcp_send, Data}, _From, #statedata{socket = Socket} = StateData) ->
	case gen_tcp:send(Socket, Data) of
		ok ->
			{reply, ok, connected, StateData};
		{error, Reason} = Reply ->
			{stop, Reason, Reply, StateData}
	end;
connected(_Event, _From, StateData) ->
	{reply, {error, badarg}, connected, StateData}.

handle_event(_Event, StateName, StateData) ->
	{next_state, StateName, StateData}.

handle_sync_event(_Event, _From, StateName, StateData) ->
	{reply, ok, StateName, StateData}.

handle_info({tcp, Socket, Data}, StateName,
		#statedata{socket = Socket} = StateData) ->
	io:fwrite("Recieved: ~w~n", [Data]),
	{next_state, StateName, StateData};
handle_info({tcp_closed, Socket}, _StateName,
		#statedata{socket = Socket} = StateData) ->
	io:fwrite("Remote end closed connection~n"),
	{stop, normal, StateData};
handle_info({'EXIT', _Connecter, normal}, connecting, StateData) ->
	{next_state, connecting, StateData, ?RETRY_TIMER};
handle_info({'EXIT', _Connecter, normal}, StateName, StateData) ->
	{next_state, StateName, StateData}.

terminate(_Reason, _StateName, _StateData) ->
	io:fwrite("Graceful exit~n").

code_change(_OldVsn, StateName, StateData, _Extra) ->
	{ok, next_state, StateName, StateData}.

connect(Fsm, Address, Port) ->
	case gen_tcp:connect(Address, Port, []) of
		{ok, Socket} ->
			ok = gen_tcp:controlling_process(Socket, Fsm),
			gen_fsm:send_event(Fsm, {self(), Socket});
		Error ->
			gen_fsm:send_event(Fsm, {self(), Error})
	end.



More information about the erlang-questions mailing list