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

Metin2 Packet Yapısı: Client-Server İletişimi

Bir karakter hareket ettiğinde, bir eşya basıldığında ya da sohbet mesajı gönderildiğinde istemci ile sunucu arasında akan her şey bir metin2 packet'idir. Metin2'nin ağ protokolü, TCP üzerinde gönderilen ve ilk byte'ı paketin türünü belirleyen ikili (binary) mesajlardan oluşur. Bu yazıda paketlerin nasıl yapılandırıldığını, header isimlendirmesinin ne anlama geldiğini, sabit ve dinamik boyutlu paketlerin farkını ve sunucunun gelen baytları nasıl çözüp doğru işleyiciye yönlendirdiğini adım adım anlatıyorum.

Bir paket neye benzer: header byte mantığı

Metin2 protokolünde her paket tek bir header byte ile başlar. Bu bir BYTE olduğu için 0–255 arası bir değer alır ve paketin türünü tanımlar. Sunucu ya da istemci akıştan ilk baytı okuduğunda, o baytın hangi paket olduğunu, dolayısıyla peşinden kaç bayt daha okuması gerektiğini bilir.

Paketler C++ tarafında struct olarak tanımlanır ve bellekte byte hizalaması (padding) olmaması için #pragma pack(1) ile sıkıştırılır. Bu kritik bir noktadır: derleyici varsayılan hizalamayı uygularsa istemci ve sunucu farklı bayt düzenleri görür ve protokol bozulur.

#pragma pack(1)

// Client -> Game: temel hareket paketi (örnek)
typedef struct command_move
{
    BYTE    bHeader;     // paket türü
    BYTE    bFunc;       // hareket türü (yürü/koş/dur)
    BYTE    bArg;
    BYTE    bRot;        // yön
    long    lX;          // hedef koordinat
    long    lY;
    DWORD   dwTime;      // istemci zaman damgası
} TPacketCGMove;

Burada CG öneki paketin yönünü gösterir; buna birazdan geliyoruz. Önemli olan şu: header tek başına paketin geri kalanının düzenini belirler. Sunucu bHeader değerini okuduğu an, TPacketCGMove yapısının sizeof'u kadar baytı okuyup yapıya doğrudan kopyalar.

CG, GC ve sunucular arası header isimlendirmesi

Metin2 birden çok süreçten oluşur (auth, db, game/core) ve paket isimleri yönü iki harfle kodlar. Bu konvansiyon kaynak kodu okurken yön bulmanın en hızlı yoludur:

  • CG — Client to Game: istemciden oyun çekirdeğine (hareket, saldırı, sohbet, eşya kullanımı).
  • GC — Game to Client: oyun çekirdeğinden istemciye (karakter ekleme, can güncelleme, sohbet yayını).
  • GD / DG — Game ile DB süreci arasındaki iletişim (oyuncu yükleme, kaydetme).
  • GG — Game to Game: çekirdekler (kanallar) arası P2P haberleşme.

Yani HEADER_CG_ATTACK istemcinin gönderdiği saldırı paketi, HEADER_GC_CHARACTER_ADD ise sunucunun ekrana yeni bir karakter koyması için istemciye gönderdiği pakettir. Header sabitleri tipik olarak bir enum içinde tutulur:

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

Buradaki sayısal değerler sürüme ve kaynağa göre değişir; private server kaynakları arasında farklılık gösterebilir. Önemli olan sayının kendisi değil, istemci ve sunucunun aynı header tablosunu paylaşmasıdır. İki taraf farklı değerler kullanırsa paketler yanlış yorumlanır.

Sabit ve dinamik boyutlu paketler

Paketlerin büyük çoğunluğu sabit boyutludur: header okununca yapının boyutu zaten bilinir. Hareket, saldırı, can güncelleme gibi paketler bu gruba girer. Ama sohbet mesajı, eşya isimleri ya da değişken uzunlukta liste taşıyan paketler önceden bilinen bir boyuta sığmaz. Bunlar dinamik paketlerdir ve header'dan hemen sonra bir WORD size alanı taşır:

#pragma pack(1)

// Client -> Game: sohbet paketi (dinamik)
typedef struct command_chat
{
    BYTE    bHeader;     // HEADER_CG_CHAT
    WORD    wSize;       // tüm paketin toplam uzunluğu
    BYTE    bType;       // normal / parti / lonca ...
    // ardından wSize'a kadar metin baytları
} TPacketCGChat;

Sunucu önce header'ı, sonra wSize alanını okur ve toplam paket uzunluğunu öğrenir. Mesaj metni bu boyuttan struct'ın sabit kısmı çıkarılarak hesaplanır. Bu nedenle dinamik paketleri işlerken iki kademeli okuma yapılır: önce sabit baş kısım, sonra wSize kadar gövde.

Bağlantı akışı: handshake'ten oyuna

İstemci doğrudan paket göndermeye başlamaz. Bağlantı belirli aşamalardan (phase) geçer ve her aşamada yalnızca o aşamaya ait paketler kabul edilir. Tipik akış şudur:

  • Handshake: TCP bağlantısı kurulduktan sonra sunucu bir handshake paketi gönderir. İstemci ve sunucu, ağ gecikmesini ölçmek ve saatleri eşitlemek için zaman damgalarını birkaç kez ileri geri yollar.
  • Login / Auth: Zaman senkronizasyonu yeterince hassas olduğunda istemci giriş paketini (oturum anahtarıyla) gönderir.
  • Select: Karakter listesi gönderilir, oyuncu karakterini seçer.
  • Loading: Harita ve karakter verisi yüklenir; istemci hazır olduğunu bildirir.
  • Game: Asıl oyun aşaması. Hareket, dövüş, sohbet paketleri artık serbestçe akar.

Handshake aşaması özellikle önemlidir: Metin2 hareket ve saldırıları zaman damgalarıyla doğruladığı için istemci ve sunucu saatinin yakın olması gerekir. Round-trip ölçümü kabul edilebilir bir eşiğin altına inene kadar handshake paketleri tekrarlanır.

Sunucu bir paketi nasıl okur?

Sunucu tarafında her bağlantının bir tampon (buffer) ve mevcut aşamasına göre bir girdi işleyicisi (input processor) vardır. TCP akış tabanlı olduğu için baytlar parça parça gelir; sunucu tamamlanmamış bir paketi asla işlemez. Çözümleme döngüsü kabaca şöyledir:

  • Soketten gelen baytlar bağlantının tamponuna eklenir.
  • Tamponda en az 1 bayt varsa header okunur (henüz tampondan tüketilmeden).
  • Header'a bakılarak paketin türü ve beklenen boyutu belirlenir. Dinamikse wSize de okunur.
  • Tamponda paketin tamamı yoksa döngü durur ve daha fazla bayt beklenir.
  • Tam paket geldiyse ilgili işleyiciye gönderilir ve o baytlar tampondan tüketilir.
// Çözümleme döngüsünün özü (sözde kod)
while (buffer.size() >= 1)
{
    BYTE header = buffer.peek_byte(0);
    int  packetSize = GetPacketSize(header);   // dinamikse wSize'dan

    if (buffer.size() < packetSize)
        break;                                 // paket henüz tamam değil

    Dispatch(header, buffer.read(packetSize)); // işleyiciye yönlendir
}

Header bilinmeyen bir değerse (header tablosunda yoksa) bu genellikle protokol ihlali sayılır ve bağlantı kapatılır; pek çok private server kaynağında bu, hile/manipülasyon tespiti için ek bir savunma noktasıdır.

Şifreleme ve güvenlik notu

İlk Metin2 sürümlerinde paketler düz (şifresiz) gönderiliyordu. Sonraki sürümler ve günümüz private server'ları handshake sırasında anahtar değişimi yaparak paket gövdesini şifreler. Bu, paket dinleme ve sahte paket enjeksiyonunu zorlaştırır. Buna rağmen sunucu hiçbir zaman istemciye güvenmemelidir: gelen her paketin alanları (koordinat, hasar, miktar) sunucu tarafında doğrulanmalıdır. İstemci kontrolündeki bir değeri sorgusuz kabul etmek, hız hilesi, ışınlanma ve eşya çoğaltma (dupe) gibi açıkların temel kaynağıdır.

Sık Sorulan Sorular

Metin2 paketleri TCP mi UDP mi kullanır?

Oyun trafiği TCP üzerinden gider. Hareket ve dövüş dahil tüm paketler güvenilir sıralı teslimat gerektirdiği için akış tabanlı TCP tercih edilir; bu yüzden sunucu tamponunda kısmi paket birikmesi normaldir ve doğru ele alınması gerekir.

Header byte neden tek byte ve bu yeterli mi?

Tek BYTE 256 farklı paket türünü adresler; bu da temel oyun protokolü için fazlasıyla yeter. Daha fazla alt tür gerektiğinde paketin içine ek bir bSubHeader veya bType alanı konur, böylece tek header byte korunurken türler genişletilir.

Kendi paketimi eklerken nelere dikkat etmeliyim?

Header sabitini hem istemci hem sunucu tarafında aynı değerle tanımlayın, struct'ı #pragma pack(1) ile sıkıştırın, sabit/dinamik kararına göre wSize ekleyip eklemeyeceğinizi belirleyin ve sunucu işleyicisinde gelen tüm alanları mutlaka doğrulayın.

Metin2 sunucunuzda özel bir paket sistemi mi geliştirmek ya da protokol kaynaklı bir hatayı mı çözmek istiyorsunuz? İstemci-sunucu iletişimini birlikte inceleyip sağlam bir çözüm kuralım. Benimle iletişime geçin.

Bu kategorideki tüm yazılar →

Devamı için