Building A Game Application Server (part 2)

2011-05-26 by Cheetah

data queue

It’s been a busy week! But we’re back at building a game application server. See part 1 of the series if you need to catch up.

So … Let’s start off building a glorified chat server, something you could build a MUD or MUCK off of. Adding tile graphics or whatever you may like is really just an extension of that, and if there’s interest I can show how that’s done as well. By aiming for a lower goal, though, we’ll get a good short-term accomplishment.

As I said in part 1, threads are sort of an optional thing; in some ways they introduce complexity. However, for certain things, my personal opinion is that they make it easier. For instance, network connection handling; doing multi-connection network code the “right” way, and making it 100% cross platform, is really difficult to do in a single thread environment. Especially when you don’t actually control both sides of the connection.

What I mean, specifically, is that in the case of a chat server we will be accepting connections across the Internet which is inherently fickle and occasionally has issues. Even worse, we’ll be dealing with humans at the other end of these connections; humans are even more fickle and have even more issues 🙂 Therefore, connections will drop randomly, routes will be lost, all kinds of “bad” and random things will happen.

Therefore, I prefer isolating all my connection handling in one thread. It means that I can allow that thread to block (which makes net code MUCH easier to write) without locking up my program, and it means that any timing loops I want to implement as part of my game (for things such as NPCs or in-game script handling) will not have to contend with whatever timer I will use on my net code.

The key to making a threaded system is efficient thread communication. And the key to efficient thread communications is a fast data structure that has a very small critical section (the critical section being the place where the threads must block each-other for access, the pivot point of sharing data). For the purpose of our game, the simple and humble queue (also known as a FIFO, First In First Out) will be the best tool for the job.

There are many, many queue implementations out there. Before you use a “canned” solution, you must make sure the queue is thread-safe. For instance, I would be wary of C++’s Standard Template Library (also known as the SGI STL) because different implementations of it may or may not be thread safe (a brief google search tends to indicate most are NOT thread safe). Because queues are such simple things, I find it’s easier to make my own. I wrote a thread-safe queue years ago that I’ve ported from project to project, and it’s very tried and true at this point, so I’m going to just use that.

My queue defines two C-struct data types: COMMAND and QUEUE. COMMAND is the data that goes into a QUEUE. Here’s the struct definitions:

typedef struct __command__ {
        unsigned int    command;        /* Command to queue up  */
        unsigned int    arg;            /* Argument for command */
        unsigned int    size;           /* Size of the data arg */
        void*           data;           /* Data for command     */
        struct __command__* next;       /* For linked list      */
} COMMAND;

typedef struct __queue__ {
        pthread_mutex_t         queue_lock;     /* Mutext for the queue */
        unsigned int            num;            /* Number of commands   */
        COMMAND*                head;           /* Linked list structs  */
        COMMAND*                tail;           /* Linked list structs  */
} QUEUE;

To see the full header file, you can look at this header file. The interesting here is the COMMAND; we will use the “cmd”, “arg”, and “data” portions of the COMMAND to pass things back and forth from our queues. For instance, a player connection will have a certain ‘cmd’ (which will turn out to be a #define called NETCMD_ACCEPT), a certain arg (which will be the user’s socket descriptor), and in that particular case NULL for data because the cmd and arg are sufficiently descriptive. If that user sends us data, it will wind up in a COMMAND with cmd NETCMD_XMIT, arg being the descriptor once again, and data being a pointer to whatever information they sent us.

The header has standard functions for creating a queue and pushing/popping COMMANDs in and out of the queue. The code itself is pretty uninteresting and standard; however, the source can be found here.

Next, we’ll talk about network code. It’ll be a blast… really!

(Diagram from Wikipedia.)