Ana içeriğe geç

Bölüm 71: Soket Programlama

13 dakikalık okumaGüncelleme:

Ağlar'a “Sen”i Katmak kitabının bir bölümü: adres çubuğuna yazdığın tek bir isteğin peşinden bakırla fiberin fiziğine inip karşı taraftan geri dönen uzun bir teknik yolculuk.

Kısım 10: Uygulamayı Kendin Kurmak (Bölüm 71-72)

Tüm bölümler
  1. Giriş
  2. Adres Çubuğundan İsteğe
  3. Paket Anahtarlamalı Yol
  4. İsimden Adrese
  5. DNS Kayıtları
  6. Önbellek, Tazelik ve Güven
  7. Port Numarası ve Soket
  8. Bağlantısız Taşıma: UDP
  9. Üç Adımlı El Sıkışma
  10. Simetrik Şifreleme
  11. Açık Anahtarlı Kriptografi
  12. TLS El Sıkışması
  13. Sertifika Zinciri
  14. HTTP Mesajının Anatomisi
  15. Durum, Çerez ve Oturum
  16. HTTP/2'den HTTP/3'e
  17. İçerik Dağıtım Ağları
  18. Diğer Uygulama Protokolleri
  19. Katmanlı Mimari
  20. Hizmet ve Protokol
  21. Encapsulation ve Başlıklar
  22. Paket Yakalama ve Çözümleme
  23. Ağ Interface Kartı
  24. Fiziksel Adresleme
  25. Hata Sezme ve CRC
  26. Adres Çözümleme: ARP
  27. Ortam Erişimi: CSMA/CD
  28. Switch'ler
  29. Ağ Topolojileri
  30. Sanal Yerel Ağlar
  31. Kablosuz Erişim: 802.11
  32. Radyo Kanalı ve Kapasite
  33. Kablosuz Ağ Güvenliği
  34. Hücresel Ağlar
  35. Spektrum ve Kuşaklar
  36. Hareketlilik ve Aktarma
  37. Adres Kiralama: DHCP
  38. Adresin Anatomisi
  39. Adres Tükenmesi ve IPv6
  40. Router Mimarisi
  41. Kuyruk Yönetimi
  42. Cihaz Yapılandırma
  43. Routing Protokolleri
  44. Kontrol Düzlemi ve SDN
  45. Ağ İzleme ve Yönetimi
  46. Alanlar Arası Routing
  47. Adres Çevirisi
  48. Ara Kutular
  49. Tünelleme ve IPsec
  50. Firewall'lar
  51. Ağ Saldırıları
  52. Operatör ve Metro Ağları
  53. Bitten Sinyale
  54. Sembol Hızı ve Kanal Kapasitesi
  55. Multiplexing Teknikleri
  56. İletim Ortamları
  57. Bakır Kablo Sonlandırma
  58. Fiber Sonlandırma
  59. Fiber Optik İletim
  60. Bakır Erişim Ağı: DSL
  61. DSL'de Hız ve Mesafe
  62. Pasif Optik Ağlar
  63. Kablo Erişim Ağları
  64. Kablo Modem ve CMTS
  65. Denizaltı Kabloları
  66. Veri Merkezi Ağları
  67. Encapsulation Çözme
  68. Güvenilir Aktarım
  69. Tıkanıklık Denetimi
  70. Trafik Çözümleme
  71. Gecikme Bütçesi
  72. Soket Programlama
  73. Eşten Eşe ve Protokol Tasarımı
  74. Uyarlanır Video Akışı
  75. Gerçek Zamanlı Ses ve Görüntü
  76. Kablosuz Sensör Ağlarının Mimarisi
  77. Sensör Ağlarında Ortam Erişimi
  78. Sensör Ağlarında Yönlendirme ve IP
  79. Zaman Eşitleme ve Konum Bulma
  80. Sensör Ağı Uygulamaları
  81. Mobil Kablosuz Sensör Ağları
  82. Son Söz

Mümkün. Tarayıcının yaptığı işin ağa dokunan kısmı, işletim sisteminin sunduğu bir avuç çağrıdan ibaret. Bu çağrılar 1983’te Berkeley’de yayımlanan 4.2BSD ile doğdu. Adları ve sırası o günden beri neredeyse hiç değişmedi; bugün Linux’ta, macOS’ta ve Windows’ta aynı mantıkla çalışıyorlar.

Bu bölümde yolculuğu bir kez de kendi kodunla yürüyeceksin. Örnekler Python’da, çünkü Python’un socket modülü C’deki çağrıların neredeyse birebir kopyası: adlar aynı, sıra aynı, hata kodları aynı. Kod bloklarını bir dosyaya yapıştırıp olduğu gibi çalıştırabilirsin. Aşağıdaki çıktıların hepsi 25 Eylül 2026’da gerçekten çalıştırılan kodlardan alındı.

İstemci tarafında kaç çağrı var ve her biri yolun hangi durağına denk geliyor?

Bir sayfa, beş çağrı

Adres çubuğuna http://example.com yazmanın kodla karşılığı şu:

import socket

bilgi = socket.getaddrinfo("example.com", 80, socket.AF_INET, socket.SOCK_STREAM)
aile, tur, proto, _, adres = bilgi[0]

s = socket.socket(aile, tur, proto)   # kernel'da boş bir uç aç
s.settimeout(5)
s.connect(adres)                      # SYN, SYN-ACK, ACK
s.sendall(b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n")

parcalar = []
while True:
    veri = s.recv(4096)               # tampondakini al, boşsa bekle
    if not veri:                      # boş bayt dizisi: karşı taraf FIN gönderdi
        break
    parcalar.append(veri)
s.close()                             # kendi FIN'ini gönder

yanit = b"".join(parcalar)
print(adres)
print(yanit.split(b"\r\n\r\n", 1)[0].decode("latin-1"))

Çıktının başı:

('172.66.147.243', 80)
HTTP/1.1 200 OK
Date: Fri, 25 Sep 2026 09:52:38 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: close
Server: cloudflare
Age: 4514
cf-cache-status: HIT
CF-RAY: a4092323bf39b64c-IST

Kodun her satırı kitaptaki bir durağa denk geliyor.

getaddrinfo, 3. bölümdeki stub resolver’ın ta kendisi. Adı sayıya çeviriyor ve bir liste döndürüyor, çünkü bir adın birden çok adresi olabilir. Tarayıcılar IPv6 ve IPv4 adreslerini 38. bölümdeki Happy Eyeballs ile yarıştırıyor; bu kod yalnızca IPv4 adreslerini istiyor ve listenin ilkini alıyor.

socket() henüz ağa tek bir paket göndermiyor. Kernel’da bir uç açıyor ve sana bir tutamak veriyor; 6. bölümde gördüğün four-tuple’ın yarısı bile henüz belli değil.

connect() iki şey yapıyor. Kaynak portu dinamik aralıktan kernel seçiyor, sonra 8. bölümdeki üç mesaj gidip geliyor. Çağrı, el sıkışma bitene kadar dönmüyor; yani connect()’in beklediği süre kabaca bir gidiş-dönüş süresi.

sendall() ile send() arasındaki fark ilk tuzak. send() verinin ancak bir kısmını kernel’ın gönderim tamponuna yazabilir ve kaç bayt yazdığını döndürür. sendall() kalanı bitene kadar tekrar dener. İkisinin de dönmesi, verinin karşıya ulaştığı anlamına gelmiyor; yalnızca kernel’ın veriyi teslim aldığı anlamına geliyor. Segmentlere bölmek, 67. ve 68. bölümdeki pencereleri gözetmek, kaybolanı yeniden göndermek artık kernel’ın işi.

recv(4096) en fazla 4.096 bayt döndürüyor, ama daha azını döndürebilir. Tamponda o anda ne varsa onu veriyor. Boş bir bayt dizisi döndürdüğünde karşı taraf FIN göndermiş demektir ve daha fazla veri gelmeyecek. Burada 868 baytlık cevap tek bir recv ile geldi; başka bir hatta ya da daha büyük bir sayfada aynı cevap onlarca parçaya bölünebilirdi.

close() kendi FIN’ini gönderiyor. Bağlantıyı önce kapatan taraf olduğun için 8. bölümdeki TIME-WAIT durumunu da sen üstleniyorsun.

Başlıklar da bir şey söylüyor. Server: cloudflare ve CF-RAY satırının sonundaki IST, cevabı Cloudflare’in İstanbul’daki kenar sunucusunun verdiğini gösteriyor. Age: 4514 ve cf-cache-status: HIT ise sayfanın o kenarda 75 dakikadır önbellekte durduğunu. Belgelerde örnek olsun diye ayrılmış example.com bile bugün 16. bölümdeki bir CDN’in arkasında duruyor.

Kilidi takmak

Aynı isteği 443 numaralı porta ve kilitli göndermek iki satır daha tutuyor:

import socket, ssl

baglam = ssl.create_default_context()      # güvenilen kökler ve ad denetimi açık
with socket.create_connection(("example.com", 443), timeout=5) as ham:
    with baglam.wrap_socket(ham, server_hostname="example.com") as s:
        print(s.version(), s.cipher()[0])
        sertifika = s.getpeercert()
        print(dict(x[0] for x in sertifika["issuer"])["commonName"])
        print(sertifika["notBefore"], "->", sertifika["notAfter"])
TLSv1.3 TLS_AES_256_GCM_SHA384
Cloudflare TLS Issuing ECC CA 3
Jul 29 22:10:08 2026 GMT -> Oct 27 22:17:21 2026 GMT

create_connection() ad çözmeyi, soket açmayı ve bağlanmayı tek çağrıda yapıyor; listedeki adresleri sırayla deniyor. Asıl iş wrap_socket() satırında. server_hostname 11. bölümdeki SNI olarak ClientHello’ya yazılıyor ve el sıkışma bu çağrının içinde tamamlanıyor. create_default_context() ise 12. bölümdeki iki denetimi açıyor: zincir işletim sisteminin güvendiği bir köke bağlanıyor mu, sertifika gerçekten bu ada mı verilmiş? Sertifikanın ömrü de yalnızca 90 gün. Azami ömrün Mart 2026’da 200 güne indiğini ve 2029’a kadar 47 güne ineceğini 12. bölümde görmüştün.

Denetim tutmadığında ne olduğunu, bu iş için kurulmuş badssl.com sunucularında görebilirsin:

wrong.host.badssl.com  -> Hostname mismatch, certificate is not valid for 'wrong.host.badssl.com'.
expired.badssl.com     -> certificate has expired
self-signed.badssl.com -> self-signed certificate

Bu hatalarla karşılaşan birinin internette bulacağı ilk “çözüm”, bağlamda check_hostname = False ve verify_mode = ssl.CERT_NONE satırlarını açmak. Hata susar, bağlantı da şifreli kalır, ama artık karşındakinin kim olduğunu doğrulayan hiçbir adım yoktur. 10. bölümdeki araya giren saldırgan tam da bu boşluğu bekliyor.

Akış mesaj tanımaz

Sıradaki tuzak, soket programlayan hemen herkesin bir kez düştüğü yer. Aynı makinede bir sunucu açıp ona iki ayrı ileti gönderelim:

istemci.sendall(b"merhaba")
istemci.sendall(b"dunya")

# karşı tarafta, kısa bir beklemeden sonra:
print(baglanti.recv(1024))
b'merhabadunya'

İki sendall, tek bir recv. Karşı tarafta iletilerin nerede ayrıldığını gösteren hiçbir iz yok. Sebebi 8. bölümde başlayan tanımda: TCP ileti değil, bayt akışı taşıyor. Tersi de aynı sıklıkla oluyor; tek bir sendall ile giden bir megabayt, karşı tarafa düzinelerce recv ile ulaşıyor. 7. bölümdeki UDP’de ise her gönderim alıcıya ayrı bir datagram olarak çıkıyor.

Bu yüzden akış üstünde konuşan her protokol, iletinin nerede bittiğini kendisi söylemek zorunda. Bunun üç yolu var ve üçünü de kitap boyunca gördün:

YöntemNasıl çalışıyorÖrnek
ayraçiletinin sonu belirli bir bayt dizisiyle işaretlenirHTTP başlıkları boş satırla, yani \r\n\r\n ile biter (13. bölüm)
uzunluk önekiher iletinin başında kaç bayt süreceği yazarTLS kaydında 2 bayt, HTTP/2 frame’inde 3 bayt uzunluk alanı (11. ve 15. bölüm)
kendini anlatan gövdebaşlık, gövdenin nasıl okunacağını söylerContent-Length ya da parça parça gelen chunked gövde

Yukarıdaki cevabın Transfer-Encoding: chunked taşıması bundan: sunucu gövdenin uzunluğunu baştan söylemedi, her parçanın başına kendi uzunluğunu yazdı. Kodumuz bunu hiç çözmedi, çünkü Connection: close istemişti ve bağlantının kapanması iletinin sonu yerine geçti. Bağlantıyı açık tutan gerçek bir istemci bu şansa sahip değil.

Kendi protokolünü yazıyorsan en sağlam yol uzunluk öneki:

import struct

def gonder(s, ileti):
    s.sendall(struct.pack("!I", len(ileti)) + ileti)   # 4 bayt uzunluk, sonra ileti

def tam_oku(s, n):
    tampon = b""
    while len(tampon) < n:                  # recv(n), n bayt döndürmek zorunda değil
        parca = s.recv(n - len(tampon))
        if not parca:
            raise ConnectionError("bağlantı ileti ortasında kapandı")
        tampon += parca
    return tampon

def al(s):
    (uzunluk,) = struct.unpack("!I", tam_oku(s, 4))
    return tam_oku(s, uzunluk)

Aynı iki ileti bu kez ayrı ayrı çıkıyor: [b'merhaba', b'dunya']. "!I" biçimindeki ünlem işareti ağ bayt sırası demek, yani en anlamlı bayt önce. 7 sayısı telde b'\x00\x00\x00\x07' olarak gidiyor. İki makinenin işlemcisi baytları farklı sırada tutabilir; ağda herkesin anlaştığı sıra bu.

Asıl ders tam_oku fonksiyonunda. recv(n) çağrısının n bayt döndürdüğünü varsayan kod yerel ağda yıllarca çalışır, sonra yavaş bir hatta ilk büyük iletide bozulur. Hata üretimde ortaya çıkar, test ortamında hiç görünmez.

Sunucu tarafı: bind, listen, accept

Karşı tarafın koduna geçelim. Gelen her baytı olduğu gibi geri gönderen bir yankı sunucusu:

import socket, threading

def hizmet_et(baglanti, adres):
    with baglanti:
        while veri := baglanti.recv(4096):
            baglanti.sendall(veri)

with socket.create_server(("0.0.0.0", 9000)) as sunucu:   # socket, bind, listen
    while True:
        baglanti, adres = sunucu.accept()                  # hazır bağlantıyı kuyruktan al
        threading.Thread(target=hizmet_et, args=(baglanti, adres), daemon=True).start()

Başka bir terminalden nc localhost 9000 ile bağlanıp yazdığın her satırın geri geldiğini görebilirsin.

create_server üç çağrıyı birden yapıyor. bind() soketi bir adrese ve porta bağlıyor. Buradaki 0.0.0.0 “bu makinenin bütün adresleri” demek; 127.0.0.1 yazsaydın sunucuya yalnızca aynı makineden ulaşılabilirdi. Geliştirme sunucusunu farkında olmadan 0.0.0.0’a bağlamak, onu kafedeki bütün ağa açmak demek.

listen() soketi pasif hâle getiriyor ve asıl sürpriz burada: el sıkışmayı senin kodun yapmıyor. SYN geldiğinde kernel SYN-ACK’i kendisi gönderiyor, üçüncü mesaj gelince bağlantıyı tamamlanmış sayıyor ve bir kuyruğa koyuyor. Kodun henüz accept() çağırmamış olsa bile istemci için bağlantı kurulmuştur. Kuyruğun boyu ve dolduğunda neler olduğu 66. bölümde duruyordu.

accept() kuyruktan tamamlanmış bir bağlantıyı alıyor ve sana yeni bir soket veriyor. Dinleyen soket yerinde kalıyor ve sıradaki bağlantıyı bekliyor. 6. bölümde aynı porta yüz binlerce konuşmanın nasıl sığdığını sormuştuk; cevabın kod tarafındaki hâli bu. Her konuşma, four-tuple’ı farklı ayrı bir soket.

Bir ayrıntı daha. Sunucuyu kapatıp hemen yeniden başlattığında bind() bazen “adres kullanımda” hatası verir, çünkü eski bağlantılar hâlâ TIME-WAIT’tedir. create_server, Linux ve macOS’ta bunu önlemek için SO_REUSEADDR seçeneğini kendiliğinden açıyor. Windows’ta açmıyor, çünkü orada aynı adlı seçenek başka bir şey yapıyor: başka bir sürecin dinlemekte olan bir portu ele geçirmesine izin veriyor.

Aynı anda birden çok istemci

Yukarıdaki sunucu her bağlantıya ayrı bir iş parçacığı açıyor. Açmasaydı ne olurdu? İlk istemciyle konuşurken ikinci istemcinin el sıkışması kernel’da yine tamamlanırdı, ama bağlantı kuyrukta bekler ve hiç cevap alamazdı. İstemci açısından sunucu “bağlandı ama konuşmuyor” gibi görünürdü.

Bu sorunun üç klasik çözümü var:

ModelNasılBedeli
bağlantı başına süreçher accept sonrası yeni süreçbellek ve süreç açma maliyeti yüksek; eski Apache böyleydi
bağlantı başına iş parçacığıyukarıdaki kodher iş parçacığının yığını ve bağlam değiştirme maliyeti; binlerce iyi, yüz binlerce zor
olay döngüsütek iş parçacığı, çok sayıda engellenmeyen soketkod “hazır olanı işle” biçiminde yazılmak zorunda

Üçüncüsünün altında kernel’ın “hangi soketlerde okunacak veri var?” sorusuna verdiği cevap duruyor: select, poll, Linux’ta epoll, BSD’lerde kqueue. Bunları 66. bölümde sunucunun gözünden görmüştün.

DerinleşmeTek iş parçacığıyla yüz bağlantı nasıl yürüyor?

Python’un asyncio modülü olay döngüsünü doğrudan kullanmanı sağlıyor. Aynı yankı sunucusu:

import asyncio

async def hizmet_et(okuyucu, yazici):
    while veri := await okuyucu.read(4096):
        yazici.write(veri)
        await yazici.drain()
    yazici.close()

async def main():
    sunucu = await asyncio.start_server(hizmet_et, "0.0.0.0", 9000)
    async with sunucu:
        await sunucu.serve_forever()

asyncio.run(main())

Bu sunucuya aynı anda yüz bağlantı açıp her birine ayrı bir sayı gönderdiğimde yüz cevabın hepsi doğru bağlantıdan döndü. Ortada tek bir iş parçacığı vardı.

İşin sırrı await sözcüklerinde. Her await, “burada bekleyebilirim, bu arada başkasına bak” demek. Döngü o noktada kernel’a hangi soketlerin hazır olduğunu soruyor ve sıradaki hazır bağlantının işini ilerletiyor. Hiçbir recv gerçekten beklemiyor; veri gelmemiş bir sokette okuma denenmiyor bile.

Bedeli iki tane. Birincisi, tek bir uzun hesap bütün sunucuyu durdurur, çünkü o sırada döngü başka kimseye bakamaz. İkincisi, engelleyen tek bir çağrı, örneğin asyncio dışından yapılan bir veritabanı sorgusu, aynı etkiyi yaratır. Bu yüzden olay döngüsüyle yazılan sunucularda ağır işler ayrı iş parçacıklarına ya da süreçlere havale edilir.

UDP soketi: bağlantı yok, sorumluluk sende

Datagram soketinde connect, listen ve accept yok. Her sendto tek bir datagram, her recvfrom tek bir datagram:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(1.0)
for deneme in range(3):
    s.sendto(b"saat kac?", ("127.0.0.1", 9999))
    try:
        veri, adres = s.recvfrom(2048)
        print(adres, veri)
        break
    except TimeoutError:
        print("yanıt yok, yeniden deneniyor")

Zaman aşımı ve yeniden deneme döngüsünü yazan sensin, çünkü 7. bölümde gördüğün gibi taşıma hiçbir söz vermiyor. 3. bölümdeki stub resolver da bunu yapıyor. Bir tuzak da var: cevap kaybolduysa, yeniden gönderdiğin istek sunucuda ikinci kez işlenir. “Saat kaç?” için sorun değil, “hesaptan 100 lira çek” için büyük sorun. Yeniden denenecek isteklerin iki kez işlenmesinin zararsız olması gerekiyor.

Aynı kodu 9999’da kimsenin dinlemediği bir makinede çalıştırınca işletim sistemleri ayrışıyor. Karşı makine kapalı porta gelen datagrama ICMP ile “port ulaşılamaz” diyor. Linux ve BSD ailesi bu hatayı yalnızca connect ile karşı ucu sabitlenmiş bir UDP soketine iletiyor; yukarıdaki kod orada üç kez zaman aşımına düşüyor. Windows ise hatayı bağlanmamış sokete de iletiyor. Bu kitabın yazıldığı makinede ilk recvfrom şununla döndü:

ConnectionResetError: [WinError 10054] Varolan bir bağlantı uzaktaki bir ana bilgisayar tarafından zorla kapatıldı

Ortada hiçbir bağlantı yokken “bağlantı sıfırlandı” demesi tuhaf. Hata aslında ICMP’nin cevabı; Windows onu TCP’nin sıfırlama hatasıyla aynı koda eşliyor. Aynı kodu iki işletim sisteminde deneyen öğrenciyi en çok şaşırtan fark bu.

Karşılaşacağın hatalar

Soketin döndürdüğü her hata, ağda olan somut bir şeyin adı:

DurumPython istisnasıLinuxWindowsAğda ne oldu
kimse dinlemiyorConnectionRefusedErrorECONNREFUSED (111)10061SYN’e RST döndü (8. bölüm)
cevap hiç gelmiyorTimeoutErrorETIMEDOUT (110)10060SYN’ler cevapsız kaldı: kapalı makine ya da paketi sessizce atan firewall (49. bölüm)
karşı taraf kestiConnectionResetErrorECONNRESET (104)10054ortada RST geldi
kapanmış bağlantıya yazıldıBrokenPipeError, Windows’ta ConnectionAbortedErrorEPIPE (32)10053karşı taraf kapatmıştı; ilk yazma başarılı görünür, cevap olarak RST gelir ve hata bir sonraki yazmada çıkar
adres kullanımdaOSErrorEADDRINUSE (98)10048port başka bir sokette ya da TIME-WAIT’te (6. bölüm)
ad çözülemedisocket.gaierrorEAI_NONAME (-2)11001NXDOMAIN ya da resolver’a ulaşılamadı (3. bölüm)

İlk iki satır arasındaki fark tanı koymada işe yarıyor. “Reddedildi” hızlı gelir: makine ayakta, yalnızca o portta kimse yok. “Zaman aşımı” yavaş gelir ve sessizliği anlatır. Kodda settimeout vermezsen bu sessizlik uzun sürebilir: Linux varsayılanında (net.ipv4.tcp_syn_retries = 6) kernel’ın pes etmesi iki dakikayı bulabiliyor.

BrokenPipeError satırının arkasında da bir tarih var. C’de kapanmış bir bağlantıya yazmak sürece SIGPIPE sinyali gönderir ve varsayılan davranış süreci öldürmektir. Python açılışta bu sinyali yok saydığı için sana süreç ölümü yerine yakalanabilir bir istisna düşüyor. C ile sunucu yazan birinin ilk öğrendiği şeylerden biri bu sinyali kapatmak.

Kitabın isteğini karşıdan görmek

Son bir deney. Aşağıdaki on beş satır, tek bir isteği karşılayıp kapanan bir web sunucusu:

import socket

with socket.create_server(("127.0.0.1", 8080)) as sunucu:
    baglanti, adres = sunucu.accept()
    with baglanti:
        istek = baglanti.recv(65536).decode("latin-1")   # yalnız deneme için: tek recv
        print(adres)
        print(istek)
        govde = "Merhaba, bu cevabı sen yazdın.\n".encode()
        baglanti.sendall(
            b"HTTP/1.1 200 OK\r\n"
            b"Content-Type: text/plain; charset=utf-8\r\n"
            + f"Content-Length: {len(govde)}\r\n".encode()
            + b"Connection: close\r\n\r\n"
            + govde
        )

Çalıştır ve tarayıcıda http://127.0.0.1:8080 adresini aç. Tarayıcı senin cümleni gösterecek, terminalde ise şuna benzer bir şey belirecek:

('127.0.0.1', 61182)
GET / HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.7977.130 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: tr

Kitabın ilk bölümünde Enter’a bastığında doğan istek bu. Bu kez onu karşı taraftan, kendi kodunun içinden görüyorsun. İlk satırdaki 61182, 6. bölümde kernel’ın tarayıcı için seçtiği kaynak port. Accept-Language: tr tarayıcının dil ayarından geliyor. Connection: keep-alive ise tarayıcının bağlantıyı açık tutmak istediğini söylüyor; sunucumuz Connection: close diyerek bunu geri çeviriyor. (User-Agent satırı kısaltıldı.)

Kodun tek recv ile yetinmesi yalnızca deneme için kabul edilebilir. Gerçek bir sunucu başlıkların bittiği boş satırı görene kadar okumayı sürdürür, gövde varsa Content-Length kadar daha okur. Yukarıdaki çerçeveleme dersinin bir sunucudaki karşılığı bu.

DerinleşmeSoket neden bir dosya gibi davranıyor?

4.2BSD’yi tasarlayanlar ağ bağlantısını Unix’in en eski fikrine oturttular: her şey bir dosya tanıtıcısıdır. Bir soket açtığında elindeki sayı, bir dosya açtığında aldığın sayıyla aynı türden. Bu yüzden read ve write soketlerde de çalışır; bir kabuk betiği bir ağ bağlantısına, bir dosyaya yazar gibi yazabilir.

Bu karar bugün de hissediliyor. select ve epoll aynı anda hem dosyaları hem soketleri izleyebiliyor. Bir sürecin açabileceği tanıtıcı sayısına konan sınır (ulimit -n) açık bağlantı sayısını da sınırlıyor. Yoğun bir sunucunun “Too many open files” hatası vermesi bundan. Dosya açmayan ama çok bağlantı tutan bir sunucu da bu sınıra çarpabilir.

Windows bu noktada ayrışıyor. 1992’de yayımlanan Windows Sockets, 1993’teki 1.1 sürümüyle Berkeley arayüzüne kasıtlı olarak yakın tutuldu. Yine de soket orada bir dosya tanıtıcısı değil, ayrı bir türde tutamak. Kapatmak için close yerine closesocket çağrılıyor ve select yalnızca soketlerle çalışıyor. Python bu farkı senden saklıyor; C ile iki platforma birden yazan biri ise her gün karşılaşıyor.

Özet

  • İstemci beş çağrıdır: getaddrinfo ad çözer, socket uç açar, connect el sıkışır, sendall ile recv veri taşır, close FIN gönderir.
  • sendall’ın dönmesi teslim anlamına gelmez: yalnızca kernel’ın veriyi aldığını söyler.
  • TCP bayt akışıdır: iki gönderim tek okumada birleşebilir, tek gönderim onlarca okumaya bölünebilir. İletinin sınırını protokol kendisi çizer: ayraçla, uzunluk önekiyle ya da gövdeyi anlatan başlıkla.
  • El sıkışmayı kernel yapar: listen sonrası bağlantılar kuyruğa girer, accept her biri için yeni bir soket verir.
  • UDP’de güvenilirliği sen kurarsın: zaman aşımı, yeniden deneme ve iki kez işlenmesi zararsız istekler.
  • Her hata ağdaki bir olaydır: reddedilme RST’dir, zaman aşımı sessizliktir.

Kodunda bir istemci bir sunucuya bağlandı; kitap boyunca da hep böyleydi. Bir taraf sabit bir adreste bekliyordu ve bütün yük onun sırtındaydı. Aynı dosyayı milyonlarca kişi aynı anda isterse o tek sunucu ne yapacak? Ya ortada hiç sunucu olmasaydı?

72. bölüme devam et: Eşten Eşe ve Protokol Tasarımı