Veri paketlemesi de ne demek oluyor? En basit anlamına bakacak olursak
bu şu anlama gelir: Verinin başına ya onu tanımlayan bir başlık bilgisi
koyacaksınız ya uzunluk bilgisi ya da her ikisi birden.
Başlık deyip durduğumuz şey neye benzer? Bu tamamen size kalmış ve
projeden projeye değişebilen bir şeydir.
Ama! Bu açıklamalar biraz havada kalmadı mı?
Tamam. Mesela çok kullanıcılı bir chat programı geliştirdiğinizi ve
bu programın da SOCK_STREAM türünden soketler
kullandığını var sayalım. Kullanıcılardan biri bir şey dediğinde
sunucuya gitmesi gereken iki bilgi parçası vardır: Kim dedi ve ne dedi?
Buraya kadar güzel. "Problem ne?" diye soruyor olabilirsiniz.
Problem şu: Gelip giden mesajların uzunluğu değişken. "Ali" isimli biri
"Selam" diyebilir ve sonra da "Veli" isimli biri "naber moruk?"
diyebilir.
Bu durumda siz de send() ile gelen verileri tüm
istemcilere yollarsınız öyle değil mi ve yolladığınız veri de şu
şekilde olur:
A l i S e l a m V e l i N a b e r m o ru k ?
Ve böyle sürüp gider. Peki istemci bir paketin bitip diğerinin
başladığını nasıl anlayacak? Eğer isterseniz tüm paketlerin boyunu
sabit yapabilirsiniz ve sonra da
sendall()
işlevini çağırırsınız (
yukarıda bir
örneği var). Fakat bu bant genişliğini boş yere harcamak
demektir! Yani herhalde
send() ile sadece
Ali'nin "Selam" demesi için 1024 byte göndermek istemeyiz değil mi?
İşte bu yüzden veriyi ufak tefek bir başlık ve paket yapısı ile
paketleriz. Hem sunucu hem de istemci verinin nasıl paketleneceğini
(marshal) ve bu paketin nasıl açılacağın (unmarshal) bilir. Sıkı
durun çünkü tam da şu andan itibaren bir sunucu ile istemcinin hangi
kurallara göre iletişim kuracağını belirleyen bir protokol
tanımlamaya başlıyoruz!
Şimdi var sayalım ki kullanıcı adı en fazla 8 karakter boyunda
olabiliyor ve eğer daha kısa ise geriye kalan bölge '\0'
ile dolduruluyor. Ve sonra varsayalım ki mesaj uzunluğu değişken
olabiliyor ve maksimum 128 karaktere izin veriyoruz. Buna göre örnek
bir pakete bakalım:
len (1 byte, unsigned) -- Paketin toplam boyu,
8 baytlık kullanıcı ismi ve chat mesajı dahil.
name (8 byte) -- Kullanıcı ismi, gerekirse sonu
NULL ile doldurulmuş.
chatdata (n-byte) --
Gönderilecek mesajın kendisi. En fazla 128 byte olabilir. Paket
boyu bu verinin boyu artı 8 (kullanıcı ismi bölümünün uzunluğu)
olarak hesaplanabilir.
Neden söz konusu veri alanları için sırasi ile 8 byte ve 128 byte
sınırlarını seçtim? Tamamen keyfime kalmış, bana yeterince uzun
göründü. Ama belki de sizin ihtiyaçlarınız için 8 byte çok az
gelebilir ve siz de 30 byte uzunluğunda bir isim alanı
kullanabilirsiniz. Size kalmış.
Yukarıdaki paket tanımlamasını kullanarak ilk paketimizin görüntüsü
şuna benzeyecektir (onaltılık ve ASCII):
0D 41 6C 69 00 00 00 00 00 53 65 6C 61 6D
(uzunluk) A l i (dolgu) S e l a m
Ve ikincisi de benzer şekilde:
13 56 65 6C 69 00 00 00 00 4E 61 62 65 72 6D 6F 72 75 6B 3F
(uzunluk) V e l i (dolgu) N a b e r m o r u k ?
(Uzunluk bilgisi de elbette Ağ Byte Sıralamasına uygun depolanmalıdır.
Ancak mevcut örnekte tek bir byte olduğu için çok önemli değil. En
genel durumda ise sakın unutmayın ki tüm ikili düzendeki tamsayılarınız
Ağ Byte Sıralamasına göre dizilmiş olmalıdır.)
Bu veriyi yollarken güvenli bir şekilde, mesela
sendall() gibi bir işlevle yollamalısınız.
Böylece birçok kez
send() işlevini çağırmış olmak
gerekse de gittiğinden emin olabilirsiniz.
Benzer şekilde bu veriyi aldığınızda çözmek için biraz iş yapmanız
gerekir. En genel durumda verinin ancak bir kısmının gelmiş
olabileceğini göz önünde bulundurmalısınız (misal sadece "00 13 56 65 6C"
size ulaşmış olabilir Veli isimli kullanıcıdan. Bu ilk parça
gördüğünüz gibi eksiktir ve verinin tamamına erişene dek birkaç kez
daha recv() işlevini çağırmanız gerekebilir.
İyi de nasıl? Paketin en başına paket uzunluğu konmuş olduğundan en baştan
itibaren bir paketin orjinal uzunluğunu biliriz. Aynı zamanda biliriz ki
azami paket boyu da 1+8+128 olarak hesaplanabilir yani toplam olara 137
byte (çünkü biz böyle tanımladık).
Bu durumda yapabileceğiniz şeylerden biri iki paketi barındırabilecek
büyüklükte bir dizi tanımlamaktır. Burası sizin çalışma diziniz olacak
ve paketler buraya vardıkça onları buradan alıp düzenleyeceksiniz.
recv() işlevini her çağırışınızda gelen veriyi
çalışma dizisine aktarırsınız ve paketin tamamlanıp tamamlanmadığına
bakarsınız. Yani dizi içindeki byte miktarının paket boyuna eşit mi
yoksa ondan büyük mü olduğunu kontrol edersiniz (+1 çünkü başlıktaki
uzunluk kendisi için kullanılan 1 byte'lık uzunluğu içermez). Eğer
tampondaki byte sayısı 1'den az ise o zaman paket tam değildir açık
olarak. Bu durumu özel olarak ele almalısınız, çünkü bu ilk byte
çöplüktür ve paket boyu için ona güvenemezsiniz.
Paket tamamlandığında artık onun üzerinde istediğiniz işlemi
gerçekleştirebilirsiniz. Onu kullanabilir, çalışma dizinizden
çıkarabilirsiniz.
Nasıl? Kafanızda evirip çevirmeye başladınız mı? Sıkı durun şimdi
ikinci darbe geliyor: bir paketi sonuna kadar okumakla kalmayıp aynı
esnada bir sonraki paketin de bir kısmını son recv()
çağrısında okumuş olabilirsiniz. Yani çalışma bölgenizde bir tam paket
ve bir de ardından gelen bir kısmi paket vardır! (İşte çalışma
tamponunu biraz geniş tutun dememin sebebi bu idi. Yani iki paketi
tutabilecek kadar -- işte şimdi o durum gerçekleşti!)
İlk paketin boyunu bildiğinize ve gelen byte'ları da takip ettiğinize
göre bu sayıları kullanarak çalışma tamponunuzdaki baytlardan
hangisinin ilk pakete hangisinin de kısmi pakete ait olduğunu tespit
edebilirsiniz. Birincisi ile uğraşmayı bitirince onu çalışma dizisinden
çıkarıp bir sonraki paketin parçasını tampon başlangıcına
taşıyabilirsiniz ve böylece tampon da bir sonraki recv()
çağrısı için hazır hale gelir.
(Bazı okuyucular belki fark etmiştir, kısmi olarak gelen ikinci paketi
tampon başına taşımak biraz vakit alabilecek bir iştir ve program buna
gerek duymaması için döngüsel tampon (circular buffer) kullanacak
şekilde kodlanabilir. Maalesef döngüsel tamponla ilgili tartışma bu
makalenin alanı dışına düşmektedir. Gerçekten merak ettiyseniz Veri
Yapıları ile ilgili güzel bir kitap alıp ilgili bölümü okuyun)
Kolay olduğunu söylememiştim. Şey, aslında söylemiştim ve öyleydi.
Öyledir, yani eğer pratik yapmaya başlarsanız gerisi çorap söküğü
gibi gelecektir. Excalibur üzerine yemin ederim öyle olacağına!