Ders 07 / 34
Eşzamanlı ve Eşzamansız API'ler
İstek–yanıt kalıbının dışına çıkan iletişim biçimleri: olay akışı, uzun yoklama ve kısa yoklama aynı olay dizisinde bağlantı sayısı, taşınan bayt ve teslim gecikmesiyle karşılaştırılır; uzun süren iş kabul edilen istek ve ayrı bir durum kaynağıyla modellenir.
İçindekiler
Buraya kadarki beş biçemin ortak bir varsayımı vardı: iletişimi istemci başlatır, sunucu yanıt verir, alışveriş biter. Kütüphane servisinin bazı işleri bu kalıba girmez.
Üç durum bu kalıbı kırar. Birincisi, olay sunucuda oluşur: bir kitabın rafa dönmesini bekleyen istemciye kimse haber vermez, istemci de ne zaman soracağını bilmez. İkincisi, iş uzun sürer: şube sayımı dakikalarca çalışır ve isteği o süre boyunca açık tutmak bağlantıyı da istemciyi de bekletir. Üçüncüsü, aynı bildirim çok sayıda tüketiciye gider: her tüketicinin ayrı ayrı sorması aynı işi tekrarlar.
Bu ders ilk durumu üç biçimde yazıp ölçer, sonra ikinci durumun kalıbını kurar.
Aynı Olayı Üç Biçimde Yayımlamak
Sunucu altı olay yayımlar — kitapların rafa dönüşü — ve aynı olay dizisini üç ayrı uçtan sunar. Olay akışı bağlantıyı açık tutar ve olay oluştukça yazar. Uzun yoklama isteği yeni olay çıkana kadar yanıtsız bekletir. Kısa yoklama her isteği hemen yanıtlar; boş dönebilir, istemci belirli aralıklarla yeniden sorar.
Üçünde de istemci en son gördüğü olayın kimliğini gönderir. Bu, yoklama biçimlerinde bir zorunluluktur: iki istek arasındaki boşlukta oluşan olaylar aksi hâlde kaybolur.
// sunucu.mjs — ayni olay akisini uc bicimde yayimlar: olay akisi, uzun yoklama, kisa yoklama import { createServer } from "node:http"; const OLAYLAR = []; // yayimlanmis olaylar; kimlik dizideki sira numarasidir const bekleyenler = []; // uzun yoklamada yanit bekleyen istekler const akislar = new Set(); // acik olay akisi baglantilari const isler = new Map(); // uzun suren islerin durumu let isSayaci = 0; const yayimla = (olay) => { OLAYLAR.push(olay); for (const y of akislar) y.write(`id: ${OLAYLAR.length}\ndata: ${JSON.stringify(olay)}\n\n`); while (bekleyenler.length) bekleyenler.pop()(); }; const json = (yanit, govde) => { const metin = JSON.stringify(govde); yanit.setHeader("Content-Type", "application/json; charset=utf-8"); yanit.setHeader("Content-Length", Buffer.byteLength(metin)); yanit.writeHead(200).end(metin); }; const sonrasi = (n) => OLAYLAR.slice(n).map((o, i) => ({ id: n + i + 1, ...o })); createServer((istek, yanit) => { yanit.sendDate = false; const adres = new URL(istek.url, "http://yerel"); const son = Number(adres.searchParams.get("son") ?? 0); // 1) Olay akisi: baglanti acik kalir, olay olustukca sunucu yazar. if (adres.pathname === "/akis") { yanit.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", Connection: "keep-alive" }); for (const o of sonrasi(son)) yanit.write(`id: ${o.id}\ndata: ${JSON.stringify(o)}\n\n`); akislar.add(yanit); istek.on("close", () => akislar.delete(yanit)); return; } // 2) Uzun yoklama: yeni olay yoksa yanit bekletilir, olay gelince yazilir. if (adres.pathname === "/uzun") { if (OLAYLAR.length > son) return json(yanit, { olaylar: sonrasi(son) }); const zamanAsimi = setTimeout(() => { const k = bekleyenler.indexOf(surdur); if (k >= 0) bekleyenler.splice(k, 1); json(yanit, { olaylar: [] }); }, 2000); const surdur = () => { clearTimeout(zamanAsimi); json(yanit, { olaylar: sonrasi(son) }); }; bekleyenler.push(surdur); return; } // 3) Kisa yoklama: her istek hemen yanitlanir, istemci belli araliklarla sorar. if (adres.pathname === "/kisa") return json(yanit, { olaylar: sonrasi(son) }); // 4) Uzun suren is: istek kabul edilir, sonuc ayri bir kaynaktan izlenir. if (adres.pathname === "/sayimlar" && istek.method === "POST") { const kimlik = `is-${++isSayaci}`; isler.set(kimlik, { durum: "calisiyor", sonuc: null }); setTimeout(() => isler.set(kimlik, { durum: "bitti", sonuc: { eksik: 3 } }), 600); yanit.setHeader("Location", `/isler/${kimlik}`); yanit.setHeader("Content-Type", "application/json; charset=utf-8"); return yanit.writeHead(202).end(JSON.stringify({ is: kimlik })); } if (adres.pathname.startsWith("/isler/")) return json(yanit, isler.get(adres.pathname.slice(7))); // Olculerin ayni kosulda calismasi icin yayin disaridan baslatilir. if (adres.pathname === "/basla") { basla(); return json(yanit, { baslatildi: true }); } return json(yanit, { hata: "bulunamadi" }); }).listen(8491, "127.0.0.1", () => console.log("olay ucu 127.0.0.1:8491")); // Alti olay, 250 ms araliklarla: bir kitabin rafa donmesi sunucuda olusan bir olaydir. const basla = () => { let n = 0; const t = setInterval(() => { yayimla({ tur: "kitap_rafa_dondu", isbn: `978-000000000${n}`, an: Date.now() }); if (++n === 6) clearInterval(t); }, 250); };
Ölçümün geçerli olması için üç istemcinin de aynı olay dizisini, aynı anda izlemesi gerekir. Bu yüzden yayın kendiliğinden başlamaz: üç istemci bağlandıktan sonra dışarıdan tetiklenir.
// olc.mjs — uc bicimi ayni olay akisinda, ayni anda olcer: baglanti sayisi, bayt, teslim gecikmesi import { Agent, request } from "node:http"; import { connect } from "node:net"; const olustur = () => { const durum = { baglanti: 0, istek: 0, bayt: 0, gecikmeler: [] }; const ajan = new Agent({ keepAlive: false }); // her istek yeni baglanti acar ajan.createConnection = (s) => { durum.baglanti++; const b = connect(s); b.on("data", (p) => (durum.bayt += p.length)); return b; }; return { durum, ajan }; }; const kaydet = (durum, olaylar) => { for (const o of olaylar) durum.gecikmeler.push(Date.now() - o.an); return olaylar.length ? olaylar[olaylar.length - 1].id : null; }; const al = (birim, yol) => new Promise((coz) => { birim.durum.istek++; request({ agent: birim.ajan, host: "127.0.0.1", port: 8491, path: yol }, (y) => { let m = ""; y.on("data", (p) => (m += p)); y.on("end", () => coz(JSON.parse(m))); }).end(); }); // 1) OLAY AKISI — tek baglanti acik kalir, sunucu yazdikca istemci okur const akis = olustur(); const akisIsi = new Promise((bitti) => { akis.durum.istek++; request({ agent: akis.ajan, host: "127.0.0.1", port: 8491, path: "/akis?son=0" }, (y) => { y.on("data", (p) => { for (const blok of String(p).split("\n\n")) { const e = /data: (.+)/.exec(blok); if (e) kaydet(akis.durum, [JSON.parse(e[1])]); } if (akis.durum.gecikmeler.length === 6) { y.destroy(); bitti(); } }); }).end(); }); // 2) UZUN YOKLAMA — sunucu yanit vermeden bekletir, olay gelince yazar; istemci yeniden baglanir const uzun = olustur(); const uzunIs = (async () => { let son = 0; while (uzun.durum.gecikmeler.length < 6) son = kaydet(uzun.durum, (await al(uzun, `/uzun?son=${son}`)).olaylar) ?? son; })(); // 3) KISA YOKLAMA — sabit aralikla sorma const kisa = olustur(); const kisaIs = (async () => { let son = 0; while (kisa.durum.gecikmeler.length < 6) { son = kaydet(kisa.durum, (await al(kisa, `/kisa?son=${son}`)).olaylar) ?? son; if (kisa.durum.gecikmeler.length < 6) await new Promise((c) => setTimeout(c, 500)); } })(); await new Promise((c) => setTimeout(c, 200)); // uc istemci de baglandiktan sonra yayin baslar await fetch("http://127.0.0.1:8491/basla"); await Promise.all([akisIsi, uzunIs, kisaIs]); const ozet = (ad, durum) => { const g = durum.gecikmeler; console.log(` ${ad.padEnd(13)} baglanti=${String(durum.baglanti).padStart(2)} ` + `bayt=${String(durum.bayt).padStart(4)} olay=${g.length} ` + `ortalama gecikme=${String(Math.round(g.reduce((t, v) => t + v, 0) / g.length)).padStart(3)} ms ` + `en buyuk=${String(Math.max(...g)).padStart(3)} ms`); }; console.log("-- alti olay, 250 ms araliklarla yayimlandi --"); ozet("olay akisi", akis.durum); ozet("uzun yoklama", uzun.durum); ozet("kisa yoklama", kisa.durum); // 4) UZUN SUREN IS — istek kabul edilir, sonuc ayri bir kaynaktan izlenir console.log("\n-- uzun suren is: kabul edilen istek --"); const t0 = Date.now(); const kabul = await fetch("http://127.0.0.1:8491/sayimlar", { method: "POST" }); const yer = kabul.headers.get("location"); console.log(` POST /sayimlar -> HTTP ${kabul.status} Location: ${yer}`); for (let i = 0; i < 5; i++) { const d = await (await fetch(`http://127.0.0.1:8491${yer}`)).json(); console.log(` GET ${yer} (+${Date.now() - t0} ms) -> ${JSON.stringify(d)}`); if (d.durum === "bitti") break; await new Promise((c) => setTimeout(c, 200)); }
node sunucu.mjs > /dev/null & p=$! curl -s --retry 20 --retry-connrefused --retry-delay 0 -o /dev/null "http://127.0.0.1:8491/kisa?son=0" node olc.mjs kill $p
-- alti olay, 250 ms araliklarla yayimlandi --
olay akisi baglanti= 1 bayt= 663 olay=6 ortalama gecikme= 3 ms en buyuk= 5 ms
uzun yoklama baglanti= 6 bayt=1170 olay=6 ortalama gecikme= 2 ms en buyuk= 4 ms
kisa yoklama baglanti= 5 bayt=1055 olay=6 ortalama gecikme=165 ms en buyuk=292 ms
-- uzun suren is: kabul edilen istek --
POST /sayimlar -> HTTP 202 Location: /isler/is-1
GET /isler/is-1 (+14 ms) -> {"durum":"calisiyor","sonuc":null}
GET /isler/is-1 (+219 ms) -> {"durum":"calisiyor","sonuc":null}
GET /isler/is-1 (+425 ms) -> {"durum":"calisiyor","sonuc":null}
GET /isler/is-1 (+629 ms) -> {"durum":"bitti","sonuc":{"eksik":3}}
Gecikme değerleri ve bayt sayıları makineye ve çalışma zamanının yazdığı başlıklara bağlıdır; altı olayın sayısı, bağlantı sayıları ve büyüklük sıralaması bağlı değildir.
Ölçümün Söyledikleri
Olay akışı tek bağlantıyla altı olayı taşır. İstemci bir kez bağlanır, sunucu yazdıkça okur; gecikme milisaniye düzeyindedir. Taşınan bayt da en düşüktür, çünkü altı olay için tek bir başlık kümesi gönderilmiştir.
Uzun yoklama aynı gecikmeyi altı bağlantıyla elde eder. Her olaydan sonra yanıt kapanır ve istemci yeniden bağlanır; her yeniden bağlanma yeni bir istek satırı ve yeni bir başlık kümesi demektir. Bayt sayısı olay akışının yaklaşık iki katıdır. Gecikme düşüktür, çünkü sunucu isteği olay gelene kadar bekletmiştir — beklemenin bedeli baytta değil, açık tutulan istek sayısındadır: her bekleyen istemci sunucuda bir yanıt nesnesi ve bir bağlantı tutar.
Kısa yoklama gecikmeyi yoklama aralığına bağlar. Ortalama gecikme 165 milisaniye, en büyüğü 292 milisaniyedir; yoklama aralığı yarım saniyedir ve beklenen ortalama gecikme aralığın yarısı kadardır. Bağlantı sayısı burada olay sayısına değil, geçen süreye bağlıdır: hiç olay olmasa da istemci sormaya devam eder ve her boş yanıt taşınan bayta eklenir.
Üçü arasındaki seçim tek bir ölçüyle yapılmaz. Kısa yoklama en basit olanıdır ve gecikmenin önemsiz olduğu yerlerde yeterlidir; ara katmanlar için sıradan bir istektir. Uzun yoklama gecikmeyi düşürür ama sunucuda açık tutulan istek sayısını yükseltir. Olay akışı her ikisinde de kazanır, karşılığında uzun ömürlü bağlantı ister: araya giren katmanların bağlantıyı kapatmaması, sunucunun her istemci için bir bağlantı ayırabilmesi ve akışın kopması hâlinde yeniden kurulabilmesi gerekir.
Son nokta ölçümdeki son parametresinin karşılığıdır. Server-Sent Events, kopan bir
bağlantıyı yeniden kurarken en son alınan olay kimliğini Last-Event-ID başlığıyla geri
gönderir; sunucu da kaldığı yerden devam eder. Bu düzenek olmadan hiçbir eşzamansız yayım
güvenilir değildir: kopma anında oluşan olaylar sessizce kaybolur.
Tek Yönlü ve Çift Yönlü
Olay akışı tek yönlüdür: sunucudan istemciye. İstemcinin de sürekli veri göndermesi gerekiyorsa — raf sayımında el uçbiriminin okuduğu her etiketi anında bildirmesi gibi — iki yönlü bir bağlantı gerekir ve bunun karşılığı WebSocket protokolüdür. Bedeli, HTTP’nin anlamlarından çıkmaktır: yöntem, durum kodu, önbellek yönergesi ve koşullu istek kavramları bağlantı kurulduktan sonra geçerli değildir. Üçüncü derste ölçülen kazançların hiçbiri açık soket üzerinde geçerli olmaz.
Seçim ölçütü basittir: veri yalnız sunucudan geliyorsa olay akışı yeterlidir; her iki taraf da sürekli veri gönderiyorsa çift yönlü bağlantı gerekir.
Kabul Edilen İstek
Ölçümün son bölümü ikinci durumu kurar. Şube sayımı istendiğinde sunucu işi bitirmeye
çalışmaz; isteği kabul eder, 202 döndürür ve Location başlığıyla işin izleneceği
kaynağı bildirir. İstemci o kaynağı sorarak durumu öğrenir; iş bitince aynı kaynak sonucu
taşır.
Bu kalıbın üç yararı vardır. İstek süresi işin süresinden ayrılır, dolayısıyla zaman aşımı sorunu ortadan kalkar. İşin durumu bir kaynak olduğu için tek biçim arayüzün içinde kalır; listelenebilir, iptal edilebilir, saklanabilir. Ve aynı iş iki kez istendiğinde ne olacağı ayrı bir karara dönüşür — bu kursun Etkisizlik Anahtarları dersinin konusudur.
İşin bittiğini öğrenmek için istemcinin sorması da zorunlu değildir: durum kaynağı bir olay akışıyla birleştirildiğinde, iş bittiğinde bildirim istemciye kendiliğinden ulaşır. İki kalıp birbirinin yerine değil, birlikte kullanılır.
Özet
- İstek–yanıt kalıbı üç durumda yetmez: olayın sunucuda oluşması, işin uzun sürmesi ve aynı bildirimin çok sayıda tüketiciye gitmesi.
- Ölçümde olay akışı altı olayı tek bağlantı ve 663 baytla, uzun yoklama altı bağlantı ve 1170 baytla taşıdı; ikisinin de gecikmesi milisaniye düzeyinde kaldı.
- Kısa yoklamada gecikme yoklama aralığına bağlıdır: yarım saniyelik aralıkta ortalama 165, en büyük 292 milisaniye ölçüldü ve bağlantı sayısı olay sayısına değil geçen süreye bağlandı.
- Her yoklama biçimi son görülen olay kimliğini taşımak zorundadır; kopma ve istekler arası boşlukta oluşan olaylar aksi hâlde kaybolur.
- Uzun süren iş
202ile kabul edilir ve ayrı bir durum kaynağıyla izlenir; böylece istek süresi işin süresinden ayrılır ve iş, tek biçim arayüzün içinde bir kaynağa dönüşür.
Sonraki Adım
Bu konu biçemleri karşılaştırdı ve her birinin neyi kolaylaştırıp neyi zorlaştırdığını ölçtü. Bundan sonrası bir seçimin ayrıntısıdır: kaynak temelli biçemin gerçekten tasarlanması. Ölçümlerde kaynaklar hazır varsayıldı — kitap, üye, ödünç, iş — oysa bunların kaynak olması bir karardı. Alan kavramlarının hangisi bir kaynağa dönüşür, hangisi bir başkasının alanı olarak kalır? “Kitabı uzat” gibi bir eylem hangi kaynağa oturur? Sonraki konunun ilk dersi bu soruyla başlar ve kaynak modelini kütüphane alanının kavramlarından çıkarır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.