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

Metin2 packet-structuur: client-server-communicatie

Wanneer een personage beweegt, een item wordt opgepakt of een chatbericht wordt verstuurd, is alles wat tussen de client en de server stroomt een metin2 packet. Het netwerkprotocol van Metin2 bestaat uit binaire berichten die over TCP worden verzonden, waarbij de allereerste byte het type van het packet bepaalt. In dit artikel loop ik door hoe packets zijn opgebouwd, wat de header-naamgeving betekent, het verschil tussen vaste en dynamische packets, en hoe de server inkomende bytes decodeert en naar de juiste handler stuurt.

Hoe een packet eruitziet: de header-byte

In het Metin2-protocol begint elk packet met één enkele header-byte. Omdat het een BYTE is, heeft hij een waarde van 0 tot 255 en identificeert hij het packet-type. Zodra de server of de client de eerste byte uit de stream leest, weet hij welk packet het is en dus hoeveel bytes hij nog moet lezen.

Packets worden gedefinieerd als C++-structs en met #pragma pack(1) samengepakt zodat er geen byte-padding in het geheugen zit. Dit is cruciaal: past de compiler zijn standaard-uitlijning toe, dan zien client en server verschillende byte-indelingen en breekt het protocol.

#pragma pack(1)

// Client -> Game: basaal bewegingspacket (voorbeeld)
typedef struct command_move
{
    BYTE    bHeader;     // packet-type
    BYTE    bFunc;       // bewegingstype (lopen/rennen/stoppen)
    BYTE    bArg;
    BYTE    bRot;        // rotatie
    long    lX;          // doelcoördinaat
    long    lY;
    DWORD   dwTime;      // client-tijdstempel
} TPacketCGMove;

Hier geeft het voorvoegsel CG de richting van het packet aan, iets wat we hierna behandelen. Het kernpunt is dat de header in zijn eentje de indeling van de rest bepaalt. Zodra de server bHeader leest, leest hij precies sizeof(TPacketCGMove) bytes en kopieert die rechtstreeks in de structuur.

CG, GC en de header-naamgeving tussen processen

Metin2 bestaat uit meerdere processen (auth, db, game/core), en packet-namen coderen hun richting met twee letters. Deze conventie is de snelste manier om je weg te vinden in de broncode:

  • CG — Client to Game: van de client naar de game-core (beweging, aanval, chat, item gebruiken).
  • GC — Game to Client: van de game-core naar de client (personage toevoegen, HP-update, chat-broadcast).
  • GD / DG — communicatie tussen de game en het DB-proces (spelers laden en opslaan).
  • GG — Game to Game: peer-to-peer-berichten tussen cores (channels).

Dus HEADER_CG_ATTACK is het aanvalspacket dat de client verstuurt, terwijl HEADER_GC_CHARACTER_ADD het packet is dat de server naar de client stuurt om een nieuw personage op het scherm te plaatsen. Header-constanten worden doorgaans bewaard in een enum:

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

De numerieke waarden variëren per versie en bron en kunnen verschillen tussen private-server-bases. Het gaat niet om het getal zelf, maar erom dat client en server dezelfde header-tabel delen. Gebruiken beide kanten verschillende waarden, dan worden packets verkeerd geïnterpreteerd.

Packets met vaste en dynamische grootte

De overgrote meerderheid van de packets heeft een vaste grootte: zodra de header is gelezen, is de grootte van de struct al bekend. Beweging, aanval en HP-update-packets vallen in deze groep. Maar een chatbericht, itemnamen of een packet met een lijst van variabele lengte past niet in een vooraf bepaalde grootte. Dit zijn dynamische packets en ze dragen een WORD size-veld direct na de header:

#pragma pack(1)

// Client -> Game: chatpacket (dynamisch)
typedef struct command_chat
{
    BYTE    bHeader;     // HEADER_CG_CHAT
    WORD    wSize;       // totale lengte van het hele packet
    BYTE    bType;       // normaal / party / guild ...
    // gevolgd door tekstbytes tot wSize
} TPacketCGChat;

De server leest eerst de header en daarna het wSize-veld om de totale packetlengte te kennen. De berichttekst wordt berekend door het vaste deel van de struct van die grootte af te trekken. Dynamische packets worden dus in twee stappen gelezen: eerst de vaste kop, daarna de body tot wSize.

Verbindingsverloop: van handshake tot game

De client begint niet meteen packets te versturen. De verbinding doorloopt meerdere fasen, en in elke fase worden alleen de packets van die fase geaccepteerd. Een typisch verloop ziet er zo uit:

  • Handshake: nadat de TCP-verbinding tot stand is gekomen, stuurt de server een handshake-packet. Client en server wisselen meermaals tijdstempels heen en weer uit om de netwerklatentie te meten en hun klokken te synchroniseren.
  • Login / Auth: zodra de tijdsynchronisatie nauwkeurig genoeg is, stuurt de client het login-packet (met zijn sessiesleutel).
  • Select: de personagelijst wordt verzonden en de speler kiest een personage.
  • Loading: de map en personagedata worden geladen; de client geeft aan klaar te zijn.
  • Game: de eigenlijke gameplay-fase. Beweging-, gevecht- en chatpackets stromen nu vrij.

De handshake-fase is bijzonder belangrijk: omdat Metin2 beweging en aanvallen tegen tijdstempels valideert, moeten de klokken van client en server dicht bij elkaar liggen. Handshake-packets worden herhaald tot de round-trip-meting onder een acceptabele drempel zakt.

Hoe leest de server een packet?

Aan de serverkant heeft elke verbinding een buffer en een input-processor die bij de huidige fase past. Omdat TCP stream-gebaseerd is, komen bytes in fragmenten binnen; de server mag nooit een onvolledig packet verwerken. De parse-lus ziet er ongeveer zo uit:

  • Bytes die van de socket binnenkomen, worden aan de buffer van de verbinding toegevoegd.
  • Als de buffer minstens 1 byte bevat, wordt de header gelezen (zonder hem al uit de buffer te verbruiken).
  • De header bepaalt het packet-type en de verwachte grootte. Bij een dynamisch packet wordt ook wSize gelezen.
  • Bevat de buffer nog niet het hele packet, dan stopt de lus en wacht op meer bytes.
  • Zodra het volledige packet aanwezig is, wordt het aan zijn handler doorgegeven en worden die bytes verbruikt.
// Kern van de parse-lus (pseudocode)
while (buffer.size() >= 1)
{
    BYTE header = buffer.peek_byte(0);
    int  packetSize = GetPacketSize(header);   // uit wSize indien dynamisch

    if (buffer.size() < packetSize)
        break;                                 // packet nog niet compleet

    Dispatch(header, buffer.read(packetSize)); // naar de handler sturen
}

Is de header een onbekende waarde (niet in de header-tabel), dan wordt dit doorgaans als een protocolschending beschouwd en wordt de verbinding gesloten; in veel private-server-bases is dit een extra verdedigingslinie om vals spelen of manipulatie te detecteren.

Versleuteling en een beveiligingsnotitie

In de vroegste Metin2-versies werden packets in platte (onversleutelde) vorm verzonden. Latere versies en de private servers van vandaag voeren tijdens de handshake een sleuteluitwisseling uit om de packet-body te versleutelen. Dat maakt packet-sniffing en het injecteren van vervalste packets moeilijker. Toch mag de server de client nooit vertrouwen: elk veld van een inkomend packet (coördinaten, schade, aantal) moet aan de serverkant worden gevalideerd. Een door de client beheerde waarde klakkeloos accepteren is de hoofdoorzaak van exploits zoals speed hacks, teleporteren en item-duplicatie (dupe).

Veelgestelde vragen

Gebruiken Metin2-packets TCP of UDP?

Het game-verkeer gaat over TCP. Alle packets, inclusief beweging en gevecht, vereisen betrouwbare, geordende aflevering, dus wordt stream-gebaseerde TCP gebruikt; daarom is het ophopen van gedeeltelijke packets in de serverbuffer normaal en moet het correct worden afgehandeld.

Waarom is de header één byte, en is dat genoeg?

Eén BYTE adresseert 256 verschillende packet-typen, ruim voldoende voor het kern-gameprotocol. Zijn er meer subtypen nodig, dan plaats je een extra bSubHeader- of bType-veld in het packet, zodat de enkele header-byte behouden blijft terwijl de typen uitbreiden.

Waar moet ik op letten bij het toevoegen van mijn eigen packet?

Definieer de header-constante met dezelfde waarde aan client- én serverkant, pak de struct met #pragma pack(1), beslis of je een wSize nodig hebt op basis van vast versus dynamisch, en valideer altijd elk inkomend veld in de server-handler.

Wil je een eigen packet-systeem op je Metin2-server bouwen of een bug op protocolniveau oplossen? Laten we de client-server-communicatie samen bekijken en een degelijke oplossing neerzetten. Neem contact met me op.

Bu kategorideki tüm yazılar →

Devamı için