← tüm yazılar
032

WebSocket: TCP Üzerine İnce Bir Katman

Üzerinde çalıştığım bir projede gerçek zamanlı, çift yönlü bir iletişim katmanına ihtiyacım oldu ve doğal olarak WebSocket’e yöneldim. “Nasıl olsa kütüphane var, kullanırım” deyip geçmek yerine protokolün altında ne döndüğünü anlamak istedim, bu yüzden doğrudan kaynağa, RFC 6455’e gittim. Aşağıdakiler o okuma sırasında tuttuğum notlar; hem ileride kendim dönüp bakayım hem de aynı yola çıkan birine faydası olsun diye toparladım.

Önce şu soru: neden WebSocket?

Bir istemciyle sunucunun sürekli karşılıklı konuşması gereken uygulamalar yeni değil. Anlık mesajlaşma, çok oyunculu oyunlar, canlı skor ekranları… Bunların hepsinin ortak derdi şu: tarayıcı sunucuya bir şey göndermek kadar, sunucunun da tarayıcıya kendiliğinden bir şey iletmesini istiyor.

WebSocket’ten önce bunu HTTP ile çözmeye çalışıyorduk, ama HTTP bu iş için tasarlanmamıştı. İstemci “yeni bir şey var mı?” diye sunucuyu sürekli yokluyor (polling), kendi göndereceği şeyi de ayrı bir istekle yolluyordu. Bu yaklaşımın bedeli ağırdı:

  • Sunucu her istemci için bir sürü ayrı TCP bağlantısı taşımak zorunda kalıyordu; veri göndermek için bir bağlantı, gelen mesajı almak için bir başkası.
  • Her mesajın başına koca bir HTTP başlığı ekleniyor, küçücük bir veri için bile ciddi bir yük çıkıyordu.
  • İstemci tarafında, hangi yanıtın hangi isteğe ait olduğunu takip etmek için elle bir eşleştirme tutmak gerekiyordu.

Çözüm aslında basit: gidiş ve dönüş trafiğinin ikisini de tek bir TCP bağlantısı üzerinden akıtmak. WebSocket tam da bunu yapıyor. Tarayıcıdaki WebSocket API ile birlikte, o eski “sürekli yoklama” yöntemine net bir alternatif sunuyor.

Tasarım derdi: mevcut dünyaya uyum

WebSocket sıfırdan, her şeyi yok sayarak tasarlanmış bir protokol değil. Aksine, kendinden önceki çift yönlü iletişim çözümlerinin yerini almayı hedefliyor. O eski çözümler taşıma katmanı olarak HTTP’yi kullanıyordu, çünkü böylece zaten var olan altyapıdan (proxy’ler, filtreler, kimlik doğrulama mekanizmaları) faydalanabiliyorlardı. Ama HTTP çift yönlü konuşma için yapılmadığından, hepsi verimlilikle güvenilirlik arasında bir tavizden ibaretti.

WebSocket bu hedeflere mevcut HTTP dünyasının içinde kalarak ulaşmaya çalışıyor. Bu yüzden 80 ve 443 portları üzerinden çalışacak ve aradaki proxy’lerle, vekil sunucularla iyi geçinecek şekilde tasarlanmış. Bu uyum, beraberinde bir miktar karmaşıklık getirse de buna değiyor.

Yine de protokol kendini HTTP’ye zincirlemiyor. İleride bir uygulama isterse, her şeyi baştan icat etmeden, ayrı bir port üzerinden çok daha sade bir handshake da kurabilir. Bu esneklik önemli, çünkü etkileşimli mesajlaşmanın trafik deseni klasik web trafiğine pek benzemez ve aradaki bazı bileşenleri beklenmedik şekilde zorlayabilir.

Handshake

İşin güzel tarafı, bağlantı normal bir HTTP isteğiyle başlıyor. Daha doğrusu, bir HTTP “Upgrade” isteğiyle. Bunun nedeni pratik: aynı port hem sıradan HTTP istemcileri hem de WebSocket istemcileri tarafından kullanılabilsin isteniyor.

İstemcinin gönderdiği handshake şöyle görünüyor:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

Sunucu kabul ederse şu yanıtı dönüyor:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

Buradaki 101 Switching Protocols kritik. 101 dışında dönen herhangi bir kod, handshake’in tamamlanmadığı ve ortada hâlâ sıradan bir HTTP konuşması olduğu anlamına geliyor.

Başlıklar ne işe yarıyor?

Handshake’teki ek başlıklar protokolün ayarlarını belirliyor:

  • Sec-WebSocket-Protocol alt protokol pazarlığı için. İstemci “ben şu protokolleri konuşabilirim” diyor, sunucu bunlardan birini seçip yanıtında geri yansıtıyor (ya da hiçbirini seçmiyor). Buradaki “alt protokol”, WebSocket’in üstüne kurduğun kendi uygulama mantığı.
  • Sec-WebSocket-Extensions istemcinin desteklediği uzantıları listeliyor.
  • Origin isteğin hangi kökenden geldiğini söylüyor.

Sunucu kendini nasıl ispatlıyor?

Sunucunun, karşısındakinin gerçekten bir WebSocket istemcisi olduğundan emin olması lazım. Aksi halde kötü niyetli biri, XMLHttpRequest ya da bir form gönderimiyle özenle hazırlanmış paketler yollayıp sunucuyu kandırabilir.

Mekanizma şöyle işliyor: istemci handshake’te rastgele bir değeri (Sec-WebSocket-Key) gönderiyor. Sunucu bu değeri, herkesçe bilinen sabit bir GUID ile birleştirip özetini (hash) alıyor ve sonucu Sec-WebSocket-Accept başlığında geri yolluyor. İstemci de aynı hesabı kendi yapıyor; dönen değer beklediğiyle uyuşuyorsa karşıdakinin gerçekten WebSocket konuştuğundan emin oluyor.

İstemci tarafında bu kontrol katı: Sec-WebSocket-Accept değeri tutmuyorsa, başlık hiç yoksa ya da durum kodu 101 değilse bağlantı kurulmuyor ve tek bir veri çerçevesi bile gönderilmiyor.

Veri akmaya başlıyor

Handshake iki taraflı tamamlandığında asıl iş başlıyor. Artık ortada tam anlamıyla çift yönlü bir kanal var; taraflardan her biri, diğerini beklemeden, canı ne zaman isterse veri gönderebiliyor.

Veri, RFC’nin “ileti” (message) dediği parçalar hâlinde gidip geliyor ve her ileti bir ya da birden fazla çerçeveden (frame) oluşuyor. Her çerçevenin bir türü var ve aynı iletiye ait çerçeveler hep aynı türü taşıyor. Kabaca üç tür var:

  • Metin — UTF-8 olarak yorumlanıyor.
  • İkili veri — nasıl yorumlanacağı tamamen uygulamaya kalmış.
  • Kontrol çerçeveleri — uygulamaya veri taşımak için değil, protokolün kendi sinyalleşmesi için. Örneğin “bağlantıyı kapatıyorum” demek gibi.

Bu sürüm toplam altı çerçeve türü tanımlıyor, on tanesini de ileride kullanmak üzere ayırıyor.

Kapanış handshake’i

Bağlantı kapanırken de bir nezaket kuralı var. Taraflardan biri, kapanışı başlatmak için özel bir kontrol çerçevesi (Close) gönderiyor. Karşı taraf bunu alınca, eğer kendisi henüz göndermediyse, yanıt olarak o da bir Close çerçevesi yolluyor. İlk taraf bu yanıtı aldıktan sonra bağlantıyı kapatıyor ve artık veri gelmeyeceğini bildiği için bunu gönül rahatlığıyla yapıyor.

Kısaca: Close gönderdikten sonra taraf başka veri yollamıyor, Close aldıktan sonra da gelen her şeyi çöpe atıyor. İki tarafın aynı anda kapanışı başlatması da sorun değil.

Felsefe: olabildiğince az şey eklemek

WebSocket’in tasarım çizgisi “mümkün olduğunca az müdahale” üzerine kurulu. Getirdiği tek çerçeveleme, protokolü akış yerine çerçeve temelli yapmak ve metin ile ikili veriyi birbirinden ayırmak için var, o kadar. Geri kalan her şeyi, tıpkı HTTP’nin TCP üzerine kurulması gibi, üstüne sen kuruyorsun.

Kavramsal olarak WebSocket, TCP’nin üzerine oturup şunları ekleyen ince bir katman:

  • Tarayıcılar için web’e uygun bir güvenlik modeli getiriyor.
  • Tek bir port üzerinde birden çok servisi ve tek bir IP üzerinde birden çok alan adını ayırt edebilmek için bir adresleme ve adlandırma mekanizması ekliyor.
  • TCP’nin üstüne, uzunluk sınırı olmayan paketlere benzer bir çerçeveleme koyuyor.
  • Proxy’ler ve aradaki diğer aracılarla düzgün çalışsın diye bir de kapanış handshake’i ekliyor.

Bunun ötesinde fazladan bir şey yapmıyor. Niyeti belli: web’in kısıtları neyse onlara saygı göstererek, ham TCP’yi geliştiriciye olabildiğince yalın hâliyle açmak. Üstüne eklenenler de (ileti kavramı gibi) sırf işi kolaylaştırmak, basit şeyleri basit tutmak için.

Bir de şu var: sunucusu istenirse aynı portu bir HTTP sunucusuyla paylaşabilsin diye handshake geçerli bir HTTP Upgrade isteği biçiminde. Aslında bu işi başka protokollerle de kurabilirdin, ama WebSocket’in bütün amacı; HTTP ve onun dağıtık altyapısıyla (proxy’ler falan) yan yana yaşayabilen, TCP’ye olabildiğince yakın duran ve kullanması kolay, sade bir protokol olmak.

TCP ve HTTP ile ilişkisi

Son olarak şunu netleştirmekte fayda var: WebSocket aslında HTTP’nin bir parçası değil. Kendi başına, TCP tabanlı, bağımsız bir protokol. HTTP’yle tek bağı, açılış handshake’inin HTTP sunucuları tarafından bir Upgrade isteği gibi okunabilmesi. O ilk tokalaşmadan sonra ortada HTTP diye bir şey kalmıyor.

Varsayılan olarak normal bağlantılar için 80 portunu, TLS üzerinden şifreli bağlantılar için ise 443 portunu kullanıyor. Yani güvenli WebSocket (wss://), tıpkı https gibi, TLS tüneli içinden geçiyor.

Kaynaklar

  • RFC 6455 — The WebSocket Protocol
  • RFC 6202 — Çift yönlü HTTP üzerine en iyi pratikler
  • RFC 3629 — UTF-8
  • RFC 2818 — HTTP Over TLS