İçeriğe geç
academia.sh

Ders 22 / 25

Sunucu Gönderimli Olaylar

Tek bir açık yanıt gövdesine peş peşe yazılan olaylar: olay akışının satır biçimi, akış yanıtının başlık kuralları, olay kimliğiyle yeniden bağlanma ve kalp atışı satırının ara katmanlara karşı işlevi.

İçindekiler

Uzun yoklama, bağlantıyı tutup tek bir yanıt yazdıktan sonra kapatıyordu. Rezervasyon sırasındaki konumun beş kez değişmesi, beş ayrı bağlantı demekti: her olay için istemci yeniden bağlanıyor, sunucu yeniden yetkilendiriyor, ara katmanlar yeniden yönlendiriyordu.

Oysa ilk bağlantı zaten açıktı. HTTP yanıt gövdesinin bir kerede yazılması zorunlu değildir; gövde açık tutulup üzerine parça parça yazılabilir. Sunucu gönderimli olaylar (Server-Sent Events, SSE) bunu standartlaştırır: sunucudan istemciye tek yönlü, metin tabanlı, üzerinde birden çok olay taşıyan bir akış.

Biçim

Akışın gövdesi satır tabanlıdır ve text/event-stream içerik türüyle işaretlenir. Bir olay, alan satırlarından oluşur ve boş bir satırla biter. Beş alan tanımlıdır.

data: olayın gövdesidir ve tek zorunlu alandır; birden çok data: satırı satır sonlarıyla birleştirilir. event: olayın türünü verir; istemci farklı türler için farklı işleyiciler bağlayabilir. id: olayın kimliğidir ve akışın kaldığı yeri işaretler. retry: istemcinin kopma sonrası kaç milisaniye bekleyeceğini bildirir. İki nokta üst üste ile başlayan satır ise yorumdur; hiçbir olay üretmez ve kalp atışı olarak kullanılır.

Biçimin tamamı düz metindir, dolayısıyla akış curl ile doğrudan okunabilir. Bu, hata ayıklama açısından SSE’nin en pratik yanıdır.

Sunucu

Aşağıdaki sunucu, bir üyenin rezervasyon sırasındaki konumunu yayınlıyor: beş adım ilerleme ve kitabın hazır olduğunu bildiren bir kapanış olayı.

// sse.mjs — rezervasyon sirasinin ilerlemesini sunucu gonderimli olaylarla yayar
import { createServer } from "node:http";

// Uyenin rezervasyon sirasindaki konumu; son olay istegin karsilandigini bildirir.
const OLAYLAR = [
  { id: 1, tur: "sira", veri: { sira: 5, tahmin_dk: 12 } },
  { id: 2, tur: "sira", veri: { sira: 4, tahmin_dk: 9 } },
  { id: 3, tur: "sira", veri: { sira: 3, tahmin_dk: 7 } },
  { id: 4, tur: "sira", veri: { sira: 2, tahmin_dk: 4 } },
  { id: 5, tur: "sira", veri: { sira: 1, tahmin_dk: 2 } },
  { id: 6, tur: "hazir", veri: { kitap: "Tutunamayanlar", sube: 3 } },
];

const sayac = { baglanti: 0, olay: 0 };
const bekle = (ms) => new Promise((c) => setTimeout(c, ms));

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  const adres = new URL(istek.url, "http://yerel");

  if (adres.pathname === "/olcum") {
    yanit.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return yanit.end(JSON.stringify(sayac) + "\n");
  }
  if (adres.pathname !== "/akis") return yanit.writeHead(404).end();

  sayac.baglanti++;
  // Akis yaniti: govde bitmez, bu yuzden uzunluk bildirilmez ve ara katmanlarin
  // yanit tamponlamasi kapatilir.
  yanit.writeHead(200, {
    "content-type": "text/event-stream; charset=utf-8",
    "cache-control": "no-store",
    "x-accel-buffering": "no",
  });

  // Istemci kaldigi yeri Last-Event-ID basligiyla bildirir; yoksa bastan baslar.
  const kaldigiYer = Number(istek.headers["last-event-id"] ?? 0);
  const kes = Number(adres.searchParams.get("kes") ?? 0);

  yanit.write("retry: 2000\n\n");                  // yeniden baglanma araligi (ms)
  yanit.write(": nabiz\n\n");                      // yorum satiri: baglantiyi canli tutar

  let gonderilen = 0;
  for (const o of OLAYLAR.filter((o) => o.id > kaldigiYer)) {
    if (kes && gonderilen === kes) break;          // baglanti kopmasinin benzetimi
    await bekle(50);
    if (yanit.writableEnded) return;               // istemci cekildi
    yanit.write(`id: ${o.id}\nevent: ${o.tur}\ndata: ${JSON.stringify(o.veri)}\n\n`);
    sayac.olay++; gonderilen++;
  }
  yanit.end();
}).listen(8371, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8371"));

Akışın Okunması

curl -N seçeneği çıktı tamponlamasını kapatır ve akışı geldiği gibi gösterir. Aşağıdaki betik önce akışı üç olayda kestiriyor, sonra kaldığı yerden sürdürüyor. Bağlantı noktası 8371 keyfîdir ve boş olmalıdır.

#!/usr/bin/env bash
# sse.mjs'yi baslatir, akisi kesintili okur, Last-Event-ID ile devam eder, durdurur.
node sse.mjs > /dev/null & sunucu=$!
sleep 1
A=http://127.0.0.1:8371

echo "--- ilk baglanti: uc olaydan sonra sunucu kapatiyor ---"
curl -sN "$A/akis?kes=3"
echo "--- yeniden baglanti: Last-Event-ID ile kaldigi yerden ---"
curl -sN -H 'Last-Event-ID: 3' "$A/akis"
echo "--- olcum ---"
curl -sS "$A/olcum"
kill "$sunucu"
--- ilk baglanti: uc olaydan sonra sunucu kapatiyor ---
retry: 2000

: nabiz

id: 1
event: sira
data: {"sira":5,"tahmin_dk":12}

id: 2
event: sira
data: {"sira":4,"tahmin_dk":9}

id: 3
event: sira
data: {"sira":3,"tahmin_dk":7}

--- yeniden baglanti: Last-Event-ID ile kaldigi yerden ---
retry: 2000

: nabiz

id: 4
event: sira
data: {"sira":2,"tahmin_dk":4}

id: 5
event: sira
data: {"sira":1,"tahmin_dk":2}

id: 6
event: hazir
data: {"kitap":"Tutunamayanlar","sube":3}

--- olcum ---
{"baglanti":2,"olay":6}

Ölçüm satırı asıl kazancı gösteriyor: altı olay, iki bağlantı. Kesinti olmasaydı tek bağlantı yeterdi. Aynı altı olay uzun yoklamayla taşınsaydı altı bağlantı, altı yetkilendirme ve altı yönlendirme kararı gerekirdi.

Yeniden Bağlanma

Akış er ya da geç kopar: sunucu yeniden dağıtılır, ara katman zaman aşımına uğrar, ağ kesilir. SSE’yi uzun yoklamadan ayıran şey, kopmanın protokolün beklediği bir durum olmasıdır.

İki alan bu davranışı yönetir. retry: ile bildirilen süre, istemcinin kopma sonrası ne kadar bekleyeceğini belirler; sunucu bu değeri yükün durumuna göre değiştirebilir ve böylece yeniden bağlanma fırtınasını yönetir. id: ile bildirilen kimlik ise istemcide saklanır ve yeniden bağlanma isteğinde Last-Event-ID başlığı olarak geri gönderilir. Sunucu bu başlığı okuyup akışa oradan devam eder.

Tarayıcı tarafındaki EventSource arayüzü ikisini de kendiliğinden yapar: kopmayı algılar, retry süresi kadar bekler, son gördüğü kimliği başlığa koyar. Sunucunun tek sorumluluğu, o kimliğin anlamlı bir sürdürme noktası olmasını sağlamaktır.

Bu, sunucu tarafında bir gereksinim doğurur. Kimliğe göre sürdürebilmek için olayların bir süre saklanması gerekir; sunucu yalnız “şu anki durumu” biliyorsa, kopma sırasında üretilmiş olaylar kaybolur. Uygulamada iki çözüm vardır: son N olayı bellekte tutan bir halka tampon, ya da olay yerine durumun tamamını göndermek. İkincisi kimliği gereksizleştirir çünkü her olay kendi başına yeterlidir; karşılığında her olay daha büyüktür.

Akış Yanıtının Kuralları

Akış yanıtı, olağan bir yanıttan dört noktada ayrılır ve bu farkların üçü başlıklarda görünür.

Gövdenin uzunluğu bildirilmez, çünkü bilinmez; Content-Length yazılamaz. Yanıt saklanmamalıdır; Cache-Control: no-store bunu söyler, aksi hâlde bir ara önbellek akışı tamponlayıp tek parça hâlinde teslim etmeye çalışır. Ara katmanların yanıt tamponlaması kapatılmalıdır; kod bunu X-Accel-Buffering başlığıyla yapıyor, bu başlık standart değil ama yaygın olarak tanınan bir yönergedir. Dördüncü fark koddadır: her yazmadan önce istemcinin çekilip çekilmediği sınanır, yoksa sunucu kapalı bir yuvaya yazmayı sürdürür.

Kalp atışı satırının işlevi de buradan gelir. Uzun süre veri akmayan bir bağlantıyı ara katmanlar ölü sayıp kapatabilir. Düzenli aralıklarla gönderilen bir yorum satırı — hiçbir olay üretmeyen : nabiz — bağlantıyı canlı tutar ve istemci tarafında hiçbir yan etkisi olmaz.

Sınırlar

SSE tek yönlüdür. İstemci sunucuya veri gönderemez; bunun için ayrı bir HTTP isteği açması gerekir. Rezervasyon sırasını izlemek için bu yeterlidir, ama iki tarafın da sürekli konuştuğu bir düzen için değildir.

İkinci sınır aktarım katmanındadır. HTTP/1.1 üzerinde tarayıcıların aynı kaynağa açtığı eşzamanlı bağlantı sayısı sınırlıdır ve sürekli açık duran bir akış bu paydan bir tanesini kalıcı olarak tutar; birkaç sekme açan kullanıcı sınırı doldurabilir. HTTP/2 üzerinde akışlar tek bağlantıda çoğullandığı için bu sınır ortadan kalkar.

Üçüncü sınır veri biçimidir: akış metindir, ikili veri taşınamaz. İkili veri gerekiyorsa kodlanması gerekir ve bu, boyutu büyütür.

Özet

  • Sunucu gönderimli olaylar, açık tutulan tek bir HTTP yanıt gövdesine peş peşe yazılan, sunucudan istemciye tek yönlü, metin tabanlı bir akıştır.
  • Olay data, event, id ve retry alanlarından oluşur ve boş satırla biter; iki nokta üst üste ile başlayan yorum satırı kalp atışı olarak kullanılır.
  • Ölçümde altı olay iki bağlantıyla taşındı; aynı olaylar uzun yoklamayla altı bağlantı gerektirirdi.
  • Kopma protokolün beklediği bir durumdur: istemci retry süresi kadar bekler ve son gördüğü kimliği Last-Event-ID başlığıyla gönderir, sunucu akışa oradan devam eder.
  • Akış yanıtı uzunluk bildirmez, saklanmamalıdır, ara katman tamponlaması kapatılmalıdır ve her yazmadan önce istemcinin bağlı olduğu sınanmalıdır.

Sonraki Adım

Akış tek yönlü olduğu sürece rezervasyon sırası için yeterli. Ama kütüphane servisinde iki tarafın da konuştuğu durumlar var: görevlinin şube stok sayacını düzeltmesi, bir üyenin sırasından çekilmesi, birden çok görevlinin aynı ödünç kaydını aynı anda düzenlemesi. Bunların her biri için ayrı bir HTTP isteği açmak, akışın kazandırdığı gecikmeyi geri verir. Sonraki ders çift yönlü bir bağlantının nasıl kurulduğunu ele alır: HTTP üzerinden başlayan, el sıkışmadan sonra kendi çerçeve biçimine geçen ve iki yönde de mesaj taşıyan bir bağlantı.

İ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