Ağ Paketlerinde Parçalanma ve Halka Arabellek (Ring Buffer) Yönetimi
Ağ programlamaya yeni başlayanların en sık karşılaştığı kavram yanılgılarından biri, TCP protokolünün gönderilen her mesajı karşı tarafa bağımsız bir paket olarak ulaştıracağını varsaymaktır. TCP bir paket (datagram) protokolü değil, kesintisiz bir bayt akışı (byte stream) protokolüdür.
Eğer karşı tarafa arka arkaya iki adet 100 baytlık mesaj gönderirseniz; alıcı taraf bunu tek seferde 200 bayt olarak okuyabileceği gibi, 50 + 150 bayt veya 10’ar baytlık 20 parça halinde de okuyabilir.
Bu rehberde; TCP akışındaki parçalanma (fragmentation) ve yapışma (sticky packets) problemlerini, protokol çerçeveleme (framing) yöntemlerini ve C++ ile bellek tahsisini asgariye indiren bir Halka Arabellek (Ring Buffer) tasarımını ele alıyoruz.
1. Neden Paketler Bölünür veya Birleşir?
- MTU (Maximum Transmission Unit): Standart Ethernet ağlarında tek bir fiziksel çerçevenin taşıyabileceği maksimum veri miktarı genellikle 1500 bayttır (IP ve TCP başlıkları düşüldüğünde MSS $\approx 1460$ bayt). Bu sınırdan büyük veriler çekirdek seviyesinde bölünür.
- Nagle Algoritması ve Gecikmeli Onay (Delayed ACK): Ağ üzerindeki küçük paket trafiğini azaltmak için işletim sistemi küçük verileri arabellekte biriktirip tek bir büyük pakette gönderebilir.
- Alıcı Soket Arabelleği Durumu: Alıcı uygulama soketten yavaş okuma yapıyorsa birden fazla TCP segmenti çekirdek tamponunda tek bir blok halinde birikir.
Sonuç: Uygulama katmanında kendi mesaj sınırlarımızı (Framing) kendimiz belirlemek zorundayız.
2. Mesaj Çerçeveleme (Framing) Stratejileri
Ağ üzerinden akan bayt selinden bağımsız mesajları ayırt etmek için üç yaygın yöntem kullanılır:
| Yöntem | Çalışma Prensibi | Avantaj | Dezavantaj |
|---|---|---|---|
| Ayırıcı Tabanlı (Delimiter) | Mesaj sonuna özel karakter konur (Örn: \r\n veya \0) | Metin tabanlı (HTTP, SMTP) için basit | Veri içinde ayırıcı karakter geçerse kaçış (escape) gerekir |
| Sabit Boyut (Fixed Size) | Her mesaj kesinlikle $N$ bayttır | Ayrıştırması en hızlısı | Kısa mesajlarda dolgu (padding) israfı |
| Uzunluk Başlıklı (Length-Prefix) | Mesajın başında gövdenin kaç bayt olduğu yazılır (Örn: 4 bayt uint32) | İkili (binary) protokoller için en güvenli ve hızlı yöntem | Başlığı okumadan gövde uzunluğu bilinemez |
3. C++ ile Halka Arabellek (Ring / Circular Buffer) Tasarımı
Soketten her veri okunduğunda yeni bir std::string veya std::vector oluşturup erase() ile baştan silmek bellekte devasa kopyalama ve parçalanma (heap fragmentation) maliyeti yaratır. Bunun yerine sabit boyutlu bir arabellek üzerinde yazma ve okuma indislerini dairesel olarak işletmek en ideal çözümdür:
#include <vector>
#include <cstdint>
#include <cstring>
#include <algorithm>
#include <stdexcept>
class RingBuffer {
public:
explicit RingBuffer(size_t capacity)
: buffer_(capacity), capacity_(capacity), head_(0), tail_(0), size_(0) {}
size_t capacity() const { return capacity_; }
size_t size() const { return size_; }
size_t free_space() const { return capacity_ - size_; }
// Soketten okunan veriyi arabelleğe ekle
bool write(const uint8_t* data, size_t len) {
if (len > free_space()) return false; // Yetersiz alan
size_t first_chunk = std::min(len, capacity_ - tail_);
std::memcpy(&buffer_[tail_], data, first_chunk);
size_t second_chunk = len - first_chunk;
if (second_chunk > 0) {
std::memcpy(&buffer_[0], data + first_chunk, second_chunk);
}
tail_ = (tail_ + len) % capacity_;
size_ += len;
return true;
}
// Veriyi silmeden incele (Peek - Başlık kontrolü için)
bool peek(uint8_t* out_data, size_t len) const {
if (len > size_) return false;
size_t first_chunk = std::min(len, capacity_ - head_);
std::memcpy(out_data, &buffer_[head_], first_chunk);
size_t second_chunk = len - first_chunk;
if (second_chunk > 0) {
std::memcpy(out_data + first_chunk, &buffer_[0], second_chunk);
}
return true;
}
// Tamamlanan mesajı arabellekten tüket
void drain(size_t len) {
if (len > size_) len = size_;
head_ = (head_ + len) % capacity_;
size_ -= len;
}
private:
std::vector<uint8_t> buffer_;
size_t capacity_;
size_t head_; // Okuma indisi
size_t tail_; // Yazma indisi
size_t size_;
};
4. Uzunluk Başlıklı Mesajları Çözümleme (Parsing Loop)
Halka arabellek ile uzunluk başlıklı (Length-Prefix) mesajları hatasız okuma döngüsü:
// Protokol: [4 Bayt Mesaj Uzunluğu (uint32_t)] + [Gövde (N Bayt)]
void process_incoming_stream(RingBuffer& ring) {
while (true) {
// 1. En az 4 baytlık başlık gelmiş mi?
if (ring.size() < sizeof(uint32_t)) {
break; // Başlık henüz tamamlanmadı, yeni soket verisi bekle
}
uint32_t payload_len = 0;
ring.peek(reinterpret_cast<uint8_t*>(&payload_len), sizeof(uint32_t));
payload_len = ntohl(payload_len); // Ağ sırasından ana makine sırasına dönüştür
// 2. Tüm gövde arabelleğe ulaştı mı?
size_t total_message_size = sizeof(uint32_t) + payload_len;
if (ring.size() < total_message_size) {
break; // Gövdenin kalanı henüz ağda, bekle
}
// 3. Mesaj tam olarak hazır!
ring.drain(sizeof(uint32_t)); // Başlığı atla
std::vector<uint8_t> payload(payload_len);
ring.peek(payload.data(), payload_len);
ring.drain(payload_len);
// Mesajı işle
handle_valid_packet(payload);
}
}
5. Çıkarımlar
Doğru tasarlanmış bir Halka Arabellek; soketten gelen ham akışı dinamik bellek tahsisatı yapmadan yönetir, CPU önbelleğini (cache) sıcak tutar ve TCP paket parçalanması sorununu zarif bir şekilde çözer.
Bu altyapıyı tamamladıktan sonra serinin 5. bölümünde tüm bu parçaları (POSIX soketleri, non-blocking kip, epoll ve arabellek yönetimi) bir araya getirerek sıfırdan hafif bir HTTP sunucusu inşa edeceğiz.