dmathieu - The simplest Erlang TCP server ever

The simplest Erlang TCP server ever

Friday, 7 October 2016 in Development Articles by Damien Mathieu Creative Commons License

For a number of months now, I’ve been wanting to learn some Erlang.
A few weeks ago, I started doing so as a pet project.

There’s a lot of Erlang resources all around. Though strangely enough, it would appear starting a server and listening on connections isn’t trivial at all (it actually is).

Looking around, I’ve found some interesting libraries, or articles.
The best one being probably (and not only for this case, but for all of Erlang), Learn you some Erlang.

I’ve found some issues to all of those solutions though.

  • gen_nb_server doesn’t allow overriding the gen_server state, preventing me from keeping state during a connection.
  • The Erlang Central article uses prim_inet, which is not a public API.
  • Learn you some Erlang is probably the best source there to fully understand everything. But it doesn’t give me a quick and dirty server.

So I’m writing my own.

The architecture

If you’re getting started with Erlang, you might still be struggling a bit with workers and supervisors (they are so awesome though).
In this article, we’re going to setup a supervisor which will listen on the port we need to accept connections for. We will then setup a number of workers, each of them being able to accept one single connection.

If you’re used to programming in Ruby, we’re going to rebuild Puma in under 100 lines of code (yes, this is a huge shortcut).

Each worker will start a new one right before starting to handle it’s request, and kill itself at the end.

The Code

The supervisor

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
-module(hello_tcp_sup).
-behaviour(supervisor).

-export([start_link/0, start_socket/0]).
-export([init/1]).

start_link() ->
  supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
  {ok, ListenSocket} = gen_tcp:listen(5000, [{active,once}]),
  %% We start our pool of empty listeners.
  %% We must do this in another, as it is a blocking process.
  spawn_link(fun empty_listeners/0),
  {ok, { {simple_one_for_one, 60, 3600},
         [
          {hello_tcp_server, {hello_tcp_server, start_link, [ListenSocket]}, temporary, 1000, worker, [hello_tcp_server]}
         ]
       } }.

start_socket() ->
  supervisor:start_child(?MODULE, []).

%% Start with 20 listeners so that many multiple connections can
%% be started at once, without serialization. In best circumstances,
%% a process would keep the count active at all times to insure nothing
%% bad happens over time when processes get killed too much.
empty_listeners() ->
  [start_socket() || _ <- lists:seq(1,20)],
  ok.

This supervisor listens on port 5000, and store the socket that we got in exchange.
It then configures a Simplified one for one worker.

The worker

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
-module(hello_tcp_server).
-behaviour(gen_server).

-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, code_change/3, terminate/2]).

-record(state, {socket}).

start_link(Socket) ->
  gen_server:start_link(?MODULE, Socket, []).

init(Socket) ->
  %% Start accepting requests
  %% We must cast this to the worker's process, as it blocks it.
  gen_server:cast(self(), accept),
  {ok, #state{socket=Socket}}.

handle_cast(accept, State = #state{socket=ListenSocket}) ->
  {ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
  %% Boot a new listener to replace this one.
  hello_tcp_sup:start_socket(),
  send(AcceptSocket, "Hello", []),
  {noreply, State#state{socket=AcceptSocket}};
handle_cast(_, State) ->
  {noreply, State}.


handle_info({tcp, Socket, "quit"++_}, State) ->
  gen_tcp:close(Socket),
  {stop, normal, State};
handle_info({tcp, Socket, Msg}, State) ->
  send(Socket, Msg, []),
  {noreply, State};
handle_info({tcp_closed, _Socket}, State) -> {stop, normal, State};
handle_info({tcp_error, _Socket, _}, State) -> {stop, normal, State};
handle_info(E, State) ->
  io:fwrite("unexpected: ~p~n", [E]),
  {noreply, State}.

handle_call(_E, _From, State) -> {noreply, State}.
terminate(_Reason, _Tab) -> ok.
code_change(_OldVersion, Tab, _Extra) -> {ok, Tab}.

%% Send a message back to the client
send(Socket, Str, Args) ->
  ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)),
  ok = inet:setopts(Socket, [{active, once}]),
  ok.

The worker starts a new gen_server inside which is tells the tcp server it is ready to accept a connection.
From there, the worker will be blocked until it receives that connection.

handle_cast/2 will receive async messages we send to the server. In this example, send only one, accept, which does the actual accepting of connections.
We don’t do this in init/1, as doing so would block the supervisor.

handle_info/2 will receive every message send by the client and send them back to him. Unless they send quit, in which case it just closes the connection.

Omg, is that all?

And that’s all! With this, we have a simple TCP listener.
We just have to start the supervisor with your app.

Inside hello_app.erl:

erlang start(_StartType, _StartArgs) -> hello_sup:start_link().