Socket Programming: Building an online multiplayer game server

In this article, We are going to explore the power of using sockets by exploiting one of its use cases as backbone for an online multiplayer game

We will walk-through building a game server with

  • socket.io, one of the undoubtedly powerful libraries for building a reliable bidirectional communication channel between clients and server.
  • Redis as a caching layer to keep track of connections and game state, also we will use it to solve some race condition using basic locking with redis.

System Requirements, What to expect

We start off by specifying the functionalities we are expecting the system to have so we have a clear mind while designing our service and not to worry about adding any more details to our interface:

  • We expect the server to handle connecting players together, basically any player who clicks ‘start a random game’ should be mated with other players that also wants to play.
  • We expect the server to handle deliberate or accidental disconnections from players either during starting the game and collecting players or during the game itself.
  • We expect the server to update all clients with other players’ status to be able to show on each player screen how many players are joining the game and who left/joined.
  • We expect to be able to chat during game.

System Design

Ok, that’s enough features to start with, let’s start with a basic design for such service.

Socket Events

As you may know already, sockets are all about events and emitting messages all around. To send a message from server to client, you need to define an event name for the message because the communication here is not sync but rather async. And vice versa, clients need to broadcast an event name for any message they want other clients or the server to listen to.

a bidirectional channel is open between your server and the client

So we will start by choosing a set of events to represent our system:

  • Connect: seems like a straightforward one, surely we need an on connect event so that we register the user info in our system and identify him when he sends new messages, luckily this even is triggered automatically whenever a client connects to our server. This event is from Client to server.
  • Pair Request: this event signals that a client wants to play a new game, so whenever it’s triggered, we should start matching this player with any available room with other players waiting for more ppl to join. This event is from Client to server.
  • Player Joined: once we find a room that needs the player who fired the pair request, we should add him to this waiting list and notify other players through this event that a new member has just joined the room. This event is from Server to client.
  • Player Left: similarly whenever a player disconnects either on purpose or clicks leave in the UI, we need to notify other player that someone left the game so they update their users with that info. This event is from Server to client.
  • Player Leave request/disconnect: from client side we need a way to get notified as a server that someone left the network -builtin disconnect event- or that someone clicked leave and wants to quit the game now, so that we announce to everyone that he left using the Player Left event. This event is from Client to server.
  • Player Ready: this event is to declare that the player is ready to start a game even if max players not available in the room, this is actually useful in games that require more than 2 players to play but they still can start the game at 3 players for example and complement the rest with bots. This event originates from client but should be broadcast-ed to all other clients from the server too.
  • Game Started: The server should fire this event to clients when either max players are available already or enough ready players.
  • Game Ended: This event should be broadcast-ed from the winner client to the server and from the server to all other clients, if no one fired this event then everybody disconnected and game has no winner.

That’s pretty much a comprehensive list for all events we want to listen to on either the server side or client side.

Targeted Events and Rooms in Socket.io

We should consider here a small detail, which is we don’t want those events to be broadcast-ed to everyone all the time! only the group of players waiting in one room should hear each other and no one else should be distracted by their updates.

That’s where Socket.io Rooms come into play, socket.io allows us to define a virtual room for our sockets where broadcasting events there only reaches the clients of those rooms and no one else gets the message, sounds neat, right?

Data Persistence

You may have noticed that I never mentioned a database here, so where all these data for rooms and current games/game chat will be persisted? Let’s take those one by one and discuss them shortly

  • For the rooms and who is playing with who, we can just store those as queues for example in redis, we don’t need to hit the DB every time a client leaves a game or disconnects, a database will make more sense at the end of the game to save the final game results only, any detail before that is hardly necessary to be persisted in the database.
  • For chatting inside the game, there is literally no need to save the chat anywhere other than the client himself, the cache needs not to keep such chat history as it gets all thrown away at the end of the game, it’s only valuable as long as the player is connected and playing the game.

So we will proceed with sockets + redis only to complete this task.

Cache Structure

We need to define a ‘schema like’ definition for how the data is going to be stored and retrieved from our cache, we have few pieces of data we need to keep track of there:

  • user info: a hash containing the user connection id and his username, current state(ready to play or not), room (which room he is in)
  • Game room: this should represent the room where players are starting a game in or already playing, it’s a virtual entity we made up to be able to connect players together in groups to play.

We need to define a structure for this, that is both easy to read and not complicated to write to.

Basic Structure of our Cache

As shown above, we will need to maintain 4 structures to hold our data, we will be taking advantage of Redis Lists in this case because we need random access(sad we won’t use queues).

  • As soon as a player connects, we set his connection id in the cache with his info.
  • When a player requests pairing and cannot find a room(all full or none available), we need to push to the new room name key the player id and push the room itself in the Recruiting rooms key.
  • When a room is full or have enough ready players, we will do a pop from the Recruiting rooms to the Playing rooms list.
  • When a player disconnects, we need to delete the key of his info and remove him from the room list.
  • If a room is available and someone asks for pairing we can simply push his id to that room and check if we have enough players to start the game.
  • When a player hits ready we update his info to make him ready and see if we have the minimum needed ready players to start the game or not.

Race Conditions and locking using redis

Now we have our main flow cleared, let’s look at some race conditions that may result into data inconsistency in our cache.

First thing to look at is the implementation for handling an event like pair request

  • we fetch the player info from the socket and look up available rooms in the cache.
  • if none found we create a new one and broadcast back to the user that he joined room x.

This two step transaction is kind of dangerous, if 2/3/4/5 users were able to pass the first step together, they will get to the second step where they all start creating a new room, that’s bad we will have at least 2 rooms with one player each while we could have created one room for all, we need to make this flow atomic, that means we need to flag this part as critical section that only one at a time can enter

We could make use of a dedicated key to lock/unlock the critical section using setnx command, or we could use a token based approach where we make a queue/list of tokens in redis and pop the token on entering the critical section and re-push when done using blpop and rpush commands.

Second case, we have the same flow but for nearly full rooms:

  • we fetch a room for the player requesting pairing, the room has one spot left.
  • we then insert the player in the room.

This flow will also cause problems as many players could pass the first step and all fire game started event with over capacity players in the room, this will luckily be solved using the same atomic transaction solution we mentioned above.

That was all for designing such service, the actual implementation is available here

Homework

There is still some missing features in the list like chatting and friends private room invites, I’m leaving those for you to try and design/implement yourself to practice, you will find a list of missing features in the repository README, have fun!

Software Engineer