Wednesday, January 26, 2011

Erlang vs Java: Simple Echo Server

For no particular good reason one of my favorite test programs to familiarize myself with a new programming language is a dead simple (read: improper error handling) TCP/IP server that simply reads bytes from the client and echoes them back. Eg:
  1. listen
  2. accept
  3. Until the socket goes away
    1. recv
    2. send [what we recv'd]
  4. Go to step 2 :)
I'm playing with Erlang lately to expand my coding horizons in the functional direction a little so lets see what this looks like in newbie-Erlang:
%% blocking echo server, take 1

-module(echo).

-export([server/1]).

%% happy path
server(Port) when is_integer(Port), Port > 0, Port < 65536 + 1 ->
  io:format("Initializing echo on port ~w~n", [Port]),
  {ok, ListenSocket} = gen_tcp:listen(Port, [binary, {packet, 0}, {active, false}]),
  listenLoop(ListenSocket);
%% bad server args  
server(Port) -> 
  io:format("Invalid port specification; must be int, 1<=port<=65536. '~p' is invalid~n", [Port]).
  
listenLoop(ListenSocket) ->
  io:format("Blocking on accept...~n"),
  {ok, Socket} = gen_tcp:accept(ListenSocket), %%block waiting for connection
  
  %% Show the address of client & the port assigned to our new connection
  case inet:peername(Socket) of
    {ok, {Address, Port}} ->
      io:format("hello ~p on ~p~n", [Address, Port]);
    {error, Reason} ->
      io:format("peername failed :( ~p~n", [Reason])
  end,
  receiveAndEcho(Socket),
  io:format("Gracefully closing ur socket!~n"),
  ok = gen_tcp:close(Socket),
  listenLoop(ListenSocket).
  
receiveAndEcho(Socket) ->
  %% block waiting for data...
  case gen_tcp:recv(Socket, 0, 60 * 1000) of
    {ok, Packet} ->
      io:format("recv'd ~p!~n", [Packet]),
      gen_tcp:send(Socket, Packet),
      receiveAndEcho(Socket);
    {error, Reason} ->
      io:format("~p~n", [Reason])
  end.
This is about as simple as we can go. Compiled and launched in the Erlang shell similar to c(echo), echo:server(8888). our server will boot up and listen on the port specified, block waiting to accept an incoming connection, echo what it reads from the new socket until something goes awry with the read/echo, then go back to listening. After starting our server we can do exciting things like telnet localhost 8888 and see our characters echo back to us!

This seems trivial but there are actually a couple of cool things about this little snippet.

First cool thing, our server(Port) function uses guards to ensure a valid port (when is_integer(Port), Port > 0, Port < 65536 + 1), opens a listen socket (), and launches our core listening loop.

Second cool thing, our listening loop is done via a recursion:
listenLoop(ListenSocket) ->
  ...stuff...,
  listenLoop(ListenSocket).
At a glance this looks like a stack overflow disaster in waiting: listenLoop doesn't loop, it just calls listenLoop again!! This is actually OK because Erlang is tail-recursive. That is, we are NOT piling up stack frames indefinitely here; if the last call is a recursion it will (roughly) drop the current stack frame before placing a new one on for the new call to listen. That is, server calls listenLoop and the stack is [server][listenLoop]. When listenLoop calls itself it drops (or potentially re-uses; doesn't really matter) the listenLoop frame so the stack is still [server][listenLoop], NOT [server][listenLoop][listenLoop]. Further details here.

Third cool thing, I think the Erlang version reads better than the rough equivalent in Java:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;


public class SimplestEchoServer {
 public static void main(String[] argv) throws Exception {
  int port = parsePort(argv);
  if (0 != port) {
   System.out.printf("Initializing echo on port %s\n", port);
   server(port);
  }    
 }
 
 //that's right, we're pretty much not error handling!
 private static void server(int port) throws Exception {
  ServerSocket listenSocket = new ServerSocket(port);
  for (;;) {
   Socket socket = null;
   try {
    socket = listenSocket.accept();
    InputStream is = socket.getInputStream();
    OutputStream os = socket.getOutputStream();
    int b;
    while ((b = is.read()) != -1) {
     os.write(b);
    }    
    os.flush();
   } catch (Exception e) {
    System.out.println("Sad panda: " + e.getMessage());    
   }
   closeQuietly(socket);
  }
 }

 private static void closeQuietly(Socket socket) { 
  if (null == socket || socket.isClosed()) {
   return;
  }
  try {   
   socket.close();   
  } catch (IOException ioe) {
   System.out.println("Failed to close socket; keep on truckin': " + ioe.getMessage());
  }
 }

 private static int parsePort(String[] argv) {
  int port = 0;
  String rawPort = "";
  if (argv.length == 1) {   
   rawPort = argv[0];
   try {
    port = Integer.parseInt(rawPort);
   } catch (NumberFormatException nfe) {
    port = 0; 
   } 
  }
  if (port < 1 || port > 65536) {
   printArgs(rawPort);     
  }   
  return port;
 }

 private static void printArgs(String arg) {
  System.out.printf("Invalid port specification; must be int, 1<=port<=65536. '%s' is invalid\n", arg);
 }
}

Making it multi-threaded, perhaps less reliant on blocking, and so on would make both versions much uglier.

I'm not sure I want to re-write my Java applications into Erlang anytime soon but I do think it would be splendiferous if Java stole some of Erlangs spiffy features! As someone else said, "I do hope more languages will borrow concepts from Erlang. It's not the end all be all, but it's a damn good step (ref).

No comments:

Post a Comment