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
應該有預設值。 否則,如果變更 ClientWebSocketOptions
,ConnectAsync
的超載將會引發一個 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 類型的攻擊,如需詳細資訊,請參閱 CRIME 和 BREACH。 強烈建議在傳送包含秘密的數據時關閉壓縮,方法是指定這類訊息的 DisableCompression
旗標。
Keep-Alive 策略
.NET 8 和更早版本,唯一可用的 Keep-Alive 策略是 無需請求的 PONG。 此策略足以防止基礎 TCP 連線閑置。然而,當遠端主機變得沒有回應時(例如,遠端伺服器當機),偵測這類情況的唯一方式是透過 Unsolicited PONG 依賴 TCP 逾時。
.NET 9 引進了備受期待的 PING/PONG Keep-Alive 策略,以新的 KeepAliveInterval
設定補充現有的 KeepAliveTimeout
設定。 從 .NET 9 開始,會選取 Keep-Alive 策略,如下所示:
- 如果 ,Keep-Alive OFF則為 。
-
KeepAliveInterval
是TimeSpan.Zero
或Timeout.InfiniteTimeSpan
-
-
未經請求的 PONG,如果
-
KeepAliveInterval
是正有限TimeSpan
、 -AND- -
KeepAliveTimeout
是TimeSpan.Zero
或Timeout.InfiniteTimeSpan
-
-
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.KeepAliveInterval 和 WebSocketCreationOptions.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 上維持擱置讀取。