Aller au contenu

Sérialisation

Pattern de sérialisation binaire du protocole R-Type.

Synopsis

#include "Protocol.hpp"

// Écriture
PlayerState state{1, 100, 200, 100, 1, 42, 1};
uint8_t buffer[PlayerState::WIRE_SIZE];
state.to_bytes(buffer);

// Lecture
auto parsed = PlayerState::from_bytes(buffer, sizeof(buffer));
if (parsed) {
    // Utiliser *parsed
}

Pattern de Sérialisation

Chaque structure du protocole implémente deux méthodes statiques:

struct ExampleMessage {
    uint16_t value;
    static constexpr size_t WIRE_SIZE = 2;

    void to_bytes(uint8_t* buf) const {
        uint16_t net_value = swap16(value);  // Host → Network (big-endian)
        std::memcpy(buf, &net_value, 2);
    }

    static std::optional<ExampleMessage> from_bytes(const void* buf, size_t len) {
        if (buf == nullptr || len < WIRE_SIZE) return std::nullopt;

        uint16_t net_value;
        std::memcpy(&net_value, buf, 2);

        ExampleMessage msg;
        msg.value = swap16(net_value);  // Network → Host
        return msg;
    }
};

Byte Order

Le protocole utilise big-endian (network byte order) pour tous les types multi-octets.

Fonctions de Conversion

// Protocol.hpp
inline uint16_t swap16(uint16_t v) { return __builtin_bswap16(v); }
inline uint32_t swap32(uint32_t v) { return __builtin_bswap32(v); }
inline uint64_t swap64(uint64_t v) { return __builtin_bswap64(v); }

Types et Tailles

Type Taille Endianness
uint8_t 1 byte -
int8_t 1 byte -
uint16_t 2 bytes Big-endian
int16_t 2 bytes Big-endian
uint32_t 4 bytes Big-endian
uint64_t 8 bytes Big-endian
float 4 bytes IEEE 754
bool 1 byte 0/1

Exemples de Structures

PlayerState (9 bytes)

struct PlayerState {
    uint8_t id;
    uint16_t x, y;
    uint8_t health;
    uint8_t alive;
    uint16_t lastAckedInputSeq;
    uint8_t shipSkin;

    static constexpr size_t WIRE_SIZE = 9;

    void to_bytes(uint8_t* buf) const {
        buf[0] = id;
        uint16_t net_x = swap16(x);
        uint16_t net_y = swap16(y);
        std::memcpy(buf + 1, &net_x, 2);
        std::memcpy(buf + 3, &net_y, 2);
        buf[5] = health;
        buf[6] = alive;
        uint16_t net_seq = swap16(lastAckedInputSeq);
        std::memcpy(buf + 7, &net_seq, 2);
        // shipSkin may be in extended format
    }

    static std::optional<PlayerState> from_bytes(const void* buf, size_t len) {
        if (buf == nullptr || len < WIRE_SIZE) return std::nullopt;
        auto* ptr = static_cast<const uint8_t*>(buf);

        PlayerState state;
        state.id = ptr[0];

        uint16_t net_x, net_y;
        std::memcpy(&net_x, ptr + 1, 2);
        std::memcpy(&net_y, ptr + 3, 2);
        state.x = swap16(net_x);
        state.y = swap16(net_y);

        state.health = ptr[5];
        state.alive = ptr[6];

        uint16_t net_seq;
        std::memcpy(&net_seq, ptr + 7, 2);
        state.lastAckedInputSeq = swap16(net_seq);

        return state;
    }
};

UDPHeader (12 bytes)

struct UDPHeader {
    uint16_t type;
    uint16_t sequence_num;
    uint64_t timestamp;

    static constexpr size_t WIRE_SIZE = 12;

    void to_bytes(void* buf) const {
        auto* ptr = static_cast<uint8_t*>(buf);
        uint16_t net_type = swap16(type);
        uint16_t net_seq = swap16(sequence_num);
        uint64_t net_ts = swap64(timestamp);

        std::memcpy(ptr, &net_type, 2);
        std::memcpy(ptr + 2, &net_seq, 2);
        std::memcpy(ptr + 4, &net_ts, 8);
    }

    static std::optional<UDPHeader> from_bytes(const void* buf, size_t len) {
        if (buf == nullptr || len < WIRE_SIZE) return std::nullopt;
        auto* ptr = static_cast<const uint8_t*>(buf);

        UDPHeader header;
        uint16_t net_type, net_seq;
        uint64_t net_ts;

        std::memcpy(&net_type, ptr, 2);
        std::memcpy(&net_seq, ptr + 2, 2);
        std::memcpy(&net_ts, ptr + 4, 8);

        header.type = swap16(net_type);
        header.sequence_num = swap16(net_seq);
        header.timestamp = swap64(net_ts);

        return header;
    }
};

Fixed-Size Strings

Pour les chaînes de taille fixe (room codes, noms):

// Écriture
char roomCode[ROOM_CODE_LEN] = "ABC123";
std::memcpy(buf + offset, roomCode, ROOM_CODE_LEN);

// Lecture
char roomCode[ROOM_CODE_LEN];
std::memcpy(roomCode, ptr + offset, ROOM_CODE_LEN);

Validation

Toujours valider la taille du buffer avant de parser:

static std::optional<T> from_bytes(const void* buf, size_t len) {
    // Validation obligatoire
    if (buf == nullptr || len < WIRE_SIZE) {
        return std::nullopt;
    }
    // Parsing...
}

Diagramme de Flux

flowchart LR
    subgraph Envoi
        A[Struct C++] --> B[to_bytes]
        B --> C[Buffer binaire]
        C --> D[swap pour big-endian]
        D --> E[Envoi réseau]
    end

    subgraph Réception
        F[Réception réseau] --> G[Buffer binaire]
        G --> H[from_bytes]
        H --> I[swap pour host order]
        I --> J[Struct C++]
    end

Constantes de Taille

Structure WIRE_SIZE
UDPHeader 12
SessionToken 32
PlayerState 9
MissileState 7
EnemyState 8
JoinGame 39
VoiceFrame (header) 5

Bonnes Pratiques

// 1. Toujours utiliser memcpy pour éviter les problèmes d'alignement
uint16_t value;
std::memcpy(&value, ptr, 2);  // OK
// uint16_t value = *reinterpret_cast<uint16_t*>(ptr);  // Dangereux!

// 2. Retourner std::optional pour gérer les erreurs de parsing
auto result = Message::from_bytes(buf, len);
if (!result) {
    // Erreur de parsing
    return;
}
auto& msg = *result;

// 3. Utiliser les constantes WIRE_SIZE
uint8_t buffer[PlayerState::WIRE_SIZE];
state.to_bytes(buffer);