OfficeFighters’da oyuncunun tuşa basması ile iki ekranda aynı maçın görünmesi arasında birkaç farklı akış bulunuyor. İstemci giriş gönderiyor, sunucu oyunu ilerletiyor, bağlantı kuyruğu durum paketlerini taşıyor ve Phaser bu veriyi çiziyor. SignalR bu zincirde iletişim katmanını sağlıyor. GameHub, bağlantı fabrikası ve ConnectionOutbox birlikte incelendiğinde asıl tasarımın mesaj göndermekten çok gecikmeyi ve bağlantı yaşam döngüsünü yönetmek olduğu görülüyor.
Tek bağlantı ve açık taşıma seçimi
İstemci HubConnectionBuilder ile tek bağlantı kuruyor. Fabrikada taşıma WebSockets olarak seçili ve skipNegotiation açık. Sunucu da /hubs/game yolunu yalnız WebSocket kabul edecek biçimde eşliyor. İki ucun aynı kararı vermesi önemli: istemci başka bir taşıma davranışına güvenmiyor. Göreli adres kullanımı geliştirmede proxy, dağıtımda aynı köken üzerinden çalışmayı destekliyor.
// OfficeFighters hub factory: selected transport and auth.
new HubConnectionBuilder()
.withUrl(options.url, {
transport: HttpTransportType.WebSockets,
skipNegotiation: true,
accessTokenFactory: options.accessTokenFactory
})
.withAutomaticReconnect(options.retry)
.configureLogging(LogLevel.None)
.build();
accessTokenFactory, bağlantı kurulurken güncel erişim token’ını sağlıyor. LogLevel.None seçimi de bu fabrikanın açık bir ayarı. İletişim ayrıntılarını ekrana veya günlüklere taşımadan, bağlantı durumunu uygulamanın kendi gözlemlenebilir durumu üzerinden göstermek gerekiyor.
Giriş, sonuç iddiası değil oyuncu niyeti
GameRoom giriş kuyruğunu tüketirken kullanıcıyı odadaki oyuncuyla eşleştiriyor ve InputGate üzerinden sıra numarası ile düğme verisini denetliyor. Sonuç olarak istemcinin gönderdiği şey oyuncunun hareket niyeti. Konum, vuruş ve maç sonucu sunucu simülasyonunun çıktısı olarak geliyor. Hub çağrısının başarılı olması tek başına bir saldırının hedefe isabet ettiği anlamına gelmiyor.
Bu ayrım testleri de değiştiriyor. Aynı giriş sıra numarasını iki kez göndermek, eski paketi gecikmeli ulaştırmak ve odada olmayan kullanıcıdan giriş üretmek ayrı durumlar. Gönderim zamanlaması kadar, sunucunun hangi girdiyi kabul ettiği de izlenmeli.
Snapshot ile olay aynı tür veri değil
Bir snapshot belirli tick’teki konumları ve durumları temsil ediyor. Sonraki snapshot geldiğinde eski konum bilgisi çoğu zaman değerini kaybediyor. Vuruş, tur sonu veya ses tetikleyicisi gibi olayların ise yalnızca eski pakette kaldıkları için kaybolmaması gerekiyor. ConnectionOutbox bu iki ihtiyacı ayrı ele alıyor.
// ConnectionOutbox: limits for waiting messages.
public const int SnapshotCapacity = 8;
public const int CriticalCapacity = 64;
public const int OtherCapacity = 16;
// When an old snapshot is removed, preserve its events:
newer with { Events = [.. oldest.Events, .. newer.Events] };
Bekleyen snapshot sayısı sekize ulaştığında en eski uygun snapshot çıkarılıyor; içindeki olaylar sıradaki snapshot’a ekleniyor. Kritik mesajlar ayrı sayılıyor. Altmış dört kritik mesajlık sınır aşılırsa bağlantı kesiliyor. Böylece yavaş bir istemcinin kuyruğu sınırsız büyümüyor. Bu bellek içi kuyruk, bağlantının o anki gönderim akışını düzenliyor; kalıcı bir teslim günlüğü olarak yorumlanmamalı.
Ağ gönderimini oyun döngüsünden ayırmak
ConnectionOutbox kendi pompasında sıralı gönderim yapıyor. Oyun döngüsü ağa yazmanın tamamlanmasını beklemek yerine kuyruğa bırakıyor. Bu sayede tek bir istemcinin yavaş bağlantısı, aynı shard üzerindeki diğer odaların simülasyonunu doğrudan bekletmiyor. Elbette kuyruk sınırları bunu destekliyor; yalnız ayrı Task açmak bellek büyümesini kendiliğinden çözmez.
Yeniden bağlanınca uygulama durumunu da kurmak
Fabrikada withAutomaticReconnect özel retry politikasıyla etkin. SignalR belgesinin özellikle ayırdığı ilk start hatası ile kurulu bağlantının kopması, uygulama seviyesinde de farklı ele alınmalı. Bağlantı yeniden açıldığında yalnız soketin Connected olması yeterli değil; oda üyeliği ve güncel oyun durumu sunucuyla tekrar uyuşmalı.
Kaynakta yeniden bağlanan oyuncuya kritik snapshot gönderiliyor. İstemci de eski bağlantıdan gelen gecikmiş yanıtların yeni oturumun durumunu bozmasına izin vermemeli. Kullanıcıya bağlantı koptu bilgisini göstermek ve girişleri hangi aşamada tekrar açacağını belirlemek bu yaşam döngüsünün görünür tarafı.
İstemcide tick ve eventId ayrı izleniyor
SnapshotBuffer aynı ya da daha eski tick’li durumları tamponlamıyor; ancak paketin içindeki daha önce görülmemiş olayları yine çıkarabiliyor. Olaylar artan eventId ile tekilleştiriliyor. Sunucunun eski snapshot olaylarını yeni pakete taşımasıyla bu davranış birlikte çalışıyor: durum gereksiz olabilirken olay hâlâ gerekli olabilir.
Test turunda bağlantıyı kesip geri açmak, snapshot üretimini gönderimden hızlı yapmak, kritik mesaj sınırını zorlamak ve eski tick içinde yeni olay göndermek değerli. Düşürülen snapshot sayısı, yeniden bağlanma süresi ve kritik kuyruk baskısı birlikte incelendiğinde yalnız ping ölçümünden daha kullanışlı bir bağlantı resmi çıkıyor.
Kaynaklar / References
Microsoft Learn — SignalR JavaScript client
OfficeFighters serisinin diğer yazıları
- OfficeFighters — Supabase Storage ve S3 ile oyun varlıklarını yönetmek
- OfficeFighters — Phaser, sabit adımlı simülasyon ve snapshot interpolasyonu
- OfficeFighters — MailKit ve outbox ile dayanıklı e-posta gönderimi
- OfficeFighters — JWT, refresh token rotasyonu ve çok sekmeli oturum yönetimi
- OfficeFighters — PostgreSQL, EF Core ve DB-first veri modeli