İçeriğe geç
academia.sh

Ders 14 / 22

Gerçek Zamanlı Veri

Sunucunun kendiliğinden ürettiği verinin istemciye taşınması; yoklama, olay akışı ve soket arasındaki seçim, akış biçiminin çözümlenmesi, kopan bağlantının kimlikle devam ettirilmesi ve olayların duruma işlenmesi.

İçindekiler

Buraya kadar veri hep istemcinin sorduğu anda geldi. Kuzey Yamaç istasyonu ise ölçümü kimse sormadan üretir: sensör on dakikada bir okuma yapar, sunucu kaydı işler. Ölçüm geçmişi ekranı açıkken üretilen bir kaydın görünmesi için kullanıcının sayfayı yenilemesi gerekiyorsa, ekran gerçeği geriden izler.

Bu ders akışın yönünü tersine çeviren taşımaları ele alır. Ortak nokta şudur: bağlantıyı her zaman istemci kurar — tarayıcıya dışarıdan bağlantı açılamaz — ama veri gönderme zamanını sunucu belirler. Farkları, bağlantının ne kadar sürdüğünde ve kaç yöne işlediğindedir.

Üç Taşıma Yolu

Yol Yön Bağlantı Yeniden bağlanma Uygun olduğu iş
Yoklama istemci sorar her seferinde yeni gerekmez seyrek ve gecikmeye toleranslı veri
Uzun yoklama istemci sorar yanıt gelene dek açık her yanıttan sonra olay seyrek, gecikme önemli
Olay akışı sunucu iter tek uzun HTTP yanıtı yerleşik tek yönlü bildirim akışı
Soket iki yönlü tek uzun bağlantı elle kurulur karşılıklı ve sık iletişim

Yoklama (polling), belirli aralıklarla istek atmaktır. Kurulumu en ucuz yoldur ve önceki iki dersin istek katmanı olduğu gibi kullanılır. Bedeli iki yerdedir: veri değişmemişken de istek atılır, ve en kötü durumda gecikme aralık kadardır. Otuz saniyede bir yoklanan bir ekran, olayı en geç otuz saniye sonra görür. Koşullu istek başlıklarıyla — Tarayıcı ve Web Platformu kursunda tanıtılan doğrulayıcılarla — gövde aktarımı azaltılabilir, ama istek sayısı azalmaz.

Uzun yoklama (long polling), sunucunun yanıtı veri oluşana kadar bekletmesidir. İstek sayısı olay sayısına iner, gecikme kaybolur. Karşılığında her olaydan sonra yeni bir istek kurulur ve sunucu tarafında açık bekleyen isteklerin tutulması gerekir.

Olay akışı (Server-Sent Events), tek bir HTTP yanıtının kapanmadan sürmesidir. Sunucu aynı yanıt gövdesine metin parçaları yazmaya devam eder, istemci geldikçe okur. Sıradan bir HTTP yanıtı olduğu için ara katmanlarla — vekil sunucular, sıkıştırma, kimlik doğrulama başlıkları — uyumludur. Yalnız tek yönlüdür: istemci bu kanaldan bir şey gönderemez, göndermek istediğinde ayrı bir istek atar.

Soket (WebSocket), HTTP üzerinden başlayıp protokol değiştirerek kurulan çift yönlü bir kanaldır. İki taraf da istediği an ileti gönderir. Karşılıklı etkileşim gerektiren işlerde — birlikte düzenleme, canlı imleç, oyun — tek seçenektir.

Seçim kuralı sadedir: veri tek yönde akıyorsa olay akışı yeterlidir ve daha az parça gerektirir. İstemcinin de aynı kanaldan sık ileti göndermesi gerekiyorsa soket kurulur.

Olay Akışının Biçimi

Akış, satır tabanlı bir metin biçimidir. Her satır alan: değer yazar, boş satır olayı tamamlar. Dört alan tanımlıdır: event olayın adını, data gövdesini, id sıra kimliğini, retry yeniden bağlanma aralığını verir. İki nokta ile başlayan satır yorumdur ve yok sayılır — bağlantıyı canlı tutan kalp atışı böyle gönderilir.

Aşağıdaki dosya hem akış üreten bir sunucu kurar hem de çözümleyiciyi ona karşı çalıştırır. Bağlantı noktasını işletim sistemi seçer; sayı çalıştıran makineye göre değişir ve çıktıda görünmez.

// olay-akisi.mjs — text/event-stream ureten yerel sunucu + akis cozumleyici
import http from "node:http";

// --- Sunucu: son kimlikten sonrasini yayimlar -------------------------------
const OLCUMLER = [
  { kimlik: 1, kod: "KY-01", deger: -4.2 },
  { kimlik: 2, kod: "KY-02", deger: -6.1 },
  { kimlik: 3, kod: "KY-01", deger: -4.4 },
  { kimlik: 4, kod: "KY-02", deger: -5.9 },
];

const sunucu = http.createServer((istek, yanit) => {
  const sonKimlik = Number(istek.headers["last-event-id"] ?? 0);
  yanit.writeHead(200, {
    "content-type": "text/event-stream",
    "cache-control": "no-store",
    connection: "keep-alive",
  });
  yanit.write("retry: 3000\n\n");            // yeniden baglanma araligi onerisi
  yanit.write(": akış açık\n\n");            // yorum satiri: kalp atisi
  for (const o of OLCUMLER.filter((o) => o.kimlik > sonKimlik)) {
    yanit.write(`id: ${o.kimlik}\n`);
    yanit.write("event: olcum\n");
    yanit.write(`data: ${JSON.stringify(o)}\n\n`);
  }
  yanit.end();
});
await new Promise((coz) => sunucu.listen(0, "127.0.0.1", coz));
const TABAN = `http://127.0.0.1:${sunucu.address().port}`;

// --- Akis cozumleyici: alan satirlarindan olay uretir -----------------------
function cozumleyici(uygula) {
  let tampon = "";
  let olay = { ad: "message", veri: [], kimlik: null, yenidenDeneme: null };
  const sifirla = () => { olay = { ad: "message", veri: [], kimlik: null, yenidenDeneme: null }; };

  return function besle(parca) {
    tampon += parca;
    let kesme;
    while ((kesme = tampon.indexOf("\n")) !== -1) {
      const satir = tampon.slice(0, kesme);
      tampon = tampon.slice(kesme + 1);

      if (satir === "") {                        // bos satir: olayi gonder
        if (olay.veri.length > 0) uygula({ ...olay, veri: olay.veri.join("\n") });
        else if (olay.yenidenDeneme !== null) uygula({ ...olay, veri: null });
        sifirla();
        continue;
      }
      if (satir.startsWith(":")) continue;       // yorum satiri yok sayilir

      const ikiNokta = satir.indexOf(":");
      const alan = ikiNokta === -1 ? satir : satir.slice(0, ikiNokta);
      let deger = ikiNokta === -1 ? "" : satir.slice(ikiNokta + 1);
      if (deger.startsWith(" ")) deger = deger.slice(1);

      if (alan === "event") olay.ad = deger;
      else if (alan === "data") olay.veri.push(deger);
      else if (alan === "id" && !deger.includes("\0")) olay.kimlik = deger;
      else if (alan === "retry" && /^\d+$/.test(deger)) olay.yenidenDeneme = Number(deger);
    }
  };
}

// --- Tuketici ---------------------------------------------------------------
async function akisiTuket(sonKimlik, enCok) {
  const alinan = [];
  const denetleyici = new AbortController();
  let dur = false;
  const besle = cozumleyici((olay) => {
    if (dur) return;                       // yeterince alindi: kalani cozumleme
    alinan.push(olay);
    if (alinan.filter((o) => o.veri !== null).length >= enCok) {
      dur = true;
      denetleyici.abort("yeter");
    }
  });

  const yanit = await fetch(`${TABAN}/akis`, {
    headers: sonKimlik ? { "last-event-id": String(sonKimlik) } : {},
    signal: denetleyici.signal,
  });
  const cozucu = new TextDecoder();
  try {
    for await (const yigin of yanit.body) besle(cozucu.decode(yigin, { stream: true }));
  } catch {
    if (!denetleyici.signal.aborted) throw new Error("akış koptu");
  }
  return alinan;
}

const yaz = (o) => console.log(
  `  ad=${o.ad.padEnd(7)} kimlik=${String(o.kimlik).padEnd(4)}` +
  ` yenidenDeneme=${String(o.yenidenDeneme).padEnd(4)} veri=${o.veri}`);

console.log("ilk bağlantı (ilk iki olaydan sonra iptal):");
const birinci = await akisiTuket(0, 2);
birinci.forEach(yaz);

const sonAlinan = birinci.filter((o) => o.kimlik !== null).at(-1).kimlik;
console.log(`son alınan kimlik: ${sonAlinan}`);

console.log("yeniden bağlantı (Last-Event-ID ile):");
const ikinci = await akisiTuket(sonAlinan, 99);
ikinci.forEach(yaz);

// --- Olaylarin depoya islenmesi: tekrar eden kimlik yok sayilir -------------
const depo = new Map();
const gorulen = new Set();
let yoksayilan = 0;
for (const olay of [...birinci, ...ikinci, ...birinci]) {  // ilk parti bilerek tekrar
  if (olay.veri === null || olay.ad !== "olcum") continue;
  if (gorulen.has(olay.kimlik)) { yoksayilan++; continue; }
  gorulen.add(olay.kimlik);
  const o = JSON.parse(olay.veri);
  depo.set(o.kod, o);
}
console.log("işlenen olay:", gorulen.size, "| yok sayılan tekrar:", yoksayilan);
console.log("son durum   :", JSON.stringify([...depo.values()]));

sunucu.close();
ilk bağlantı (ilk iki olaydan sonra iptal):
  ad=message kimlik=null yenidenDeneme=3000 veri=null
  ad=olcum   kimlik=1    yenidenDeneme=null veri={"kimlik":1,"kod":"KY-01","deger":-4.2}
  ad=olcum   kimlik=2    yenidenDeneme=null veri={"kimlik":2,"kod":"KY-02","deger":-6.1}
son alınan kimlik: 2
yeniden bağlantı (Last-Event-ID ile):
  ad=message kimlik=null yenidenDeneme=3000 veri=null
  ad=olcum   kimlik=3    yenidenDeneme=null veri={"kimlik":3,"kod":"KY-01","deger":-4.4}
  ad=olcum   kimlik=4    yenidenDeneme=null veri={"kimlik":4,"kod":"KY-02","deger":-5.9}
işlenen olay: 4 | yok sayılan tekrar: 2
son durum   : [{"kimlik":3,"kod":"KY-01","deger":-4.4},{"kimlik":4,"kod":"KY-02","deger":-5.9}]

Kopmayı Kabul Etmek

Uzun süren bağlantı kopar. Kablosuz ağ değişir, aradaki vekil sunucu boşta kalan bağlantıyı kapatır, dizüstü bilgisayar uyanır. Bu bir hata değil, akış tabanlı çalışmanın normal durumudur; tasarım kopmayı önlemeye değil, kopmadan sonra doğru yerden devam etmeye yönelir.

Devamı sağlayan alan id’dir. İstemci aldığı son kimliği saklar, yeniden bağlanırken Last-Event-ID başlığıyla gönderir, sunucu o kimlikten sonrasını yayımlar. Çıktıda bu akış görünüyor: ilk bağlantı iki olaydan sonra kesildi, ikinci bağlantı üç ve dörtten başladı. Bu düzenek çalışsın diye sunucunun yayımladığı olayları bir süre saklaması gerekir; saklamıyorsa kopma aralığındaki olaylar kaybolur ve istemcinin yeniden bağlandıktan sonra tam durumu bir kez daha çekmesi gerekir.

retry alanı yeniden bağlanma aralığını milisaniye olarak önerir. Kendi çözümleyicisini yazan kod bu değeri dikkate almalı ve sabit bir aralık yerine artan bir bekleme uygulamalıdır; sunucu çöktüğünde bütün istemcilerin aynı aralıkla dönmesi, ayağa kalkmaya çalışan sunucuyu yeniden düşürür. Bu hesabın kendisi bir sonraki dersin konusudur.

Son iki satır üçüncü kuralı gösteriyor: aynı olay iki kez gelebilir. Yeniden bağlanma sırasında sunucu son kimliği kapsayıcı yorumlayabilir, ya da istemci kaydettiği kimliği yazamadan kopabilir. İşleme kodu bu yüzden değişmez olmalıdır: aynı olayı iki kez uygulamak, bir kez uygulamakla aynı sonucu vermelidir. Örnekte bu, görülen kimliklerin kümede tutulmasıyla sağlanıyor; ilk partinin tekrarı yok sayıldı ve son durum değişmedi.

Değişmezliğin ikinci yolu, olayı bir komut değil bir duyuru olarak yazmaktır. “Ölçüm sayacını bir artır” iki kez uygulandığında bozulur; “KY-01 istasyonunun son ölçümü o-114, değeri −4.4” iki kez uygulandığında aynı sonucu verir. Akış üzerinden taşınan olaylar mümkün olduğunca ikinci biçimde tasarlanır.

Çift Yönlü Soketin Farkları

Soket, sıradan bir HTTP isteğiyle başlar ve sunucu kabul ederse bağlantı protokol değiştirir. Bu andan sonra HTTP semantiği — yöntem, durum kodu, başlıklar — geçerli değildir; kanal üzerinde metin ya da ikili iletiler taşınır. Uygulamanın kendi ileti biçimini tanımlaması gerekir; hangi alanın ileti türünü söylediği, sıra numarasının nerede durduğu istemci ile sunucunun anlaşmasına kalmıştır.

Dört noktada olay akışından ayrılır. Yeniden bağlanma yerleşik değildir; kopmayı saptamak ve yeniden kurmak uygulamanın işidir. Kopma her zaman fark edilmez; iletim sessizce kesildiğinde bağlantı açık görünmeye devam edebilir, bu yüzden iki taraf düzenli aralıklarla yoklama iletisi gönderip yanıt bekler ve yanıt gelmezse bağlantıyı ölü sayar. Sıraya alma sorumluluğu istemcidedir: bağlantı kopukken gönderilmek istenen iletiler bir kuyrukta bekletilir, bağlantı kurulunca boşaltılır — ya da bilinçli olarak atılır. Kimlik doğrulaması ilk el sıkışmada yapılır; kanal açıldıktan sonra başlık gönderilemez, bu yüzden oturumun kanal ömrü boyunca geçerliliği ayrı bir sorundur ve Oturum Yenileme dersinde ele alınır.

Akışın Duruma İşlenmesi

Gelen olayın tek başına bir anlamı yoktur; anlamı, üzerine uygulandığı duruma göre oluşur. Üç sorun her uygulamada karşımıza çıkar.

Başlangıç durumu ile akışın buluşması. Ekran açıldığında önce tam liste çekilir, sonra akış dinlenir. İki işlem arasında geçen sürede üretilen olaylar kaçar. Doğru sıra tersidir: önce akış açılır ve gelen olaylar tampona alınır, sonra tam liste çekilir, listenin damgasından sonraki tampon olayları uygulanır, tampon boşaltılır.

Sıra dışı gelme. Olaylar farklı yollardan gelirse — biri akıştan, biri kullanıcının kendi işleminin yanıtından — daha eski bir değer daha yeninin üstüne yazılabilir. Çözüm, her kaydın bir sürüm ya da zaman damgası taşıması ve işleme kodunun yalnız daha yeni damgayı kabul etmesidir.

Görünmeyen sekme. Arka plandaki bir sekmede akışı açık tutmak, hem sunucuda bağlantı tüketir hem istemcide iş üretir. Görünürlük değiştiğinde akış kapatılır, sekme öne geldiğinde son kimlikten devam edilerek yeniden açılır. Bu, aynı zamanda kaçırılan olayların telafi yolunu kullanmanın doğal bir örneğidir.

Özet

  • Bağlantıyı her zaman istemci kurar; taşımaların farkı bağlantının süresinde ve yön sayısındadır. Yoklama en ucuz kurulumdur, gecikmesi aralığı kadardır.
  • Olay akışı tek yönlü ve sıradan bir HTTP yanıtıdır; soket çift yönlüdür ve protokol değiştirerek kurulur, karşılığında yeniden bağlanma ile canlılık denetimini uygulamaya bırakır.
  • Akış biçimi satır tabanlıdır: event, data, id ve retry alanları, boş satırla tamamlanan olaylar, yorum satırıyla gönderilen kalp atışı.
  • Kopma normaldir; son olay kimliği saklanır ve yeniden bağlanmada Last-Event-ID ile bildirilir. Sunucu yayımladığı olayları bir süre saklamıyorsa devam mümkün olmaz.
  • Aynı olay iki kez gelebilir; işleme kodu değişmez olmalı, olaylar artırım komutu değil son durum duyurusu biçiminde tasarlanmalıdır.
  • Başlangıç listesi ile akış buluşturulurken önce akış açılıp olaylar tamponlanır; sıra dışı gelen olaylar sürüm damgasıyla elenir.

Sonraki Adım

Son üç ders veriyi almanın yollarını kurdu; hepsinin ortak noktası, verinin bir süre henüz gelmemiş olmasıdır. Ekran o sırada bir şey göstermek zorunda: boş bir çerçeve, bir yükleme göstergesi, eski veri, ya da hata. Bu durumları ayrı ayrı mantıksal bayraklarla tutmak — yükleniyor, hata var, veri var — kısa sürede birbiriyle çelişen kombinasyonlar üretir. Sonraki ders görünümün hangi durumlarda bulunabileceğini sayılı bir kümeye indirger ve geçişleri bir tabloyla sınar.

İ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