Merhabalar, bu yazıda web geliştirme alanında oldukça pratik ve etkili bir teknoloji olan Server-Sent Events’i (SSE) inceleyeceğiz.
Günümüz web uygulamalarında gerçek zamanlı güncellemeler vazgeçilmez hale geldi. Düşünelim ki, bir borsa uygulamasında kripto para fiyatları anında dalgalanırken sayfayı yenilemeden takip ediyoruz veya bir spor sitesinde maç skorları gol atıldığı anda güncelleniyor. Bu tür özellikler, kullanıcı deneyimini büyük ölçüde arttırıp, uygulamayı daha dinamik kılmaktadır.
SSE, tam da bu senaryolarda devreye girerek sunucudan tarayıcıya tek yönlü, kesintisiz veri akışı sağlayan bir HTML5 teknolojisidir. SSE’nin güzelliği, mevcut HTTP altyapısını akıllıca kullanarak çalışmasında yatar. Bu sayede karmaşık kurulumlara veya yeni protokollere ihtiyaç duymayız. Şimdi gelin bu teknolojiyi biraz daha detaylı inceleyelim.
Neden SSE’ye İhtiyaç Duyuldu?
Web’in evrimi sırasında, gerçek zamanlı veri aktarımı her zaman bir zorluk olmuştur. İlk dönemlerde, bu tür güncellemeler için sınırlı ve verimsiz yöntemler kullanılıyordu. Bu yöntemler, kullanıcı deneyimini olumsuz etkiliyor ve sunucu kaynaklarını boşa harcıyordu. SSE’nin ortaya çıkışını anlamak için, bu geleneksel yaklaşımları ve onların dezavantajlarını inceleyelim. Bu sayede, SSE’nin neden daha üstün bir çözüm olduğunu göreceğiz. Konuyu somutlaştırmak için, bir restoran metaforu kullanacağız; bu benzetme, sorunları daha kolay anlamamıza yardımcı olacak. Her yöntem için kısa bir kod örneği vererek mantığını da açıklayacağız.
1. Polling (Klasik Yoklama)
Bu yaklaşımda, tarayıcı belirli aralıklarla (örneğin her 5 saniyede bir) sunucuya HTTP isteği gönderir ve yeni veri olup olmadığını sorar.
- Restoran Metaforu: Restoranda her 30 saniyede bir garsonu çağırıp “Siparişim hazır mı?” diye sormak gibidir. Garson her seferinde mutfağa gidip bakar ve çoğu zaman “Hayır, henüz değil” diye döner.
- Dezavantajları: Kaynak israfı (sunucu her isteği işler), gereksiz ağ trafiği, gecikme (güncellemeler en iyi ihtimalle yoklama aralığı kadar gecikir) ve ölçeklenme sorunları.
// Polling örneği: Her 5 saniyede sunucudan veri çek
setInterval(() => {
fetch('/api/price-update')
.then(response => response.json())
.then(data => {
if (data.isPriceChanged) {
console.log('Yeni fiyat:', data.price);
// UI'yi güncelle
} else {
console.log('Değişiklik yok'); // Çoğu zaman bu çalışır
}
})
.catch(error => console.error('Hata:', error));
}, 5000);
2. Long Polling (Uzun Yoklama)
Bu yöntemde, tarayıcı sunucuya istek gönderir ve sunucu yeni veri gelene kadar bağlantıyı açık tutar. Veri hazır olduğunda yanıt verir ve bağlantı kapanır; tarayıcı hemen yeni bir istek yapar.
- Restoran Metaforu: Garsona “Siparişim hazır olunca getir” dersiniz. Garson sipariş hazır olana kadar mutfakta bekler ve hazır olduğunda masanıza getirir. Ancak yemeği getirdikten sonra gider ve bir sonraki siparişiniz için yeni bir garson çağırmanız gerekir.
- Dezavantajları: Sürekli yeni TCP bağlantısı kurma yükü, karmaşık hata yönetimi (timeout’lar) ve bazı proxy/firewall’ların uzun süreli bağlantıları kesme riski.
// Long Polling örneği: Sunucu veri gelene kadar bekletir
function longPoll() {
fetch('/api/price-update', { timeout: 30000 }) // 30 saniye bekle
.then(response => response.json())
.then(data => {
console.log('Yeni fiyat:', data.price);
// UI'yi güncelle
longPoll(); // Hemen yeniden sorgula
})
.catch(error => {
console.error('Hata veya timeout:', error);
setTimeout(longPoll, 1000); // Hata sonrası yeniden dene
});
}
longPoll(); // Başlat
3. WebSocket
WebSocket, sunucu ve tarayıcı arasında kalıcı, çift yönlü bir iletişim kanalı açar. Veriler her iki yöne de serbestçe ve düşük gecikmeyle akabilir.
- Restoran Metaforu: Masanıza özel bir telefon hattı çekilmesi gibidir. Hem siz mutfağı arayıp bir şey sorabilirsiniz hem de mutfak sizi arayıp siparişinizle ilgili bir detay bildirebilir. İletişim kesintisiz ve iki yönlüdür.
- Dezavantajları: HTTP’den farklı bir protokol (ws://) kullanır, bu da bazı ağ altyapılarında (proxy, firewall) uyumluluk sorunlarına yol açabilir. Kurulumu ve yönetimi daha karmaşıktır. Tek yönlü veri akışı (sunucudan tarayıcıya) için gereğinden fazla özellik sunar.
// WebSocket örneği: Çift yönlü bağlantı kur
const socket = new WebSocket('ws://localhost:5000/price-ws');
socket.onopen = () => {
console.log('Bağlantı kuruldu');
socket.send('Fiyat güncellemelerini izle'); // Tarayıcıdan sunucuya mesaj
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Yeni fiyat:', data.price);
// UI'yi güncelle
};
socket.onerror = (error) => console.error('Hata:', error);
SSE Ne Zaman ve Neden Ortaya Çıktı?
Yukarıda bahsedilen yöntemler, web geliştiricilerinin “gerçek zamanlı” ihtiyacına bulduğu yaratıcı ama dolambaçlı çözümlerdi. SSE’nin ortaya çıkışı, bu ihtiyaca standart ve tarayıcı tabanlı bir çözüm getirme arzusundan doğmuştur.
HTML5 öncesi dönemde tarayıcıların, sunucudan istemciye başlatılan veri akışını dinleyebilmek için yerleşik bir API’si bulunmuyordu. Geliştiriciler, bu eksikliği gidermek için Polling gibi “çekme” tabanlı yöntemlere ya da Adobe Flash ve Java Applet gibi eklentilere başvurmak zorundaydı. Bu çözümler hem performans sorunları yaratıyor hem de bakım açısından ek yük getiriyordu.
Server-Sent Events, HTML5 standardının bir parçası olarak bu karmaşık yaklaşımların yerine geçti. Tarayıcıların yerel desteği sayesinde, istemcinin sunucudan sürekli ve kesintisiz veri alabilmesi mümkün hale geldi. Böylece geliştiriciler, ekstra eklentiye ihtiyaç duymadan basit ve verimli bir şekilde gerçek zamanlı veri akışı sağlayabilir oldu.
Kısacası, SSE web’in doğal evriminin bir ürünüdür; karmaşık geçici çözümlerin yerini alan standart ve sürdürülebilir bir teknoloji olarak doğmuştur.
Hangi Senaryoda Hangi Teknoloji?
Bu farklı farklı yöntemlerin hepsinin kendine göre uygun bir kullanım alanı bulunmaktadır. Şimdi bunları inceleyelim.
- Polling: Güncelliğin kritik olmadığı ve verinin nadiren değiştiği durumlar için uygundur. Örneğin, bir web sayfasındaki hava durumu bilgisini her 15 dakikada bir güncellemek için ideal ve basit bir çözümdür.
- Long Polling: Gerçek zamanlılığa daha yakın bir deneyim istenen ancak WebSocket karmaşıklığından kaçınılan senaryolar için bir ara çözümdür. Basit bildirim sistemleri (“yeni bir mesajınız var” uyarısı gibi) için kullanılabilir.
- WebSocket: Çift yönlü (bi-directional) ve çok düşük gecikmeli iletişimin zorunlu olduğu uygulamalar için standarttır. Canlı sohbet uygulamaları, çok oyunculu çevrimiçi oyunlar veya ortaklaşa doküman düzenleme araçları (Google Docs gibi) için vazgeçilmezdir.
- Server-Sent Events (SSE): Veri akışının tek yönlü (server -> client) olduğu durumlar için en verimli ve en basit çözümdür. Borsa fiyat akışları, canlı spor skorları, haber akışları, bir işlemin ilerleme durumunu gösteren bildirimler (örneğin, “Raporunuz oluşturuluyor… %75 tamamlandı”) gibi senaryolar için mükemmeldir.
SSE’nin İç Yüzü
SSE’nin temel işlevi, sunucudan tarayıcıya kesintisiz bir “push” tabanlı veri akışı sağlamaktır. Sunucu yeni veri hazır olduğunda otomatik olarak gönderir, tarayıcının sürekli sorgu yapmasına gerek kalmaz.
Çalışma Prensibi:
- Bağlantı: Tarayıcı, standart bir HTTP GET isteği gönderir, ancak bu isteğin Accept: text/event-stream başlığı vardır.
- Sunucu Yanıtı: Sunucu, 200 OK yanıtı ile birlikte Content-Type: text/event-stream ve Connection: keep-alive başlıklarını döner. En önemlisi, bağlantıyı kapatmaz.
- Veri Akışı: Sunucu, bu açık bağlantı üzerinden belirli bir formatta veri parçaları (“olaylar”) göndermeye başlar. Her mesaj data: ön ekiyle başlar ve \n\n ile sonlanır.
- data: Gönderilecek asıl veriyi (genellikle JSON formatında) içerir.
- event: Olaya özel bir isim atamak için kullanılır. Bu sayede istemci tarafında farklı olay türleri için farklı dinleyiciler oluşturulabilir.
- id: Her mesaja benzersiz bir kimlik atar. Bağlantı koparsa, tarayıcı tekrar bağlandığında Last-Event-ID başlığı ile bu kimliği sunucuya gönderir ve sunucu veri akışına kaldığı yerden devam edebilir.
- retry: Bağlantı koptuğunda tarayıcının ne kadar süre sonra yeniden bağlanmayı deneyeceğini milisaniye cinsinden belirtir.
Örnek Mesaj Yapısı:
event: price-update
id: 123
retry: 5000
data: {"kripto": "BTC", "fiyat": 61500}
event: score-update
id: 124
data: {"mac": "TakimA vs TakimB", "skor": "2-1"}
Tarayıcı tarafında EventSource API’si bu süreci otomatik olarak yönetir, gelen veriyi ayrıştırır, bağlantı koptuğunda otomatik olarak yeniden bağlanır. Bu da SSE’yi son derece dayanıklı ve kullanımı kolay kılar.
Visual Studio’da Adım Adım SSE Uygulaması
Şimdi pratik bir örnek yapalım:
Adım 1: Proje Oluşturma Visual Studio 2022’de “ASP.NET Core Web API” projesi oluşturalım. (Proje Adı: SSEDemoApp, Framework: .NET 9.0)
Adım 2: SSE Controller Oluşturma Controllers klasörüne SSEController.cs adında yeni bir sınıf ekleyelim:
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/sse")]
public class SSEController : ControllerBase
{
[HttpGet("price-stream")]
public async Task PriceStream()
{
Response.ContentType = "text/event-stream";
Response.Headers.Append("Cache-Control", "no-cache");
Response.Headers.Append("Connection", "keep-alive");
Response.Headers.Append("X-Accel-Buffering", "no");
var rnd = new Random();
var symbols = new[] { "BTC", "ETH", "SOL" };
var prices = new Dictionary<string, decimal>
{
{ "BTC", 60000m }, { "ETH", 3000m }, { "SOL", 150m }
};
var messageId = 0;
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
try
{
await Response.WriteAsync("event: connected\n");
await Response.WriteAsync("data: {\"status\":\"Bağlantı kuruldu, kripto fiyat güncellemeleri başlıyor\"}\n\n");
await Response.Body.FlushAsync();
while (!HttpContext.RequestAborted.IsCancellationRequested)
{
messageId++;
var symbol = symbols[rnd.Next(symbols.Length)];
var changePercent = (decimal)(rnd.NextDouble() - 0.5) * 2; // -1% … +1%
var newPrice = prices[symbol] * (1 + changePercent / 100);
prices[symbol] = Math.Round(newPrice, 2);
var payload = new
{
id = messageId,
timestamp = DateTime.UtcNow,
symbol,
newPrice = prices[symbol],
changePercent = Math.Round(changePercent, 2)
};
await Response.WriteAsync($"id: {messageId}\n");
await Response.WriteAsync("event: price-update\n");
await Response.WriteAsync($"data: {JsonSerializer.Serialize(payload, jsonOptions)}\n\n");
await Response.Body.FlushAsync();
await Task.Delay(rnd.Next(2000, 4000), HttpContext.RequestAborted);
}
}
catch (Exception ex)
{
Console.WriteLine($"SSE error: {ex}");
}
finally
{
Console.WriteLine("SSE connection ended.");
}
}
}
Adım 3: Program.cs Düzenlemesi (CORS için) Program.cs dosyasında app.MapControllers(); satırından önce CORS politikasını ekleyelim:
// ...
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
var app = builder.Build();
// ...
app.UseCors("AllowAll"); // Bu satırı ekleyin
app.UseStaticFiles(); // index.html için
Adım 4: HTML Test Sayfası Oluşturma Projemize wwwroot adında bir klasör ekleyelim ve içine aşağıdaki index.html dosyasını oluşturalım:
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8" />
<title>SSE Kripto Fiyat Akışı</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #f4f7f9;
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
}
.container {
max-width: 900px;
width: 100%;
background: #fff;
padding: 25px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,0.08);
}
h1 {
text-align: center;
color: #2c3e50;
}
#status {
text-align: center;
font-weight: bold;
padding: 10px;
border-radius: 6px;
margin-bottom: 20px;
transition: all .3s;
}
.price-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.card {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
transition: transform .2s ease-in-out, box-shadow .2s ease-in-out;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.symbol {
font-size: 20px;
color: #3498db;
font-weight: 600;
}
.price {
font-size: 28px;
font-weight: 700;
margin: 10px 0;
}
.change {
font-size: 16px;
font-weight: 500;
}
#log-container {
margin-top: 30px;
}
#log {
padding: 15px;
background: #ecf0f1;
border: 1px solid #dcdcdc;
border-radius: 6px;
max-height: 200px;
overflow-y: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 14px;
}
.log-entry {
padding: 5px 0;
border-bottom: 1px solid #dcdcdc;
}
.log-entry:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Kripto Para Fiyat Akışı (SSE)</h1>
<div id="status">Bağlanıyor...</div>
<div class="price-cards">
<div class="card" id="btc-card">
<div class="symbol">BTC</div>
<div class="price">Yükleniyor...</div>
<div class="change">-</div>
</div>
<div class="card" id="eth-card">
<div class="symbol">ETH</div>
<div class="price">Yükleniyor...</div>
<div class="change">-</div>
</div>
<div class="card" id="sol-card">
<div class="symbol">SOL</div>
<div class="price">Yükleniyor...</div>
<div class="change">-</div>
</div>
</div>
<div id="log-container">
<h3>Güncelleme Logu</h3>
<div id="log"></div>
</div>
</div>
<script>
const source = new EventSource('/api/sse/price-stream');
const statusEl = document.getElementById('status');
const logEl = document.getElementById('log');
source.onopen = () => {
statusEl.textContent = 'Bağlı! Fiyat güncellemeleri alınıyor.';
statusEl.style.color = '#27ae60';
statusEl.style.backgroundColor = '#e8f8f0';
};
source.addEventListener('connected', (evt) => {
console.log('Bağlantı onayı alındı:', evt.data);
});
source.addEventListener('price-update', (evt) => {
const data = JSON.parse(evt.data);
const cardId = `${data.symbol.toLowerCase()}-card`;
const card = document.getElementById(cardId);
if (!card) return;
card.querySelector('.price').textContent = `$${Number(data.newPrice).toLocaleString()}`;
const changeEl = card.querySelector('.change');
const cp = Number(data.changePercent);
changeEl.textContent = `${cp > 0 ? '+' : ''}${cp}%`;
changeEl.style.color = cp >= 0 ? '#28a745' : '#dc3545';
const entry = document.createElement('div');
entry.className = 'log-entry';
const t = new Date(data.timestamp);
entry.textContent = `[${t.toLocaleTimeString()}] ${data.symbol}: $${data.newPrice} (${cp}%)`;
logEl.appendChild(entry);
logEl.scrollTop = logEl.scrollHeight;
});
source.onerror = () => {
statusEl.textContent = 'Bağlantı koptu! Yeniden bağlanılıyor...';
statusEl.style.color = '#c0392b';
statusEl.style.backgroundColor = '#fdeded';
};
</script>
</body>
</html>
Burada tarayıcı tarafında `EventSource` nesnesi `/api/sse/price-stream` adresine uzun ömürlü bir HTTP isteği açar. Sunucu, `Response.WriteAsync` çağrıları ile bu isteğin cevabına satır satır veri yazar. Her yazılan veri, aslında HTTP response body’sine işlenir ve Kestrel üzerinden TCP soketiyle tarayıcıya gönderilir. SSE protokolü `event:` ve `data:` satırları ile çalışır: `event` olay tipini, `data` ise JSON verisini taşır. Tarayıcı bu akışı otomatik ayrıştırır ve `price-update` gibi event türlerine karşılık gelen JavaScript event listener’ları tetiklenir. Böylece sunucu sürekli canlı fiyat güncellemelerini gönderirken, frontend bu verileri alır, kartlara işler ve log alanına kaydeder.
Adım 5: Projeyi Çalıştırma ve Test Etme Projeyi F5 ile çalıştıralım. Tarayıcıda /index.html adresine gidelim. Kripto paraların 2-4 saniye aralıklarla rastgele güncellendiğini ve log alanının anlık olarak dolduğunu göreceksiniz. Sunucuyu durdurduğunuzda, istemci tarafındaki “Bağlantı koptu” mesajını ve sunucuyu yeniden başlattığınızda bağlantının otomatik olarak nasıl yeniden kurulduğunu gözlemleyebilirsiniz. Aşağıda bunun için demo bir video yer almaktadır.
SSE: Sınırlamalar ve Dikkat Edilmesi Gerekenler
Server-Sent Events, anlattığımız senaryolar için harika bir teknoloji olsa da, her teknolojide olduğu gibi onun da kendine has sınırlamaları ve ilk bakışta endişe yaratabilecek yönleri vardır. Bu bölümde, SSE ile ilgili sıkça dile getirilen endişeleri ve bilmemiz gereken gerçek sınırlamaları inceleyelim.
Açık Bağlantıların Yönetimi ve Kaynak Kullanımı
En yaygın endişelerden biri, sunucu ile tarayıcı arasında sürekli açık kalan bir HTTP bağlantısının hem sunucu hem de istemci tarafında aşırı kaynak (özellikle RAM) tüketip tüketmeyeceğidir.
Aslında bu bağlantı sanıldığı kadar maliyetli değildir. Bunu açık bir telefon hattına benzetebiliriz: Karşı taraftan biri konuşmadığı (yani veri akışı olmadığı) sürece hat sadece mevcuttur, ancak aktif olarak enerji veya dikkat harcamaz. Teknik olarak, işletim sistemi tarafından yönetilen açık bir TCP bağlantısı oldukça hafiftir. Asıl verimlilik, Polling yöntemindeki gibi bağlantıyı sürekli kapatıp yeniden kurma (her seferinde TCP el sıkışması, HTTP başlıklarının işlenmesi vb.) maliyetinden kaçınmaktır. Bu sayede SSE, kaynakları Polling’e göre çok daha verimli kullanır.
Ancak burada dikkat edilmesi gereken bir nokta vardır: Her sunucunun aynı anda yönetebileceği maksimum bağlantı sayısı bir limitle sınırlıdır. Çok yüksek sayıda eş zamanlı kullanıcıya hizmet veren bir uygulamada (örneğin on binlerce anlık kullanıcı), bu limit bir sorun teşkil edebilir. Ayrıca, eski HTTP/1.1 protokolü, bir tarayıcının aynı alan adına aynı anda açabileceği bağlantı sayısını genellikle 6 ile sınırlar. Bu limit, aynı sayfada başka API istekleri de yapılıyorsa SSE bağlantısını etkileyebilir. (HTTP/2 bu sorunu büyük ölçüde çözer.)
Sunucu Taraflı Döngü ve Asenkron Verimlilik
Pratik örneğimizdeki while döngüsünü gören bir geliştirici, bunun sunucu işlemcisini sürekli meşgul ederek performansı düşüreceğinden endişelenebilir.
Bu endişe, asenkron programlamanın çalışma mantığı anlaşıldığında ortadan kalkar. Örnekteki döngü, işlemciyi yoran “boş bir döngü” değildir. İçindeki await Task.Delay(...) komutu kilit noktadır. Bu komut, sisteme “Beni belirtilen süre sonra uyandır, o zamana kadar beni çalıştıran bu işlemci kaynağını (thread) serbest bırakıyorum. Sen onu başka işler için kullanabilirsin.” der. Yani, döngümüz zamanının %99’unu “uyuyarak” geçirir ve bu sırada sunucu kaynaklarını tüketmez. Sunucu, serbest kalan bu kaynakla başka kullanıcıların isteklerini rahatlıkla karşılayabilir. Bu yapı, sık aralıklarla yapılan Polling’e kıyasla sunucu için çok daha dinlendiricidir.
Best Practices
- Bağlantı Yönetimi: Sunucunuzun aynı anda kaldırabileceği maksimum bağlantı sayısını göz önünde bulundurun. .NET’te SemaphoreSlim gibi yapılarla eşzamanlı SSE bağlantılarını sınırlayabilirsiniz.
- Ölçeklendirme: Uygulamanız birden fazla sunucu arkasında çalışıyorsa (load balanced), bir mesajın tüm istemcilere gitmesi için Redis Pub/Sub gibi bir mesajlaşma sistemi kullanmanız gerekir.
- Güvenlik: CORS politikanızı production ortamında AllowAll yerine sadece güvendiğiniz alan adlarına izin verecek şekilde sıkılaştırın.
- HTTP/2: Mümkünse sunucunuzu HTTP/2 üzerinden hizmet verecek şekilde yapılandırın. HTTP/2, tek bir TCP bağlantısı üzerinden çoklu akışa (multiplexing) izin verdiği için çok sayıda SSE bağlantısını daha verimli yönetir.
Sonuç
Server-Sent Events, özellikle sunucudan tarayıcıya tek yönlü veri akışı gerektiren senaryolar için verimli ve güçlü bir araçtır. Bu yazıda, SSE’nin tarihsel gelişimini, Polling ve WebSocket gibi alternatiflere karşı üstünlüklerini, çalışma prensibini ve .NET ile pratik bir uygulamasını inceledik. Borsa fiyatları, canlı skorlar veya bildirim sistemleri gibi projelerinizde WebSocket’in karmaşıklığına ihtiyaç duymuyorsanız, SSE’nin basitliği ve dayanıklılığı bizlere hem geliştirme sürecinde zaman kazandıracak hem de sunucu kaynaklarınızı daha verimli kullanmamızı sağlayacaktır.
Demo projenin kaynak kodlarına buradan erişebilirsiniz.
Bir sonraki yazılarda görüşmek üzere. İyi çalışmalar!