POSIX Soketleri ile TCP ve UDP İletişiminin Temelleri: C++ ile Ağ Programlama
Modern yazılım geliştirmede ağ haberleşmesi genellikle yüksek seviyeli kütüphaneler (libcurl, Boost.Asio, gRPC veya HTTP istemcileri) üzerinden yürütülür. Ancak sistem seviyesinde çalışan bir servis, düşük gecikmeli bir veri dağıtım motoru veya gömülü bir cihaz geliştirirken işletim sisteminin çekirdeğiyle doğrudan konuşan POSIX Soket API’si vazgeçilmez bir temeldir.
Bu rehberde; Linux çekirdeğinin ağ yığınını yöneten Berkeley/POSIX soket mekanizmasını, TCP ve UDP protokolleri arasındaki mimari farkları ve C++ RAII prensibiyle kaynak sızıntısı yapmayan temiz bir soket sınıfı tasarımını inceliyoruz.
1. POSIX Soket Mimarisi: “Her Şey Bir Dosyadır”
Linux işletim sisteminde ağ soketleri de tıpkı diskteki bir dosya veya donanım aygıtı gibi birer dosya tanımlayıcısıdır (File Descriptor – FD). Bir soket açıldığında çekirdek arka planda iletim ve alım için iki yönlü tampon bellekler (Send/Receive Buffers) ayırır ve kullanıcı uzayına tamsayı türünde bir tanıtıcı (int fd) döner.
TCP İstemci-Sunucu Yaşam Döngüsü
[Sunucu] [İstemci]
socket() socket()
│ │
bind() (IP ve Porta Bağla) │
│ │
listen() (Bağlantı Kuyruğu Aç) │
│ │
accept() <────── 3 Yollu El Sıkışma ──────── connect()
│ (SYN -> SYN-ACK -> ACK) │
read() / write() <─── Veri Aktarımı ───> read() / write()
│ │
close() <─────── Bağlantı Sonlandırma ──────── close()
2. TCP vs UDP: Hangisi Ne Zaman Tercih Edilmeli?
| Karşılaştırma Kriteri | TCP (Transmission Control Protocol) | UDP (User Datagram Protocol) |
|---|---|---|
| Bağlantı Durumu | Bağlantı yönelimli (El sıkışma şarttır) | Bağlantısız (Doğrudan paket fırlatılır) |
| Güvenilirlik | Sıralı iletim, kayıp paket tekrarı garantilidir | Paket kaybı veya sıra bozulması kontrol edilmez |
| Başlık Boyutu | 20 – 60 Bayt (Akış kontrolü, ACK bayrakları) | 8 Bayt (Son derece hafif) |
| Gecikme (Overhead) | Orta / Yüksek | Minimum |
| Kullanım Alanı | Web, dosya aktarımı, finansal emir iletimi | Gerçek zamanlı ses/video, telemetri, DNS, oyunlar |
3. C++ ile RAII Tabanlı Soket Sarmalayıcısı
C API’sinde en sık yapılan hata, erken fonksiyon çıkışlarında (early return) close(fd) çağrısının unutulması ve sistemde dosya tanımlayıcısı sızıntısı (FD leak) oluşmasıdır.
Bu sorunu modern C++ ile çözmek için basit bir RAII sınıfı tasarlayalım:
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdexcept>
#include <string>
class SafeSocket {
public:
SafeSocket(int domain, int type, int protocol = 0) {
fd_ = ::socket(domain, type, protocol);
if (fd_ < 0) {
throw std::runtime_error("Soket olusturulamadi!");
}
}
~SafeSocket() {
if (fd_ >= 0) {
::close(fd_);
}
}
// Kopyalamayı engelle, sadece taşımaya izin ver (Move-only)
SafeSocket(const SafeSocket&) = delete;
SafeSocket& operator=(const SafeSocket&) = delete;
SafeSocket(SafeSocket&& other) noexcept : fd_(other.fd_) {
other.fd_ = -1;
}
SafeSocket& operator=(SafeSocket&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) ::close(fd_);
fd_ = other.fd_;
other.fd_ = -1;
}
return *this;
}
int get_fd() const { return fd_; }
private:
int fd_ = -1;
};
4. Pratik Bir TCP Sunucusu Kurulumu
Belirli bir porta bağlanıp gelen ilk istemciye yanıt veren minimalist C++ sunucu akışı:
#include <iostream>
#include <cstring>
void run_tcp_server(int port) {
SafeSocket server_sock(AF_INET, SOCK_STREAM, 0);
// Portun hemen yeniden kullanılabilmesi için SO_REUSEADDR
int opt = 1;
setsockopt(server_sock.get_fd(), SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
sockaddr_in server_addr{};
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 0.0.0.0
server_addr.sin_port = htons(port); // Host to Network Short
if (bind(server_sock.get_fd(), (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
throw std::runtime_error("Bind hatasi: Port mesgul!");
}
if (listen(server_sock.get_fd(), 10) < 0) {
throw std::runtime_error("Listen hatasi!");
}
std::cout << "Sunucu " << port << " portunda dinleniyor...\n";
sockaddr_in client_addr{};
socklen_t client_len = sizeof(client_addr);
int client_fd = accept(server_sock.get_fd(), (struct sockaddr*)&client_addr, &client_len);
if (client_fd >= 0) {
char buffer[256];
ssize_t bytes_read = read(client_fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
std::cout << "Gelen Veri: " << buffer << "\n";
const char* response = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK";
write(client_fd, response, strlen(response));
}
close(client_fd);
}
}
5. Çıkarımlar
Standart POSIX soketleri varsayılan olarak bloklayan (blocking) modda çalışır. Yani accept() ya da read() çağrıldığında ağdan veri gelene kadar çağıran iş parçacığı (thread) tamamen durur. Yüzlerce istemciyi eşzamanlı idare etmek istediğimizde her istemciye ayrı bir thread açmak bellek tüketimini patlatır.
Bu nedenle serinin bir sonraki bölümünde soketleri bloklamayan (non-blocking) moda almayı ve arabellek yönetimini ele alacağız.