İçeriğe geç
academia.sh

Ders 11 / 16

WebSocket

Yükseltme el sıkışmasının bir rejim değişimi olması: 101'den sonra aracının tünele düşmesi, iki yüz kararın tamamının uçlara taşınması, çerçevenin taşıdığı alanların hiçbirinden karar çıkmaması ve gizlemenin bayt cinsinden bedeli.

İçindekiler

Önceki ders taşıma zarfını kapattı ve aracının kaybını saydı: iki yüz kararın yüz altmışı ortadan uçlara taşındı, geriye yalnız konak adından çıkarılan kırk karar kaldı. Kaybın büyüklüğüne karşın bir şey yerinde durmuştu. Bağlantı hâlâ istek–yanıt biçimindeydi: istemci bir ileti gönderiyor, sunucu bir ileti döndürüyor, sıra yeniden istemciye geçiyordu. Aracı iletinin içini göremese de kaç ileti olduğunu, her birinin nerede başlayıp nerede bittiğini biliyordu.

Bu dersin sorusu şudur: bağlantının biçimi de değişirse ne kalır? WebSocket tam bunu yapar. Olağan bir HTTP isteğiyle başlar, tek bir yanıtla kendi çerçeve biçimine geçer ve o noktadan sonra bağlantı üzerinde ne istek satırı, ne başlık alanı, ne de durum kodu vardır. Aynı bağlantı üzerinde iki uç da birbirine istediği anda yazabilir.

Yükseltme El Sıkışması

Bağlantı, aracının okuyabildiği tek bir alışverişle açılır. İstemci olağan bir GET isteği gönderir; isteği ayıran şey, bağlantının yükseltileceğini bildiren başlık alanlarıdır. Buna yükseltme el sıkışması (upgrade handshake) denir.

GET /akis HTTP/1.1
Host: istasyon.ornek
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <on altı baytlık rastgele değerin base64 kodu>
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <anahtar ile belirtimdeki sabitin özetinin base64 kodu>

Sec-WebSocket-Accept değeri, istemcinin gönderdiği anahtardan belirtimde sabitlenmiş bir dizgeyle birlikte türetilir. Bu türetmenin hiçbir gizlilik işlevi yoktur; yalnız yanıtı gerçekten yükseltmeyi anlamış bir uçtan geldiğine bağlar ve aradaki bir önbelleğin eski bir yanıtı yeniden oynatmasını ayırt eder. Kabul değerinin hesabı ve sunucu tarafındaki kurulumu Önbellekleme, Kuyruklar ve Eşzamansız İşleme kursunda yazıldı; orada ölçülen şey sunucunun her bağlantı için tuttuğu durumdu. Burada ölçülen, aradaki aracının iletiden alabildiği karardır.

Belirleyici satır 101 Switching Protocols’tür. 100 sınıfı bir ara yanıttır: istek sürüyor, sonuç henüz yok. 101 ise sürecin sonucunu değil, artık başka bir protokol konuşulacağını bildirir. Bu satırdan sonra bağlantı üzerinde HTTP yoktur.

Aracının Ölçüsü

Kursun kurgusu bu derste de aynıdır. Kuzey Yamaç ölçüm istasyonu bir istemciye ölçüm sunar; arada uygulamayı bilmeyen bir aracı durur ve yalnız iletide yazana bakarak karar verir.

  • DG1. Kırk alışveriş ve alışveriş başına beş karar ölçülür: saklanabilir, paylasilabilir, taze, yinelenebilir, yonlendirilebilir. Küme iki yüz karardır.
  • DG2. Kâhin — kararın gerçeği — kurguyu biz ürettiğimiz için bilinir. Aracının verdiği yanıt bu gerçekle karşılaştırılır; sormak zorunda kaldığı her alışveriş bir tur borcudur.
  • DG3. açık rejimde aracı yöntem, yol, konak adı, kişiye özel imi ve doğrulayıcıyı okur. tünel rejimde hiçbirini okumaz.
  • DG4. Bir WebSocket bağlantısının üzerinde kırk alışverişin tamamı taşınır; bağlantı bir kez açılır ve kapanmaz.
  • DG5. Dökümler ve çerçeve boyları kurgudur; ders içinde hiçbir bağlantı açılmaz.
TOHUM = 20260809
GUVENLI = {"GET", "HEAD"}
ETKISIZ = {"GET", "HEAD", "PUT", "DELETE"}
YONTEMLER = ["GET", "GET", "GET", "HEAD", "POST", "PUT", "DELETE"]
YOLLAR = ["/olcum/kuzey", "/olcum/yamac", "/ozet", "/kayit", "/oturum"]
KARARLAR = ("saklanabilir", "paylasilabilir", "taze", "yinelenebilir",
            "yonlendirilebilir")
GORUNUR = {"açık": ("yontem", "yol", "konak", "ozel_imi", "dogrulayici"),
           "tünel": ()}


def uretec(tohum: int):
    d = tohum % 2147483646 + 1

    def r(n: int) -> int:
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def alisverisler(sayi: int = 40) -> list[dict]:
    r, liste = uretec(TOHUM), []
    for i in range(sayi):
        yontem, yol = YONTEMLER[r(7)], YOLLAR[r(5)]
        ozel = yol == "/oturum" or r(5) == 0
        degisti = r(3) == 0
        liste.append({"no": i + 1, "yontem": yontem, "yol": yol, "ozel": ozel,
                      "degisti": degisti, "ozel_imi": ozel and r(4) != 0,
                      "damga_atladi": degisti and r(6) == 0})
    return liste


def kahin(a: dict) -> dict:
    return {"saklanabilir": a["yontem"] in GUVENLI and not a["ozel"],
            "paylasilabilir": not a["ozel"], "taze": not a["degisti"],
            "yinelenebilir": a["yontem"] in ETKISIZ, "yonlendirilebilir": True}


def ileti(a: dict, gorunur: tuple) -> dict:
    tam = {"yontem": a["yontem"], "yol": a["yol"], "konak": "istasyon.ornek",
           "ozel_imi": a["ozel_imi"],
           "dogrulayici": a["degisti"] and not a["damga_atladi"]}
    return {k: v for k, v in tam.items() if k in gorunur}


def araci(i: dict) -> dict:
    k = {}
    if "yontem" in i:
        k["yinelenebilir"] = i["yontem"] in ETKISIZ
    if "ozel_imi" in i:
        k["paylasilabilir"] = not i["ozel_imi"]
        if "yontem" in i:
            k["saklanabilir"] = i["yontem"] in GUVENLI and not i["ozel_imi"]
    if "dogrulayici" in i:
        k["taze"] = not i["dogrulayici"]
    if "konak" in i:
        k["yonlendirilebilir"] = True
    return k


def olc(ad: str) -> tuple:
    dogru = yanlis = alinamayan = tur = bayt = 0
    for a in alisverisler():
        gercek, verilen = kahin(a), araci(ileti(a, GORUNUR[ad]))
        eksik = False
        for karar in KARARLAR:
            if karar not in verilen:
                alinamayan, eksik = alinamayan + 1, True
            elif verilen[karar] == gercek[karar]:
                dogru += 1
            else:
                yanlis += 1
        tur += 1 if eksik else 0
        bayt += sum(len(k) + len(str(v)) + 4
                    for k, v in ileti(a, GORUNUR[ad]).items())
    return dogru, yanlis, alinamayan, tur, bayt


print(f"{'rejim':<7s} {'doğru':>6s} {'yanlış':>7s} {'alınamayan':>11s} "
      f"{'tur borcu':>10s} {'bayt':>6s}")
for ad in ("açık", "tünel"):
    d, y, e, t, b = olc(ad)
    print(f"{ad:<7s} {d:6d} {y:7d} {e:11d} {t:10d} {b:6d}")
rejim    doğru  yanlış  alınamayan  tur borcu   bayt
açık       195       5           0          0   3524
tünel        0       0         200         40      0

Yükseltme öncesinde aracı kırk alışverişin tamamını okuyordu ve iki yüz kararın iki yüzünü kendi başına alıyordu; sunucuya fazladan hiçbir tur atmıyordu. Yükseltmeden sonra elinde kalan karar sayısı sıfırdır ve kırk alışverişin kırkı birer tur borcuna dönüşür.

İki satır arasındaki fark, taşıma zarfının farkından da büyüktür. Zarf rejiminde aracı konak adını görüyor ve yonlendirilebilir kararını kırk kez alabiliyordu; tünelde onu da kaybeder. Bunun tek nedeni şifreleme değildir: 101’den sonra bağlantı üzerinde yonlendirilebilir kararını besleyecek bir alan yeniden görünmez. Yönlendirme kararı bağlantının kurulduğu anda bir kez alınmış, sonrasında dondurulmuştur.

El Sıkışma, Aracının Okuduğu Son İletidir

Yükseltme el sıkışması bir istisna değil, bir sınırdır. Bağlantı üzerinde taşınan iletileri tek tek aynı karar işlevinden geçirmek bunu gösterir.

GUVENLI = {"GET", "HEAD"}
ETKISIZ = {"GET", "HEAD", "PUT", "DELETE"}


def araci(i: dict) -> dict:              # önceki bloktaki karar işlevinin aynısı
    k = {}
    if "yontem" in i:
        k["yinelenebilir"] = i["yontem"] in ETKISIZ
    if "ozel_imi" in i:
        k["paylasilabilir"] = not i["ozel_imi"]
        if "yontem" in i:
            k["saklanabilir"] = i["yontem"] in GUVENLI and not i["ozel_imi"]
    if "dogrulayici" in i:
        k["taze"] = not i["dogrulayici"]
    if "konak" in i:
        k["yonlendirilebilir"] = True
    return k


BAGLANTI = [
    ("yükseltme isteği", {"yontem": "GET", "yol": "/akis",
                          "konak": "istasyon.ornek", "yukseltme": "websocket"}),
    ("101 yanıtı", {"yukseltme": "websocket", "kabul": "kurgu-deger"}),
    ("metin çerçevesi", {"fin": 1, "opkod": 1, "maskeli": True, "uzunluk": 88}),
    ("kapat çerçevesi", {"fin": 1, "opkod": 8, "maskeli": True, "uzunluk": 2}),
]

print(f"{'ileti':<18s} {'alan':>5s} {'karar':>6s}  alınan kararlar")
for ad, m in BAGLANTI:
    k = araci(m)
    print(f"{ad:<18s} {len(m):5d} {len(k):6d}  {', '.join(k) or '-'}")
ileti               alan  karar  alınan kararlar
yükseltme isteği       4      2  yinelenebilir, yonlendirilebilir
101 yanıtı             2      0  -
metin çerçevesi        4      0  -
kapat çerçevesi        4      0  -

Yükseltme isteği beş kararın ikisini verir: yöntem alanından yinelenebilir, konak alanından yonlendirilebilir. Kalan üçü yanıtın alanlarını gerektirir ve o yanıt hiç gelmez — gelen yanıt 101’dir ve kişiye özel imi de doğrulayıcı da taşımaz. Bu iki karar, bağlantının ömrü boyunca aracının elindeki tek karardır.

Çerçeve Görünse de Karar Çıkmazdı

Yukarıdaki tabloda çerçeveler dört alan taşıyor ve yine de sıfır karar üretiyor. Bu, kaybın tek başına gizlemeden gelmediğini söyler. Çerçevenin yerleşimi belirtimde sabitlenmiştir ve taşıdığı alanların hepsi taşıma hakkındadır, ileti hakkında değil.

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| işlem |M|   uzunluk   |  genişletilmiş uzunluk (16 bit)|
|I|S|S|S| kodu  |A|    (7 bit)  |  uzunluk alanı 126 ise         |
|N|V|V|V| (4bit)|S|             |                               |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|      maske anahtarı (32 bit, maske biti 1 ise)                |
+---------------------------------------------------------------+
|                        yük verisi                             |
+---------------------------------------------------------------+

Çerçeveleme (framing), bayt akışını ileti sınırlarına bölmenin kuralıdır. Uzunluk alanı yedi bittir; 125’e kadar olan yükler doğrudan yazılır, 126 değeri iki baytlık, 127 değeri sekiz baytlık bir uzunluk alanının izlediğini bildirir. Maske biti kurulduğunda dört baytlık bir anahtar eklenir ve yük bu anahtarla katlanır. Maske istemci yönünde zorunludur.

ISLEM = {0: "sürdürme", 1: "metin", 2: "ikili", 8: "kapat", 9: "yokla",
         10: "karşılık"}


def basluk_boyu(yuk: int, maskeli: bool) -> int:
    n = 2                                  # FIN, RSV, işlem kodu, maske biti, uzunluk
    n += 8 if yuk > 65535 else 2 if yuk > 125 else 0   # genişletilmiş uzunluk
    n += 4 if maskeli else 0               # maske anahtarı yalnız istemci yönünde
    return n


print("işlem kodları:", ", ".join(f"{k}={v}" for k, v in ISLEM.items()))
print()
print(f"{'yük':>6s} {'yön':<15s} {'başlık':>7s} {'toplam':>7s} {'başlık payı':>12s}")
for yuk, maskeli in ((2, True), (88, True), (125, True), (126, True),
                     (88, False), (70000, False)):
    b = basluk_boyu(yuk, maskeli)
    yon = "istemci→sunucu" if maskeli else "sunucu→istemci"
    print(f"{yuk:6d} {yon:<15s} {b:7d} {b + yuk:7d} {b / (b + yuk):12.3%}")
işlem kodları: 0=sürdürme, 1=metin, 2=ikili, 8=kapat, 9=yokla, 10=karşılık

   yük yön              başlık  toplam  başlık payı
     2 istemci→sunucu        6       8      75.000%
    88 istemci→sunucu        6      94       6.383%
   125 istemci→sunucu        6     131       4.580%
   126 istemci→sunucu        8     134       5.970%
    88 sunucu→istemci        2      90       2.222%
 70000 sunucu→istemci       10   70010       0.014%

Açık rejimde aracının okuduğu 3524 bayt kırk alışverişe bölündüğünde alışveriş başına 88 bayt düşer; çerçeve başlığı aynı yükü sunucu yönünde iki bayta sarar. Kazanç gerçektir ve küçük iletilerde belirgindir. Ama karşılığı da tam olarak ölçülmüştür: 88 baytın iki yüz kararı, iki baytın sıfır kararıdır.

Bu, kursun ikinci iddiasının tersten okunuşudur. Orada alan eklemenin karar kazandırmadığını görmüştük: üç alan daha, bayt artışı, sıfır yeni karar. Burada aynı şey öbür yönde çıkıyor — baytı kısmak da karar kaybettirmez; kararı kaybettiren, alanın kendisinin ortadan kalkmasıdır. Bayt ile karar arasında bir bağ yoktur; bağ, alan ile karar arasındadır.

Aracının Elinde Kalan Tek Kol

Aracı karar alamaz ama bağlantıyı taşımaya devam eder ve taşıdığı şey üzerinde tek bir yetkisi kalır: onu kapatmak. Boşta duran bir bağlantıyı belirli bir süre sonra kesmek — boşta bekleme süresi (idle timeout) — bir aracının açık tuttuğu bağlantı sayısını sınırlamak için başvurduğu olağan kuraldır. Açık rejimde bu kural zararsızdır; istek–yanıt biçiminde bağlantı zaten iletiler arasında boşalır ve yeniden kurulabilir. Tünelde ise bağlantının kendisi durumun taşıyıcısıdır: kesilen bağlantı, iki ucun üzerinde anlaştığı ne varsa onu da götürür.

Belirtim buna iki denetim çerçevesi ile karşılık verir. Yokla (ping) ve karşılık (pong) işlem kodları veri taşımaz; tek işlevleri bağlantı üzerinde bayt akıtmak ve iki ucun ayakta olduğunu doğrulamaktır. Bunlar aracıya bir şey anlatmaz — tabloda ölçüldüğü gibi karar üretmezler — yalnız aracının boşta sayacını sıfırlar. Aracının iletiden alabildiği karar sıfırken bile bağlantı üzerinde bir tasarım kısıtı doğurması, bu dersin sessiz sonucudur.

Bir alan daha yükseltme isteğinde taşınabilir: Sec-WebSocket-Protocol başlığı, çerçevelerin hangi kurala göre yorumlanacağını adlandırır ve sunucu seçtiği adı yanıtta yineler. Bu ad, aracının bağlantı üzerinde göreceği anlamla ilgili tek işarettir ve o da yalnız açılış alışverişinde bir kez geçer. Sonrasında aracı, adı bildiği ama içeriğini okuyamadığı bir akışı iletir.

Kararların Taşındığı Yer

İki yüz karar yok olmaz, taşınır. Tünel rejiminde her biri artık bir uçta alınmak zorundadır ve bu, uygulamaya yazılacak iş demektir.

Karar Açık rejimde kimin Tünelde kimin Uçta ne gerektirir
yinelenebilir aracı gönderen uç ileti kimliği ve yineleme kuralı
saklanabilir aracı alan uç uygulama içi kopya ve geçersizleştirme
paylasilabilir aracı sunucu kapsamı çerçevenin içine yazmak
taze aracı alan uç sürüm ya da sayaç alanı
yonlendirilebilir aracı bağlantı kurulumu açılışta bir kez, sonra sabit

Tablonun sağ sütunu, WebSocket kullanan bir uygulamanın yeniden yazmak zorunda kaldığı şeydir. HTTP’de bu kararların hepsi belirtimin içinde tanımlıydı ve yolun üzerindeki herhangi bir aracı onları uygulayabiliyordu. Çerçeve biçiminde tanımlı olan tek şey taşımadır; anlamı taşıyan alanları iki uç kendi aralarında kararlaştırır.

Bunun bir kusur olmadığını da kayda geçirmek gerekir. Ölçüm tablosunda tünel satırının yanlış sütunu sıfırdır. Aracı hiçbir yanlış karar almaz, çünkü hiç karar almaz. Açık rejimin beş yanlışı, bildirime inanmanın bedeliydi; tünel o bedeli de ödemez. Seçim, kararı yolun üzerinde ucuza almakla, kararı uçta pahalıya ama tam almak arasındadır.

Özet

  • Yükseltme el sıkışması olağan bir HTTP alışverişidir; 101 Switching Protocols satırı bir sonucu değil, bağlantı üzerinde artık başka bir protokol konuşulacağını bildirir.
  • Aynı kırk alışveriş açık rejimde 195 doğru ve 5 yanlış kararla, hiç tur borcu doğurmadan ölçülürken tünel rejiminde 200 kararın 200’ü alınamaz ve kırk tur borcu doğar.
  • Aracının bağlantı boyunca okuyabildiği tek ileti yükseltme isteğidir ve o da beş kararın yalnız ikisini verir; 101 yanıtı ile çerçevelerin hiçbiri karar üretmez.
  • Çerçeve başlığı iki ile on dört bayt arasındadır ve taşıdığı alanlar — FIN, işlem kodu, maske, uzunluk — taşıma hakkındadır; bayt kısmak karar kaybettirmez, alanın yok olması kaybettirir.
  • Taşınan iki yüz karar uçların işine dönüşür; buna karşılık tünel rejiminde alınan yanlış karar sayısı sıfırdır.

Sonraki Adım

Tünel, aracıyı iletiden tümüyle kopardığı için iki yüz kararın tamamını uçlara taşıdı. Ama sunucunun istemciye kendiliğinden veri göndermesi için bağlantının biçimini değiştirmek zorunlu mudur? Yanıtın gövdesi bir kerede yazılmak zorunda değildir: açık tutulup üzerine parça parça yazılabilir ve bu, HTTP’nin kendi kuralları içinde kalır. Sonraki ders bu seçeneği ölçer — aracı iletiyi hâlâ okuyabildiğinde elinde kaç karar kalır ve okuyabilmenin kendisi ne getirir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat