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,idveretryalanları, 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-IDile 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.