Rafael Wenzel bio photo

Rafael Wenzel

Modern web expert

Twitter Github Stackoverflow

In this article series that I call Adventures in Erlang I am trying to teach the readers how to properly start an Erlang endeavor on their own.

Please note that this article prerequisites Erlang/OTP 18 as well as Rebar as a package manager, in order to follow on your own system. I further assume that you are running on a Linux operating system. If you are running on anything else, you need to adapt the commands respectively.

I have created a free github project for this article series, that you can find here.

If you want to learn more about the Erlang syntax, and all the stuff we use in these articles, please refer to the rather awesome free online book Learn you some Erlang.

Molding the idea

So after laying a good foundation in the last two articles, it is about time to think about the next steps. Up to now, we could practically go everywhere with our application. We have a working backend, we have a working frontend. Now we can utilize this basis for anything we want. And, for this article series, we are going to learn how to make a game server.

But, before we jump straight into coding, throwing in new modules all willy nilly, we need to hold on a little, and think about an architecture we want to establish in this project. This doesn’t need to be complete, or perfect, just take some time to actually plan out your application.

Here is what I worked out after planing a little for this game.

[Architecture]
  * -> Many servers
  # -> One server (singleton)

  [Modules]
    #[Server] - One server for hosting games
    *[Communication] - Handles communication with one peer
    *[Game] - Handles one game with one map
      -> Needs a NAME for directory to save data into
    *[Map] - Everything map handling
    *[MapGenerator] - Everything for generating a map

Disregard the specifics about the game for now, this is stuff for another article. What’s important here is, that we tried to figure out how we span out our architecture. First we have a Server module, which acts as our game server. This is supposed to be a singleton, as we only make one server (thus ignoring scaling throughout multiple servers for now, this should not be impossible to change later on, if necessary).

The Communication module will handle all the socket communication with the game client, which we will write in another language (also at a later article, obviously not about erlang). And it will be a close companion in the Game module, which will handle one basic game. So there can be multiple Communication objects working with one game, as we want to make a multiplayer game.

We want to ignore the Map and MapGenerator modules for now, as we want to focus on the socket connection in this article.

Creating the server

First we create our new server.erl module, which will be a gen_server. Quick heads up on the rebar command:

$ rebar create template=simplesrv srvid=server

In it, we first establish a new record, just out of convenience, that will describe the state.

-record(state, {games}). % local record -> games: a list of all games

With this, it is easier to know how our state actually looks like.

Now we want to immediately start listening on our predefined port, as soon as we start up our server. So we make a function that handles that, and call it in our init/1 function.

init(_Args) ->
  State = #state{games=[]},
  renew_listener(State).

renew_listener(State) ->
  gen_server:start_link(communication, [self()], []),
  {ok, State}.

We created a function for the job to open a new communication, because we will be needing to call it through a message, everytime we receive a connection through our open socket. We are also sending a reference to our server process to the communication, so we can call to renew this one on a successfull connection.

The calling function for renewing the listener through an outside message looks like this:

handle_call({renew_listener}, _From, State) ->
  case renew_listener(State) of
    {ok, NewState} -> {reply, ok, NewState};
    {stop, Reason} -> {stop, Reason, State}
  end.

Tiny primer on sockets

For our communication with the game server we are chosing the TCP protocol. Why not UDP? Don’t ask me, I want to do TCP first, to see if what we do with the server can run fluently with this protocol. I know, UDP is probably faster, but I also know it is less reliable. I don’t want to deal with unreliability, so let’s just bear with me here and go the proper TCP way alright?

Basically this protocol works over the internet, listening to a virtual number called port and answering connections to this port, establishing a connection with the peer. As soon as a connection is set up, we want to put this connection aside, and start listening on our port again, as we want to support multiple connections to the server, as does a true multiplayer game.

The communication

To start out our communication module, we need to make sure we understand some erlang internals, and how a tcp connection within erlang actually works, because this gave me some initial headaches.

First, we need to be certain that we know which process we are currently residing in. Because, despite what the erlang documentation says in this note here, the connection got closed for me multiple times (I bet the error was in my place, and everything does make sense, but it was a frustrating factor for some time).

Alright, so we want to make sure the owning process that starts listening on our port, is also the one that accepts it, and keeps it for communication. We don’t want the process to close on us, because otherwise the socket will be closed right under us along with the process.

So in the init/1 function of our communication module, we first spawn a process that starts listening:

init([Server]) ->
  State = #state{server=Server},
  spawn_link(?SERVER, listener_loop, [self(), Server]),
  {ok, State}.

Why spawn? Because the init/1 function is being called on calling gen_server:start_link/3, and if this doesn’t properly reply within a reasonable time, the calling process crashes. So we just spawn a new process, as processes in erlang are cheap, pass our own reference to it with self/0 so we can come back to it at any time. Also we save the link to the server, that we passed in our renew_listener/1 function, so we can also call the server, once we correctly established a socket connection, for it to create a new listening process.

Now our listener_loop/2 function looks as follows:

start_listening(Ip, Port) ->
  gen_tcp:listen(Port, [binary, {active, true}, {reuseaddr, true}, {ip, Ip}, {keepalive, true}]).

% Quick side note: the defines for ?IP and ?PORT are residing in a defines.hrl file, which can be found in the source code to this project
listener_loop(From, Server) ->
  case start_listening(?IP, ?PORT) of
    {ok, ListenSocket} -> case gen_tcp:accept(ListenSocket) of
                            {ok, Socket}    -> gen_tcp:controlling_process(Socket, From),
                                               gen_server:call(From, {register_socket, Socket}),
                                               gen_server:call(Server, {renew_listener}),
                                               gen_server:call(Server, {start_game, From});
                            {error, Reason} -> {stop, Reason}
                          end;
    _                  -> gen_server:call(Server, {renew_listener})
  end.

The listener loop receives a link to our communication process, for proper socket ownership handling, and also one to our server process. We start listening on the predefined port (in our example it is 9210) and wait for a connection to it in the gen_tcp:accept/1 function. This is the blocking one, and the reason we spawned a second process to start listening in that, rather than directly listen to it in our init/1 function. As soon as we get a connection-accept, we move the ownership of our retrieved socket to our main process. We then call to register the socket in the state socket list, and also call the server to renew the listener, and finally to start a new game session (for now, because in the future we want to be able to join other games).

Registering our socket looks as follows:

handle_call({register_socket, Socket}, _From, State) ->
  NewState = State#state{socket=Socket},
  {reply, ok, NewState}.

We use the gen_server functionalities to properly handle messaging to our server. In it we take our preexisting state and add the socket parameter to it, so we can always access it in the future. Erlang/OTP has some nifty conveniences that we sure want to use here. And using the gen_server architecture is only one of them. While creating the NewState we just take our state that we receive as a parameter to the function, and as it is a record, only exchange the socket parameter in it, leaving everything else as it is. We then return the NewState and are clear to go.

The game module

Now we can start the heart of our endeavor, the main game module. Remember, we want to be able to have multiple games running at the same time in our server, but also have the ability for multiple peers to join one game (this is just the back thought of building up our game module. We won’t specify how to do that in this article, but rather in one of the next ones).

First we create a record for our state, because it is pretty convenient, as we saw above.

-record(state, {communications, name, map, server}).

We keep our communications module, the name of the game (which should be unique), the map of the game, and also a link to the server.

Now in this server we have a call ready that creates a new game instance:

handle_call({start_game, CommPid}, _From, State) ->
  UniqueName = tools:get_random_string(8),
  case gen_server:start_link(game, [self(), CommPid, UniqueName], []) of
    {ok, Pid} -> NewGame = #gameinfo{name=UniqueName, pid=Pid},
                 NewState = State#state{games=State#state.games ++ [NewGame]},
                 {reply, ok, NewState};
    _         -> {reply, error, State}
  end.

We receive the CommPid which holds our created communication module. We create a little random string for the game with a tool function we created (please see the github code if you want to see this function). Then we create a new game process, and pass all the necessary parameters, like the link to our server (self/0), the communication, and the name. We also add it to the server’s state, where we created a games list. For easier handling we wrote a gameinfo record, that just holds the name and the pid.

And finally in our game module we call the init function:

init([From, Communication, Name]) ->
  io:format("New game started~n", []),
  State = #state{communications=[Communication], name=Name, server=From},
  gen_server:call(Communication, {send_message, "Ready Player One\n"}),
  {ok, State}.

In here, for debugging purposes we create an output in our server, build up our state record, and also call the communication module to send a message to the peer. That is being done in the following function, which resides in our communication module.

handle_call({send_message, Message}, _From, State) ->
  gen_tcp:send(State#state.socket, Message),
  {reply, ok, State}.

We use the gen_tcp module, which belongs to OTP, and send the received message directly to the socket, which conveniently resides in our state record.

Wrapping up

Now we just add the call to our main server module in our adventures_sup module, so it will be called within the application along with everything else we already created:

init([]) ->
    {ok, { {one_for_one, 5, 10}
      , [?CHILD(server, worker), ?CHILD(web_sup, supervisor), ?CHILD(adventures_server, worker)]
    }}.

We just added it to our supervisor calling list, and should be good to go. Let’s compile and run.

$ rebar compile && ./run.sh

Then we are set up, and can try to connect to our server.

$ telnet localhost 9210
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Ready Player One

The connection is being held up, and a new communication module is being created. We can test it out by calling telnet again in another terminal, which should answer in the same manner.

Conclusion

We see, with some initial planning, some proper reading and evaluating of the erlang documentation, we are able to create a decent socket server for communication over the internet. Now we have a good base to create a multiplayer game in erlang.

In the next articles we will be creating some initial game functions, maybe secure the connection, and do whatever is necessary to create a little game. Stay tuned.