-
Notifications
You must be signed in to change notification settings - Fork 6k
WebSocket Keep-Alive #44611
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WebSocket Keep-Alive #44611
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 <xref:System.Net.WebSockets.ClientWebSocketOptions.KeepAliveInterval?displayProperty=nameWithType> value is <xref:System.Net.WebSockets.WebSocket.DefaultKeepAliveInterval?displayProperty=nameWithType> (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 <xref:System.Net.WebSockets.ClientWebSocketOptions.KeepAliveTimeout?displayProperty=nameWithType> is enough: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good suggest that this is an implication of the behavior described before.
Suggested change
|
||||||
|
||||||
```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 <xref:System.Net.WebSockets.WebSocketCreationOptions.KeepAliveInterval?displayProperty=nameWithType> and <xref:System.Net.WebSockets.WebSocketCreationOptions.KeepAliveTimeout?displayProperty=nameWithType> 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`: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
```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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
> [!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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
To avoid tearing up good connections, users are advised to maintain a pending read on all WebSockets that have Keep-Alive Timeout configured. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to use
<xref>
here instead of code blocks.