Client-server framework for a multiplayer game (part 1)

Now that I’ve got a working game engine it’s time to add some interesting features to it. One that I’ve always desired to implement is online multiplayer, the ability to play with another player (possibly more) over the net. As it turned out, it’s not so trivial, and there are a lot of different concepts involved in making a network application (network protocols, game objects synchronization and replication, multithreading ecc…). I’ve been put off so many times but now I decided to tackle this intricated but very fun aspect of game development. In this first (in a series, hopefully) post I present a very simple client-server framework, that should be the base of the netcode for my game engine. I used Winsock as the socket API, but the API is very similar (I dare to say, identical, since it’s largely based on it) to  the BSD sockets API, so porting the code to Linux should be a breeze. By the way I suggest reading the great Beej’s guide to network programming if you want to get started with sockets programming (which I don’t cover here, as well as I don’t cover client-server architecture concepts), and this series of videos on making a multiplayer MMORPG (by the way, Javidx9 is a great great teacher and its videos are a wonderful resource for game developers, I warmly suggest you follow him), even if it uses the ASIO library, but the underlying concepts are nonetheless the same (though with less socket hair pulling and less concurrency headaches).

Disclaimer: I know my implementation is quite naive, and maybe it’s not lightning fast, and that there’re lots of ways to improve the design out there, but this is my first attempt at a network framework so please bear with me.

A client-server framework

Here’s a diagram that shows the nitty-gritty details of how the framework works. It might seem quite intricate at first, but don’t worry, I’m gonna go over the inner workings of the framework and show you all the code:

Messsages are sent from clients to the server and from the server to the clients (i know, that’s quite an insight isn’t it? more to come). Networking is a highly asynchronous process, messages are sent and received who knows when, connection fails unexpectedly, so when I hear “asynchronous” I understand “threads”. Messages are stored in (threadsafe) queues, and sockets do the hard work of sending and receiving data over the connection in their own thread. Queues act as buffers between connection endpoints so the application can merrily run while messages are sent and received. As shown in the diagram, the framework is made up of the following classes:

  • A message is represented by the Message class. It’s a class template (as every other class in the framework) and the template type parameter is an enum (or enum class) that specifies the different types of messages that will be defined by the custom application.  A message has a header, which contains the type of the message (an enumerator from the enumeration defined by the application) and the size of the payload. The content of the message itself is stored inside a vector of bytes. The Message class defines operations to serialize and deserialize data into and from it (overloading the insertion/output and extraction/input operators), and some overloads for dealing with strings in particular. The header file also defines an OwnedMessage class template, that derives from Message and is simply a standard message which contains the sender, in the form of a pointer to a connection object (used internally by the server to know who sent a particular message, since messages from all clients are stored into the same queue):

  • The Connection class represents a connection endpoint (the other side of the connection, a connected client if the owner of the connection object is the server, or the server if the connection object is owned by a client). A connection object contains,among other things, a socket, a queue of outgoing messages and a reference to a queue of incoming messages, owned by the owner of the connection and passed to the connection object during construction. When a connection is created, a thread is started that manages the sending and receiving of messages: the thread task (the Connection<T>::Run() member function) checks if there is a message to be sent in the queue, dequeues the message and sends it over the socket connection; it then checks if the socket has available data (the socket is in non-blocking mode), receives the message and enqueues it into the incoming message queue. As the diagram shows, the server mantains a list of connections (all clients currently connected), while a client has only one connection (the server):

  • The Server class is the base abstract class to be derived from by a custom server application (which will provide the implementation for all the pure virtual member functions callbacks). It contains a message queue of incoming messages (a queue of OwnedMessages) and a list of connection objects. It has a constructor that lets the user specify on which port the server is going to run. It has a Start() member function that starts the server and puts it in listening mode, ready to accept connections from clients. Whenever a client connects, the server creates a connection object and stores it in the list of connections (the server continuosly listens for connections in a dedicated thread that is started in the Start() member function). The ProcessMessage() member function gets a message from the incoming queue and calls the OnMessage() member function (overridden by the custom application), that will perform the necessary operations based on the type of message. The Available() method checks if the queue has messages and it should be called before trying to get a message from the queue. The Send() and SendAll() member functions are used respectively to send messages to a specific client or to broadcast a message to all clients:

  • The Client class is the base abstract class to be derived from by a custom client application (which, like the concrete server class, will implement the pure virtual event handlers). Like the Server class, it contains a queue of incoming messages but unlike the server it has a single connection object. A client connects to a server with the Connect() method, and can disconnect from the server using the Disconnect() member function. Similarly to the Server class, a client can send messages with Send(), and process messages with ProcessMessage(), after checking if the queue is not empty with a call to Available():

Both the Client and Server classes internally use a thread to check wheter a connection is closed from the other side (a client disconnecting from server, or a server crashing, for example). This thread waits on a condition variable and is notified by a connection object if the other side disconnects or a network error occurs.

The FIFO queue used to store incoming and outgoing messages is a (quite naive) implementation of a thread-safe queue (since it is manipulated from various threads). It’s not the most efficient implementation (it just synchronizes all operations on a mutex and returns elements by value), but it gets the work done.

Testing the framework: a simple client-server application

A custom application provides the enumeration that defines all the types of messages, and derives from the Server and Client abstract classes to provide implementation for the pure virtual member functions/event handlers in the base classes. The main thread of the example application here just loops infinitely processing messages:

The server application defines the message enumeration and implements the callbacks in the base class. The OnMessage() override handles different types of messages inside a switch statement. The client side of the application is similar, it just spawns a new thread that accepts input from the console, in order to send text from client to server and between clients:

Running the server and a couple of clients on the same machine we can send text messages from one client application to another, with the server routing the messages between clients.

This is it for now, a basic implementation of a network framework that I’m going to use in my game engine. There’s a lot to be improved: for example I don’t think I’ll end up using TCP for my game, maybe I’ll go for UDP with a custom-tailored protocol on top, and maybe I’ll get rid of some threads if I can. Synchronizing the state of the game among clients is another quite fun challenge.

Leave a Reply

Your email address will not be published.