SignalR: Hub, Scaling ve Backplane

Gerçek zamanlı iletişimde SignalR hub yapısı, bağlantı yönetimi (connect, reconnect, disconnect) ve çok sunuculu ortamlarda Redis backplane ile mesaj yayını konuları ele alınır.

SignalR: Hub, Scaling ve Backplane

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 nedenle private int _counter gibi 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.AddToGroupAsync kullanı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 Hub ve OnConnected/Disconnected yeterlidir.

  • 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.

Tüm yazılar