SignalR ile Gerçek Zamanlı .NET Uygulamaları
# .NET ile SignalR Kullanarak Gercel Zamanli Uygulamalar
Canli panolar, sohbet sistemleri, bildirim akislari, ortak duzenleme araclar -- bunlar klasik istek-yanit modeliyle yapilamaz. Kalici baglanti, tasinma protokolu secimi ve yeniden baglanma mantigi gerektirir. SignalR bu karmasikligi soyutlayarak temiz bir programlama modeli sunar. Gercek zamanli izleme panolari gelistirdigim projelerde, SignalR her zaman fikirden uretime en hizli yol oldu.
Neden Ham WebSocket Yerine SignalR
SignalR sadece bir WebSocket sarmalayicisi degil. Tasinma protokolu geri donusu (WebSockets, Server-Sent Events, Long Polling), otomatik yeniden baglanma, baglanti yasam dongusu yonetimi ve mesaj serializasyonu gibi kritik islevleri icerir. Bunlarin hepsini sifirdan yazmak mumkun ama is uygulamalari icin nadiren deger.
Temel avantajlar:
SignalR Hub Kurulumu
Hub, istemci iletisiminin sunucu tarafindaki giris noktasidir. Hub uzerindeki her public metot, bagli istemciler tarafindan cagrilabilir.
Temel Hub
public class BildirimHub : Hub
{
private readonly ILogger<BildirimHub> _logger;
public BildirimHub(ILogger<BildirimHub> logger)
{
_logger = logger;
}
public async Task BildirimGonder(string kullanici, string mesaj)
{
_logger.LogInformation(
class="code-string">"{ConnectionId} kullanicisinden {Kullanici} kullanicisina bildirim",
Context.ConnectionId, kullanici);
await Clients.User(kullanici).SendAsync(class="code-string">"BildirimAl", mesaj);
}
public async Task HerkeseMesajGonder(string mesaj)
{
await Clients.All.SendAsync(class="code-string">"MesajAl", Context.User?.Identity?.Name, mesaj);
}
public override async Task OnConnectedAsync()
{
_logger.LogInformation(class="code-string">"Istemci baglandi: {ConnectionId}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation(
class="code-string">"Istemci ayrildi: {ConnectionId}, Sebep: {Sebep}",
Context.ConnectionId, exception?.Message ?? class="code-string">"temiz ayrilma");
await base.OnDisconnectedAsync(exception);
}
}Program.cs Kaydi
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.KeepAliveInterval = TimeSpan.FromSeconds(class="code-number">15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(class="code-number">30);
options.MaximumReceiveMessageSize = class="code-number">64 * class="code-number">1024; class=class="code-string">"code-comment">// class="code-number">64 KB
});
var app = builder.Build();
app.MapHub<BildirimHub>(class="code-string">"/hubs/bildirimler");
app.Run();Strongly-Typed Hub'lar
`SendAsync("MesajAl", ...)` seklindeki string tabanli metot isimleri kirilgandir. Bir yazim hatasi derleme sirasinda yakalanmaz, calisma zamaninda sessizce basarisiz olur. Strongly-typed hub'lar bu sorunu tamamen ortadan kaldirir.
Istemci Arayuzu Tanimla
public interface IBildirimIstemcisi
{
Task BildirimAl(string mesaj);
Task MesajAl(string kullanici, string mesaj);
Task KullaniciGrubaKatildi(string kullanici, string grup);
Task KullaniciGruptenAyrildi(string kullanici, string grup);
Task PanelGuncellemesiAl(PanelVerisi veri);
}Strongly-Typed Hub Uygulamasi
public class BildirimHub : Hub<IBildirimIstemcisi>
{
public async Task BildirimGonder(string kullanici, string mesaj)
{
class=class="code-string">"code-comment">// Derleme zamaninda kontrol edilir -- artik sihirli string yok
await Clients.User(kullanici).BildirimAl(mesaj);
}
public async Task HerkeseMesajGonder(string mesaj)
{
var gonderen = Context.User?.Identity?.Name ?? class="code-string">"Anonim";
await Clients.All.MesajAl(gonderen, mesaj);
}
public async Task PanelGuncellemesiGonder(PanelVerisi veri)
{
await Clients.Group(class="code-string">"panel-izleyicileri").PanelGuncellemesiAl(veri);
}
}Gercek zamanli panolar gelistirirken, strongly-typed hub'lar derleme sirasinda birden fazla hatanin yakalanmasini sagladi. Arayuz tanimlamanin ilk maliyeti kucuk ama geri donusu aninda.
Istemci-Sunucu Iletisim Kaliplari
SignalR farkli senaryolara uygun cesitli iletisim kaliplarini destekler.
Sunucudan Istemciye
En yaygin kalip. Sunucu, bagli istemcilere veriyi istemciler talep etmeden iter.
class=class="code-string">"code-comment">// Hub metodu icinden
await Clients.All.MesajAl(kullanici, mesaj); class=class="code-string">"code-comment">// Tum istemciler
await Clients.Caller.BildirimAl(class="code-string">"Onaylandi"); class=class="code-string">"code-comment">// Sadece arayan
await Clients.Others.MesajAl(kullanici, mesaj); class=class="code-string">"code-comment">// Arayan haric herkes
await Clients.User(userId).BildirimAl(msg); class=class="code-string">"code-comment">// Belirli kullanici
await Clients.Group(class="code-string">"yoneticiler").BildirimAl(msg); class=class="code-string">"code-comment">// Grup uyeleriIstemciden Sunucuya
Istemciler hub metotlarini dogrudan cagirabilir. JavaScript istemci ornegi:
const connection = new signalR.HubConnectionBuilder()
.withUrl(class="code-string">"/hubs/bildirimler", {
accessTokenFactory: () => getAccessToken()
})
.withAutomaticReconnect([class="code-number">0, class="code-number">2000, class="code-number">5000, class="code-number">10000, class="code-number">30000])
.configureLogging(signalR.LogLevel.Information)
.build();
connection.on(class="code-string">"MesajAl", (kullanici, mesaj) => {
console.log(`${kullanici}: ${mesaj}`);
mesajiArayuzeEkle(kullanici, mesaj);
});
connection.on(class="code-string">"PanelGuncellemesiAl", (veri) => {
paneliGuncelle(veri);
});
async function mesajGonder(mesaj) {
await connection.invoke(class="code-string">"HerkeseMesajGonder", mesaj);
}
await connection.start();Hub Disinda Hub Metotlarini Cagirma
Uretim sistemlerinde mesajlari genellikle arka plan servisleri, API controller'lar veya olay isleyicilerinden gondermeniz gerekir.
public class SiparisIslemServisi : BackgroundService
{
private readonly IHubContext<BildirimHub, IBildirimIstemcisi> _hubContext;
public SiparisIslemServisi(
IHubContext<BildirimHub, IBildirimIstemcisi> hubContext)
{
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var guncellemeler = await BekleyenGuncellemeleriGetir();
foreach (var guncelleme in guncellemeler)
{
await _hubContext.Clients.User(guncelleme.KullaniciId)
.BildirimAl($class="code-string">"Siparis {guncelleme.SiparisId} durumu: {guncelleme.Durum}");
}
await Task.Delay(TimeSpan.FromSeconds(class="code-number">5), stoppingToken);
}
}
}Grup ve Kullanici Yonetimi
Gruplar, hangi istemcilerin hangi mesajlari alacagini organize etmenin temel mekanizmasidir. Hafif, dinamik ve on kayit gerektirmez.
Grup Islemleri
public class IsbirligiHub : Hub<IIsbirligiIstemcisi>
{
private readonly IGrupTakipci _grupTakipci;
public IsbirligiHub(IGrupTakipci grupTakipci)
{
_grupTakipci = grupTakipci;
}
public async Task ProyeKatil(string projeId)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $class="code-string">"proje-{projeId}");
var kullaniciAdi = Context.User?.Identity?.Name ?? class="code-string">"Anonim";
await _grupTakipci.KullaniciGrubaEkle(projeId, kullaniciAdi, Context.ConnectionId);
await Clients.Group($class="code-string">"proje-{projeId}")
.KullaniciGrubaKatildi(kullaniciAdi, projeId);
}
public async Task ProjeyiTerkEt(string projeId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $class="code-string">"proje-{projeId}");
var kullaniciAdi = Context.User?.Identity?.Name ?? class="code-string">"Anonim";
await _grupTakipci.KullaniciGruptanCikar(projeId, Context.ConnectionId);
await Clients.Group($class="code-string">"proje-{projeId}")
.KullaniciGruptenAyrildi(kullaniciAdi, projeId);
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var gruplar = await _grupTakipci.BaglantiIcinGruplariGetir(Context.ConnectionId);
foreach (var grup in gruplar)
{
await _grupTakipci.KullaniciGruptanCikar(grup, Context.ConnectionId);
await Clients.Group($class="code-string">"proje-{grup}")
.KullaniciGruptenAyrildi(Context.User?.Identity?.Name ?? class="code-string">"Anonim", grup);
}
await base.OnDisconnectedAsync(exception);
}
}Kritik detay: SignalR gruplari kalici degildir. Sunucu yeniden basladiginda tum grup uyelikleri kaybolur. Uygulamaniz kalici grup uyeligi gerektiriyorsa, bunu kendi veri deponuzda tutun ve yeniden baglanma sirasinda gruplara tekrar katilin.
SignalR ile Kimlik Dogrulama
SignalR, ASP.NET Core kimlik dogrulamasiyla dogrudan entegre olur. API endpoint'lerinizi koruyan ayni JWT veya cerez kimlik dogrulamasi hub'larinizi da korur.
SignalR icin JWT Kimlik Dogrulama
WebSocket baglantilari ilk el sikismadan sonra ozel HTTP basliklar gonderemez. SignalR bu sorunu, muzakere asamasinda token'i query string parametresi olarak kabul ederek cozer.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration[class="code-string">"Jwt:Issuer"],
ValidAudience = builder.Configuration[class="code-string">"Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration[class="code-string">"Jwt:Key"]!))
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query[class="code-string">"access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments(class="code-string">"/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});Hub Metotlarini Guvenli Hale Getirme
[Authorize]
public class GuvenliHub : Hub<IBildirimIstemcisi>
{
[Authorize(Roles = class="code-string">"Admin")]
public async Task YoneticiDuyurusu(string mesaj)
{
await Clients.All.BildirimAl(mesaj);
}
[Authorize(Policy = class="code-string">"PremiumKullanici")]
public async Task PremiumOzellik(string veri)
{
await Clients.Caller.BildirimAl($class="code-string">"Premium sonuc: {veri}");
}
}Redis Backplane ile SignalR Olceklendirme
Tek bir sunucu tum baglantilari bellekte yonetir. Bir yuk dengeleyici arkasinda birden fazla sunucuya olceklendiginde, Sunucu A'ya bagli bir istemci Sunucu B'den gonderilen mesajlari alamaz. Redis backplane bu sorunu tum sunucular arasinda mesaj yayinlayarak cozer.
Yapilandirma
builder.Services.AddSignalR()
.AddStackExchangeRedis(builder.Configuration.GetConnectionString(class="code-string">"Redis")!, options =>
{
options.Configuration.ChannelPrefix = RedisChannel.Literal(class="code-string">"SignalR");
});Nasil Calisir
Ne Zaman Azure SignalR Service Kullanilmali
Birkac bin esanlamli baglantinin uzerindeki uretim yuklerinde Azure SignalR Service'i dusunun. Baglanti yonetimini tamamen devreder, yapiskan oturum, Redis altyapisi ve baglanti sayisi kapasite planlamasi ihtiyacini ortadan kaldirir.
builder.Services.AddSignalR()
.AddAzureSignalR(builder.Configuration[class="code-string">"Azure:SignalR:ConnectionString"]);Hata Yonetimi ve Yeniden Baglanma Stratejisi
Gercek zamanli baglantiler dogasi geregi kirilgandir. Aglar kesilir, sunucular yeniden baslar, istemciler uyku moduna gecer. Saglikli bir yeniden baglanma stratejisi uretim sistemleri icin tartisilamaz.
Sunucu Tarafinda Hata Yonetimi
public class DayanikliHub : Hub<IBildirimIstemcisi>
{
private readonly ILogger<DayanikliHub> _logger;
public DayanikliHub(ILogger<DayanikliHub> logger)
{
_logger = logger;
}
public async Task VeriIsle(VeriIstegi istek)
{
try
{
var sonuc = await DogrulaVeIsle(istek);
await Clients.Caller.BildirimAl($class="code-string">"Basarili: {sonuc}");
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, class="code-string">"{ConnectionId} icin dogrulama basarisiz", Context.ConnectionId);
throw new HubException($class="code-string">"Dogrulama hatasi: {ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, class="code-string">"{ConnectionId} icin beklenmeyen hata", Context.ConnectionId);
throw new HubException(class="code-string">"Beklenmeyen bir hata olustu. Lutfen tekrar deneyin.");
}
}
}`HubException` firmak mesaji istemciye gonderir. Diger exception turleri `EnableDetailedErrors` acik degilse genel bir hata mesajiyla sonuclanir. Uretimde asla detayli hatalari etkinlestirmeyin -- yigin izlerini ve dahili durumu sizdirir.
Istemci Tarafinda Yeniden Baglanma
const connection = new signalR.HubConnectionBuilder()
.withUrl(class="code-string">"/hubs/bildirimler")
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
if (retryContext.elapsedMilliseconds < class="code-number">60000) {
return Math.random() * class="code-number">10000;
}
return null; class=class="code-string">"code-comment">// class="code-number">60 saniye sonra denemeyi birak
}
})
.build();
connection.onreconnecting((error) => {
baglantiDurumunuGoster(class="code-string">"Yeniden baglaniyor...");
});
connection.onreconnected((connectionId) => {
baglantiDurumunuGoster(class="code-string">"Bagli");
class=class="code-string">"code-comment">// Yeniden baglanma sonrasi gruplara tekrar katil
gruplaraYenidenKatil();
});
connection.onclose((error) => {
baglantiDurumunuGoster(class="code-string">"Baglanti kesildi");
setTimeout(() => baglantiBaslat(), class="code-number">5000);
});Bircogu takimin kacirdigi kritik detay: yeniden baglanma sonrasinda istemci yeni bir ConnectionId alir. Onceki baglantidaki grup uyelikleri kaybolur. Gruplara acikca tekrar katilmaniz gerekir. Gercek zamanli panolar gelistirirken, istemci tarafinda aktif aboneliklerin listesini tutar ve her yeniden baglanmada bunlari tekrar oynatirdim.
SignalR vs WebSockets vs Server-Sent Events
Her teknoloji farkli kullanim senaryolarina hizmet eder. Yanlis secim gereksiz karmasikliga veya eksik yeteneklere yol acar.
| Ozellik | SignalR | Ham WebSockets | Server-Sent Events (SSE) |
|---|---|---|---|
| Yon | Cift yonlu | Cift yonlu | Sadece sunucudan istemciye |
| Tasinma | Otomatik | Sadece WebSocket | HTTP akisi |
| Yeniden baglanma | Yerlesik | Manuel | Yerlesik (EventSource) |
| Tarayici destegi | Evrensel (geri donuslu) | Modern tarayicilar | Modern tarayicilar |
| Mesaj formati | JSON/MessagePack | Ham byte/metin | Sadece metin |
| Grup/Kullanici | Yerlesik | Manuel | Manuel |
| Olceklendirme | Redis/Azure backplane | Ozel cozum | Ozel cozum |
| Kimlik dogrulama | ASP.NET Core yerlesik | Manuel | Standart HTTP |
| Karmasiklik | Dusuk | Yuksek | Dusuk |
| En uygun | .NET tam yigin uygulamalar | Ozel protokoller, oyunlar | Basit bildirimler, akislar |
SignalR'i secin -- .NET ekosistemindeyseniz ve minimum sablonla cift yonlu iletisim gerekiyorsa. Ham WebSockets'i secin -- ozel ikili protokol veya cok oyunculu oyun motoru yapiyorsaniz. SSE'yi secin -- sadece sunucudan istemciye akis gerekiyorsa ve en basit uygulamayi istiyorsaniz.
Yaygin SignalR Hatalari
1. Hub Orneklerinde Durum Saklamak
Hub'lar gecicidir. Her metot cagrisinda yeni bir ornek olusturulur. Hub sinifinda alan olarak saklanan her durum cagrilar arasinda kaybolur.
class=class="code-string">"code-comment">// YANLIS -- bu sayac her cagride sifirlanir
public class KotuHub : Hub
{
private int _mesajSayisi = class="code-number">0;
public async Task MesajGonder(string msg)
{
_mesajSayisi++; class=class="code-string">"code-comment">// Her zaman class="code-number">1
await Clients.All.SendAsync(class="code-string">"Sayac", _mesajSayisi);
}
}
class=class="code-string">"code-comment">// DOGRU -- enjekte edilmis singleton servis kullanin
public class IyiHub : Hub
{
private readonly IMesajSayaci _sayac;
public IyiHub(IMesajSayaci sayac) => _sayac = sayac;
public async Task MesajGonder(string msg)
{
var sayi = _sayac.Artir();
await Clients.All.SendAsync(class="code-string">"Sayac", sayi);
}
}2. Hub'i Uzun Sureli Islemlerle Bloklamak
Hub metotlari SignalR baglantisinin thread'inde calisir. Bloklama, istemcinin diger mesajlari almasini engeller.
3. Yeniden Baglanma Sonrasi Gruplara Tekrar Katilmayi Unutmak
Yeniden baglanma yeni bir baglanti olusturur. Gruplar otomatik olarak geri yuklenmez. Bu, "gelistirmede calisiyor ama uretimde calismiyor" hatalarinin bir numarali kaynagindir.
4. CORS'u Dogru Yapilandirmamak
SignalR hem HTTP (muzakere icin) hem WebSocket kullanir. CORS her ikisine de izin vermelidir.
builder.Services.AddCors(options =>
{
options.AddPolicy(class="code-string">"SignalRPolitikasi", policy =>
{
policy.WithOrigins(class="code-string">"https:class="code-commentclass="code-string">">//uygulama.ornek.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); class=class="code-string">"code-comment">// SignalR icin gerekli
});
});5. Mesaj Boyutu Sinirlarini Gormezden Gelmek
Varsayilan maksimum mesaj boyutu 32 KB'dir. Bu siniri ayarlamadan buyuk yukler gondermek sessiz baglantiyi koparmaya neden olur.
6. Yuksek Is Hacmi Senaryolarinda MessagePack Kullanmamak
JSON varsayilan protokoldur, ama yuksek frekansl guncellemeler icin MessagePack ikili serializasyonu yuk boyutunu %30-50 azaltir.
builder.Services.AddSignalR()
.AddMessagePackProtocol();Sonuc
SignalR, gercek zamanli iletisimi sistem duzeyinde bir zorluktan basit bir uygulama duzeyinde ozellige donusturur. Strongly-typed hub kaliplari, yerlesik grup yonetimi ve kesintisiz kimlik dogrulama entegrasyonu, canli guncellemelere ihtiyac duyan .NET uygulamalari icin dogal tercih yapar. Kritik kararlar olceklendirme stratejisi (Redis vs Azure SignalR Service), yeniden baglanma yonetimi ve hub'larin gecici oldugunu anlamak etrafinda doner. Bunlari dogru yapin ve SignalR arka plana karisir -- iyi altyapinin yapmasi gereken tam da budur.
Gercek zamanli ozelliklerinizi tasarlamanizda ve SignalR olceklendirme stratejinizi uretim icin gozden gecirmemde yardimci olabilirim.
İlgili Makaleler
ASP.NET Core ile RESTful API Geliştirme
ASP.NET Core ile production-ready REST API geliştirmenin temellerini öğrenin. Controller, routing ve best practice'ler.
.NET'te Authentication ve Authorization: JWT ve Identity
.NET'te güvenli kimlik doğrulama ve yetkilendirme uygulayın. JWT, ASP.NET Core Identity ve OAuth2.
.NET'te Caching Stratejileri: In-Memory, Distributed ve Redis
.NET'te etkili caching stratejileri uygulayın. In-memory cache, distributed cache ve Redis entegrasyonu.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç