Sdílet prostřednictvím


Podpora webSocketů v .NET

Protokol WebSocket umožňuje obousměrnou komunikaci mezi klientem a vzdáleným hostitelem. System.Net.WebSockets.ClientWebSocket odhaluje schopnost navázat připojení WebSocket prostřednictvím zahajovacího handshake, která je vytvořena a odesílána metodou ConnectAsync.

Rozdíly v protokolech HTTP/1.1 a HTTP/2 WebSocket

WebSockety přes HTTP/1.1 používají jediný TCP spoj, a proto je spravován hlavičkami platnými pro celé připojení; další informace naleznete v RFC 6455. Podívejte se na následující příklad vytvoření protokolu WebSocket přes HTTP/1.1:

Uri uri = new("ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx");

using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, default);

var bytes = new byte[1024];
var result = await ws.ReceiveAsync(bytes, default);
string res = Encoding.UTF8.GetString(bytes, 0, result.Count);

await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closed", default);

Vzhledem ke své multiplexní povaze je třeba zvolit jiný přístup k HTTP/2. WebSockety jsou vytvořeny pro každý datový proud, další informace viz RFC 8441. S protokolem HTTP/2 je možné použít jedno připojení pro více datových proudů webových soketů společně s běžnými datovými proudy HTTP a rozšířit efektivnější využití sítě na webSockety. Existuje speciální přetížení ConnectAsync(Uri, HttpMessageInvoker, CancellationToken), které přijímá HttpMessageInvoker, aby umožnilo opětovné využití stávajících připojení ve fondu:

using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);

Nastavení verze a zásad HTTP

Ve výchozím nastavení ClientWebSocket používá HTTP/1.1 k odeslání úvodního spojení handshake a umožňuje downgrade. V .NET 7 jsou k dispozici webové sockety přes HTTP/2. Před voláním ConnectAsyncje možné ho změnit:

using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();

ws.Options.HttpVersion = HttpVersion.Version20;
ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;

await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);

Nekompatibilní možnosti

ClientWebSocket má vlastnosti System.Net.WebSockets.ClientWebSocketOptions, které může uživatel nastavit před navázáním připojení. Pokud je však k dispozici HttpMessageInvoker, má také tyto vlastnosti. Aby nedocházelo k nejednoznačnosti, měly by být vlastnosti nastaveny na HttpMessageInvokera ClientWebSocketOptions by měly mít výchozí hodnoty. V opačném případě, pokud ClientWebSocketOptions jsou změněny, přetížení ConnectAsync vyvolá ArgumentException.

using HttpClientHandler handler = new()
{
    CookieContainer = cookies;
    UseCookies = cookies != null;
    ServerCertificateCustomValidationCallback = remoteCertificateValidationCallback;
    Credentials = useDefaultCredentials
        ? CredentialCache.DefaultCredentials
        : credentials;
};
if (proxy is null)
{
    handler.UseProxy = false;
}
else
{
    handler.Proxy = proxy;
}
if (clientCertificates?.Count > 0)
{
    handler.ClientCertificates.AddRange(clientCertificates);
}
HttpMessageInvoker invoker = new(handler);
using ClientWebSocket cws = new();
await cws.ConnectAsync(uri, invoker, cancellationToken);

Komprese

Protokol WebSocket podporuje deflaci na úrovni jednotlivých zpráv, jak je definováno v RFC 7692. System.Net.WebSockets.ClientWebSocketOptions.DangerousDeflateOptionsto ovládá. Pokud jsou k dispozici, jsou možnosti posílány na server během fáze navázání spojení. Pokud server podporuje kompresi jednotlivých zpráv a volby jsou akceptovány, vytvoří se instance ClientWebSocket automaticky s povolenou kompresí pro všechny zprávy.

using ClientWebSocket ws = new()
{
    Options =
    {
        DangerousDeflateOptions = new WebSocketDeflateOptions()
        {
            ClientMaxWindowBits = 10,
            ServerMaxWindowBits = 10
        }
    }
};

Důležitý

Než povolíte kompresi, mějte na paměti, že její zapnutí činí aplikaci zranitelnou vůči útokům typu CRIME/BREACH. Pro více informací viz CRIME a BREACH. Důrazně doporučujeme vypnout kompresi při odesílání dat obsahujících tajné kódy zadáním příznaku DisableCompression pro tyto zprávy.

strategie Keep-Alive

Ve verzi .NET 8 a starších verzích je jedinou dostupnou strategií Keep-Alive Nevyžádané PONG. Tato strategie je dostatečná k tomu, aby zabránila základnímu připojení TCP v přechodu do nečinného stavu. V případě, že vzdálený hostitel přestane reagovat (například pokud se vzdálený server zhroutí), jediným způsobem, jak takové situace zjistit pomocí nevyžádaného PONG, je spolehnout se na vypršení časového limitu TCP.

.NET 9 zavedla dlouhou požadovanou strategii ping/PONG Keep-Alive, která doplňuje stávající nastavení KeepAliveInterval novým nastavením KeepAliveTimeout. Počínaje rozhraním .NET 9 je strategie Keep-Alive vybrána takto:

  1. Keep-Alive je vypnuto, pokud
    • KeepAliveInterval je TimeSpan.Zero nebo Timeout.InfiniteTimeSpan
  2. Nevyžádaná PONG, pokud
    • KeepAliveInterval je pozitivní konečný TimeSpan, -AND-
    • KeepAliveTimeout je TimeSpan.Zero nebo Timeout.InfiniteTimeSpan
  3. ping/pong, pokud
    • KeepAliveInterval je pozitivní konečný TimeSpan, -AND-
    • KeepAliveTimeout je pozitivní konečný TimeSpan

Výchozí hodnota KeepAliveTimeout je Timeout.InfiniteTimeSpan, takže výchozí chování Keep-Alive zůstává konzistentní mezi verzemi .NET.

Pokud používáte ClientWebSocket, výchozí hodnota ClientWebSocketOptions.KeepAliveInterval je WebSocket.DefaultKeepAliveInterval (obvykle 30 sekund). To znamená, že ClientWebSocket má ve výchozím nastavení Keep-Alive ZAPNUTO s nevyžádaným PONG jako výchozí strategií.

Pokud chcete přepnout na strategii PING/PONG, stačí přepsání ClientWebSocketOptions.KeepAliveTimeout:

var ws = new ClientWebSocket();
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(20);
await ws.ConnectAsync(uri, cts.Token);

Pro základní WebSocketje Keep-Alive ve výchozím nastavení VYPNUTO. Pokud chcete použít strategii PING/PONG, je potřeba nastavit WebSocketCreationOptions.KeepAliveInterval i WebSocketCreationOptions.KeepAliveTimeout:

var options = new WebSocketCreationOptions()
{
    KeepAliveInterval = WebSocket.DefaultKeepAliveInterval,
    KeepAliveTimeout = TimeSpan.FromSeconds(20)
};
var ws = WebSocket.CreateFromStream(stream, options);

Pokud se používá strategie Unsolicited PONG, rámce PONG se používají jako jednosměrný prezenční signál. Pravidelně odesílali v intervalech KeepAliveInterval, bez ohledu na to, zda vzdálená cílová stanice komunikuje, nebo ne.

Pokud je strategie PING/PONG aktivní, odešle se rámec PING po uplynutí času KeepAliveInterval od poslední komunikace ze vzdáleného koncového bodu. Každý rámec PING obsahuje celočíselné tokeny, které se mají spárovat s očekávanou odpovědí PONG. Pokud po uplynutí KeepAliveTimeout nedorazí žádná odpověď PONG, vzdálený koncový bod se považuje za nereagující a připojení WebSocket se automaticky přeruší.

var ws = new ClientWebSocket();
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
await ws.ConnectAsync(uri, cts.Token);

// NOTE: There must be an outstanding read at all times to ensure
// incoming PONGs are processed
var result = await _webSocket.ReceiveAsync(buffer, cts.Token);

Pokud časový limit uplyne, nevyrovnaný ReceiveAsync vyvolá OperationCanceledException:

System.OperationCanceledException: Aborted
 ---> System.AggregateException: One or more errors occurred. (The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.) (Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request..)
 ---> System.Net.WebSockets.WebSocketException (0x80004005): The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.
   at System.Net.WebSockets.ManagedWebSocket.KeepAlivePingHeartBeat()
...

Pokračujte ve čtení pro zpracování PONGů

Poznámka

V současné době WebSocket zpracovává pouze příchozí rámce, pokud je ReceiveAsync ve stavu čekání.

Důležitý

Pokud chcete použít časový limit Keep-Alive, je zásadní, že odpovědi PONG musí být okamžitě zpracovány. I když je vzdálený koncový bod naživu a správně odesílá odpověď PONG, ale WebSocket nezpracovává příchozí rámce, může mechanismus Keep-Alive vydat "falešně pozitivní" přerušení. K tomuto problému může dojít, pokud rám PONG není nikdy vyzvednut z přenosového proudu před vypršením časového limitu.

Aby se zabránilo přerušení dobrých připojení, doporučujeme uživatelům udržovat proces čtení ve stavu čekání na všech WebSocketech, které mají nakonfigurovaný časový limit Keep-Alive.