aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Metin2

Structure des paquets Metin2 : communication client-serveur

Quand un personnage se déplace, qu'un objet est ramassé ou qu'un message de chat est envoyé, tout ce qui circule entre le client et le serveur est un metin2 packet. Le protocole réseau de Metin2 repose sur des messages binaires envoyés via TCP, où le tout premier octet identifie le type du paquet. Dans cet article, je détaille comment les paquets sont structurés, ce que signifie la nomenclature des en-têtes, la différence entre paquets fixes et dynamiques, et comment le serveur décode les octets entrants pour les router vers le bon gestionnaire.

À quoi ressemble un paquet : l'octet d'en-tête

Dans le protocole Metin2, chaque paquet commence par un unique octet d'en-tête. Comme il s'agit d'un BYTE, il prend une valeur de 0 à 255 et identifie le type de paquet. Lorsque le serveur ou le client lit le premier octet du flux, il sait de quel paquet il s'agit et donc combien d'octets supplémentaires il doit lire.

Les paquets sont définis comme des struct C++ et compactés avec #pragma pack(1) afin qu'il n'y ait aucun remplissage (padding) en mémoire. C'est essentiel : si le compilateur applique son alignement par défaut, le client et le serveur voient des dispositions d'octets différentes et le protocole se brise.

#pragma pack(1)

// Client -> Game : paquet de déplacement de base (exemple)
typedef struct command_move
{
    BYTE    bHeader;     // type de paquet
    BYTE    bFunc;       // type de déplacement (marche/course/arrêt)
    BYTE    bArg;
    BYTE    bRot;        // rotation
    long    lX;          // coordonnée cible
    long    lY;
    DWORD   dwTime;      // horodatage client
} TPacketCGMove;

Ici, le préfixe CG indique la direction du paquet, sujet que nous abordons juste après. Le point clé est que l'en-tête détermine à lui seul la disposition du reste. Dès que le serveur lit bHeader, il lit exactement sizeof(TPacketCGMove) octets et les copie directement dans la structure.

CG, GC et la nomenclature inter-processus des en-têtes

Metin2 est composé de plusieurs processus (auth, db, game/core), et les noms de paquets encodent leur direction avec deux lettres. Cette convention est le moyen le plus rapide de s'orienter dans le code source :

  • CG — Client to Game : du client vers le cœur du jeu (déplacement, attaque, chat, utilisation d'objet).
  • GC — Game to Client : du cœur du jeu vers le client (ajout de personnage, mise à jour des PV, diffusion du chat).
  • GD / DG — communication entre le jeu et le processus DB (chargement et sauvegarde des joueurs).
  • GG — Game to Game : messagerie pair-à-pair entre les cores (canaux).

Ainsi, HEADER_CG_ATTACK est le paquet d'attaque envoyé par le client, tandis que HEADER_GC_CHARACTER_ADD est le paquet que le serveur envoie au client pour placer un nouveau personnage à l'écran. Les constantes d'en-tête sont généralement regroupées dans un enum :

enum
{
    HEADER_CG_HANDSHAKE       = 253,
    HEADER_CG_LOGIN           = 1,
    HEADER_CG_ATTACK          = 2,
    HEADER_CG_MOVE            = 3,
    HEADER_CG_CHAT            = 4,
    // ...
};

Les valeurs numériques varient selon la version et la source et peuvent différer d'une base de serveur privé à l'autre. Ce qui compte n'est pas le chiffre lui-même, mais que le client et le serveur partagent la même table d'en-têtes. Si les deux côtés utilisent des valeurs différentes, les paquets sont mal interprétés.

Paquets de taille fixe et dynamique

La grande majorité des paquets sont de taille fixe : une fois l'en-tête lu, la taille de la structure est déjà connue. Les paquets de déplacement, d'attaque et de mise à jour des PV appartiennent à ce groupe. Mais un message de chat, des noms d'objets ou un paquet transportant une liste de longueur variable ne tiennent pas dans une taille prédéterminée. Ce sont des paquets dynamiques et ils portent un champ WORD size juste après l'en-tête :

#pragma pack(1)

// Client -> Game : paquet de chat (dynamique)
typedef struct command_chat
{
    BYTE    bHeader;     // HEADER_CG_CHAT
    WORD    wSize;       // longueur totale de tout le paquet
    BYTE    bType;       // normal / groupe / guilde ...
    // suivi des octets de texte jusqu'à wSize
} TPacketCGChat;

Le serveur lit d'abord l'en-tête, puis le champ wSize pour connaître la longueur totale du paquet. Le texte du message se calcule en soustrayant la partie fixe de la structure à cette taille. Les paquets dynamiques se lisent donc en deux temps : d'abord l'en-tête fixe, ensuite le corps jusqu'à wSize.

Flux de connexion : du handshake au jeu

Le client ne commence pas à envoyer des paquets immédiatement. La connexion traverse plusieurs phases, et dans chacune seuls les paquets propres à cette phase sont acceptés. Un flux typique ressemble à ceci :

  • Handshake : après l'établissement de la connexion TCP, le serveur envoie un paquet de handshake. Le client et le serveur échangent des horodatages dans les deux sens à plusieurs reprises pour mesurer la latence réseau et synchroniser leurs horloges.
  • Login / Auth : une fois la synchronisation temporelle suffisamment précise, le client envoie le paquet de connexion (avec sa clé de session).
  • Select : la liste des personnages est envoyée et le joueur choisit son personnage.
  • Loading : la carte et les données du personnage sont chargées ; le client signale qu'il est prêt.
  • Game : la phase de jeu proprement dite. Les paquets de déplacement, de combat et de chat circulent désormais librement.

La phase de handshake est particulièrement importante : comme Metin2 valide les déplacements et les attaques par rapport aux horodatages, les horloges du client et du serveur doivent être proches. Les paquets de handshake sont répétés jusqu'à ce que la mesure d'aller-retour passe sous un seuil acceptable.

Comment le serveur lit-il un paquet ?

Côté serveur, chaque connexion dispose d'un tampon et d'un processeur d'entrée correspondant à sa phase actuelle. Comme TCP est orienté flux, les octets arrivent par fragments ; le serveur ne doit jamais traiter un paquet incomplet. La boucle d'analyse est à peu près la suivante :

  • Les octets reçus du socket sont ajoutés au tampon de la connexion.
  • Si le tampon contient au moins 1 octet, l'en-tête est lu (sans encore le consommer).
  • L'en-tête détermine le type de paquet et sa taille attendue. S'il est dynamique, wSize est également lu.
  • Si le tampon ne contient pas encore le paquet entier, la boucle s'arrête et attend d'autres octets.
  • Une fois le paquet complet présent, il est confié à son gestionnaire et ces octets sont consommés.
// Cœur de la boucle d'analyse (pseudocode)
while (buffer.size() >= 1)
{
    BYTE header = buffer.peek_byte(0);
    int  packetSize = GetPacketSize(header);   // depuis wSize si dynamique

    if (buffer.size() < packetSize)
        break;                                 // paquet pas encore complet

    Dispatch(header, buffer.read(packetSize)); // router vers le gestionnaire
}

Si l'en-tête est une valeur inconnue (absente de la table d'en-têtes), c'est généralement considéré comme une violation de protocole et la connexion est fermée ; dans de nombreuses bases de serveurs privés, c'est une ligne de défense supplémentaire pour détecter la triche ou la manipulation.

Chiffrement et note de sécurité

Dans les toutes premières versions de Metin2, les paquets étaient envoyés en clair (sans chiffrement). Les versions ultérieures et les serveurs privés actuels effectuent un échange de clés pendant le handshake pour chiffrer le corps du paquet. Cela rend plus difficiles l'écoute de paquets et l'injection de faux paquets. Malgré tout, le serveur ne doit jamais faire confiance au client : chaque champ d'un paquet entrant (coordonnées, dégâts, quantité) doit être validé côté serveur. Accepter aveuglément une valeur contrôlée par le client est la cause première d'exploits comme le speed hack, la téléportation et la duplication d'objets (dupe).

Questions fréquentes

Les paquets Metin2 utilisent-ils TCP ou UDP ?

Le trafic de jeu passe par TCP. Tous les paquets, y compris le déplacement et le combat, exigent une livraison fiable et ordonnée, d'où l'usage de TCP orienté flux ; c'est pourquoi l'accumulation de paquets partiels dans le tampon serveur est normale et doit être correctement gérée.

Pourquoi l'en-tête tient-il sur un seul octet, et est-ce suffisant ?

Un seul BYTE adresse 256 types de paquets distincts, ce qui est largement suffisant pour le protocole de jeu de base. Quand davantage de sous-types sont nécessaires, on place un champ supplémentaire bSubHeader ou bType dans le paquet, ce qui préserve l'octet d'en-tête unique tout en étendant les types.

À quoi faire attention en ajoutant mon propre paquet ?

Définissez la constante d'en-tête avec la même valeur côté client et côté serveur, compactez la structure avec #pragma pack(1), décidez si vous avez besoin d'un wSize selon le caractère fixe ou dynamique, et validez toujours chaque champ entrant dans le gestionnaire serveur.

Vous souhaitez créer un système de paquets personnalisé sur votre serveur Metin2 ou corriger un bug au niveau du protocole ? Examinons ensemble la communication client-serveur et mettons en place une solution solide. Contactez-moi.

Bu kategorideki tüm yazılar →

Devamı için