SignalR ile Gerçek-Zamanlı Sistemler: Hub, Scaling, Backplane ve Bağlantı Yönetimi
SignalR, .NET'in gerçek zamanlı web uygulamaları için en olgun çözümüdür. WebSocket'i önceler, desteklemiyorsa Server-Sent Events (SSE) veya Long Polling'e otomatik düşer. Ancak gerçek dünyada (prod) işler, basit bir sohbet uygulamasından çok daha karmaşıktır: Uygulamanız birden fazla sunucuda (Web Farm, Kubernetes) çalışıyorsa, bir sunucudaki bağlantı diğer sunucudaki bağlantıya mesaj gönderemez. İşte bu noktada Backplane ve doğru bağlantı yaşam döngüsü yönetimi devreye girer.
1. Hub: İletişimin Kalbi
Hub, istemciler (client) ve sunucu arasında güçlü tipli (strongly-typed) metot çağrıları yapmanızı sağlayan bir sınıftır.
csharp
public class ChatHub : Hub
{
// İstemciler sunucudaki bu metodu çağırır
public async Task SendMessage(string user, string message)
{
// Tüm bağlı istemcilere mesajı gönder
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
// Belirli bir gruba mesaj gönderme
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("ReceiveMessage", "Sistem", $"{Context.ConnectionId} gruba katıldı.");
}
}
Hub'da Bağlam (Context): Context.ConnectionId her istemci için benzersizdir. Context.User ile kimlik bilgilerine erişebilirsiniz.
2. Connection Lifecycle: Bağlantı Doğumu, Yaşamı ve Ölümü
SignalR bağlantıları kırılgan olabilir (ağ dalgalanmaları, sunucu yeniden başlatma). Bu yüzden Bağlantı Olayları (Lifecycle Hooks) hayati önem taşır.
csharp
public class ChatHub : Hub
{
private readonly ILogger<ChatHub> _logger;
// Yeni istemci bağlandığında
public override async Task OnConnectedAsync()
{
_logger.LogInformation($"Client connected: {Context.ConnectionId}");
// Örneğin, kullanıcıyı online listesine ekle
await Clients.All.SendAsync("UserJoined", Context.ConnectionId);
await base.OnConnectedAsync();
}
// İstemci bağlantısı koptuğunda (manuel veya hata)
public override async Task OnDisconnectedAsync(Exception? exception)
{
if (exception != null)
{
_logger.LogError(exception, $"Client disconnected with error: {Context.ConnectionId}");
}
else
{
_logger.LogInformation($"Client disconnected: {Context.ConnectionId}");
}
// Kullanıcıyı online listesinden çıkar
await Clients.All.SendAsync("UserLeft", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
İstemci Tarafında (JavaScript/TypeScript) Yeniden Bağlanma:
.NET istemcisinde otomatik yeniden bağlanma (auto-reconnect) etkinleştirilmelidir:
csharp
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chatHub")
.WithAutomaticReconnect() // Varsayılan (0, 2, 10, 30 saniye aralıklarla)
.Build();
// Yeniden bağlanma durumunu yakala
connection.Reconnecting += (error) =>
{
Console.WriteLine($"Yeniden bağlanıyor... Hata: {error?.Message}");
return Task.CompletedTask;
};
connection.Reconnected += (connectionId) =>
{
Console.WriteLine($"Yeniden bağlandı. Yeni ID: {connectionId}");
return Task.CompletedTask;
};
⚠️ Kritik Nokta: OnDisconnectedAsync, bağlantı her koptuğunda çalışır. Ancak otomatik yeniden bağlanma başarılı olursa, aynı istemci tekrar OnConnectedAsync fırlatır. Bu durumda, istemciyi online listeye tekrar eklemeden önce eski kayıtlarını temizlemediğiniz sürece, aynı kullanıcıyı iki kez görebilirsiniz. Bu nedenle ConnectionId'yi değil, Context.User'daki benzersiz ID'yi (örn. UserId) kullanarak state yönetimi yapmak zorunludur.
3. Scaling ve Backplane (Redis): Çok Sunuculu Ortam
Tek bir sunucuda sorun yoktur. Ancak 2 veya 10 sunucunuz varsa (load balancer arkasında), bir sunucuya bağlanan istemci, diğer sunucudaki istemciye doğrudan mesaj gönderemez.
Çözüm: Redis Backplane
SignalR, mesajları tüm sunuculara yaymak için bir Backplane kullanır. Redis, bu iş için en yaygın kullanılan yayın-abilgi (pub/sub) mekanizmasıdır. Bir sunucu bir mesaj gönderdiğinde, Redis bu mesajı diğer tüm sunuculara iletir; onlar da kendi bağlı istemcilerine iletir.
Kurulum (NuGet: Microsoft.AspNetCore.SignalR.StackExchangeRedis):
csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379", options => {
options.Configuration.ChannelPrefix = "MyApp"; // Farklı uygulamaları ayırmak için prefix
});
app.MapHub<ChatHub>("/chatHub");
Redis Backplane'in Getirdikleri ve Götürdükleri:
-
Artılar: Sunucu sayısını sınırsızca artırabilirsiniz (yatay ölçeklendirme). Bağlantı kopmalarına karşı dayanıklıdır.
-
Eksiler: Her mesaj tüm sunuculara gider, bu da ağ trafiğini artırır. Aşırı yüksek trafikte (milyonlarca mesaj/sn) Redis darboğaz olabilir.
-
Alternatif: Azure SignalR Service, Backplane ihtiyacını tamamen ortadan kaldıran, yönetilen bir servistir. Sunucularınızın hiçbir istemci bağlantısını tutmasına gerek kalmaz; tüm bağlantılar Azure'a gider. (Ölçeklendirme için en kolay yol budur).
4. Hub State Yönetimi ve Performans İpuçları
-
Hub'da Instance Field (Değişken) KULLANMAYIN! SignalR, her çağrı için yeni bir
Hubörneği oluşturur (Transient). Bu nedenleprivate int _countergibi değişkenler işe yaramaz ve veri kaybına yol açar. State'i (ör. kullanıcı listesi) statik (static) veya Singleton bir servis üzerinden yönetin. -
Groups (Gruplar): Bir kullanıcıyı belirli bir odaya (group) eklemek, tüm bağlı istemcilere mesaj göndermekten çok daha performanslıdır.
Groups.AddToGroupAsynckullanın. -
MessagePack Protokolü: JSON yerine MessagePack (ikili format) kullanarak bant genişliğini ve serileştirme süresini azaltabilirsiniz. (NuGet:
Microsoft.AspNetCore.SignalR.Protocols.MessagePack)
csharp
builder.Services.AddSignalR()
.AddMessagePackProtocol();
-
Keep-Alive ve Timeout: Varsayılan keep-alive süreleri (15 sn) çoğu durumda yeterlidir. Ancak mobil veya düşük bant genişlikli ağlarda, bağlantının kopmaması için süreleri artırın:
csharp
builder.Services.AddSignalR(options =>
{
options.KeepAliveInterval = TimeSpan.FromSeconds(30);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
});
5. Sık Yapılan Hatalar ve Çözümleri
| Hata | Çözüm |
|---|---|
| CORS Hatası | SignalR endpoint'ine gelen istekler için WithOrigins veya AllowAnyOrigin (geliştirme) ayarını yapın. |
| ConnectionId Değişiyor | Yeniden bağlanmada ID değişir. Kullanıcıyı tanımak için Context.User veya Query String'den gelen özel bir token kullanın. |
| Redis Bağlantı Kopması | Redis bağlantısı koparsa, SignalR çalışmaya devam eder ancak sunucular arası iletişim çalışmaz. AddStackExchangeRedis içinde ConnectionMultiplexer'ı özelleştirerek otomatik yeniden bağlanma (reconnect) politikası belirleyin. |
| Büyük Mesaj Gönderimi | Varsayılan max message size 32KB'dir. Büyük dosya veya veri gönderecekseniz AddSignalR(options => options.MaximumReceiveMessageSize = 1024 * 1024) (1MB) gibi artırın. Ancak mümkünse dosyaları ayrı bir endpoint'ten yükleyin. |
Sonuç:
SignalR, doğru yapılandırıldığında ölümsüz bir canavardır.
-
Tek sunucu için: Basit
HubveOnConnected/Disconnectedyeterlidir. -
Birden çok sunucu için: Redis Backplane (veya Azure SignalR Service) şarttır.
-
Unutmayın: Hub'lar state tutmaz; state'i dışarıda (cache, veritabanı, statik sözlük) tutun ve istemci bağlantı olaylarında (reconnect) bu state'i güncelleyin. Yeniden bağlanma stratejisini mutlaka istemci tarafında da yapılandırın, aksi takdirde kullanıcılar sessizce koptuğunu fark etmez.