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

C++ Socket Programlama: Basit TCP Sunucu Yazma

Bir oyun sunucusu, sohbet servisi ya da basit bir API yazarken işin temelinde her zaman aynı şey vardır: ağ üzerinden iki makinenin birbiriyle konuşması. C++ tarafında bu işi yapan en alt katman c++ socket API'sidir — yani 1980'lerden beri standart olan ve bugün hâlâ Linux çekirdeğinin altında çalışan Berkeley soketleri. Bu yazıda dışarıdan bir kütüphane kullanmadan, yalnızca POSIX sistem çağrılarıyla gelen istemcileri dinleyen, gelen veriyi okuyan ve cevap dönen basit bir TCP sunucusunu sıfırdan yazacağız.

Socket nedir ve TCP neden önemli?

Soket, işletim sisteminin sunduğu bir iletişim uç noktasıdır. Programınız açısından bir dosya tanıtıcısı (file descriptor) gibi davranır: ondan okur, ona yazarsınız. Aradaki tüm ağ karmaşasını çekirdek halleder. İki ana taşıma protokolü vardır:

  • TCP (SOCK_STREAM): bağlantı tabanlı, sıralı ve güvenilir. Gönderdiğiniz baytlar karşı tarafa eksiksiz ve doğru sırayla ulaşır. Sohbet, HTTP, oyun girişi gibi her şeyin yerine ulaşması gereken yerlerde kullanılır.
  • UDP (SOCK_DGRAM): bağlantısız, hızlı ama garantisiz. Paket kaybolabilir ya da sıra değişebilir. Gerçek zamanlı oyun pozisyonu gibi gecikmenin güvenilirlikten önemli olduğu yerlerde tercih edilir.

Bu yazıda TCP'ye odaklanıyoruz çünkü "basit sunucu" denince akla gelen kalıbı en iyi o anlatır. Akış her zaman aynıdır: socket()bind()listen()accept()recv()/send()close().

Gerekli başlık dosyaları

Linux'ta soket programlamak için birkaç POSIX başlığına ihtiyacımız var. Windows'ta Winsock (winsock2.h ve WSAStartup) kullanılır; burada Linux/macOS varsayıyoruz.

#include <iostream>
#include <cstring>      // memset, strlen
#include <unistd.h>     // close, read, write
#include <sys/socket.h> // socket, bind, listen, accept
#include <netinet/in.h> // sockaddr_in, htons
#include <arpa/inet.h>  // inet_ntop

Dinleyici soketi oluşturmak

İlk adım socket() çağrısıyla bir uç nokta yaratmaktır. AF_INET IPv4'ü, SOCK_STREAM ise TCP'yi söyler. Çağrı başarısız olursa -1 döner; her sistem çağrısının dönüş değerini kontrol etmek soket programlamada şart.

int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
    perror("socket");
    return 1;
}

// Sunucuyu yeniden başlatınca "Address already in use" hatasını önler
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

SO_REUSEADDR seçeneği küçük ama önemlidir: sunucuyu kapatıp hemen tekrar açtığınızda port hâlâ TIME_WAIT durumunda olabilir ve bind() başarısız olur. Bu seçenek o engeli kaldırır.

Adres bağlama, dinleme ve kabul etme

Şimdi soketi bir IP ve porta bağlıyoruz. Adres bilgisini sockaddr_in yapısıyla doldururuz. Burada en kritik nokta bayt sırasıdır: port numarası ağ üzerinde big-endian beklenir, bu yüzden htons() (host-to-network short) ile çeviririz. INADDR_ANY ise "makinedeki tüm ağ arayüzlerini dinle" demektir.

sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;   // tüm arayüzler
addr.sin_port = htons(8080);         // 8080 portu, ağ bayt sırasında

if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) == -1) {
    perror("bind");
    return 1;
}

if (listen(server_fd, SOMAXCONN) == -1) {  // bekleme kuyruğu
    perror("listen");
    return 1;
}
std::cout << "Sunucu 8080 portunda dinliyor...\n";

listen()'in ikinci argümanı, kabul edilmeyi bekleyen bağlantılar için kuyruk boyutudur; SOMAXCONN sistemin izin verdiği en büyük makul değeri kullanır. Artık accept() ile gelen istemcileri tek tek karşılayabiliriz. Bu çağrı, yeni bir bağlantı gelene kadar bloklar (bekler) ve her bağlantı için ayrı bir soket tanıtıcısı döner.

Veri okumak, yazmak ve döngüyü kurmak

Bağlantı kurulduğunda istemciyle konuşmak için recv() ve send() kullanırız (eşdeğer olarak read()/write() de çalışır). Burada en sık yapılan hata, recv()'in istediğiniz kadar değil, o an mevcut olduğu kadar bayt döndürebileceğini unutmaktır. Dönen değer okunan bayt sayısıdır; 0 ise karşı taraf bağlantıyı kapatmıştır, -1 ise hata vardır.

while (true) {
    sockaddr_in client{};
    socklen_t len = sizeof(client);
    int client_fd = accept(server_fd, (sockaddr*)&client, &len);
    if (client_fd == -1) { perror("accept"); continue; }

    char ip[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &client.sin_addr, ip, sizeof(ip));
    std::cout << "Yeni bağlantı: " << ip << "\n";

    char buffer[1024];
    ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
    if (n > 0) {
        buffer[n] = '\0';
        std::cout << "Gelen: " << buffer;
        const char* reply = "Merhaba, mesajini aldim!\n";
        send(client_fd, reply, strlen(reply), 0);
    }
    close(client_fd);   // bu istemciyle isimiz bitti
}
close(server_fd);

Bu döngü tek seferde tek istemciye hizmet eder. Programı derleyip (g++ -std=c++17 server.cpp -o server) çalıştırdıktan sonra başka bir terminalde nc localhost 8080 ya da telnet localhost 8080 ile bağlanıp mesaj yazarak test edebilirsiniz.

Üretime giderken nelere dikkat etmeli?

Yukarıdaki sunucu öğretici olarak mükemmel ama gerçek dünyada tek başına yetmez. Aklınızda bulunması gereken birkaç nokta:

  • Eşzamanlılık: tek döngü aynı anda yalnızca bir istemciye bakar. Birden çok istemci için her bağlantıyı bir thread'e vermek, ya da daha ölçeklenir bir çözüm olarak epoll tabanlı bir olay döngüsü kurmak gerekir.
  • Kısmi okuma/yazma: send() de tıpkı recv() gibi tüm baytları tek seferde göndermeyebilir. Tüm tamponu gönderene kadar bir döngüde tekrar çağırmalısınız.
  • Sinyaller: kapanmış bir sokete yazınca işlem SIGPIPE ile ölebilir; send() çağrısında MSG_NOSIGNAL bayrağı kullanmak iyi bir alışkanlıktır.
  • Mesaj sınırları: TCP bir bayt akışıdır, "mesaj" kavramı yoktur. Kendi protokolünüzde mesaj uzunluğunu önek olarak göndermek ya da bir ayraç kullanmak gerekir.

Sık Sorulan Sorular

TCP mi yoksa UDP mi kullanmalıyım?

Verinin eksiksiz ve sıralı ulaşması şartsa TCP kullanın: giriş/kimlik doğrulama, sohbet, envanter işlemleri gibi. Birkaç paketin kaybı sorun olmuyor ama düşük gecikme kritikse (oyuncu pozisyonu, hareket güncellemeleri) UDP daha uygundur. Birçok oyun ikisini birlikte kullanır.

Aynı kod Windows'ta da çalışır mı?

Mantık aynıdır ama Windows'ta Winsock kütüphanesi gerekir: programın başında WSAStartup() çağırılır, soket tipi int değil SOCKET'tir ve close() yerine closesocket() kullanılır. Bu farkları soyutlamak için çapraz platform katmanı yazmak yaygın bir yaklaşımdır.

recv() neden beklediğimden az veri döndürüyor?

Çünkü TCP bir akıştır, paket değil. Çekirdek tamponunda o an ne varsa onu verir. Tam bir mesajı okumak için, beklenen bayt sayısına ulaşana kadar recv()'i bir döngüde tekrar çağırmalısınız.

Düşük seviyeli ağ programlamada yardıma mı ihtiyacınız var? C++ ile yüksek performanslı oyun ve sohbet sunucuları, özel protokoller ve epoll tabanlı mimariler konusunda destek almak isterseniz benimle iletişime geçin — projenizi birlikte sağlam temeller üzerine kuralım.

Bu kategorideki tüm yazılar →

Devamı için