Monday, January 31, 2011

Erlang Concurrent Echo Server

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.

1 comment:

Anonymous said...

Can i achieve 1million connection in this way

Post a Comment