In this post, I’ll be looking at an example of a simple Erlang program: an echo server. An echo server accepts connections from clients and simply responds to any data received with the same data. While the example given here is quite simple, it is able to deal with multiple simultaneous client connections and handles most errors in a sensible way.

The Module Header

1
2
3
4
5
6
-module(echo).
-export([start/1, handle_connections/1,
    handle_client/1]).

-define(TCP_OPTIONS, [binary, {packet, raw},
    {active, false}, {reuseaddr, true}]).

First off I define the module, which is named echo and so it should go in the file “echo.erl”. Then I export the functions that make up the server. The last line of the header defines a constant called TCP_OPTIONS with a value which is a list of options which will be used by the sockets in the server program. This constant may be used in code later as ?TCP_OPTIONS (note the question mark). The options are as follows:

binary
Data received through the sockets should be in the form of a binary object. There’s no need for me to cover binary terms right now, as they aren’t really covered in this example.
{packet, raw}
The data received should just be the raw data. It is possible to have the data delivered with a header specifying how much data was received, but I don’t want that here.
{active, false}
Data will only be read when requested by the program. It is possible to have the data delivered to a process in the form of a message, but again that isn’t what is wanted here.
{reuseaddr, true}
This makes sure that the listening socket can be bound even if there is already a socket bound there. This prevents errors if the server crashes and is restarted before the original socket becomes properly closed.

Starting the Server

8
9
10
11
12
13
14
start(Port) ->
  case gen_tcp:listen(Port, ?TCP_OPTIONS) of
    {ok, LSock} ->
      spawn(?MODULE, handle_connections, [LSock]);
    {error, Reason} ->
      io:fwrite("Error: ~p~n", [Reason])
  end.

This function starts the server. It takes one argument which is the port to listen for connections on. First it calls the function listen from the gen_tcp library with the port number and the TCP options constant as defined in the header. This attempts to open the listening socket.

If this succeeds, spawn is called to create a new process. The three arguments to this function are the module in which the function to run in this new process resides, the name of the function and a list of arguments to the function. In this case the function is called handle_connections and it is in the current module (?MODULE is a macro which is replaced with the name of the current module). We pass the listening socket opened with gen_tcp:listen to the function.

If opening the listening socket fails, we call io:fwrite to write an error message containing the reason for the error.

Handling Connections

16
17
18
19
20
21
22
23
handle_connections(LSock) ->
  case gen_tcp:accept(LSock) of
    {ok, Sock} ->
      spawn(?MODULE, handle_client, [Sock]);
    {error, Reason} ->
      io:format("Error: ~p~n", [Reason])
  end,
  handle_connections(LSock).

This is the function that is run in a process created by the start function. I takes as an argument a listening socket to listen for connections on. This function calls gen_tcp:accept to accept a connection from the listening socket. Again if this results in an error we print a message. If it is successful, a new process is started to handle the client that connected. This runs the function handle_client in the current module and passes it the client socket as an argument.

Once this function has completed, it calls itself again with the listening socket. As Erlang provides no looping constructs, loops are created by recursive calls. As long as the call to the function is the last line executed in that function, this will not cause stack problems as it would in many languages such as C and its derivatives. This function keeps accepting connections and spawning client handler processes until the process it is running in is killed.

Handling Clients

25
26
27
28
29
30
31
32
33
34
handle_client(Sock) ->
  case gen_tcp:recv(Sock, 0) of
    {ok, Data} ->
      gen_tcp:send(Sock, Data),
      handle_client(Sock);
    {error, closed} ->
      ok;
    {error, Reason} ->
      io:fwrite("Error: ~p~n", [Reason])
  end.

This is the function spawned whenever a client connects to the server. It first attempts to receive some data from the client socket passed to it with the gen_tcp:recv function which takes the socket to receive from and a number of bytes to receive. As we want to receive all available data, we pass 0 which means there is no specified maximum number of bytes to accept.

If successful, the data is sent back to the client (this is what the echo server is designed to do) with the gen_tcp:send function. The function then calls itself to create a loop as with the connection handler. Here the recursive call is not the last line of the function, but as none of the rest of the code in the case statement will be run if the data is successfully received, it is the last line executed. There are therefore again no problems with the call stack overflowing.

If the result {error, closed} is obtained, this means that the client has closed the connection. This is normal behaviour, so no error message is printed, the function simply exits and the process will stop. Unfortunately, the case statement must evaluate to a value, so it is necessary to give a value here. In this case I have chosen ok although any value would do.

The final part of the case statement prints an error message again if there was a problem receiving data from the client. It is assumed that this error is fatal and so the process stops.

The Complete Program

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-module(echo).
-export([start/1, handle_connections/1,
    handle_client/1]).

-define(TCP_OPTIONS, [binary, {packet, raw},
    {active, false}, {reuseaddr, true}]).

start(Port) ->
  case gen_tcp:listen(Port, ?TCP_OPTIONS) of
    {ok, LSock} ->
      spawn(?MODULE, handle_connections, [LSock]);
    {error, Reason} ->
      io:fwrite("Error: ~p~n", [Reason])
  end.

handle_connections(LSock) ->
  case gen_tcp:accept(LSock) of
    {ok, Sock} ->
      spawn(?MODULE, handle_client, [Sock]);
    {error, Reason} ->
      io:format("Error: ~p~n", [Reason])
  end,
  handle_connections(LSock).

handle_client(Sock) ->
  case gen_tcp:recv(Sock, 0) of
    {ok, Data} ->
      gen_tcp:send(Sock, Data),
      handle_client(Sock);
    {error, closed} ->
      ok;
    {error, Reason} ->
      io:fwrite("Error: ~p~n", [Reason])
  end.

Here is the complete code for the program, put this in a file called “echo.erl”. To run it start an Erlang shell in the directory where you have saved the file and run the following two commands to get it started:

c(echo).
echo:start(1234).

This will start the server listening for connections on port 1234. You can then connect to it via telnet or something similar, anything you send to the server should be echoed back to you. There is no way of stopping the server appart from killing all the processes that are handling connections and clients so it’s probably easier to just to close Erlang down.