-
-
Notifications
You must be signed in to change notification settings - Fork 386
Network architecture design
This document contains concepts and ideas of network support architecture design for Free Heroes of Might and Magic II project (fheroes2). fheroes2 is a game engine for turn-based strategy called Heroes of Might and Magic 2. This game genre is based on a fact that only one player can execute an action at the time. In other words, it is not a real time game which requires instant updates of player statuses.
Network support is an important but at the same time complicated part of the project which requires detailed logic explanation. That is why this document exists. The complexity of network implementation leads to dividing it into separate stages.
Integrate a third party library into fheroes2 project responsible for TCP/IP connection between clients (fheroes2 application) and between a client and server (to be developed separately). The library must be OS-independent and lightweight.
asio library was chosen for this stage. network
branch in the main repository was created with automated setup of the library.
Implement basic handshaking between clients and initial messaging system integration.
- as any network implementation the smallest item in the messaging is a message. NetworkMessage is a base class for any kind of messaging. It is responsible for adding message header, message type and message body size which are not encrypted. The header for each message is
fh2
which is 3 bytes. The message type is 4 bytes. The size of the message body is 4 bytes. The pseudo code is shown below:
namespace fheroes2
{
class NetworkMessage
{
public:
// These values are fixed for every message for networking.
// They are used for network connection manager while reading data to decide where the message ends.
enum
{
HEADER_SIZE = 3,
MESSAGE_SIZE = 4
};
NetworkMessage( Data ); // ideally std::vector<uint8_t>
virtual NetworkMessageType type() const; // returns base network message type
const std::vector<uint8_t> data();
protected:
std::vector<uint8_t> _data;
};
}
Enumeration of Network Messaging types is stored in a separate header:
namespace fheroes2
{
enum NetworkMessageType : uint32_t
{
HANDSHAKE = 0,
...
};
}
All data is stored using big endianess.
- handshaking implementation includes a separate class which is responsible for generation of handshake messages and their validation. Handshake messages are used for verification of connecting applications and subsequent establishment of a secured connection between host and client. The handshake message must not be dependent on network library. The pseudo code of the class should look like this:
namespace fheroes2
{
class NetworkHandshake
{
public:
enum ReturnCode : uint8_t
{
NO_ERROR = 0,
INVALID_MESSAGE = 1,
INCOMPATBILE_VERSIONS = 2,
NOT_A_HOST = 3 // client cannot connect to another application which is not marked as a host
};
Message createRequestMessage( ServerInfo );
Message createReplyMessage( ServerInfo, clientHandshakeMessage, NetworkEncryption ); // NetworkEncryption is covered below
ReturnCode verifyRequestMessage() const;
ReturnCode verifyResponseMessage() const;
};
}
Handshake messaging works as a filter for invalid and spamming connections. Both handshake messages from client and from host are no encrypted. The handshake message from a client must include the following information:
- Version of the game. 1 byte for major version, 1 byte for minor version and 1 byte for intermediate version. For example, 0x00090B corresponds to 0.9.11 version of the game. (3 bytes)
Once the host receives the first handshake message it generates private and public keys. The reply message consists of:
- Reply code: 0x0 means that the original message from the client is valid. (1 byte)
- Public encryption key from the host. For stage 2 it can be all zeroes (16 bytes)
- encryption setup stage is mandatory for security reasons and to avoid cheating. Encryption is not network library dependent code. A pseudo code for this should look like:
namespace fheroes2
{
class NetworkEncryptionManager
{
public:
PublicKey addClient( ServerInfo );
// generate a response message using host public key. For simplification it can be encrypted `fheroes2` message.
Message createClientResponse( ServerInfo );
bool encrypt( ServerInfo );
bool decrypt( ServerInfo );
void removeConnection( ServerInfo );
};
}
- connection manager is responsible for all connections. This primary goal to accept conenction, handle messages and close connections when necessary. A pseudo code for the manager looks like:
namespace fheroes2
{
class ConnectionManager
{
public:
void setHost(); // by default every application is a client which doesn't accept any incoming connections.
bool connect( ServerInfo ); // establish connection to a host
void closeConnection( ServerInfo ); // explicit call in case the player closes an ongoing game
bool sendMessage( Message, ServerInfo ); // this method must be called only from NetworkMessageHandler.
private:
void listenToIncomingConnections(); // a separate thread to listen to all incoming connections
void processMessages(); // a separate thread to read connections from all established connections
};
}
The above class is not responsible for processing incoming connections. The only thing what it does is that it verifies message header (which is `fh2) and size of the message. Once all required data is read it generates NetworkMessage which is later processed by NetworkMessageHandler.
- to process all incoming messages a dedicated message handler must exist. This is a pseudo code for this class:
namespace fheroes2
{
class NetworkMessageHandler
{
public:
void addIncomingMessage( Message &&, ServerInfo ); // this method adds a message into a queue which is processed in a separate thread
void closeConnection( ServerInfo ); // should be called only by ConnectionManager to clear all remaining data here
void addHostInfo( ServerInfo ); // used upon connecting to a host
bool sendMessage( Message, ServerInfo );
private:
enum State
{
AWAITING_CLIENT_HANDSHAKE = 0,
AWAITING_HOST_HANDSHAKE = 1,
AWAITING_CLIENT_PUBLIC_KEY = 2,
AWAITING_HOST_PUBLIC_KEY_RESPONSE = 3,
READY = 4
};
std::queue<std::pair<ServerInfo>, Message> _messageQueue;
std::map<ServerInfo, State> _serverInfo;
NetworkEncryptionManager _encryptionManager;
void processMessages(); // a separate thread to process incoming messages.
};
}