Building a Game Server in C++ #2: Let there be Code
In the previous article of this series, I brainstormed about what my game server should look like. In this one, I will try to put all (or most) of that into code. The goal for this phase is to make a very basic version that:
- listens and accepts TCP connections
- spawns client threads that echo back received prompts
- maintains a basic client data list
Warming up
That said, let’s start with main():
int main(int argc,char *argv[]) {
return Server(getParams(argc,argv)).run();
}
Pretty straightforward: We create a server object with the appropriate parameters, then we return the result of its run() method. The getParams() function is supposed to do all the heavy lifting regarding the runtime parameters, but that’s an issue for another day, so for now let’s get it out of the way real quick:
struct serverParams {
int port;
string dbHost,dbName,dbUser,dbPassword;
};
serverParams getParams(int argc,char *argv[]) {
serverParams params={
9999,
"localhost","gamesrv","user","opensessame",
};
return params;
}
Now let’s concentrate on the Server object. From what we know so far, its class definition should look something like this:
class Server: public SessionInterface, public GameInterface {
public:
Server(serverParams);
int run();
};
Leaving aside the two interfaces for now, the constructor should initialize the whole context, using those serverParams. And by context, I mean the database backend, the logger, the TCP socket and the UDP one:
#define VERSION (string) "0.1.0"
Server::Server(serverParams p) {
log=new Logger();
log->info("Game Server v"+VERSION);
storage=new Storage(p.dbHost,p.dbName,p.dbUser,p.dbPassword);
sockaddr_in addr{};
addr.sin_family=AF_INET;
addr.sin_addr.s_addr=INADDR_ANY;
addr.sin_port=htons(p.port);
tcp=sockBind(SOCK_STREAM,addr);
udp=sockBind(SOCK_DGRAM,addr);
listen(tcp,5);
log->info("Server listening at port "+to_string(p.port));
}
The creation and binding procedure for both sockets is pretty similar, so I decided to make a sockBind() method for it:
int Server::sockBind(int protocol,sockaddr_in address) {
int sock=socket(AF_INET,protocol,0);
int opt=1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
bind(sock,(sockaddr*)&address,sizeof(address));
return sock;
}
The rest of the missing class members are log, storage, tcp and udp, so let’s add them to the class definition:
class Server: public SessionInterface, public GameInterface {
private:
Storage *storage;
Logger *log;
int tcp,udp,sockBind(int,sockaddr_in);
public:
Server(serverParams);
int run();
};
Of course, we now have to define the two backend classes. I don’t want to deal with the storage backend right now:
class Storage {
public:
Storage(string,string,string,string);
};
Storage::Storage(string h,string d,string u,string p) { }
As for the logger, I want to output messages in three different ways, depending on the severity level — info, warning or error. However, all three ways will use the same underlying mechanism, which in our case translates to output either to stdout or stderr:
class Logger {
private:
void message(string,ostream&);
public:
Logger();
void info(string);
void warning(string);
void error(string);
};
void Logger::message(string msg,ostream& s=cout) {
auto t=time(nullptr);
stringstream ss;
ss<<"["<<put_time(localtime(&t),"%Y-%m-%d %H:%M:%S")<<"] "<<msg<<endl;
s<<ss.str();
}
Logger::Logger() { }
void Logger::info(string msg) { message(msg); }
void Logger::warning(string msg) { message("\033[33m[WRN] "+msg+"\033[0m",cerr); }
void Logger::error(string msg) { message("\033[31;1m[ERR] "+msg+"\033[0m",cerr); }
And that’s that for the constructor and the whole initialization fuss.
Running the server
Now we finally get to deal with the run() method, where all the juicy stuff happens:
int Server::run() {
fd_set fds;
int max_fd=max(tcp,udp)+1;
while(!shutdown) {
FD_ZERO(&fds);
FD_SET(tcp,&fds);
FD_SET(udp,&fds);
int activity=select(max_fd,&fds,nullptr,nullptr,nullptr);
if(activity<0) continue;
// Handle new TCP client
if(FD_ISSET(tcp,&fds)) handleTCP();
// handle UDP communication
if(FD_ISSET(udp,&fds)) handleUDP();
}
return 0;
}
So here we have a loop that runs until shutdown turns to true, which is a new private field of our Server class. Within this loop, we monitor the two sockets for new activity using select(). Incoming activity is handled using the two new methods, handleTCP() and handleUDP(), depending on the socket. I want to start with the latter, since it’s the most straightforward at this point:
void Server::handleUDP() {
char buffer[1024];
sockaddr_in client_addr{};
socklen_t len=sizeof(client_addr);
ssize_t bytes=recvfrom(udp,buffer,sizeof(buffer)-1,0,(sockaddr*)&client_addr,&len);
if(bytes<=0) return;
buffer[bytes]='\0';
string msg(buffer);
log->info("[UDP] "+msg);
}
The only thing that this method does for now is receive packets from the socket into a buffer and logs them as strings. In the future, this is the place to implement TCP/UDP handshaking and the routing of incoming UDP packets to games.
Before we move on to handleTCP(), I want to define the client data list. From the assumptions I made in the previous article, two fields that need to be associated with the session id are the corresponding UDP client address and the client’s username:
struct sessionEntry {
sockaddr_in udpAddr{};
string username;
};
Also, assuming that (1) sessions is a map of session ids to session entries, and (2) sessionMutex is a recursive mutex protecting the list, I do want to define some more methods relevant to the client data list, albeit they are not used in this context:
string Server::getSessionUsername(int id) {
lock_guard<recursive_mutex> lock(sessionMutex);
if(sessions[id]) return sessions[id]->username;
return "";
}
sockaddr_in Server::getSessionUDPAddr(int id) {
lock_guard<recursive_mutex> lock(sessionMutex);
if(sessions[id]) return sessions[id]->udpAddr;
return sockaddr_in{};
}
Having covered those, we are now ready to implement TCP handling. Whenever a new connection is made, we have to accept it, produce its id, add an entry to the client data list and spawn a thread to handle further communication:
void Server::handleTCP() {
int conn=accept(tcp,nullptr,nullptr);
if(conn>=0) {
lock_guard<recursive_mutex> lock(sessionMutex);
int id=nextId++;
sessions[id]=new sessionEntry;
thread(sessionThread,conn,id,this).detach();
log->info("[TCP] Client "+to_string(id)+" connected");
}
}
In the above piece of code, nextId is a field initialized to 1. Other than that, the only thing missing is the thread function.
Session threads and the SessionInterface
At the beginning of the article we required the Server class to implement two interfaces, one of which is about the communication with game instances, so we will leave it empty for now:
class GameInterface { };
The other one will be used by the session threads. It should define two methods for the threads to call. Let’s define it, along with the thread function:
struct cmdResult {
bool disconnect=false;
string msg;
};
class SessionInterface {
public:
virtual cmdResult clientCommand(int,string)=0;
virtual void clientDisconnect(int)=0;
};
void sessionThread(int connection,int id,SessionInterface *si) {
string welcome="id "+to_string(id)+"\n";
send(connection,welcome.c_str(),welcome.size(),0);
string input;
char buffer[1024];
bool disconnect=false;
ssize_t bytes;
while(!disconnect&&(bytes=read(connection,buffer,sizeof(buffer)-1))) {
buffer[bytes]='\0';
input+=string(buffer);
size_t pos;
string delimiter="\n";
while((pos=input.find(delimiter))!=string::npos) {
string command=input.substr(0,pos);
input.erase(0,pos+delimiter.length());
if(!command.length()) continue;
cmdResult reply=si->clientCommand(id,command);
send(connection,reply.msg.c_str(),reply.msg.size(),0);
if(disconnect=reply.disconnect) break;
}
}
close(connection);
si->clientDisconnect(id);
}
Obviously, the thread function needs the newly accepted connection and the session id, but it also needs a pointer to the Server object, through SessionInterface, in order to communicate with it. Its first job is to send the session id to the client using a recognizable packet (id {id}). After that, it enters a loop until the client disconnects, in which case it calls the following clientDisconnect() method before terminating:
void Server::clientDisconnect(int id) {
lock_guard<recursive_mutex> lock(sessionMutex);
delete sessions[id];
sessions.erase(id);
log->info("[TCP] Client "+to_string(id)+" disconnected.");
}
Now back to the thread’s loop, incoming packets are aggregated in the input string. Although it is a bit early to fully define the application protocol, I know for sure that it will be text-based and every prompt must end with a newline character. The client should be able to send fragmented packets as well as multiple prompts at once. The thread’s job is to assemble them, parse for newlines and eliminate zero-length prompts. Then, the individual prompts are processed by the server, using the clientCommand() function, which for now will merely echo them back to the client:
cmdResult Server::clientCommand(int id,string command) {
log->info("[TCP] ("+to_string(id)+"): "+command);
return {false,"ECHO "+command+"\n"};
}
The above method returns a cmdResult, which contains a bool along with the reply message to be sent. This bool field indicates whether or not the last command should cause a client disconnect.
That was the last piece of the puzzle, so let’s see what the Server class definition would finally look like:
class Server: public SessionInterface, public GameInterface {
private:
Storage *storage;
Logger *log;
int tcp,udp,sockBind(int,sockaddr_in);
void handleTCP(),handleUDP();
bool shutdown=false;
int nextId=1;
map<int,sessionEntry*> sessions;
recursive_mutex sessionMutex;
string getSessionUsername(int);
sockaddr_in getSessionUDPAddr(int);
public:
Server(serverParams);
void clientDisconnect(int);
cmdResult clientCommand(int,string);
int run();
};
Testing
After compiling and running the server, I ran nc from another terminal:
$ nc localhost 9999 id 1 test ECHO test Hello World! ECHO Hello World! ^C
As for the UDP functionality, I tested it in a similar way from yet another terminal:
$ nc -u localhost 9999 Hello from UDP ^C
And here are the results from the server’s terminal:
[2025-11-04 01:01:30] Game Server v0.1.0 [2025-11-04 01:01:30] Server listening at port 9999 [2025-11-04 01:01:36] [TCP] Client 1 connected [2025-11-04 01:01:41] [TCP] (1): test [2025-11-04 01:01:50] [TCP] (1): Hello World! [2025-11-04 01:02:11] [UDP] Hello from UDP
Well, mission accomplished, but our game server is still …gameless. To fix that, in the next part we will implement a simple Rock Paper Scissors game.
