Skip to content

Network architecture design

Ihar Hubchyk edited this page May 10, 2022 · 27 revisions

Introduction

This document contains concepts and ideas of network support architecture design for fheroes2 project. 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 complex part of the project which requires detailed logic design. That is why this document exists. The complexity of network implementation leads to dividing it into separate stages. All data being send through the network is based on big endianess.

Terminology

  • server - a machine running the application.
  • host - application which serves as a listener for the incoming connections.
  • client - application which connects to server through given IP address and port ID.

Stage 1 (completed)

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.

Stage 2 (in progress)

Implement basic handshaking between host and client, and make initial messaging system integration.

  • as any network implementation the smallest item in the messaging system is a message. NetworkMessage is a base class for this purpose. It is responsible for adding message header, message type (overridden by child classes) 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,
        ...
    };
}
  • 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 a network IO 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( ConnectionInfo );

        Message createReplyMessage( ConnectionInfo, clientHandshakeMessage, NetworkEncryption ); // NetworkEncryption is covered below

        ReturnCode verifyRequestMessage() const;

        ReturnCode verifyResponseMessage() const;
    };
}

Handshake messaging works as a filter for invalid or spamming connections. Both handshake messages from client and host are no encrypted. The handshake message from a client includes the following information:

  1. 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 from the host consists of:

  1. Reply code: 0x00 means that the original message from the client is valid. (1 byte)
  2. Public encryption key from the host. For stage 2 it is all zeroes. (16 bytes)
  • encryption setup stage is mandatory for security reasons and to avoid cheating. Encryption is not network IO library dependent code. A pseudo code for this should look like:
namespace fheroes2
{
    class NetworkEncryptionManager
    {
    public:
        PublicKey addClient( ConnectionInfo );

        // generate a response message using host public key. For simplification it can be encrypted `fheroes2` message.
        Message createClientResponse( ConnectionInfo ); 

        bool encrypt( ConnectionInfo );

        bool decrypt( ConnectionInfo );

        void removeConnection( ConnectionInfo );
    };
}
  • connection manager is responsible for all connections. Its primary goal to accept connections, handle incoming data and close connections when necessary. This is the only code dependent on network IO library. A pseudo code for the manager looks like:
namespace fheroes2
{
    class ConnectionManager
    {
    public:
        // By default every application is a client which doesn't accept any incoming connections.
        // This method enables listening and accepting incoming connections if enableHost is True.
        void setHost( const bool enableHost );

        bool connect( IPAddress, PortId ); // establish a connection to a host

        void closeConnection( ConnectionInfo );

        void closeAllConnections();

        bool sendMessage( Message, ConnectionInfo ); // this method must be called only from NetworkMessageHandler.

    private:
        // A callback function to accept or decline an incoming connection from a client.
        void acceptIncomingConnection();

        // A callback function to read incoming data and then generate NetworkMessage object being passed to NetworkMessageHandler.
        void processIncomingData();
    };
}

The above class is not responsible for processing incoming data. The only thing what it does is that it verifies message header (which is fh2) and the 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 &&, ConnectionInfo ); // this method adds a message into a queue which is processed in a separate thread

        void closeConnection( ConnectionInfo ); // should be called only by ConnectionManager to clear all remaining data here

        void addHostInfo( ConnectionInfo ); // used upon connecting to a host

        bool sendMessage( Message, ConnectionInfo );

        // subscribe to process certain type of messages from a particular server. NetworkMessageTypes can't be HANDSHAKE.
        void subscribe( NetworkMessageTypes, ConnectionInfo, NetworkMessageSubscriptionPointer ); // NetworkMessageSubscription is a class

        void unsubscribe( NetworkMessageSubscriptionPointer );

    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<ConnectionInfo>, Message> _messageQueue;

        std::map<ConnectionInfo, State> _connectionInfo;

        NetworkEncryptionManager _encryptionManager;

        // This method should be run in a separate thread to do not block incoming data reading but for simplification
        // for this stage it will be called within addIncomingMessage method.
        void processMessages();
    };
}
  • processing certain (except handshaking) messages are done outside any network related code. NetworkMessageSubscription class serves this purpose and can be overridden:
namespace fheroes2
{
    class NetworkMessageSubscription
    {
    public:
        NetworkMessageSubscription(); // by default constructor does nothing.

        ~NetworkMessageSubscription(); // destructor calls unsubscribe() method of NetworkMessageHandler

        void subscribe( std::set<NetworkMessageType> NetworkMessageTypes, waitTimeInSeconds );

        void wait(); // synchronous waiting for an incoming message

        bool isMessageReceived(); // asynchronous check whether a message is received.

    private:
        std::mutex _event; // the mutex is used for synchronization while waiting for the incoming message.
    };
}

Stage 3

Add human-readable text to be send over the network to be used in a chat before starting a map. This stage is important to test capabilities of the previous stages and verify that messaging system is stable and robust. The main idea is based on separate message type which contains the following information:

  1. Language of the text. Please refer to fheroes2::SupportedLanguage enumeration. 4 bytes
  2. The text itself.

A new class is derived from NetworkMessage:

namespace fheroes2
{
    class NetworkTextMesssage : public NetworkMessage
    {
    public:
        NetworkTextMesssage( SupportedLanguage, Text );

        virtual NetworkMessageType type() const; // returns a new enumeration type
    };
}

Stage 4

Add variable host port instead of using a fixed port to avoid possible clashes with running on user's machine applications.