Due date: Tuesday, Feb 16th, 2010
In this project you will create a UDP-based client and server which simulate a lobby for locating other game players. If you have played any mulitplayer games, the lobby is the server which your game connects to in order to locate other game sessions and players. Typical lobbies also have chat features, allowing players to communicate and set up games with friends they see online.
In this project you will use C++ since, for the most part, only smaller projects are still written purely in C. C++ is useful here because we can still have access to all the nice, fast bit-twiddling features of C while being able to think of the project from an object-oriented perspective.
This project will consist of two pieces: a Server and a Client. The Server will listen on a particular port (port 5555) and clients will send Register and Request messages to this port. The Register messages will be in the form of pretend-games which are being registered with the server. Register messages will include the name of the game, the maximum number of players, the current number of players in the game, and the type of game. The name and type will both be character strings while the number of players (max and current) will be a 16-bit integers.
The Requests will be in the form of lists of those registered games. Once a client has Registered a game with the Server, the server will periodically ping the client to make sure it is still alive. After 20 seconds, if the client is no longer around, the registered game will be removed from its internal list.
To keep us from having to deal with ICMP packets (e.g., for a ping message), we will instead add two messages: a Ping request and a Pong response. The Ping will include a timestamp and the Pong will copy the timestamp so that the Ping'er can calculate how much time has elapsed from when it was sent.
class Serializable { public: virtual void writeObject(Buffer &out) = 0; virtual void readObject(Buffer &in) = 0; };
This abstract class allows any class to inherit from it and designate it as serializable to and from a network stream. Note that each class that inherits from it must define how it is written to or read from the stream and what is written to or read from the stream.
class Buffer { public: void writeInt32(int32_t i); void writeInt16(int16_t i); ... int32_t readInt32(); int16_t readInt16(); ... };
This class is represents a Buffer and its purpose is really to read and write to a memory buffer in a network and host portable way (you will want to use the cbuffer you created from project 1). As such, your conversions from int32_t and such should use the network functions for reading and writing ints.
In addition to these classes for serialization, you will need to define Client and Server classes that set up the networking code and read and write to the sockets using the Buffer class you created.
In terms of messages, you need 6:
The classes need to inherit from Serializable and must have only the following fields written to and from the data stream:
#include <stdint.h> #include <vector> #include <string> // use gettimeofday() to get this time for the Ping class. Note that on a 32-bit system, // you will gettimeofday() will only return an int32_t for the seconds field. Make sure you // byte-order it correctly into a 64-bit integer class PingMsg : public Serializable { private: int64_t mSeconds; int32_t mMicroseconds; }; class PongMsg : public Serializable { int64_t mSeconds; int32_t mMicroseconds; }; class RegisterMsg : public Serializable { private: int16_t mNumPlayers; int16_t mMaxPlayers; std::string mGameName; std::string mGameType; }; class RequestListMsg : public Serializable { private: int16_t mMaxList; }; // note that this is not a message ever sent, but part of the GameListMsg class GameEntry : public Serializable { private: int16_t mNumPlayers; int16_t mMaxPlayers; std::string mGameName; std::string mGameType; PingMsg mPingResult; } class GameListMsg : public Serializable { private: std::vector<GameEntry> mGameEntries; }; class ErrorMsg : public Serializable { private: std::string mErrorMsg; };
The order of the class members is the order of the class data that should be serialized. In addition, you must serialize a uint16_t at the start of each class to represent its type. Here we will just monotonically increase the value starting with PingMsg at 1, and continuing down the list. In other words, PingMsg will be 1, PongMsg will be 2, and so on.
In addition, std::vector and std::string need to be serialized. The 'type' field of these should be 8 and 9 respectively. For the vector, you will first serialize the size of the vector and then serialize each element of it consequetively. For the string you will first serialize its size and then write its bytes into the buffer.
As an example, the data for the PingMsg class would be sent as a series of bytes as follows, for a total of 14 bytes:
<- 32-bits -> +-------------------------+ | type | mSeconds | +------------+ | | | | +------------+ | | mMicroSec | +------------+------------+ | mMicroSec | +------------+
You should begin with the UDP networking code on the Server and Client side. Once you think you have this working, add the Ping and Pong messages. Continue down the list of classes once you are sure those are working.
Working versions of the cbuffer can be found at cbuffer.h and cbuffer.c.
Follow the advice given to the undergraduates. You also have an additional feature you must implement: selective serialization.
In selective serialization, you only write the parts of a class that need to be written to the data stream. In other words, you have to flag variables when they are changed so that if they are sent over the network, only the changes will be sent.
The term flag should be a hint here on how to implement this: You will need to serialize a flag(s) that indicates which variables in a class needs to be updated. The bit number of the flag will let the reader or writer know which member variable should be read. In addition, you will also need to serialize a unique ID. This allows both ends, the server and client, to update the same object. The unique ID should be 32-bits long and should immediately follow the class type.
The dirty flag should immediately follow the unique ID. This allows our flag sizes to be determined apriori since the type indicates how many member variables a class has. For this project, a flag only needs to be 1 byte long, since no message contains more than 8 member variables. In theory you could use fewer bits if your class had fewer member variables, but it would require an overly complicated bit shifting mechanism to write data on non-byte boundaries.