Condividi tramite


Supporto di WebSocket in .NET

Il protocollo WebSocket consente la comunicazione bidirezionale tra un client e un host remoto. Il System.Net.WebSockets.ClientWebSocket consente di stabilire una connessione WebSocket tramite un handshake di apertura; questa viene creata e inviata dal metodo ConnectAsync.

Differenze nei WebSocket HTTP/1.1 e HTTP/2

WebSockets su HTTP/1.1 usa una singola connessione TCP, pertanto viene gestita da intestazioni a livello di connessione. Per altre informazioni, vedere RFC 6455. Si consideri l'esempio seguente di come stabilire WebSocket su 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);

Un approccio diverso deve essere adottato con HTTP/2 a causa della sua natura di multiplexing. I WebSocket vengono stabiliti per ogni flusso. Per altre informazioni, vedere RFC 8441. Con HTTP/2 è possibile usare una connessione per più flussi web socket insieme ai normali flussi HTTP ed estendere l'uso più efficiente della rete http/2 a WebSocket. Esiste un overload speciale di ConnectAsync(Uri, HttpMessageInvoker, CancellationToken) che accetta un HttpMessageInvoker per consentire il riutilizzo delle connessioni pooled esistenti.

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

Configurare la versione e i criteri HTTP

Per impostazione predefinita, ClientWebSocket usa HTTP/1.1 per inviare un handshake di apertura e consente il downgrade. In .NET 7 i web socket su HTTP/2 sono disponibili. Può essere modificato prima di chiamare ConnectAsync:

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);

Opzioni incompatibili

ClientWebSocket dispone di proprietà System.Net.WebSockets.ClientWebSocketOptions che l'utente può configurare prima di stabilire la connessione. Tuttavia, quando viene fornito HttpMessageInvoker, include anche queste proprietà. Per evitare ambiguità, in tal caso, le proprietà devono essere impostate su HttpMessageInvokere ClientWebSocketOptions devono avere valori predefiniti. In caso contrario, se ClientWebSocketOptions vengono modificati, l'overload di ConnectAsync genererà un 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);

Compressione

Il protocollo WebSocket supporta la deflazione per messaggio come definito in RFC 7692. È controllato da System.Net.WebSockets.ClientWebSocketOptions.DangerousDeflateOptions. Quando presente, le opzioni vengono inviate al server durante la fase di handshake. Se il server supporta la deflazione per-messaggio e le opzioni vengono accettate, l'istanza di ClientWebSocket verrà creata con la compressione abilitata per impostazione predefinita per tutti i messaggi.

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

Importante

Prima di usare la compressione, tenere presente che l'abilitazione rende l'applicazione soggetta al tipo di attacco CRIME/BREACH. Per altre informazioni, vedere CRIME e BREACH. È consigliabile disattivare la compressione quando si inviano dati contenenti segreti specificando il flag DisableCompression per tali messaggi.

strategie Keep-Alive

In .NET 8 e versioni precedenti, l'unica strategia disponibile di Keep-Alive è PONG non richiesto. Questa strategia è sufficiente per evitare l'inattività della connessione TCP sottostante. Tuttavia, in un caso in cui un host remoto non risponde (ad esempio, un server remoto si arresta in modo anomalo), l'unico modo per rilevare tali situazioni con PONG non richiesto dipende dal timeout TCP.

.NET 9 ha introdotto la strategia di Keep-Alive PING/PONG desiderata, completando l'impostazione di KeepAliveInterval esistente con la nuova impostazione di KeepAliveTimeout. A partire da .NET 9, la strategia di Keep-Alive viene selezionata come segue:

  1. Keep-Alive è OFF, se
    • KeepAliveInterval è TimeSpan.Zero o Timeout.InfiniteTimeSpan
  2. PONG non richiesto, se
    • KeepAliveInterval è un TimeSpanfinito positivo , -AND-
    • KeepAliveTimeout è TimeSpan.Zero o Timeout.InfiniteTimeSpan
  3. PING/PONG, se
    • KeepAliveInterval è un TimeSpanfinito positivo , -AND-
    • KeepAliveTimeout è un TimeSpan finito positivo

Il valore KeepAliveTimeout predefinito è Timeout.InfiniteTimeSpan, pertanto il comportamento predefinito Keep-Alive rimane coerente tra le versioni di .NET.

Se si usa ClientWebSocket, il valore ClientWebSocketOptions.KeepAliveInterval predefinito è WebSocket.DefaultKeepAliveInterval (in genere 30 secondi). Ciò significa che ClientWebSocket ha il Keep-Alive ON per impostazione predefinita, con PONG non richiesto come strategia predefinita.

Se vuoi passare alla strategia PING/PONG, è sufficiente sovrascrivere ClientWebSocketOptions.KeepAliveTimeout:

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

Per un WebSocketdi base, il Keep-Alive è DISATTIVATo per impostazione predefinita. Se si desidera utilizzare la strategia PING/PONG, entrambi WebSocketCreationOptions.KeepAliveInterval e WebSocketCreationOptions.KeepAliveTimeout devono essere impostati.

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

Se viene utilizzata la strategia PONG non richiesta, i fotogrammi PONG vengono utilizzati come heartbeat unidirezionale. Vengono inviati regolarmente con intervalli di KeepAliveInterval, indipendentemente dal fatto che l'endpoint remoto comunichi o meno.

Nel caso in cui la strategia PING/PONG sia attiva, viene inviato un frame PING dopo che è trascorso il tempo di KeepAliveInterval dalla ultima comunicazione dall'endpoint remoto. Ogni fotogramma PING contiene un token integer da associare alla risposta PONG prevista. Se non è arrivata alcuna risposta PONG dopo KeepAliveTimeout trascorso, l'endpoint remoto viene considerato non risponde e la connessione WebSocket viene interrotta automaticamente.

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);

Se il timeout è trascorso, un ReceiveAsync in sospeso genera un 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()
...

Continuare la lettura per elaborare i PONG

Nota

Attualmente, WebSocket ELABORA SOLO i fotogrammi in ingresso mentre è presente un ReceiveAsync in sospeso.

Importante

Se vuoi usare Keep-Alive Timeout, è fondamentale che le risposte PONG siano elaborate tempestivamente. Anche se l'endpoint remoto è attivo e invia correttamente la risposta PONG, se il WebSocket non elabora i pacchetti in ingresso, il meccanismo Keep-Alive può generare un'interruzione per "falso positivo". Questo problema può verificarsi se il telaio PONG non viene mai prelevato dal flusso di trasporto prima che il timeout sia trascorso.

Per evitare di strappare connessioni valide, è consigliabile che gli utenti mantengano una lettura in sospeso in tutti i WebSocket con configurati il timeout Keep-Alive.