To continue on from the simplest echo server in Erlang vs a Java equivalent (see
here) I decided to upgrade to a basic concurrent server. The idea is basically to have N threads that can each handle doing read/echo on a socket. When idle workers must somehow await the arrival of a new connection. This means we need to either share a listening socket among threads or have a thread that can loop on accept and dole out the resulting connections to handler threads. The former probably fits Erlang better; the latter sounds very likely in an OO implementation.
A little research suggests in Erlang it is safe to have many Erlang processes all calling gen_tcp:accept on a single socket; when a connection comes in one of the processes will unblock from accept, receiving a shiny new socket! This makes life relatively easy: we essentially want a program that starts N processes, each of which runs the exact same logic as the simplest echo server we wrote
earlier. In newb-Erlang this winds up looking like this:
%% blocking echo server, take 2
%% based on http://www.erlang.org/doc/man/gen_tcp.html#examples
-module(echoConcurrent).
-export([server/2]).
%% happy path; yay guards!
%% server will open a listen socket on Port and boot WorkerCount workers to accept/echo on it
server(WorkerCount, Port) when
is_integer(WorkerCount), WorkerCount > 0, WorkerCount < 1000,
is_integer(Port), Port > 0, Port < 65536 + 1 ->
io:format("~p is lord and master; open the listen socket and release the gerbils!~n", [self()]),
case gen_tcp:listen(Port, [{active, false}, {packet, 0}]) of
{ok, ListenSocket} ->
spawnServers(WorkerCount, ListenSocket),
ok;
{error, Reason} -> {error, Reason}
end;
%% badargs to server; show a message and die
server(WorkerCount, Port) ->
io:format("Must provider worker count between 0 and 1000, port between 1 and 65536."),
io:format("WorkerCount='~p', Port='~p' is invalid~n", [WorkerCount, Port]),
{error, badarg}.
%% spawning 0 servers is relatively easy
spawnServers(0, _) -> ok;
%% to spawn Count servers on ListenSocket you just spawn 1 and recurse for Count-1
spawnServers(Count, ListenSocket) ->
spawn(fun() -> acceptEchoLoop(ListenSocket) end),
%% to do this we have to export acceptEchoLoop: spawn(?MODULE, acceptEchoLoop, [ListenSocket]),
spawnServers(Count-1, ListenSocket).
%% The heart of our server: Our core worker function
%% Accept's an incoming connection, blocking until one shows up, then read/echoes
%% until that connection goes away or errors, then ... does it all again!
acceptEchoLoop(ListenSocket) ->
io:format("Gerbil ~p is waiting for someone to talk to~n", [self()]),
{ok, Socket} = gen_tcp:accept(ListenSocket),
%% Show the address of client & the port assigned to our new connection
case inet:peername(Socket) of
{ok, {Address, Port}} ->
io:format("Gerbil ~p will chat with ~p:~p~n", [self(), Address, Port]);
{error, Reason} ->
io:format("peername failed :( reason=~p~n", [Reason])
end,
receiveAndEcho(Socket),
ok = gen_tcp:close(Socket),
acceptEchoLoop(ListenSocket).
%% read/echo raw data from a socket, print it blindly, and echo it back
receiveAndEcho(Socket) ->
%% block waiting for data...
case gen_tcp:recv(Socket, 0, 60 * 1000) of
{ok, Packet} ->
io:format("Gerbil ~p to recv'd ~p; echoing!!~n", [self(), Packet]),
gen_tcp:send(Socket, Packet),
receiveAndEcho(Socket);
{error, Reason} ->
io:format("Sad Gerbil %p: ~p~n", [self(), Reason])
end.
If we start the server on port 8888 and open a couple of telnet localhost 8888 connections each is picked up by a different process in the Erlang server, as can be seen in the console output from our Erlang server:
<0.30.0> is lord and master; open the listen socket and release the gerbils!
Gerbil <0.36.0> is waiting for someone to talk to
Gerbil <0.37.0> is waiting for someone to talk to
Gerbil <0.38.0> is waiting for someone to talk to
Gerbil <0.39.0> is waiting for someone to talk to
Gerbil <0.40.0> is waiting for someone to talk to
Gerbil <0.41.0> is waiting for someone to talk to
Gerbil <0.42.0> is waiting for someone to talk to
Gerbil <0.43.0> is waiting for someone to talk to
Gerbil <0.44.0> is waiting for someone to talk to
Gerbil <0.45.0> is waiting for someone to talk to
ok
2> Gerbil <0.36.0> will chat with {127,0,0,1}:11067
2> Gerbil <0.36.0> to recv'd "h"; echoing!!
2> Gerbil <0.36.0> to recv'd "e"; echoing!!
2> Gerbil <0.36.0> to recv'd "l"; echoing!!
2> Gerbil <0.36.0> to recv'd "l"; echoing!!
2> Gerbil <0.36.0> to recv'd "o"; echoing!!
2> Gerbil <0.36.0> to recv'd " "; echoing!!
2> Gerbil <0.36.0> to recv'd "w"; echoing!!
2> Gerbil <0.36.0> to recv'd "o"; echoing!!
2> Gerbil <0.36.0> to recv'd "r"; echoing!!
2> Gerbil <0.36.0> to recv'd "l"; echoing!!
2> Gerbil <0.36.0> to recv'd "d"; echoing!!
2> Gerbil <0.37.0> will chat with {127,0,0,1}:11068
2> Gerbil <0.37.0> to recv'd "w"; echoing!!
2> Gerbil <0.37.0> to recv'd "a"; echoing!!
2> Gerbil <0.37.0> to recv'd "s"; echoing!!
2> Gerbil <0.37.0> to recv'd "s"; echoing!!
2> Gerbil <0.37.0> to recv'd "z"; echoing!!
2> Gerbil <0.36.0> to recv'd "a"; echoing!!
2> Gerbil <0.37.0> to recv'd "b"; echoing!!
Note that Erlang process <0.30.0> initially ran and booted 10 additional processes, each of which spins in the acceptEchoLoop. When a telnet session connects and sends "hello world" the <0.36.0> process takes the connection. When another telnet session connects the <0.37.0> process picks it up. Thus we can handle many connections concurrently.
Disclaimer: I have used the things I'm claiming to be somewhat complicated "for real" (eg production) but I have not used Erlang in production so the next paragraph is partially speculation.
In "normal" server coding each thread or process started to handle communication with a single client is quite expensive; we thus find ourselves doing complex fiddly multiplexing of many clients onto a single processing thread when possible using I/O completion ports, Java NIO, thread pools, and so on. As Erlang gives us a very cheap process (Erlang process, not OS process) we can afford to spawn a new process for each client and this may lend itself to a simpler and thus less error-prone coding concurrent coding model.