Bir pano oluşturuyorsanız ve sayıların güncel kalması gerekiyorsa, genellikle iki seçenekten birine başvurursunuz: her saniye bir istek gönderen bir zamanlayıcı veya bir WebSocket. Veri yalnızca tek yönlü akıyorsa, üçüncü bir seçenek daha vardır.
Sunucu Tarafından Gönderilen Olaylar'ı (Server-Sent Events) sıfırdan açıklamayacağım; bunun için yüzlerce yazı var. Yazmak istediğim şey, bağlantı kesildiğinde ne olduğu. İşte bu kısım, bu teknolojiyi bir oyuncaktan gerçekten kullanabileceğiniz bir şeye dönüştürüyor.
- Bir istek, asla bitmeyen bir yanıt
HTTP normalde şöyle çalışır: istemci sorar, sunucu yanıt verir, alışveriş biter. Sunucunun kendi başına konuşma imkanı yoktur. SSE tam olarak bir şeyi değiştirir: sunucu yanıtı bitirmez.
Tarayıcı bir kez bağlanır, bağlantı açık kalır ve sunucu aynı yanıta yazmaya devam eder. Tarayıcı yeni istekler göndermez; sadece dinler.
Yeni bir protokol de değildir, hala HTTP'dir. Yanıt Content-Type: text/event-stream taşıdığında, tarayıcı yanıtın tamamlanmasını beklemez; veriyi geldikçe işler.
İşte kablo üzerinde nasıl göründüğü:
event: tick
data: {"cpu":47.2,"requests":188}
id: 29
Bir alan adı, iki nokta üst üste, bir değer ve olayı sonlandıran boş bir satır. Elle yazmak için yeterince basit.
Belirtmeye değer: ChatGPT'nin bir yanıtı token token yazması da bu şekildedir. Muhtemelen yıllardır adını bilmeden SSE kullanıyorsunuz.
2. Bir üretici, her istemci için bir kuyruk
İlk versiyonumda, sayıları doğrudan isteğin kendisi üretiyordu. Sonra ikinci bir terminal açtım ve iki istemci farklı değerler gösterdi, çünkü iki üretici vardı.
Çözüm, veriyi bir kez üretip dağıtmaktır. Üretici, isteklerden bağımsız olarak yaşamalıdır: bir istek gelir ve gider, ancak üretici uygulama çalıştığı sürece çalışır. .NET'te bu bir BackgroundService'dır.
Dağıtım tarafında, her istemci kendi kuyruğunu alır. Sebep basit: paylaşılan bir kuyrukla, bir istemcinin okuduğu şey diğerleri için kaybolur. Her istemciye kendi Channel'ını verin ve kimse kimsenin verisini çalmaz.
public int Broadcast(string eventType, string data)
{
var item = new SseItem<string>(data, eventType)
{
EventId = Interlocked.Increment(ref _sequence).ToString()
};
foreach (var channel in _clients.Values)
channel.Writer.TryWrite(item);
return _clients.Count;
}
Olay kimliğinin tek bir sayıcıdan gelmesi önemlidir, çünkü bir sonraki bölümün tamamı bu sayının yalnızca artmasına bağlıdır.
3. İlginç kısım: bağlantı kesildiğinde
Peki bir kullanıcı sekmesini kapatıp tekrar açtığında ne olur? EventSource kendiliğinden yeniden bağlanır. Ancak daha değerli olan şudur: gördüğü son olay kimliğini hatırlar ve bunu bir Last-Event-ID başlığı olarak geri gönderir.
Bu, sunucunun "bu istemci nerede kaldı" bilgisini bedavaya alması demektir. Böylece kaçırılan olay sayısını söyleriz:
if (long.TryParse(lastEventId, out var lastId))
{
var missed = Math.Max(0, Interlocked.Read(ref _sequence) - lastId);
yield return new SseItem<string>(JsonSerializer.Serialize(new { missed }), "resume");
}
İstemcinin gördüğü şudur:
event: resume
data: {"missed":23}
event: tick
data: {"cpu":38.9,"requests":87}
id: 28
Burada iki detay var. İlk olarak, resume olayının bir id'si yoktur. Bu bir veri değil, bilgidir; bir kimliği olsaydı, tarayıcının son kimlik hafızasını üzerine yazar ve bir sonraki hesaplamayı bozardı.
İkincisi, Math.Max(0, ...). Sunucu yeniden başlatılırsa, sayaç sıfırlanır ve fark negatif olur. Sunucunun bu olaylar için geçmişi yoktur, bu yüzden "sıfır kaçırdın" dürüst bir yanıttır. Bu koruma olmadan panel missed: -195 gibi bir şey gösterirdi: yanlış ve insanların sayılara güvenmemesine yetecek kadar kafa karıştırıcı.
4. Nerede uyar, nerede uymaz
Uygun olduğu yerler: bildirimler, ilerleme çubukları, canlı panolar, uzun süren iş durumları, fiyat ve stok güncellemeleri. Kural şu: kullanıcı sadece izliyorsa SSE; geri konuşuyorsa WebSockets.
Uygun olmadığı yerler: sohbet mesajları gönderme, oyunlar, ortak düzenleme. Ayrıca bilinmesi gereken: tarayıcılara yerleşiktir, ancak mobil uygulamalar gibi yerel istemciler bir kütüphaneye ihtiyaç duyar.
Klasik tuzak: bir ters vekil sunucu yanıtı tamponlarsa, olaylar asla istemciye ulaşmaz. nginx'te proxy_buffering off; gerekir.
5. Bu kod aslında nasıl ortaya çıktı
Sürekli atıfta bulunduğum "tasarım" tek parça halinde gelmedi. Kod, altı küçük bozulmanın ürünüdür:
- İkinci bir istemci farklı sayılar gördü → bir üretici
- Üretici kime göndereceğini bilmiyordu → bağlı istemcilerin listesi
- Aynı değer iki yere ulaşamıyordu → her istemci için bir kuyruk
- Bağlantısı kesilen bir istemci listede kaldı → iptal ve temizlik
- Bir sayfa yenileme sıfırdan başladı →
Last-Event-ID - Sunucu yeniden başlatma negatif bir sayı üretti →
Math.Max(0, ...)
Tasarım genellikle sadece bu listedir, sabırla doldurulur. Kimse baştan her şeyi kafasında tutmaz. Kırarsın, fark edersin, düzeltirsin.
Tam kod, nasıl çalıştırılacağı ve dürüst notlar burada: https://github.com/beriltzcn/sse-live-demo
Yorumlar
Yorum yapmak için giriş yapmalısın.
Henüz yorum yok. İlk yorumu sen yap!