Bölüm 15: HTTP/2'den HTTP/3'e
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 3: İstek Yazılıyor (Bölüm 13-17)
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
Bir sayfa yüzlerce parçadan oluşuyor ve her parça için yolu baştan yürümek eski çözümdü. İstek satırını ve durum kodlarını açarken, tek bağlantıya kaç isteğin sığdığını tek cümleyle bırakmıştım.
Borcu burada ödüyorum. Aynı yolun üstünde kaç araba var ve arkadaki neden bekliyor?
Bağlantıyı açık bırakmak neyi kurtarıyor?
Kalıcı olmayan kipte bir taşıma bağlantısı en çok tek bir nesne taşıyordu. Klasik anlatım dört adımdı: bağlan, ne istediğini söyle, cevabı al, bağlantıyı kes. Dördü de her nesnede baştan tekrarlanıyordu.
Bedeli RTT’yle ölçebilirsin. Kalıcı olmayan kipte her nesne iki tur yiyor: biri bağlantı kurulumu, biri istek ve yanıtın ilk baytları. 8. bölümde üç adımlı el sıkışmanın bedelini sayarken bu turun ilkini adım adım görmüştük.
Sayıya dökelim. 60 bağlı nesne taşıyan bir sayfa düşün ve RTT 40 ms olsun. Bağlantılar teker teker açılırsa yalnızca kurulum ve istek turları 4.800 ms tutuyor, yani 4,8 saniye. Dosya aktarım süreleri bu hesaba daha hiç girmedi; sayfa açılmadan önce sırf yolu kurmak için beş saniyeye yakın bekliyordun.
Ödediğin şey yalnızca gecikme de değil. Her yeni bağlantı işletim sistemi tarafında tampon, durum kaydı ve kontrol bloğu istiyor; bunların hepsi altmış kez birden kuruluyor.
HTTP/1.1 varsayılanı tersine çevirdi. RFC 9112’de kalıcı bağlantı varsayılandır: sunucu yanıtı gönderdikten sonra bağlantıyı açık bırakıyor ve aynı bağlantıdan birden çok istek ile yanıt geçiyor. Varsayılandan çıkmak için Connection başlığına close yazman gerekiyor. Kapatmak artık istisna.
Kazanç burada görünüyor. Kalıcı bağlantı kurulum turunu bir kereye indiriyor: aynı sayfa bir kurulum artı altmış istek, yani 61 tur ve 2.440 ms sürüyor. Süre yarıya indi ama her nesne hâlâ kendi turunu bekliyor, çünkü istemci bir yanıtı almadan sıradaki isteği göndermiyor.
Sıra zorunluluğu kimi bekletiyor?
HTTP/1.1 bu yüzden bir de boru hattını (pipelining) getirdi. İstemci yanıtı beklemeden istekleri arka arkaya gönderebiliyor. Altmış isteğin hepsi aynı turda yola çıkarsa sayfa iki gidiş-dönüşe, yani 80 ms’ye iniyor: biri kurulum, öbürü bütün nesneler için. İlk hesaba göre fark 60 kat. Sunucu güvenli metotları paralel işleyebilir ama yanıtları isteklerin geldiği sırayla göndermek zorunda.
Zorunluluğun sebebi sade. HTTP/1.1’de istekleri yanıtlarla eşleştiren bir kimlik alanı yok; tek dayanak varış sırası. Protokolün tek varsayımı da güvenilir ve sırayla teslim eden bir taşıma. HTTP/1.0’da durum daha dardı: bir bağlantıda aynı anda yalnızca tek bir istek açık kalabiliyordu.
Sonuç şu: küçük bir nesne, önündeki büyük nesnenin arkasında beklemek zorunda kalıyor. Buna baş engellemesi (head-of-line blocking) deniyor. Burada gördüğün, application layer’daki baş engellemesi. Boru hattı standartta yer aldığı hâlde tarayıcıların hiçbiri onu varsayılan olarak açmadı; bu engelleme ve yoldaki hatalı proxy’ler kazancından fazla sorun çıkarıyordu.
tek bağlantı, yanıtlar isteklerin geliş sırasında
istekler : A ---> B ---> C
yanıtlar : [============ A ============][ B ][ C ]
^ B hazırdı, A bitene kadar bekledi2. bölümde paket anahtarlamayı ve kuyruk gecikmesini otoyol şeritleriyle anlatmıştım; tek şeritte önündeki aracı geçemezsin. Tarayıcılar bu şeritten kaçmak için aynı sunucuya paralel bağlantılar açıyor. RFC 9112 kaçışın sebebini açıkça yazıyor. Standardın eski sürümü paralel bağlantı sayısını ikiyle sınırlıyordu. O tavan kaldırıldı, çünkü pek çok uygulama için pratik değildi; yerine ölçülü olma öğüdü kaldı. Tarayıcılar bugün aynı sunucuya en fazla altı bağlantı açıyor.
Ölçülü olmayan taraf kazanıyor. 9 bağlantının paylaştığı bir hatta tek bağlantı isteyen onda bir pay alıyor, 11 bağlantı açan ise hattın yarısından fazlasını alıyor. Tıkanıklık denetimini polisleyen kimse yok. Dahası, tıkanık olmayan bir ağda bile eşzamanlı başlayan gönderimler, daha az bağlantıyla hiç oluşmayacak bir tıkanıklık yaratabiliyor.
Frame’e bölmek yetiyor mu?
HTTP/2 ilk olarak 2015’te RFC 7540 ile yayımlandı; yürürlükteki belgesi RFC 9113 oldu. Metotlar, durum kodları ve başlık alanlarının çoğu HTTP/1.1’den olduğu gibi kaldı. Değişen anlam değil, taşıma biçimi.
İkili framing geldi. Nesneler frame’lere bölünüyor ve frame’ler iç içe geçirilerek gönderiliyor; küçük nesne artık büyüğün bitmesini beklemiyor. Gönderim sırası da geliş sırasına göre değil, istemcinin bildirdiği önceliğe göre kurulabiliyor. Başlık alanları ayrıca HPACK adlı bir yöntemle sıkıştırılarak kodlanıyor.
HTTP/2 bir yenilik daha getirmişti: sunucu itmesi (server push). Sunucu, tarayıcının birazdan isteyeceğini tahmin ettiği nesneleri istenmeden gönderebiliyordu. Pratikte tarayıcının önbelleğinde zaten duran nesneleri de yeniden yolladığı için kazançtan çok israf üretti ve Chrome 2022’de desteği kaldırdı.
Paralellik sayıyla korunuyor. SETTINGS_MAX_CONCURRENT_STREAMS için önerilen alt sınır 100 eşzamanlı stream ve gerekçesi açık: paralelliği gereksiz yere kısmamak. Her stream 65.535 baytlık bir başlangıç penceresiyle açılıyor. Pencere, alıcının “şu kadar baytı onaylamadan gönderme” demesinin yoludur ve adı flow control (akış denetimi); hızlı bir göndereni yavaş bir alıcının tamponunu taşırmaktan alıkoyar.
İş burada bitmiyor. HTTP/2 application layer’daki baş engellemesini çözüyor, transport layer’dakini çözmüyor. RFC 9113 bunu kendi giriş bölümünde açıkça yazıyor. Sebebi tek cümle: tek taşıma bağlantısı verinin sırayla teslim edilmesini garanti ediyor. Kaybolan ya da sırası bozulan tek bir paket, o paketle hiç ilgisi olmayan bütün işlemleri de durduruyor.
Multiplexing’i application layer’a koydular ama alttaki taşımanın kayıp kurtarma mekanizması onu göremiyor. Tarayıcıların hâlâ paralel bağlantı açma teşviki buradan geliyor.
Taşımanın kendisi neden değişti?
Çıplak bir taşıma bağlantısı kendi başına hiçbir güvenlik getirmiyor. Şifreleme ayrı bir katmandan geliyor ve ayrı bir el sıkışma istiyor. Eski kurulumda iki el sıkışma arka arkaya çalışıyordu: önce taşımanın güvenilirlik ve tıkanıklık durumu, sonra güvenlik katmanının kimlik ve anahtar durumu. Biri bitmeden öbürü başlamıyordu.
HTTP/3 anlamları hiç değiştirmiyor. RFC 9114 aynı HTTP anlamlarını yeni bir taşıma protokolünün üstüne taşıyor: QUIC. Yığın şöyle diziliyor: HTTP/3, altında QUIC, onun altında UDP, onun altında IP.
QUIC yeni bir fikir de icat etmiyor. Bağlantı kurulumu, hata denetimi ve tıkanıklık denetimi için bilinen taşıma çözümlerini uygulama tarafına taşıyor. Spesifikasyon algoritmaların koşut olduğunu kendisi söylüyor.
Asıl fark akışlarda. QUIC’te stream multiplexing’i ve stream başına flow control taşıma protokolünün kendi işi. Güvenilir aktarım stream başına ayrı tutuluyor, tıkanıklık denetimi ise ortak: bütün stream’ler tek bir bütçeyi paylaşıyor. Paket kaybında yalnızca o pakette verisi bulunan stream’ler yeniden gönderimi bekliyor. Ötekiler yoluna devam ediyor.
Üç kuşağı aynı kayıp üstünde karşılaştırmak istersen:
B hazır olsa bile A bitene kadar bekliyor.- HTTP/1.1: sıra zorunluluğu. Tek bağlantıda yanıtlar isteklerin geldiği sırayla dönmek zorunda, çünkü hangi yanıtın hangi isteğe ait olduğunu söyleyen bir kimlik alanı yok.
Bhazır olsa bileAbitene kadar bekliyor. - HTTP/2: aynı bağlantıda üç akış birden. Her nesne kendi akış kimliğini taşıyan frame'lere bölünüyor ve frame'ler karışık akabiliyor. Application layer'ındaki engelleme kalkıyor:
BartıkAyı beklemiyor. - Ama engelleme yok olmadı, bir kat aşağı indi. Üç akış tek bir TCP bağlantısında. Bir segment kaybolduğunda TCP sırayı korumak için bütün akışları bekletiyor; kaybolan frame
Cye ait olsa bileAveBde duruyor. Transport layer akışların varlığından habersiz. - HTTP/3: sıra her akışta ayrı tutuluyor. QUIC akış kavramını transport layer'ına indiriyor ve her akış kendi sıra numarasını taşıyor. Kaybolan paket yalnızca kendi akışını bekletiyor;
AileBakmayı sürdürüyor.
Ayrım paket sınırında duruyor. Multiplexing, farklı stream’lerin STREAM frame’lerini aynı QUIC paketlerine iç içe koyuyor. Tek pakete birden çok stream’in verisi konabiliyor. O paketin kaybı hepsini birden durduruyor. Gerçekleştirimlere pakete gereğinden fazla stream koymamaları öğütleniyor.
DerinleşmeQUIC neden kernel'a değil, UDP üstüne kuruldu?
Cevap teknik olmaktan çok dağıtımla ilgili.
Klasik bir taşıma protokolü kernel’ın içinde yaşar. Yeni bir taşıma yazmak, dünyadaki bütün işletim sistemlerinin onu göndermesini beklemek demek. Üstelik yoldaki kutular, yani adres çeviren NAT’lar (46. bölüm) ve firewall’lar, tanımadıkları bir protokol numarasını geçirmiyor; 2000’de standartlaşan SCTP’nin geniş internete hiç yayılamamasının sebebi buydu. İki klasik taşıma protokolü 40 yıl boyunca alanın tamamını tuttu; sebebi rakipsiz olmaları değil, rakibin oraya girememesiydi.
QUIC kilidi kırmak için taşımayı kernel’ın dışına çıkardı. Altta kalan tek şey UDP ve UDP her yerde zaten var: yeni bir kernel sürümü de yeni bir soket ailesi de gerekmiyor.
Bunun pratik karşılığı yayılma hızı. QUIC önce Google sunucularında, Chrome’da ve mobil YouTube uygulamasında çalıştı; standartlaşması sonradan geldi. Bir tarayıcı güncellemesi, bir sunucu güncellemesi ve yeni taşıma sahada. Aynı işi kernel üstünden yapmak yıllar alırdı.
Bedeli de var. Uygulama tarafında çalışan bir taşıma, kernel’ın yıllarca ayarlanmış kodundan ve donanım hızlandırmalarından mahrum kalıyor. Şifreleme her pakette çalışıyor ve bu işlemci zamanı kullanıcının makinesinden çıkıyor.
El sıkışma da tek tura indi. QUIC, TLS 1.3’ü transport layer’ının içine aldı; güvenilirlik, tıkanıklık denetimi, kimlik doğrulama ve şifreleme durumu tek el sıkışmada birlikte kuruluyor. İstemci daha önce sakladığı oturum biletini kullanırsa el sıkışma gecikmesi sıfıra iniyor ve uygulama verisi ilk pakette gidiyor. O biletin bedelini biliyorsun: tekrar oynatma saldırısına karşı koruma yok. RFC 9114 bu yüzden karşı önlemleri zorunlu tutuyor.
Bir kazanç daha var. QUIC bağlantısı adresle değil, bağlantı kimliğiyle tanınıyor ve bu kimlik QUIC sürüm 1’de en çok 20 bayt olabiliyor. Uç noktanın adresi ve portu değişse bile bağlantı yaşamaya devam ediyor; buna bağlantı göçü deniyor. Değişim her zaman kasıtlı da değil; aradaki NAT kutusu yeni bir giden port atadığında da oluyor. Her iki durumda da yeni yolun doğrulanması gerekiyor; doğrulama başarısızsa yalnızca o yol kullanılmıyor. Göçü yalnızca istemci başlatabiliyor, üstelik el sıkışma doğrulanmadan başlatamıyor.
HTTP/3 tarafında eşleme sade. Her istek, istemcinin açtığı çift yönlü bir QUIC stream’inde gidiyor ve bir stream’de yalnızca tek bir istek gönderilebiliyor. İkinci istek yeni stream demek. Stream kimliği 62 bitlik bir tam sayı; en düşük bit stream’i kimin açtığını, ikinci bit tek yönlü mü çift yönlü mü olduğunu söylüyor. İki bit dört stream türü üretiyor.
HTTP/3 mesajının parçaları frame tür kodu
başlık bölümü HEADERS 0x01
gövde DATA 0x00
kuyruk alanları (trailers) HEADERS 0x01
bağlantı ayarları SETTINGS 0x04
kapanış duyurusu GOAWAY 0x07Gövdesi olmayan bir yanıtta DATA frame’i hiç gönderilmiyor; gövde 0 bayt ise ortada frame de yok. Her uç ayrıca en az 3 tek yönlü stream’e izin vermek zorunda: bir HTTP denetim stream’i ve başlıkları sıkıştıran QPACK’in iki stream’i. Bu stream’lere en az 1.024 baytlık flow control kredisi verilmesi öğütleniyor.
Bunu kendi makinende görebilirsin. Sürüm seçimi TLS el sıkışmasında ALPN uzantısındaki h3 belirteciyle bildiriliyor, sunucu da desteğini Alt-Svc yanıt başlığıyla duyuruyor. HTTP/3 için zorunlu tek bir UDP portu yok; duyuruda port bu yüzden açıkça yazılıyor.
Alt-Svc’nin kusuru ilk bağlantıda görünüyor: duyuru bir yanıt başlığında geldiği için tarayıcı onu ancak eski protokolle bir tur attıktan sonra öğreniyor. 4. bölümde DNS kayıtlarını sayarken andığım HTTPS kaydı o turu siliyor. Kayıt, DNS cevabının içinde “bu sunucu h3 konuşuyor” diyor ve tarayıcı daha ilk bağlantısını QUIC ile açıyor.
curl -sI https://cloudflare-quic.com/ | grep -i '^alt-svc'
# alt-svc: h3=":443"; ma=86400Sonra trafiği dinle. İlk QUIC datagramı en az 1.200 bayt olmak zorunda ve gerekirse PADDING frame’leriyle şişiriliyor; sebebi yükseltme saldırısını sınırlamak. Yol o boyu taşıyamıyorsa QUIC hiç kullanılmıyor. QUIC en küçük IP paketini 1.280 bayt varsayıyor. IPv6’da bundan 40 baytlık IP başlığı ile 8 baytlık UDP başlığı düşülünce her yolda taşınacağı garanti edilen UDP yükü 1.232 bayt çıkıyor. IPv4 başlığı daha kısa olduğu için orada bu sayı 1.252 bayt.
sudo tcpdump -n -c 5 'udp port 443'QUIC de 443’ü kullanıyor, ama UDP üstünde. UDP’nin engellendiği bir ağda hiçbir şey göremezsin, çünkü orada QUIC bağlantısı hiç kurulamıyor. Standart bu durumda istemcinin eski sürümlere düşmesini bekliyor.
Özet
- Kalıcı bağlantı varsayılan oldu: sunucu yanıttan sonra kapatmıyor, kapatmak için Connection başlığına close yazmak gerekiyor.
- Baş engellemesi sıra zorunluluğundan doğuyor: istekleri yanıtlarla eşleştiren bir kimlik alanı olmayınca tek dayanak varış sırası kalıyor.
- HTTP/2 veriyi frame’lere böldü ama taşımayı değiştirmedi: iç içe geçen frame’ler application layer’ı kurtarıyor, kaybolan tek paket yine hepsini durduruyor.
- QUIC güvenilirliği stream başına ayırdı: kayıp yalnızca o pakette verisi olan stream’leri bekletiyor, el sıkışma ise tek tura iniyor.
Aynı yolun üstünde artık birden çok araba var ve hiçbiri ötekini beklemiyor. İsteğin yola çıktı; peki gerçekten adres çubuğuna yazdığın makineye mi gidiyor?
16. bölüme devam et: İçerik Dağıtım Ağları