Mert Özen Her Satırda Daha İleri
Bilgisayar Bilimi Temelleri Networking ve HTTP Nasıl Çalışır — #01

Bir URL'e Enter'a Bastığında Ne Olur: DNS'ten Render'a Uçtan Uca

Mert Özen 8 Kas 2026 11 dk 6 görüntülenme
Bir URL'e Enter'a Bastığında Ne Olur: DNS'ten Render'a Uçtan Uca

Enter'a bastığında tarayıcı URL'i parçalar, DNS ile IP'yi bulur, TCP ve TLS 1.3 ile bağlantı kurar, HTTP isteğini gönderir ve cevabı render eder; yeni bir bağlantıda ilk byte'tan önce en az üç-dört RTT harcanır. Kendi Kestrel sunucumuza curl -v ve curl -w ile her fazı satır satır okuyoruz.

Sürüm notu: Bu yazıdaki komutlar .NET 10 (ASP.NET Core 10 Kestrel), curl 8.7.1 ve dig 9.10 ile denenmiştir; protokol anlatımı RFC (Request for Comments) 9110 (HTTP Semantics) ve RFC 9846 (TLS 1.3) esas alır.

Önce Tahmin Et

Tarayıcının adres çubuğuna https://bookstore.example/api/books yazıp Enter'a bastın. Bu alan adını daha önce hiç ziyaret etmedin; tarayıcının cache'inde ne IP (Internet Protocol) adresi ne de açık bir bağlantı var. Sunucudan cevabın ilk byte'ı gelmeden önce ağ üzerinden kaç kez gidip gelinir? Bir mi, iki mi, dört mü?

İkinci soru: aynı sayfayı bir saniye sonra yenilediğinde bu sayı ne olur? Bu yazıda bir isteğin yolculuğunun haritasını çıkaracağız ve serinin geri kalanında her durağı ayrı ayrı açacağız. Cevabı kendi makinemde, kendi Kestrel sunucumda curl ile ölçeceğiz.

Haritanın Tamamı

Enter'a basmanla sayfanın ekrana çizilmesi arasında kabaca beş adım var. Tarayıcı URL'i (Uniform Resource Locator) parçalar ve kendi kayıtlarına bakar. DNS (Domain Name System) ile alan adını bir IP adresine çevirir. O adrese bir TCP (Transmission Control Protocol) bağlantısı açar. Bağlantının üzerine TLS (Transport Layer Security) ile şifreli bir kanal kurar. Ve ancak bundan sonra asıl HTTP (Hypertext Transfer Protocol) isteğini gönderir, cevabı alır, parse eder ve render eder.

01-request-journey-tr.png

Diyagram tahmin sorusunun cevabını da veriyor. DNS cache'te değilse bir gidiş-dönüş (RTT, round-trip time) DNS'e gider. TCP'nin 3-way handshake'i bir RTT. TLS 1.3 handshake'i bir RTT. HTTP isteği ve cevabı bir RTT artı sunucunun işleme süresi. Toplam en az dört RTT; DNS cache'teyse üç. Sunucu Frankfurt'taysa ve İstanbul'dan RTT 50 milisaniyeyse, sunucu tek bir satır kod çalıştırmadan önce 150–200 milisaniye harcanmış olur. Sayfayı yenilediğinde ise bağlantı açık kaldıysa (keep-alive) yalnızca son adım tekrarlanır.

Serinin geri kalanı bu haritanın duraklarını sırayla açıyor: ikinci yazıda IP adresleri, port'lar ve NAT (Network Address Translation); üçüncüde TCP; dördüncüde DNS; beşincide TLS 1.3. Sonra HTTP'ye geçiyoruz: altıncı yazıda HTTP/1.1'in kablodaki hâli, yedincide caching, sekizincide HTTP/2, dokuzuncuda HTTP/3 ve UDP (User Datagram Protocol) üzerinde çalışan QUIC (adı artık kısaltma sayılmayan yeni transport protokolü). Son yazıda bütün bunları yavaş bir isteğin teşhisinde birleştiriyoruz.

URL'den IP'ye: Ön Kontroller ve DNS

Tarayıcı ilk iş URL'i parçalar: scheme (https), host (bookstore.example), port (yazılmamışsa https için 443), path (/api/books) ve varsa query string. Ağa çıkmadan önce birkaç yerel kontrol yapar. Alan adı HSTS (HTTP Strict Transport Security) listesindeyse http:// yazılmış olsa bile isteği doğrudan https://'e çevirir. Aynı kaynak için taze bir cevap HTTP cache'te duruyorsa ağa hiç çıkmayabilir; bu konuyu serinin yedinci yazısında açıyoruz. HSTS'in ASP.NET Core tarafı, CORS (Cross-Origin Resource Sharing) ve HTTPS (HTTP Secure) yönlendirmesiyle birlikte ASP.NET Core Web API (Application Programming Interface) serisinin "ASP.NET Core Web API'de CORS ve Güvenlik: HTTPS, HSTS, Header'lar ve Secret'lar" yazısında.

Sonra isim çözümleme gelir. Tarayıcı ve işletim sistemi önce kendi cache'lerine ve /etc/hosts dosyasına bakar. macOS'ta işletim sisteminin resolver'ının localhost için ne döndüğünü görmek için:

$ dscacheutil -q host -a name localhost
name: localhost
ipv6_address: ::1

name: localhost
ip_address: 127.0.0.1

localhost iki adrese çözülüyor ve IPv6 adresi (::1) önce geliyor. Bu ayrıntı biraz sonra curl çıktısında karşımıza çıkacak. Gerçek bir alan adı için yerel kayıtlarda bir şey yoksa sorgu ağdaki recursive resolver'a gider. Ev ağımdaki resolver'a sorulmuş bir sorgu:

$ dig example.com A +noall +answer +stats
example.com.		132	IN	A	104.20.23.154
example.com.		132	IN	A	172.66.147.243
;; Query time: 16 msec
;; SERVER: 192.168.8.1#53(192.168.8.1)

İki A kaydı, her biri 132 saniyelik bir TTL (Time To Live) ile: resolver bu cevabı 132 saniye daha cache'te tutacak. Sorgu 16 milisaniye sürdü. Resolver'ın cevabı bilmediği durumda root, TLD (Top-Level Domain) ve authoritative sunuculara giden zinciri, record tiplerini ve TTL'in production'daki etkilerini serinin dördüncü yazısında ayrıntısıyla ele alıyoruz.

curl -v'yi Satır Satır Okumak

Haritayı somutlaştırmak için Bookstore API'nin küçük bir kopyasını Kestrel ile yazdım ve bir Linux container'ında çalıştırdım. Sunucu kendi oluşturduğum yerel bir CA'nın (Certificate Authority) imzaladığı sertifikayı kullanıyor; curl'e bu CA'yı --cacert ile tanıtıyorum. Önce localhost ile bağlanırken çıktının başı:

$ curl -v --http1.1 --cacert certs/ca.crt https://localhost:5592/api/books/1
* Host localhost:5592 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:5592...
* connect to ::1 port 5592 from ::1 port 62043 failed: Connection refused
*   Trying 127.0.0.1:5592...
* Connected to localhost (127.0.0.1) port 5592

İlk dört satır DNS adımı: localhost iki adrese çözüldü. curl önce IPv6'yı denedi ve reddedildi, çünkü container'ın port'u yalnızca 127.0.0.1'e publish edilmiş. Sonra IPv4'e düştü ve bağlandı. Buradaki "Connected" satırı TCP'nin 3-way handshake'inin tamamlandığı an. Reddedilme burada anında oldu; ama bir firewall paketi sessizce düşürseydi, curl IPv4'e geçmeden önce bir süre beklerdi. Bu tuzağı ikinci yazıda inceliyoruz. Aynı isteği doğrudan 127.0.0.1 ile yapınca gelen çıktının geri kalanı:

*   Trying 127.0.0.1:5592...
* Connected to 127.0.0.1 (127.0.0.1) port 5592
* ALPN: curl offers http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
} [293 bytes data]
*  CAfile: certs/ca.crt
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* (304) (IN), TLS handshake, Unknown (8):
{ [21 bytes data]
* (304) (IN), TLS handshake, Certificate (11):
{ [456 bytes data]
* (304) (IN), TLS handshake, CERT verify (15):
{ [78 bytes data]
* (304) (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* (304) (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / AEAD-AES256-GCM-SHA384 / [blank] / UNDEF
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=bookstore.localhost
*  start date: Nov  8 09:39:16 2026 GMT
*  expire date: Dec  8 09:39:16 2026 GMT
*  subjectAltName: host "127.0.0.1" matched cert's IP address!
*  issuer: CN=Bookstore Local Root CA
*  SSL certificate verify ok.
* using HTTP/1.x
> GET /api/books/1 HTTP/1.1
> Host: 127.0.0.1:5592
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sun, 08 Nov 2026 09:59:52 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
<
{ [67 bytes data]
* Connection #0 to host 127.0.0.1 left intact
{"id":1,"isbn":"978-0000000001","title":"Title 1","price":11}

Satır satır gidelim. ALPN: curl offers http/1.1: ALPN (Application-Layer Protocol Negotiation), TLS handshake'inin içinde istemcinin hangi HTTP sürümlerini konuşabildiğini söylediği extension; burada --http1.1 verdiğim için yalnızca HTTP/1.1'i önerdi. OUT ve IN ile işaretli satırlar TLS mesajları: istemcinin Client hello'su, sunucunun Server hello'su, ardından şifreli gelen extension'lar (curl bu macOS build'inde onları "Unknown (8)" diye adlandırıyor, TLS 1.3'teki karşılığı EncryptedExtensions), sertifika, sertifikanın private key'e sahip olunduğunu kanıtlayan CERT verify ve iki taraftan Finished. Sunucunun bütün mesajları tek bir uçuşta geliyor; TLS 1.3'ün 1-RTT handshake'i bu. Ardından curl sertifikanın kimliğini kontrol ediyor: SAN (Subject Alternative Name) alanında bağlandığı host var mı, imza zinciri güvendiği bir CA'ya çıkıyor mu. SSL certificate verify ok ikisinin de geçtiğini söylüyor.

> ile başlayan satırlar giden HTTP isteği, < ile başlayanlar gelen cevap. İstek dört satır: method, path ve sürüm; Host header'ı; curl'ün kendini tanıttığı User-Agent; ve kabul ettiği içerik tipleri. Cevapta status line, header'lar ve body var. Transfer-Encoding: chunked dikkat çekici: Kestrel JSON'u (JavaScript Object Notation) yazarken boyutunu önceden bilmediği için Content-Length yerine body'yi parça parça gönderiyor. Bu framing'i altıncı yazıda kablodan okuyacağız. Son satırdaki left intact, bağlantının kapatılmadığını, bir sonraki istek için açık tutulduğunu söylüyor.

HTTP'nin kendisini ve curl ile okumayı daha önce Web Güvenliği serisinin "Güvenlik Laboratuvarı: ASP.NET Core 10 ile Bookstore API ve HTTP'yi curl ile Okumak" yazısında görmüştük; burada odağımız, HTTP'nin altında kalan katmanlar.

Fazları Ölçmek: curl -w

-v neyin olduğunu gösterir, ne kadar sürdüğünü göstermez. curl'ün -w (write-out) seçeneği her fazın bittiği anı, isteğin başından itibaren saniye cinsinden yazar. Bir format dosyası hazırladım:

   namelookup: %{time_namelookup}s\n      connect: %{time_connect}s\n   appconnect: %{time_appconnect}s\n  pretransfer: %{time_pretransfer}s\nstarttransfer: %{time_starttransfer}s\n        total: %{time_total}s\n      version: HTTP/%{http_version}  remote: %{remote_ip}:%{remote_port}\n

Sunucuda 120 milisaniye bekleyip cevap veren bir endpoint'e aynı curl komutunda arka arkaya iki istek attım. curl ikinci istekte ilk bağlantıyı yeniden kullanır:

$ curl -s -w "@curl-format.txt" --cacert certs/ca.crt -o /dev/null 'https://localhost:5592/slow?ms=120' -o /dev/null 'https://localhost:5592/slow?ms=120'
   namelookup: 0.000009s
      connect: 0.000276s
   appconnect: 0.005367s
  pretransfer: 0.005408s
starttransfer: 0.133954s
        total: 0.134047s
      version: HTTP/2  remote: 127.0.0.1:5592
   namelookup: 0.000014s
      connect: 0.000000s
   appconnect: 0.000000s
  pretransfer: 0.000053s
starttransfer: 0.123424s
        total: 0.123567s
      version: HTTP/2  remote: 127.0.0.1:5592

Değerler kümülatif; bir fazın süresi, iki ardışık değerin farkı. İlk istekte DNS 9 mikrosaniye (localhost yerel), TCP bağlantısı 0,27 milisaniye (IPv6 denemesi dahil), TLS handshake'i appconnect − connect = 5,1 milisaniye. Sunucunun ilk byte'ı göndermesi (starttransfer − pretransfer) 128,5 milisaniye; bunun 120'si bilerek koyduğum bekleme. İkinci istekte connect ve appconnect sıfır: yeni bağlantı açılmadı, handshake yapılmadı.

Bu ölçüm loopback üzerinde; paketler makineden hiç çıkmıyor, RTT mikrosaniyeler. Gerçek bir ağda TCP ve TLS fazlarının her biri en az bir RTT olur ve haritadaki hesap devreye girer. Burada bile TLS'in 5 milisaniyesi dikkat çekici: loopback'te ağ maliyeti yok, bu süre neredeyse tamamen kriptografik işlem ve sertifika doğrulaması. Bu fazları gerçek bir yavaşlık teşhisinde nasıl kullanacağımızı serinin son yazısında göreceğiz.

Katmanlar: OSI ve TCP/IP

curl çıktısında DNS, TCP, TLS ve HTTP satırlarının ayrı ayrı görünmesi tesadüf değil; ağ protokolleri katmanlar hâlinde tasarlanır. Her katman bir alttakinin sunduğu hizmeti kullanır ve göndereceği veriye kendi başlığını ekler. Buna encapsulation denir:

01-encapsulation-tr.png

HTTP mesajı TLS record'unun içinde şifrelenir, TLS record'u bir TCP segment'inin payload'ı olur, TCP segment'i bir IP packet'ine konur, IP packet'i de Ethernet veya Wi-Fi frame'inin içinde kabloya çıkar. Karşı tarafta her katman kendi başlığını okuyup çıkarır ve içeriği bir üsttekine verir.

Okullarda ve mülakatlarda OSI (Open Systems Interconnection) modelinin yedi katmanı öğretilir. Dürüst olmak gerekirse OSI bir öğretim ve terminoloji modeli olarak hayatta kaldı; internetin gerçek protokol yığını TCP/IP'nin dört katmanı: link, internet, transport ve application. "Layer 4 load balancer" veya "Layer 7 firewall" gibi ifadelerde OSI numaralarını görmeye devam edeceksin; bunlar transport ve application katmanını kastediyor. TLS'in hangi katmana ait olduğu ise net değil: TCP'nin üzerinde, HTTP'nin altında duruyor ve OSI'nin 5. ve 6. katmanlarına kabaca denk geliyor.

Tarayıcı Tarafı: DevTools Timing ve Render

Tarayıcının DevTools'undaki Network panelinde bir isteğe tıklayıp Timing sekmesini açtığında aynı fazları farklı isimlerle görürsün; TLS'in eski adı SSL (Secure Sockets Layer) burada hâlâ yaşıyor. Kabaca karşılıkları:

DevTools Timingcurl -w karşılığıAnlamı
Queueing, StalledyokTarayıcı içinde bekleme: öncelik, host başına bağlantı limiti
DNS Lookuptime_namelookupİsim çözümleme
Initial connectiontime_connectTCP handshake (TLS dahil gösterilir)
SSLtime_appconnect − time_connectTLS handshake
Waiting for server response (TTFB)time_starttransfer − time_pretransferİlk byte'a kadar geçen süre
Content Downloadtime_total − time_starttransferBody'nin inmesi

TTFB (Time To First Byte) sütunu en çok yanlış okunan değer: içinde hem bir RTT hem de sunucunun işleme süresi var. Tarayıcı cevabı aldıktan sonra işin ikinci yarısı başlar: HTML (HyperText Markup Language) parse edilir, CSS (Cascading Style Sheets) ve JavaScript için yeni istekler atılır, layout hesaplanır ve ekran çizilir. Render pipeline'ı ve Core Web Vitals ilerideki Web Performansı serisinin konusu; DevTools panellerinin kullanımı da ilerideki Browser DevTools serisinde. Bu seride ağın kendisinde kalıyoruz.

Üretimde Dikkat

Haritadaki en pahalı şey bağlantı kurmak. Yeni bir bağlantı en az iki RTT (TCP + TLS) demek ve bu maliyet istemciye çok uzak bir sunucuda yüzlerce milisaniyeye çıkar. Bu yüzden bağlantıları yeniden kullanmak (keep-alive, HTTP/2'de tek bağlantı üzerinde çoklu istek) ve kullanıcıya yakın sunucular, yani bir CDN (Content Delivery Network) gerçek dünyadaki en büyük kazanımlar. .NET'te bağlantı yeniden kullanımının HttpClient ile nasıl bozulduğunu üçüncü yazıda göreceğiz.

Yavaşlık şikâyeti geldiğinde ilk iş fazları ayırmak. "API yavaş" cümlesi DNS'in, bağlantı kurmanın, sunucunun veya indirmenin yavaş olduğu anlamına gelebilir ve her birinin çözümü farklı. curl -w çıktısı veya DevTools Timing'i ekran görüntüsü yerine sayı olarak ticket'a eklemek, sorunu doğru ekibe yönlendirmenin en hızlı yolu.

Ölçümü kullanıcının bulunduğu yerden yap. Sunucunun kendi üzerinde curl localhost ile ölçtüğün 5 milisaniye, kullanıcının mobil ağdan yaşadığı 600 milisaniyeyi hiçbir zaman göstermez.

Sık Düşülen Tuzaklar

TTFB'yi sunucu süresi sanmak. TTFB bir RTT'yi de içerir; uzak bir istemcide 200 milisaniyelik TTFB'nin 150'si ağ olabilir.

Loopback ölçümlerini genellemek. Yerel makinede TCP ve TLS neredeyse bedava görünür; gerçek ağda her biri en az bir RTT'dir.

localhost'u tek bir adres sanmak. İki adrese çözülür ve IPv6 genelde önce denenir; sunucun yalnızca IPv4'te dinliyorsa her bağlantı önce bir başarısız deneme yapar.

OSI katman numaralarına fazla anlam yüklemek. Gerçek protokoller bu kutulara tam oturmaz; L4 (Layer 4) ve L7 (Layer 7) ifadelerini transport ve application diye oku.

Sertifika hatasında -k ile geçmek. curl -k doğrulamayı tamamen kapatır; yerel testte bile doğru yol, bu yazıda yaptığım gibi CA'yı --cacert ile tanıtmak.

Küçük Bir Deneme

★☆☆ Kendi makinende sık kullandığın bir sitenin alan adı için dig çalıştır ve TTL değerini not et. Bir dakika sonra tekrar çalıştır: TTL nasıl değişti, neden?

★★☆ Herhangi bir yerel ASP.NET Core uygulamasına bu yazıdaki curl-format.txt ile istek at. Aynı komutta iki URL vererek bağlantı yeniden kullanımını, iki ayrı curl komutuyla yeni bağlantıyı ölç. appconnect farkı ne kadar?

★★★ Bir tarayıcıda DevTools Network panelini açıp aynı isteği iki kez yap; Timing sekmesindeki değerleri curl çıktısıyla eşleştir. İkinci istekte hangi satırlar kayboluyor? "Disable cache" kutusunu işaretlemek neyi değiştiriyor?

Haritanın ilk durağı adresler. Bir sonraki yazıda IP adreslerine, subnet'lere, port'lara ve NAT'a iniyoruz ve bu yazıda gördüğümüz ::1 reddinin, Docker'da "çalışıyor ama erişilemiyor" sorununun ve localhost ile 0.0.0.0 farkının nereden geldiğini açıyoruz.