HTTP, tüm web geliştiricilerin bilmesi gereken bir protokoldür, çünkü tüm web’i destekler. HTTP’yi bilmek, kesinlikle daha iyi uygulamalar geliştirmenize yardımcı olabilir.
Bu makalede, HTTP’nin ne olduğunu, nasıl ortaya çıktığını, bugünkü durumunu ve nasıl bu noktaya geldiğimizi tartışacağım.
HTTP nedir?
İlk olarak, HTTP nedir? HTTP, istemcilerin ve sunucuların birbirleriyle iletişim kurma şeklini standardize eden, TCP/IP tabanlı bir uygulama katmanı iletişim protokolüdür. İnternet üzerinde içeriğin nasıl istendiğini ve iletilmekte olduğunu tanımlar. Uygulama katmanı protokolü olarak, sadece ana bilgisayarların (istemciler ve sunucular) nasıl iletişim kurduğunu standartlaştıran bir soyutlama katmanı olduğunu ifade eder. HTTP, kendisi istekleri ve yanıtları almak için TCP/IP’ye bağımlıdır. Varsayılan olarak TCP portu 80 kullanılır, ancak diğer portlar da kullanılabilir. Ancak HTTPS, 443 numaralı portu kullanır.
HTTP/0.9 – The One Liner (1991)
HTTP’nin ilk belgelenmiş sürümü HTTP/0.9’dur ve 1991 yılında sunulmuştur. Bu, şimdiye kadar en basit protokoldü ve yalnızca GET adında bir yönteme sahipti. Bir istemcinin sunucuda bir web sayfasına erişmesi gerektiğinde, aşağıdaki gibi basit bir istekte bulunurdu.
GET /index.html
Ve sunucudan gelen yanıt aşağıdaki gibi görünürdü.
(response body)
(connection closed)
Yani, sunucu isteği alır, yanıt olarak HTML ile yanıt verir ve içerik aktarıldıktan sonra bağlantı kapatılırdı.
- Hiçbir başlık yoktu.
- GET yöntemi tek izin verilen yöntemdi.
- Yanıt HTML olmak zorundaydı.
Görüldüğü gibi, protokol gelecek için bir basamak taşından başka bir şey değildi.
HTTP/1.0 – 1996
1996 yılında HTTP’nin bir sonraki sürümü olan HTTP/1.0 geliştirildi ve orijinal sürüme göre büyük ölçüde iyileştirildi.
HTTP/0.9’dan farklı olarak, sadece HTML yanıtı için tasarlanan HTTP/1.0, artık diğer yanıt formatlarıyla da başa çıkabiliyordu; örneğin resimler, video dosyaları, düz metin veya herhangi bir içerik türü. Daha fazla yöntem eklendi (POST ve HEAD gibi), istek/yanıt formatları değişti, HTTP başlıkları isteklere ve yanıtlara eklendi, yanıtı tanımlamak için durum kodları eklendi, karakter seti desteği tanıtıldı, çoklu parça türleri, yetkilendirme, önbelleğe alma, içerik kodlama ve daha fazlası dahil edildi.
İşte bir örnek HTTP/1.0 isteği ve yanıtı nasıl görünebilir:
GET / HTTP/1.0
Host: cs.fyi
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
Görüldüğü gibi, istekle birlikte istemci kişisel bilgilerini, istenen yanıt türünü vb. göndermiştir. Oysa HTTP/0.9’da istemci başlıklar olmadığı için böyle bilgileri asla gönderemezdi.
Yukarıdaki isteğe örnek bir yanıt aşağıdaki gibi görünebilirdi:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
(response body)
(connection closed)
Yanıtın başlangıcında HTTP/1.0 (HTTP’nin ardından gelen sürüm numarası) bulunur, ardından 200 durum kodu ve onu takip eden açıklama (durum kodunun açıklaması) yer alır.
Bu yeni sürümde, istek ve yanıt başlıkları hala ASCII kodlamasında tutulurken, yanıt gövdesi herhangi bir türde olabilirdi; örneğin resim, video, HTML, düz metin veya herhangi bir içerik türü. Bu nedenle, sunucu artık istemciye herhangi bir içerik türü gönderebilirdi; ancak tanıtımdan kısa bir süre sonra HTTP’deki “Hyper Text” terimi yanlış bir ad haline geldi. HMTP veya Hypermedia transfer protokolü daha mantıklı olabilirdi, ancak sanırım hayat boyu bu isimle devam ediyoruz.
HTTP/1.0’ın başlıca dezavantajlarından biri, her bağlantıda birden fazla isteğe izin vermemesiydi. Yani bir istemcinin sunucudan bir şey istediğinde, yeni bir TCP bağlantısı açması ve ardından tek bir isteğin karşılanmasının ardından bağlantının kapatılması gerekecekti. Ve herhangi bir sonraki gereksinim için yeni bir bağlantıda olması gerekecekti. Neden kötü bir durum? Şöyle düşünelim, bir web sayfasını ziyaret ettiğinizi varsayalım ve bu sayfada 10 resim, 5 stil dosyası ve 5 JavaScript dosyası olmak üzere toplamda 20 öğe alınması gerekiyor. Sunucu, isteğin karşılanmasının hemen ardından bağlantıyı kapatır, bu nedenle her öğe ayrı ayrı bağlantılarında tek tek sunulur. Bu çok sayıda bağlantı, yeni bir TCP bağlantısının gerektirdiği üç yönlü el sıkışma ve yavaş başlangıç gibi önemli bir performans darbesine neden olur.
Three-way Handshake
Üç yönlü el sıkışma, tüm TCP bağlantılarının, istemci ve sunucunun uygulama verilerini paylaşmaya başlamadan önce bir dizi paketi paylaşmasıyla başladığı en basit haliyle tanımlanır.
- SYN – İstemci, rastgele bir sayı seçer, diyelim ki x, ve bunu sunucuya gönderir.
- SYN ACK – Sunucu, isteği kabul ederek istemciye bir ACK paketi gönderir. Bu ACK paketi, sunucu tarafından seçilen rastgele bir sayı olan y ve istemcinin gönderdiği sayı olan x+1’den oluşur.
- ACK – İstemci, sunucudan aldığı y sayısını artırır ve y+1 sayısıyla bir ACK paketi gönderir.
Üç yönlü el sıkışma tamamlandığında, istemci ve sunucu arasında veri paylaşımı başlayabilir. Not edilmelidir ki, istemci, son ACK paketini gönderdikten sonra hemen uygulama verilerini göndermeye başlayabilir, ancak sunucunun isteği karşılayabilmesi için ACK paketinin alınmasını beklemesi gerekmektedir.
Ancak, bazı HTTP/1.0 uygulamaları, sunucuya “Hey sunucu, bu bağlantıyı kapatma, tekrar ihtiyacım olacak” anlamına gelen Connection: keep-alive adında yeni bir başlık ekleyerek bu sorunu aşmaya çalıştı. Ancak yine de bu çok geniş desteklenmedi ve sorun hala devam etti.
Bağlantısız olmasının yanı sıra, HTTP aynı zamanda durumsuz bir protokoldür, yani sunucu istemci hakkında bilgi tutmaz ve her istek, eski isteklerle ilişkili olmadan sunucunun isteği kendi başına yerine getirmek için gerekli bilgilere sahip olmalıdır. Bu da ateşe benzin ekler; istemcinin açması gereken büyük sayıda bağlantının yanı sıra, artan bant genişliği kullanımına neden olan gereksiz verileri de iletmek zorunda kalır.
HTTP/1.1 – 1997
HTTP/1.1, HTTP/1.0’dan sadece 3 yıl sonra 1999 yılında piyasaya sürüldü ve önceki sürüme göre birçok iyileştirme yapıldı. HTTP/1.0’a göre önemli geliştirmeler şunları içeriyordu:
- Yeni HTTP yöntemleri eklendi ve PUT, PATCH, OPTIONS, DELETE gibi yöntemler tanıtıldı.
- Ana Bilgisayar Kimlik Doğrulaması: HTTP/1.0’da Host başlığı zorunlu değildi, ancak HTTP/1.1’de zorunlu hale getirildi.
- Kalıcı Bağlantılar: Yukarıda belirtildiği gibi, HTTP/1.0’da her bağlantıda yalnızca bir istek bulunur ve istek karşılandığında bağlantı kapatılırdı, bu da ciddi bir performans düşüşüne ve gecikme sorunlarına yol açardı. HTTP/1.1 kalıcı bağlantıları tanıttı, yani bağlantılar varsayılan olarak kapatılmadı ve açık tutuldu, böylece ardışık çoklu istekler yapılabiliyordu. Bağlantıları kapatmak için, istekte Connection: close başlığının bulunması gerekiyordu. İstemciler genellikle bağlantıyı güvenli bir şekilde kapatmak için bu başlığı son istekte gönderirler.
- Pipelining: HTTP/1.1 ayrıca pipelining desteğini de tanıttı. Bu, istemcinin aynı bağlantı üzerinden sunucuya yanıtı beklemeden birden çok istek gönderebilmesini sağladı ve sunucunun istekleri alındığı sırada aynı sırayla yanıtı göndermesi gerekiyordu. Ancak istemcinin, ilk yanıt indirmesinin tamamlandığı ve sonraki yanıt içeriğinin başladığı noktayı nasıl bileceğini sorabilirsiniz! Bu sorunu çözmek için, yanıtın nerede bittiğini ve sonraki yanıtı beklemeye başlayabileceğini belirlemek için Content-Length başlığı bulunmalıdır.
Özellikle kalıcı bağlantılardan veya pipelining’den faydalanmak için, yanıtta Content-Length başlığının bulunması gerektiğini belirtmek gerekir. Bu, istemcinin iletimin tamamlandığını ve bir sonraki isteği gönderebileceğini (normal sıralı istek gönderme şekliyle) veya bir sonraki yanıtı beklemeye başlayabileceğini (pipelining etkinse) bilmesini sağlar.
Ancak, bu yaklaşımla hala bir sorun vardı. Peki ya veri dinamikse ve sunucu önceden içeriğin uzunluğunu bulamazsa ne olacak? Bu durumda kalıcı bağlantılardan faydalanamazsınız, değil mi?! İşte bu sorunu çözmek için HTTP/1.1 chunked encoding’i tanıttı. Bu tür durumlarda, sunucu chunked encoding lehine Content-Length’i atlayabilir (buna birazdan daha detaylı değineceğiz). Bununla birlikte, bunlardan hiçbiri mevcut değilse, bağlantı isteğin sonunda kapatılmalıdır.
- Parçalı Aktarımlar: Dinamik içerik durumunda, sunucu iletim başladığında Content-Length’i gerçekten bulamazsa, içeriği parça parça (chunk) göndermeye başlayabilir ve her parça gönderildiğinde ilgili parça için Content-Length ekleyebilir. Ve tüm parçalar gönderildiğinde, yani iletim tamamlandığında, bir boş parça gönderilir, yani Content-Length değeri sıfır olarak ayarlanmış bir parça gönderilir ve bu şekilde istemciye iletimin tamamlandığını bildirilir. Sunucu, parçalı aktarımı istemciye bildirmek için Transfer-Encoding: chunked başlığını içerir.
- HTTP/1.0’da yalnızca Temel kimlik doğrulaması bulunurken, HTTP/1.1’e sırasıyla digest kimlik doğrulaması ve proxy kimlik doğrulaması eklendi.
- Önbellekleme
- Bayt Aralıkları
- Karakter setleri
- Dil müzakeresi
- İstemci çerezleri
- Gelişmiş sıkıştırma desteği
- Yeni durum kodları
- …ve daha fazlası
Bu yazıda HTTP/1.1’in tüm özellikleri hakkında detaylı bir şekilde bahsetmeyeceğim çünkü bu konu başlı başına bir konudur ve zaten birçok kaynakta bulunabilir. Önerim, HTTP/1.0 ve HTTP/1.1 arasındaki temel farklar hakkında bilgi almak için “Key differences between HTTP/1.0 and HTTP/1.1” başlıklı belgedir. İlgilenenler için orijinal RFC belgesinin de bağlantısını veriyorum.
HTTP/1.1 1999 yılında tanıtıldı ve uzun yıllar boyunca bir standart olarak kullanıldı. Ancak, web her geçen gün değişirken, HTTP/1.1’in yaşını göstermeye başladı. Günümüzde bir web sayfasını yüklemek eskisinden daha fazla kaynak gerektiriyor. Basit bir web sayfası bile 30’dan fazla bağlantı açmak zorunda kalabilir. Peki, HTTP/1.1 kalıcı bağlantıları kullanıyorsa neden bu kadar çok bağlantı gerekiyor derseniz! Bunun nedeni, HTTP/1.1’de herhangi bir anda yalnızca bir bekleyen bağlantı olabilmesidir. HTTP/1.1, pipelining’i tanıtarak bu sorunu çözmeye çalıştı, ancak yavaş veya yoğun bir istekler ardındaki istekleri engelleyebilen head-of-line blocking sorunu nedeniyle bu tam olarak çözülmedi. Bir istek bir pipeline’a takılırsa, diğer isteklerin tamamlanmasını beklemek zorunda kalacaktır. HTTP/1.1’in bu kısıtlamalarını aşmak için geliştiriciler çeşitli çözüm yolları uygulamaya başladı, örneğin sprite tabanlı görüntüler, CSS’de kodlanmış resimler, tek bir devasa CSS/Javascript dosyası, etki alanı bölümlemesi vb.
SPDY – 2009
Google ileri giderek webi daha hızlı hale getirmek, web güvenliğini artırmak ve web sayfalarının gecikmesini azaltmak için alternatif protokollerle deneyler yapmaya başladı. 2009 yılında SPDY’yi duyurdular.
SPDY, Google’ın bir ticari markasıdır ve bir kısaltma değildir.
Eğer bant genişliğini artırırsak, ağ performansının başlangıçta arttığını ancak bir noktadan sonra önemli bir performans artışının olmadığını gözlemlemişizdir. Ancak, aynısını gecikme için yaparsak, yani gecikmeyi azaltmaya devam edersek, sürekli bir performans artışı sağlanır. Bu, SPDY’nin performans artışı için temel fikriydi, ağ performansını artırmak için gecikmeyi azaltmaktı.
Bilmeyenler için farkı açıklamak gerekirse, gecikme (latency) verinin kaynaktan hedefe kadar seyahat etmesi için geçen süredir ve milisaniye cinsinden ölçülür. Bant genişliği (bandwidth) ise saniye başına transfer edilen veri miktarıdır ve bit/saniye olarak ölçülür.
SPDY’nin özellikleri arasında çoklu bağlantı (multiplexing), sıkıştırma (compression), önceliklendirme (prioritization), güvenlik vb. bulunuyordu. SPDY’nin ayrıntılarına girmeyeceğim çünkü HTTP/2’ye geçtiğimizde SPDY’den ilham alınan detaylara değineceğiz.
SPDY, HTTP’yi tamamen değiştirmeyi amaçlamadı; bunun yerine, uygulama katmanında var olan HTTP’nin üzerine bir çeviri katmanı olarak faaliyet gösterdi ve isteği telde göndermeden önce değiştiriyordu. SPDY giderek standart bir yaklaşım haline geldi ve çoğu tarayıcı tarafından uygulanmaya başlandı.
2015 yılında Google’da, iki rekabet eden standart olmasını istemedikleri için SPDY’yi HTTP’ye birleştirmeye ve HTTP/2’yi ortaya çıkarmaya karar verdiler. Bu süreçte SPDY kullanımı azaltıldı.
HTTP/2 – 2015
Artık, neden HTTP protokolünün başka bir sürümüne ihtiyaç duyduğumuzu anlamış olmalısınız. HTTP/2, içeriğin düşük gecikme süresiyle iletilmesi için tasarlanmıştır. Eski HTTP/1.1 sürümünden olan temel özellikler veya farklar şunları içerir:
- Metinsel yerine ikili (binary) format
- Tek bir bağlantı üzerinde birden fazla asenkron HTTP isteği için çoklu bağlantı (multiplexing)
- HPACK kullanarak başlık sıkıştırması
- Sunucu İletimi – Tek bir istek için birden fazla yanıt
- İstek Önceliklendirme
- Güvenlik
1. Binary Protocol
HTTP/2, HTTP/1.x’deki artan gecikme sorununu ikili bir protokol haline getirerek ele almaktadır. İkili bir protokol olması, analiz etmesi daha kolay olmasına rağmen HTTP/1.x gibi insan gözüyle okunabilir değildir. HTTP/2’nin temel yapı taşları Çerçeveler (Frames) ve Akışlar (Streams)’dır.
Çerçeveler ve Akışlar
HTTP mesajları artık bir veya daha fazla çerçeveden oluşur. Meta veriler için HEADERS çerçevesi ve taşıma için DATA çerçevesi bulunur ve HTTP/2 spesifikasyonlarında kontrol edebileceğiniz birkaç farklı çerçeve türü vardır (HEADERS, DATA, RST_STREAM, SETTINGS, PRIORITY vb.).
Her HTTP/2 isteği ve yanıtı benzersiz bir akış kimliği (stream ID) alır ve çerçevelere bölünür. Çerçeveler, basitçe ikili veri parçalarıdır. Çerçevelerin bir araya gelmesine Akış (Stream) denir. Her çerçevenin ait olduğu akışı tanımlayan bir akış kimliği bulunur ve her çerçevenin ortak bir başlığı vardır. Ayrıca, benzersiz olan akış kimliğine ek olarak, belirtmekte fayda var ki, istemci tarafından başlatılan her istek tek sayılar kullanırken, sunucudan gelen yanıtlar çift sayılarla belirlenmiş akış kimliklerini kullanır.
Başlıklar ve Veri (DATA) dışında burada bahsetmeye değer bir diğer çerçeve türü ise RST_STREAM’dır. Bu özel çerçeve türü, belirli bir akışı sonlandırmak için kullanılır. İstemci, sunucuya artık bu akışa ihtiyaç duymadığını bildirmek için bu çerçeveyi gönderebilir. HTTP/1.1’de sunucunun yanıtı göndermeyi durdurması için tek yol bağlantıyı kapatmaktı, bu da artan gecikmeye neden olurdu çünkü ardışık istekler için yeni bir bağlantı açılması gerekiyordu. Oysa HTTP/2’de istemci RST_STREAM kullanarak belirli bir akışı almayı durdurabilirken, bağlantı hala açık kalacak ve diğer akışlar devam edecektir.
2. Multiplexing
HTTP/2, şimdi ikili bir protokol olduğundan ve yukarıda belirttiğim gibi istekler ve cevaplar için frame’ler ve stream’ler kullanıyor, bir TCP bağlantısı açıldığında tüm stream’ler aynı bağlantı üzerinden asenkron bir şekilde gönderilirken ek bağlantı açılmaz. Bunun karşılığında sunucu da aynı asenkron şekilde yanıt verir, yani cevapların bir sırası yoktur ve istemci belirli bir paketin hangi stream’e ait olduğunu belirlemek için atanmış stream ID’sini kullanır. Bu aynı zamanda HTTP/1.x’deki head-of-line blocking sorununu çözer, yani istemci, zaman alan bir isteğin tamamlanmasını beklemek zorunda kalmaz ve diğer istekler hala işlenir.
3. Header Compression
Header sıkıştırma, başlıkları optimize etmeyi amaçlayan ayrı bir RFC’nin bir parçasıydı. Temel olarak, aynı istemciden sürekli olarak sunucuya eriştiğimizde başlıklarda tekrarlanan birçok gereksiz veri gönderiyoruz ve bazen başlık boyutunu artıran çerezler gibi unsurlar olabilir, bu da bant genişliği kullanımı ve artan gecikmeye neden olur. Bunu aşmak için, HTTP/2 başlık sıkıştırmasını tanıttı.
İstek ve cevaplar gibi başlıklar gzip veya compress gibi formatlarda sıkıştırılmaz, bunun yerine başlık sıkıştırması için farklı bir mekanizma kullanılır. Literal değerler Huffman kodu kullanılarak kodlanır ve istemci ve sunucu tarafından bir başlık tablosu tutulur. Hem istemci hem de sunucu, tekrarlayan başlıkları (örneğin kullanıcı aracısı vb.) sonraki isteklerde atlayarak başlık tablosunu kullanarak başvuruda bulunurlar.
Başlıklardan bahsederken, burada eklemek istediğim şey, başlıkların HTTP/1.1 ile aynı olduğudur, ancak bazı yarı başlıkların (pseudo headers) eklendiği “:method”, “:scheme”, “:host” ve “:path” olarak belirtilebilir.
4. Server Push
HTTP/2’nin bir diğer önemli özelliği de sunucu itme (server push) özelliğidir. Sunucu, istemcinin belirli bir kaynağı isteyeceğini bildiği durumlarda istemciden talep gelmeden bu kaynağı istemciye gönderebilir. Örneğin, bir tarayıcı bir web sayfasını yüklerken, sunucudan yüklemesi gereken uzak içeriği bulmak için sayfayı ayrıştırır ve ardından bu içeriği almak için ardışık istekler gönderir.
Sunucu itmesi, sunucunun istemcinin talep edeceği kaynağı önceden tahmin ederek dönüş yolculuklarını azaltmasına olanak sağlar. Nasıl yapıldığına gelince, sunucu, “Hey, bu kaynağı size göndermeyi düşünüyorum! Bana sormayın.” şeklinde istemciyi bilgilendiren PUSH_PROMISE adlı özel bir çerçeve gönderir. PUSH_PROMISE çerçevesi, itmenin gerçekleştiği akışla ilişkilidir ve itilen kaynağı göndermek için sunucunun kullanacağı söz verilen akış kimliğini (stream ID) içerir.
5. Request Prioritization
Bir istemci, bir akışın açıldığı HEADERS çerçevesine öncelik bilgilerini dahil ederek bir akışa öncelik atayabilir. Başka bir zamanda istemci, bir akışın önceliğini değiştirmek için bir PRIORITY çerçevesi gönderebilir.
Öncelik bilgisi olmadan, sunucu istekleri sırasız olarak asenkron olarak işler. Bir akışa öncelik atandığında ise sunucu, bu öncelik bilgilerine dayanarak hangi isteği işlemek için hangi kaynakların ne kadarının verilmesi gerektiğine karar verir.
6. Security
HTTP/2 için güvenliğin (TLS aracılığıyla) zorunlu olup olmaması konusunda kapsamlı bir tartışma yapıldı. Sonunda, zorunlu hale getirilmemesine karar verildi. Bununla birlikte, çoğu satıcı HTTP/2’yi yalnızca TLS üzerinden kullanıldığında destekleyeceklerini belirttiler. Bu nedenle, HTTP/2’nin özellikler gereği şifreleme zorunluluğu olmasa da, bir şekilde varsayılan olarak zorunlu hale gelmiştir. Bu konuyu hallettikten sonra, HTTP/2 TLS üzerinde uygulandığında bazı gereksinimler ortaya çıkar. Bunlar arasında TLS sürümünün 1.2 veya daha yüksek olması, belirli bir minimum anahtar boyutu gerekliliği ve geçici anahtarların kullanılması gibi şartlar bulunur.