diff --git a/docs/fundamentals/networking/websockets.md b/docs/fundamentals/networking/websockets.md index ab97617044680..598e23f168a12 100644 --- a/docs/fundamentals/networking/websockets.md +++ b/docs/fundamentals/networking/websockets.md @@ -99,3 +99,76 @@ using ClientWebSocket ws = new() > [!IMPORTANT] > Before using compression, please be aware that enabling it makes the application subject to CRIME/BREACH type of attacks, for more information, see [CRIME](https://en.wikipedia.org/wiki/CRIME) and [BREACH](https://en.wikipedia.org/wiki/BREACH). It is strongly advised to turn off compression when sending data containing secrets by specifying the `DisableCompression` flag for such messages. + +## Keep-Alive strategies + +On **.NET 8** and earlier, the only available Keep-Alive strategy is _Unsolicited PONG_. This strategy is enough to keep the underlying TCP connection from idling out. However, in a case when a remote host becomes unresponsive (for example, a remote server crashes), the only way to detect such situations with Unsolicited PONG is to depend on the TCP timeout. + +**.NET 9** introduced the long-desired _PING/PONG_ Keep-Alive strategy, complementing the existing `KeepAliveInterval` setting with the new `KeepAliveTimeout` setting. Starting with .NET 9, the Keep-Alive strategy is selected as follows: + +1. Keep-Alive is **OFF**, if + - `KeepAliveInterval` is `TimeSpan.Zero` or `Timeout.InfiniteTimeSpan` +2. **Unsolicited PONG**, if + - `KeepAliveInterval` is a positive finite `TimeSpan`, -AND- + - `KeepAliveTimeout` is `TimeSpan.Zero` or `Timeout.InfiniteTimeSpan` +3. **PING/PONG**, if + - `KeepAliveInterval` is a positive finite `TimeSpan`, -AND- + - `KeepAliveTimeout` is a positive finite `TimeSpan` + +The default `KeepAliveTimeout` value is `Timeout.InfiniteTimeSpan`, so the default Keep-Alive behavior remains consistent between .NET versions. + +If you use `ClientWebSocket`, the default value is (typically 30 seconds). That means, `ClientWebSocket` has the Keep-Alive ON by default, with Unsolicited PONG as the default strategy. + +If you want to switch to the PING/PONG strategy, overriding is enough: + +```csharp +var ws = new ClientWebSocket(); +ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(20); +await ws.ConnectAsync(uri, cts.Token); +``` + +For a basic `WebSocket`, the Keep-Alive is OFF by default. If you want to use the PING/PONG strategy, both and need to be set: + +```csharp +var options = new WebSocketCreationOptions() +{ + KeepAliveInterval = WebSocket.DefaultKeepAliveInterval, + KeepAliveTimeout = TimeSpan.FromSeconds(20) +}; +var ws = WebSocket.CreateFromStream(stream, options); +``` + +If the Unsolicited PONG strategy is used, PONG frames are used as a unidirectional heartbeat. They sent regularly with `KeepAliveInterval` intervals, regardless whether the remote endpoint is communicating or not. + +In case the PING/PONG strategy is active, a PING frame is sent after `KeepAliveInterval` time passed since the _last communication_ from the remote endpoint. Each PING frame contains an integer token to pair with the expected PONG response. If no PONG response arrived after `KeepAliveTimeout` elapsed, the remote endpoint is deemed unresponsive, and the WebSocket connection is automatically aborted. + +```csharp +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); +``` + +If the timeout elapses, an outstanding `ReceiveAsync` throws an `OperationCanceledException`: + +```txt +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() +... +``` + +### Keep Reading To Process PONGs + +> [!NOTE] +> Currently, `WebSocket` ONLY processes incoming frames while there's a `ReceiveAsync` pending. + +> [!IMPORTANT] +> If you want to use Keep-Alive Timeout, it's _crucial_ that PONG responses are _promptly processed_. Even if the remote endpoint is alive and properly sends the PONG response, but the `WebSocket` isn't processing the incoming frames, the Keep-Alive mechanism can issue a "false-positive" Abort. This problem can happen if the PONG frame is never picked up from the transport stream before the timeout elapsed. + +To avoid tearing up good connections, users are advised to maintain a pending read on all WebSockets that have Keep-Alive Timeout configured.