Bölüm 71: Soket Programlama
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
- Giriş
- Adres Çubuğundan İsteğe
- Paket Anahtarlamalı Yol
- İsimden Adrese
- DNS Kayıtları
- Önbellek, Tazelik ve Güven
- Port Numarası ve Soket
- Bağlantısız Taşıma: UDP
- Üç Adımlı El Sıkışma
- Simetrik Şifreleme
- Açık Anahtarlı Kriptografi
- TLS El Sıkışması
- Sertifika Zinciri
- HTTP Mesajının Anatomisi
- Durum, Çerez ve Oturum
- HTTP/2'den HTTP/3'e
- İçerik Dağıtım Ağları
- Diğer Uygulama Protokolleri
- Katmanlı Mimari
- Hizmet ve Protokol
- Encapsulation ve Başlıklar
- Paket Yakalama ve Çözümleme
- Ağ Interface Kartı
- Fiziksel Adresleme
- Hata Sezme ve CRC
- Adres Çözümleme: ARP
- Ortam Erişimi: CSMA/CD
- Switch'ler
- Ağ Topolojileri
- Sanal Yerel Ağlar
- Kablosuz Erişim: 802.11
- Radyo Kanalı ve Kapasite
- Kablosuz Ağ Güvenliği
- Hücresel Ağlar
- Spektrum ve Kuşaklar
- Hareketlilik ve Aktarma
- Adres Kiralama: DHCP
- Adresin Anatomisi
- Adres Tükenmesi ve IPv6
- Router Mimarisi
- Kuyruk Yönetimi
- Cihaz Yapılandırma
- Routing Protokolleri
- Kontrol Düzlemi ve SDN
- Ağ İzleme ve Yönetimi
- Alanlar Arası Routing
- Adres Çevirisi
- Ara Kutular
- Tünelleme ve IPsec
- Firewall'lar
- Ağ Saldırıları
- Operatör ve Metro Ağları
- Bitten Sinyale
- Sembol Hızı ve Kanal Kapasitesi
- Multiplexing Teknikleri
- İletim Ortamları
- Bakır Kablo Sonlandırma
- Fiber Sonlandırma
- Fiber Optik İletim
- Bakır Erişim Ağı: DSL
- DSL'de Hız ve Mesafe
- Pasif Optik Ağlar
- Kablo Erişim Ağları
- Kablo Modem ve CMTS
- Denizaltı Kabloları
- Veri Merkezi Ağları
- Encapsulation Çözme
- Güvenilir Aktarım
- Tıkanıklık Denetimi
- Trafik Çözümleme
- Gecikme Bütçesi
- Soket Programlama
- Eşten Eşe ve Protokol Tasarımı
- Uyarlanır Video Akışı
- Gerçek Zamanlı Ses ve Görüntü
- Kablosuz Sensör Ağlarının Mimarisi
- Sensör Ağlarında Ortam Erişimi
- Sensör Ağlarında Yönlendirme ve IP
- Zaman Eşitleme ve Konum Bulma
- Sensör Ağı Uygulamaları
- Mobil Kablosuz Sensör Ağları
- 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-ISTKodun 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 GMTcreate_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 certificateBu 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öntem | Nasıl çalışıyor | Örnek |
|---|---|---|
| ayraç | iletinin sonu belirli bir bayt dizisiyle işaretlenir | HTTP başlıkları boş satırla, yani \r\n\r\n ile biter (13. bölüm) |
| uzunluk öneki | her iletinin başında kaç bayt süreceği yazar | TLS kaydında 2 bayt, HTTP/2 frame’inde 3 bayt uzunluk alanı (11. ve 15. bölüm) |
| kendini anlatan gövde | başlık, gövdenin nasıl okunacağını söyler | Content-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:
| Model | Nasıl | Bedeli |
|---|---|---|
| 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 kod | her 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 soket | kod “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ı:
| Durum | Python istisnası | Linux | Windows | Ağda ne oldu |
|---|---|---|---|---|
| kimse dinlemiyor | ConnectionRefusedError | ECONNREFUSED (111) | 10061 | SYN’e RST döndü (8. bölüm) |
| cevap hiç gelmiyor | TimeoutError | ETIMEDOUT (110) | 10060 | SYN’ler cevapsız kaldı: kapalı makine ya da paketi sessizce atan firewall (49. bölüm) |
| karşı taraf kesti | ConnectionResetError | ECONNRESET (104) | 10054 | ortada RST geldi |
| kapanmış bağlantıya yazıldı | BrokenPipeError, Windows’ta ConnectionAbortedError | EPIPE (32) | 10053 | karşı taraf kapatmıştı; ilk yazma başarılı görünür, cevap olarak RST gelir ve hata bir sonraki yazmada çıkar |
| adres kullanımda | OSError | EADDRINUSE (98) | 10048 | port başka bir sokette ya da TIME-WAIT’te (6. bölüm) |
| ad çözülemedi | socket.gaierror | EAI_NONAME (-2) | 11001 | NXDOMAIN 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: trKitabı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:
getaddrinfoad çözer,socketuç açar,connectel sıkışır,sendallilerecvveri taşır,closeFIN 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:
listensonrası bağlantılar kuyruğa girer,accepther 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ı