共用方式為


.NET 中的 WebSocket 支援

WebSocket 通訊協定可啟用客戶端與遠端主機之間的雙向通訊。 System.Net.WebSockets.ClientWebSocket 會展示通過開啟握手來建立 WebSocket 連線的功能,這是由 ConnectAsync 方法創建並傳送的。

HTTP/1.1 和 HTTP/2 WebSockets 的差異

透過 HTTP/1.1 的 WebSockets 使用單一 TCP 連線,因此由整個連線的標頭進行管理。有關更多資訊,請參閱 RFC 6455。 請考慮下列如何透過 HTTP/1.1 建立 WebSocket 的範例:

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

由於 HTTP/2 的多任務處理本質,必須採用不同的方法。 WebSocket 是針對每個資料流建立,如需詳細資訊,請參閱 RFC 8441。 使用 HTTP/2 時,可以透過一個連線同時應用於多個 WebSocket 串流和一般 HTTP 串流,並將 HTTP/2 的高效網路利用延伸至 WebSocket。 有一個特殊的多載 ConnectAsync(Uri, HttpMessageInvoker, CancellationToken),可接受 HttpMessageInvoker,以允許重複使用現有的集區連線:

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

設定 HTTP 版本和原則

根據預設,ClientWebSocket 會使用 HTTP/1.1 傳送開啟交握,並允許降級。 在 .NET 7 中,Web 套接字已可以透過 HTTP/2 使用。 在呼叫 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);

不相容的選項

ClientWebSocket 有一些屬性 System.Net.WebSockets.ClientWebSocketOptions,使用者可以在連線建立之前進行設定。 不過,當提供 HttpMessageInvoker 時,它也具有這些屬性。 為了避免模棱兩可,在此情況下,應該在 HttpMessageInvoker上設定屬性,而且 ClientWebSocketOptions 應該有預設值。 否則,如果變更 ClientWebSocketOptionsConnectAsync 的超載將會引發一個 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);

壓縮

WebSocket 通訊協議支援 RFC 7692 中所定義的每個訊息壓縮。 它是由 System.Net.WebSockets.ClientWebSocketOptions.DangerousDeflateOptions所控制。 當存在時,選項會在交握階段傳送至伺服器。 如果伺服器支援每則訊息壓縮並接受這些選項,則預設會自動啟用壓縮並建立 ClientWebSocket 實例來處理所有訊息。

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

重要

在使用壓縮之前,請注意啟用它會使應用程式受限於 CRIME/BREACH 類型的攻擊,如需詳細資訊,請參閱 CRIMEBREACH。 強烈建議在傳送包含秘密的數據時關閉壓縮,方法是指定這類訊息的 DisableCompression 旗標。

Keep-Alive 策略

.NET 8 和更早版本,唯一可用的 Keep-Alive 策略是 無需請求的 PONG。 此策略足以防止基礎 TCP 連線閑置。然而,當遠端主機變得沒有回應時(例如,遠端伺服器當機),偵測這類情況的唯一方式是透過 Unsolicited PONG 依賴 TCP 逾時。

.NET 9 引進了備受期待的 PING/PONG Keep-Alive 策略,以新的 KeepAliveInterval 設定補充現有的 KeepAliveTimeout 設定。 從 .NET 9 開始,會選取 Keep-Alive 策略,如下所示:

  1. 如果 ,Keep-Alive OFF則為 。
    • KeepAliveIntervalTimeSpan.ZeroTimeout.InfiniteTimeSpan
  2. 未經請求的 PONG,如果
    • KeepAliveInterval 是正有限 TimeSpan、 -AND-
    • KeepAliveTimeoutTimeSpan.ZeroTimeout.InfiniteTimeSpan
  3. PING/PONG,如果
    • KeepAliveInterval 是正有限 TimeSpan、 -AND-
    • KeepAliveTimeout 是正有限 TimeSpan

默認 KeepAliveTimeout 值為 Timeout.InfiniteTimeSpan,因此 .NET 版本之間的預設 Keep-Alive 行為會保持一致。

如果您使用 ClientWebSocket,則預設 ClientWebSocketOptions.KeepAliveInterval 值為 WebSocket.DefaultKeepAliveInterval (通常是 30 秒)。 也就是說,根據預設設定,ClientWebSocket 的 Keep-Alive 是開啟狀態,而 Unsolicited PONG 是默認策略。

如果您想要切換至 PING/PONG 策略,覆寫 ClientWebSocketOptions.KeepAliveTimeout 就夠了:

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

對於基本 WebSocket,預設 Keep-Alive 為 OFF。 如果您要使用 PING/PONG 策略,必須設定 WebSocketCreationOptions.KeepAliveIntervalWebSocketCreationOptions.KeepAliveTimeout

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

如果使用「未請求的 PONG 策略」,PONG 幀會作為單向心跳信號。 無論遠端端點是否進行通訊,都會以 KeepAliveInterval 的間隔定期傳送。

如果 PING/PONG 策略處於作用中狀態,則從遠端端端點 KeepAliveInterval 之後,就會在經過 時間之後傳送 PING 畫面格。 每個 PING 框架都包含一個整數標記,以與預期的 PONG 回應配對。 如果在經過 KeepAliveTimeout 之後未收到 PONG 回應,遠端端端點會被視為沒有回應,且 WebSocket 聯機會自動中止。

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

如果逾時時間到,未完成的 ReceiveAsync 會拋出 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()
...

繼續閱讀以了解如何處理PONG

注意

目前,WebSocket 僅在有 ReceiveAsync 擱置時處理傳入幀。

重要

如果您想要使用 Keep-Alive 逾時,,PONG 回應會 立即處理。 即使遠端端點保持運作並正確地傳送 PONG 回應,但 WebSocket 未處理傳入數據幀,Keep-Alive 機制仍可能會發出誤判的中止。 如果在超時前未能從傳輸流中提取 PONG 帧,則會發生此問題。

為了避免撕毀良好的連線,建議使用者在已設定 Keep-Alive 逾時的所有 WebSocket 上維持擱置讀取。