ASP.NET Core'da Çıplak WebSocket: UseWebSockets, Receive Döngüsü, Close Handshake ve Angular Client
Çıplak WebSocket'te mesajı EndOfMessage gelene kadar biriktirip boyutla sınırlaman, socket'e tek bir döngüden yazman ve Origin'i kendin kontrol etmen gerekir. Receive döngüsü, close handshake, KeepAliveTimeout, Channel ile gönderim kuyruğu, connection registry, compression riski ve Angular'da RxJS webSocket() ile backoff'lu yeniden bağlanma.
Sürüm notu: Bu yazıdaki kodlar .NET 10, Angular 22.2 ve RxJS 7.8 ile derlenip denenmiştir.
Önce Tahmin Et
İki soru. Birincisi: tarayıcı 2.997 byte'lık tek bir JSON (JavaScript Object Notation) mesajı gönderiyor, sunucuda 1.024 byte'lık bir buffer ile ReceiveAsync çağırıyorsun. Mesajın tamamını almak için ReceiveAsync kaç kez döner? İkincisi: aynı WebSocket nesnesine 20 farklı thread aynı anda SendAsync çağırırsa ne olur? Exception mı fırlar, mesajlar sırayla mı gider, yoksa başka bir şey mi?
İkinci sorunun cevabı bu yazının en önemli dersi ve beni şaşırttı: sunucuda hiçbir exception yok, ama istemci bağlantıyı protokol hatasıyla düşürüyor.
UseWebSockets ve Bağlantıyı Kabul Etmek
Bir önceki yazıda çıplak WebSocket'i beş seçenekten biri olarak gördük. Bu yazıda onu production'a yakın hâle getiriyoruz: Bookstore'da sipariş durumunu izleyen istemciler, bir connection registry ve sunucunun her yerinden yayın.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ConnectionRegistry>();
WebApplication app = builder.Build();
WebSocketOptions webSocketOptions = new()
{
KeepAliveInterval = TimeSpan.FromSeconds(30),
KeepAliveTimeout = TimeSpan.FromSeconds(10)
};
webSocketOptions.AllowedOrigins.Add("http://localhost:4200");
app.UseWebSockets(webSocketOptions);
app.Map("/ws/orders", async (HttpContext context, ConnectionRegistry registry, ILogger<WebSocketConnection> logger) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
return;
}
using System.Net.WebSockets.WebSocket socket = await context.WebSockets.AcceptWebSocketAsync();
WebSocketConnection connection = new(socket, logger);
registry.Add(connection);
try
{
await connection.RunAsync(context.RequestAborted);
}
finally
{
registry.Remove(connection);
}
});
UseWebSockets middleware'i (ASP.NET Core Web API (Application Programming Interface) serisinin Middleware yazısındaki pipeline'ın bir parçası) Upgrade: websocket isteğini tanıyor ve context.WebSockets'i dolduruyor. AcceptWebSocketAsync 101 yanıtını gönderiyor ve elinde artık bir TCP (Transmission Control Protocol) bağlantısını sarmalayan bir WebSocket nesnesi var.
Kritik bir kural: endpoint delegate'i bağlantı açık kaldığı sürece bitmemeli. RunAsync'i await etmeden dönersen pipeline biter, response tamamlanır ve sonraki her SendAsync "Cannot write to the response body, the response has completed" diye patlar. Socket'i bir yere koyup "sonra kullanırım" diyemezsin; isteğin yaşam süresi bağlantının yaşam süresidir.
app.MapGet yerine app.Map kullanmamın da sebebi var: .NET 7'den beri Kestrel WebSocket'i HTTP (Hypertext Transfer Protocol) sürüm 2, yani HTTP/2 üzerinden de kabul ediyor ve orada el sıkışma GET değil, RFC (Request for Comments) 8441'deki extended CONNECT isteği. MapGet yazarsan HTTP/2 üzerinden gelen CONNECT isteği bu endpoint'le eşleşmez. Chrome, Edge ve Firefox 128+ HTTP/2 WebSocket'i varsayılan olarak kullanıyor.
Seçeneklere gelince: KeepAliveInterval sunucunun ne sıklıkla ping frame'i göndereceği (varsayılan 2 dakika), .NET 9 ile gelen KeepAliveTimeout ise ping'e pong gelmezse bağlantının ne kadar sonra koparılacağı (varsayılan Timeout.InfiniteTimeSpan, yani kapalı). İkisini birlikte ayarlamazsan, kablosu çekilen bir istemcinin bağlantısı sunucuda TCP katmanı fark edene kadar açık kalır.
Origin kontrolü
Tarayıcı WebSocket isteği için CORS (Cross-Origin Resource Sharing) preflight'ı yapmıyor ve Access-Control-* header'larına bakmıyor. Kötü niyetli bir site kullanıcının tarayıcısından senin sunucuna WebSocket açarsa çerezler de gider; buna CSWSH (Cross-Site WebSocket Hijacking) deniyor. Tek savunma Origin header'ı. AllowedOrigins listesini doldurunca:
$ curl -s -i http://localhost:17602/ws/orders -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Origin: http://localhost:4200"
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Date: Sun, 08 Nov 2026 02:08:10 GMT
Server: Kestrel
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
$ curl -s -i http://localhost:17602/ws/orders -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Origin: https://evil.example"
HTTP/1.1 403 Forbidden
Content-Length: 0
Date: Sun, 08 Nov 2026 02:08:12 GMT
Server: Kestrel
Aynı isteği Origin header'ı olmadan attığımda 101 aldım. Bu kontrol yalnızca tarayıcıları kısıtlar; curl ya da bir script istediği header'ı yazar. Yani Origin kontrolü authentication'ın yerine geçmez, onun yanına konur.
Receive Döngüsü: Parçalı Mesajlar ve Boyut Limiti
private async Task ReceiveLoopAsync(CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
ArrayBufferWriter<byte> message = new();
int reads = 0;
try
{
while (true)
{
ValueWebSocketReceiveResult result = await socket.ReceiveAsync(buffer.AsMemory(0, 1024), cancellationToken);
reads++;
if (result.MessageType == WebSocketMessageType.Close)
{
logger.LogInformation("Client {Id} closed: {Status} {Description}",
Id, socket.CloseStatus, socket.CloseStatusDescription);
await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "bye", cancellationToken);
return;
}
if (message.WrittenCount + result.Count > MaxMessageBytes)
{
logger.LogWarning("Client {Id} sent more than {Max} bytes, closing", Id, MaxMessageBytes);
await socket.CloseAsync(WebSocketCloseStatus.MessageTooBig, $"max {MaxMessageBytes} bytes", cancellationToken);
return;
}
message.Write(buffer.AsSpan(0, result.Count));
if (!result.EndOfMessage)
{
continue;
}
logger.LogInformation("Client {Id} sent {Bytes} bytes in {Reads} reads", Id, message.WrittenCount, reads);
Handle(message.WrittenSpan);
message.ResetWrittenCount();
reads = 0;
}
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
logger.LogWarning("Client {Id} disconnected without a close handshake", Id);
}
catch (OperationCanceledException)
{
logger.LogWarning("Client {Id} connection aborted", Id);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
Bir Node 24 istemcisinden önce 33 byte'lık bir subscribe mesajı, sonra 2.997 byte'lık bir mesaj gönderdim. Sunucu log'u:
info: Bookstore.Realtime.WebSocketConnection[0]
Client 471a2573-33ac-4e3a-ad48-aa8f939b6722 sent 33 bytes in 1 reads
info: Bookstore.Realtime.WebSocketConnection[0]
Client 471a2573-33ac-4e3a-ad48-aa8f939b6722 sent 2997 bytes in 3 reads
Baştaki ilk sorunun cevabı: üç kez. İstemci bu mesajı tek parça gönderdi, ama ReceiveAsync frame'i değil, buffer'a sığanı veriyor; ilk iki çağrıda EndOfMessage false. Yani EndOfMessage "mesaj bitti" demek, "frame bitti" değil. Mesajı tamamlamadan JSON parse etmeye kalkarsan yarım bir JSON alırsın; birçok örnek kodun 4 KB'lık buffer ile tek ReceiveAsync yaptığını görürsün, küçük mesajlarla test edildiği için bu hata production'a kadar saklanır.
Biriktirmenin bir bedeli var: sınırsız biriktirirsen istemci sana 1 GB gönderip belleğini tüketebilir. Bu yüzden toplam boyutu sayıp MessageTooBig (1009) ile kapatıyoruz. 5.000 byte gönderen istemcinin gördüğü:
big close: 1009 max 4096 bytes
ArrayPool ile kiralanan buffer'ın finally'de iade edilmesi de önemli; binlerce bağlantı açıkken her birine yeni bir array ayırmak GC (garbage collector) üzerinde gereksiz yük.
Close Handshake
WebSocket'i kapatmanın kibar yolu iki yönlü bir el sıkışma: bir taraf Close frame'i gönderir, diğeri kendi Close frame'iyle cevap verir, sonra TCP kapanır. Yukarıdaki döngüde istemci kapattığında CloseOutputAsync ile cevap veriyoruz. İstemci ws.close(1000, "page closed") dediğinde iki taraftaki çıktı:
info: Bookstore.Realtime.WebSocketConnection[0]
Client 471a2573-33ac-4e3a-ad48-aa8f939b6722 closed: NormalClosure page closed
close: 1000 bye
CloseAsync ile CloseOutputAsync arasındaki fark: CloseAsync kendi Close frame'ini gönderir ve karşı tarafın cevabını bekler; CloseOutputAsync yalnızca gönderir. Karşı taraf zaten kapattıysa cevap beklemek anlamsız, o yüzden burada CloseOutputAsync. Kapatmayı biz başlattığımızda (mesaj çok büyük) CloseAsync kullanıyoruz.
Gerçek hayatta bağlantıların çoğu kibarca kapanmaz: laptop kapağı kapanır, telefon tünele girer, süreç ölür. Node istemcisini process.exit() ile öldürdüğümde sunucunun gördüğü:
warn: Bookstore.Realtime.WebSocketConnection[0]
Client cc669afc-89ba-4300-86ee-a5410d339770 disconnected without a close handshake
Burada en azından TCP bağlantısı kapandı ve işletim sistemi bunu sunucuya bildirdi. Kablo çekildiğinde o bildirim bile gelmez; işte KeepAliveTimeout bu yüzden var.
Tek Writer Kuralı: Channel ile Gönderim Kuyruğu
.NET'in WebSocket dokümantasyonu açık: aynı nesne üzerinde aynı anda en fazla bir gönderim ve bir okuma. Ama kuralı çiğnediğinde ne olduğunu görmek istedim. Şu kodu çalıştırdım; 20 thread aynı socket'e iki parçalı birer mesaj gönderiyor:
Task[] tasks = Enumerable.Range(0, 20).Select(i => Task.Run(async () =>
{
byte[] part = Encoding.UTF8.GetBytes($"[{i}-a]" + new string('x', 2_000_000));
await socket.SendAsync(part, WebSocketMessageType.Text, endOfMessage: false, CancellationToken.None);
await socket.SendAsync(Encoding.UTF8.GetBytes($"[{i}-b]"), WebSocketMessageType.Text, endOfMessage: true, CancellationToken.None);
})).ToArray();
await Task.WhenAll(tasks);
Sunucuda Task.WhenAll hiçbir exception fırlatmadı. .NET istemcisi ise şunu söyledi:
after 31 receives: WebSocketException: The WebSocket expected a continuation frame after having received a previous non-final frame.
Node 24'ün yerleşik WebSocket'i de tek mesaj almadan 1006 koduyla kapandı. Sebep: .NET içeride frame'leri bir kilitle sıraya sokuyor, böylece iki frame'in byte'ları birbirine karışmıyor. Ama bir thread'in mesajının ilk parçasından sonra başka bir thread'in mesajının ilk parçası araya girebiliyor; RFC 6455 buna izin vermiyor. Küçük, tek parçalı mesajlarla bu hatayı görmeyebilirsin, ki bu daha da tehlikeli.
Çözüm socket'e yazan tek bir yer olması. Her bağlantıya bir Channel (C# Asenkron serisinin Channels yazısındaki producer/consumer yapısı) ve onu boşaltan tek bir döngü veriyoruz:
public sealed class WebSocketConnection(WebSocket socket, ILogger<WebSocketConnection> logger)
{
private const int MaxMessageBytes = 4 * 1024;
// tek okuyucu: socket'e yalnızca SendLoopAsync yazar
private readonly Channel<byte[]> _outbox = Channel.CreateBounded<byte[]>(new BoundedChannelOptions(64)
{
SingleReader = true,
FullMode = BoundedChannelFullMode.DropOldest
});
private readonly ConcurrentDictionary<int, bool> _orders = new();
public Guid Id { get; } = Guid.NewGuid();
public bool IsWatching(int orderId) => _orders.ContainsKey(orderId);
public bool TryEnqueue(byte[] payload) => _outbox.Writer.TryWrite(payload);
public async Task RunAsync(CancellationToken cancellationToken)
{
Task sendLoop = SendLoopAsync(cancellationToken);
try
{
await ReceiveLoopAsync(cancellationToken);
}
finally
{
_outbox.Writer.TryComplete();
await sendLoop;
}
}
private async Task SendLoopAsync(CancellationToken cancellationToken)
{
try
{
await foreach (byte[] payload in _outbox.Reader.ReadAllAsync(cancellationToken))
{
if (socket.State != WebSocketState.Open)
{
break;
}
await socket.SendAsync(payload, WebSocketMessageType.Text, endOfMessage: true, cancellationToken);
}
}
catch (OperationCanceledException)
{
}
}
}
Bu yapı iki sorunu birden çözüyor. Gönderimler artık sıralı, çünkü tek bir okuyucu var. İkincisi yavaş istemci: mobil bağlantıdaki bir istemci mesajları yavaş okursa SendAsync beklemeye başlar. Kuyruk olmasaydı yayın yapan kod da onu beklerdi ve tek yavaş istemci herkesi yavaşlatırdı. Bounded channel ve DropOldest ile "yavaş istemci en eski mesajları kaybeder, diğerleri etkilenmez" diyoruz. Sipariş durumu gibi son değerin önemli olduğu veride bu doğru seçim; chat mesajı gibi her mesajın önemli olduğu yerde ise kuyruk dolduğunda bağlantıyı kapatıp istemciyi yeniden senkronize olmaya zorlamak daha dürüst.
Connection Registry ve Broadcast
public sealed class ConnectionRegistry
{
private readonly ConcurrentDictionary<Guid, WebSocketConnection> _connections = new();
public void Add(WebSocketConnection connection) => _connections[connection.Id] = connection;
public void Remove(WebSocketConnection connection) => _connections.TryRemove(connection.Id, out _);
public int Broadcast(int orderId, OrderStatusMessage message)
{
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(message, JsonSerializerOptions.Web);
int delivered = 0;
foreach (WebSocketConnection connection in _connections.Values)
{
if (connection.IsWatching(orderId) && connection.TryEnqueue(payload))
{
delivered++;
}
}
return delivered;
}
}
app.MapPost("/orders/{id:int}/status", (int id, StatusUpdate update, ConnectionRegistry registry) =>
{
int delivered = registry.Broadcast(id, new OrderStatusMessage("status", id, update.Status));
return TypedResults.Ok(new { delivered });
});
JSON'u bir kez serialize edip aynı byte dizisini bütün kuyruklara koyuyoruz. ConcurrentDictionary.Values o anki değerlerin bir kopyasını döndürüyor (bunun için kısa süreliğine bütün iç kilitleri alıyor); döngü sırasında eklenen ya da çıkan bağlantı sorun çıkarmıyor. Çok sık yayın yapıyorsan bu kopyanın maliyetini de hesaba kat. İstemci {"type":"subscribe","orderId":42} gönderdikten sonra:
received: {"type":"status","orderId":42,"status":"Shipped"}
POST: {"delivered":1}
Bu registry tek sürece ait. İkinci bir sunucu eklediğin an Broadcast yalnızca kendi bağlantılarını görür; bunu serinin sekizinci yazısında çözüyoruz.
Angular: RxJS webSocket() ve Backoff ile Yeniden Bağlanma
İstemci tarafında RxJS'in webSocket() fonksiyonu hem Observable hem Observer olan bir subject döndürüyor: abone olunca bağlanıyor, next() ile mesaj gönderiyor. RxJS operator'lerini ilerideki RxJS serisinde ayrıntılı anlatacağız; burada üç tanesi yeterli.
import { Injectable } from '@angular/core';
import { Observable, retry, timer } from 'rxjs';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
export interface OrderStatusMessage {
type: 'status';
orderId: number;
status: string;
}
type ClientMessage = { type: 'subscribe' | 'unsubscribe'; orderId: number };
@Injectable({ providedIn: 'root' })
export class OrderSocketService {
private readonly socket: WebSocketSubject<OrderStatusMessage | ClientMessage> = webSocket({
url: 'ws://localhost:17602/ws/orders',
openObserver: { next: () => console.log('[ws] open') },
closeObserver: { next: (event: CloseEvent) => console.log(`[ws] close ${event.code}`) },
});
watchOrder(orderId: number): Observable<OrderStatusMessage> {
// multiplex: abone olunca subscribe, bırakınca unsubscribe mesajı gönderir
const messages = this.socket.multiplex(
() => ({ type: 'subscribe', orderId }),
() => ({ type: 'unsubscribe', orderId }),
(message) => message.type === 'status' && message.orderId === orderId,
) as Observable<OrderStatusMessage>;
return messages.pipe(
retry({
// 1, 2, 4, 8 ... en fazla 30 saniye bekleyerek yeniden bağlan
delay: (_error, retryCount) => {
const delayMs = Math.min(1000 * 2 ** (retryCount - 1), 30_000);
console.log(`[ws] retry #${retryCount} in ${delayMs} ms`);
return timer(delayMs);
},
resetOnSuccess: true,
}),
);
}
}
multiplex burada asıl işi yapıyor: aynı socket üzerinde mantıksal bir kanal açıyor, abone olunduğunda subscribe, abonelik bırakıldığında unsubscribe mesajını kendisi gönderiyor. retry yeniden abone olduğunda multiplex subscribe mesajını yeniden gönderiyor; yani yeniden bağlandıktan sonra "hangi siparişleri izliyordum?" diye ayrıca düşünmen gerekmiyor. resetOnSuccess ise başarılı bir mesajdan sonra sayacı sıfırlıyor, bir sonraki kopuşta bekleme yine 1 saniyeden başlıyor.
import { Component, inject, input } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs';
import { OrderSocketService } from './order-socket.service';
@Component({
selector: 'app-order-tracker',
template: `
<h2>Order #{{ orderId() }}</h2>
<p>Status: {{ status()?.status ?? 'waiting...' }}</p>
`,
})
export class OrderTrackerComponent {
private readonly orders = inject(OrderSocketService);
readonly orderId = input.required<number>();
// component yok olunca toSignal aboneliği bırakır, multiplex de unsubscribe mesajını gönderir
protected readonly status = toSignal(
toObservable(this.orderId).pipe(switchMap((id) => this.orders.watchOrder(id))),
);
}
Uygulamayı build edip tarayıcıda açtım, sunucu sürecini kill -9 ile öldürdüm, 6 saniye sonra yeniden başlattım ve bir durum güncellemesi POST ettim. Tarayıcı konsolu:
[ws] open
[ws] close 1006
[ws] retry #1 in 1000 ms
WebSocket connection to 'ws://localhost:17602/ws/orders' failed:
[ws] retry #2 in 2000 ms
[ws] close 1006
WebSocket connection to 'ws://localhost:17602/ws/orders' failed:
[ws] retry #3 in 4000 ms
[ws] close 1006
[ws] open
Sayfada "Status: OutForDelivery" göründü ve yeni sunucu sürecinin log'unda 33 byte'lık subscribe mesajı vardı; yani multiplex aboneliği kendiliğinden yeniledi. Ama dikkat: sunucu kapalıyken gönderilen güncellemeler kayboldu. Yeniden bağlanınca güncel durumu sıradan bir HTTP isteğiyle çekmek hâlâ senin işin.
Compression: Neden Adında "Dangerous" Var?
using WebSocket socket = await context.WebSockets.AcceptWebSocketAsync(
new WebSocketAcceptContext { DangerousEnableCompression = true });
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Date: Sun, 08 Nov 2026 02:08:37 GMT
Server: Kestrel
Upgrade: websocket
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=15
İstemci permessage-deflate istediğinde sunucu kabul ediyor ve mesajlar sıkıştırılıyor. Bu seçenek kapalıyken aynı istek bu header olmadan 101 alıyor, yani bağlantı sıkıştırmasız kuruluyor. İsimdeki "Dangerous" kelimesi TLS (Transport Layer Security) üzerinde sıkıştırmanın CRIME (Compression Ratio Info-leak Made Easy) ve BREACH (Browser Reconnaissance and Exfiltration via Adaptive Compression of Hypertext) türü saldırılara kapı açmasından geliyor: saldırgan kendi kontrol ettiği veriyi bir secret ile aynı mesaja sokabiliyorsa, sıkıştırılmış boyutun değişiminden secret'ı byte byte tahmin edebilir. Token, kişisel veri gibi içerik taşıyan mesajlarda ya sıkıştırmayı kapalı tut ya da o mesajı WebSocketMessageFlags.DisableCompression ile gönder.
Üretimde Dikkat
Buraya kadar yazdıklarımızı say: receive döngüsü, boyut limiti, close handshake, kopuş tespiti, gönderim kuyruğu, registry, abonelik protokolü, istemcide yeniden bağlanma ve aboneliği yenileme. Ve hâlâ eksikler var:
- Authentication: tarayıcı WebSocket isteğine header ekleyemiyor, token'ı query string'den almak ve log'lara düşmesini engellemek gerekiyor.
- Kullanıcıya göre gönderim: aynı kullanıcının üç sekmesi üç ayrı bağlantı.
- Kaçırılan mesajlar: kopuş sırasında gönderilenler için sıra numarası ve tekrar gönderim.
- Scale-out: ikinci sunucuda registry'nin bilmediği bağlantılar.
- Transport fallback: WebSocket'i engelleyen proxy arkasındaki kullanıcılar.
Bunların her biri SignalR'da hazır geliyor. Çoğu ekibin çıplak WebSocket'ten SignalR'a geçmesinin sebebi tam bu liste. Çıplak WebSocket'in hâlâ doğru seçim olduğu yerler de var: SignalR client library'si olmayan bir karşı taraf (IoT (Internet of Things) cihazı, başka bir dilde yazılmış bir oyun istemcisi), mevcut bir protokolün (STOMP (Simple Text Oriented Messaging Protocol), MQTT (Message Queuing Telemetry Transport) over WebSocket) taşınması ya da her byte'ın önemli olduğu binary akışlar.
İzleme tarafında açık bağlantı sayısını, kuyruktan düşürülen mesaj sayısını ve MessageTooBig ile kapatılan bağlantıları metric olarak yayınla. "Kuyruktan düşen mesaj" sayısının artması, yavaş istemcilerin ya da yayın hızının arttığının ilk işareti.
Sık Düşülen Tuzaklar
Tek ReceiveAsync çağrısının bütün mesajı getirdiğini varsaymak. EndOfMessage true olana kadar biriktir ve toplam boyutu sınırla.
Aynı socket'e birden fazla yerden SendAsync çağırmak. Exception görmeyeceğin için test ortamında yakalanmaz; tek writer döngüsü kullan.
Endpoint delegate'inden bağlantı açıkken dönmek. Socket'i bir listeye koyup endpoint'i bitirirsen response tamamlanır ve socket kullanılamaz hâle gelir.
WebSocket endpoint'ini MapGet ile tanımlamak. HTTP/2 üzerinden gelen extended CONNECT isteğini kaçırırsın; Map kullan.
Origin kontrolünü authentication sanmak. Yalnızca tarayıcı kaynaklı CSWSH'yi engeller; tarayıcı dışı istemciler header'ı istediği gibi yazar.
Yeniden bağlanmayı sabit aralıkla yapmak. Sunucu yeniden başladığında 10.000 istemcinin hepsi aynı saniyede bağlanmaya çalışır. Üstel backoff'a küçük bir rastgele sapma (jitter) eklemek bu dalgayı yayar.
Küçük Bir Deneme
★☆☆ Receive buffer'ını 1.024'ten 16 byte'a indir ve 33 byte'lık subscribe mesajının kaç okumada geldiğini log'dan doğrula.
★★☆ OrderSocketService'teki backoff'a ±%20 jitter ekle ve sunucuyu öldürüp başlatarak konsoldaki bekleme sürelerinin artık her seferinde farklı olduğunu gözle.
★★★ BoundedChannelFullMode.DropOldest yerine Wait kullan ve mesajları hiç okumayan bir istemci yaz. Sonra saniyede 100 broadcast yapan bir endpoint ekle. Diğer istemcilerin gecikmesi nasıl değişti? Kuyruk dolunca bağlantıyı PolicyViolation ile kapatan bir sürüm yaz.
Bir sonraki yazıda tek yönlü tarafa geçiyoruz: Server-Sent Events'i derinlemesine ele alıp SseItem<T>, Last-Event-ID ile kaldığı yerden devam, heartbeat ve proxy'lerin SSE (Server-Sent Events) akışını nasıl sessizce bozduğunu göreceğiz.