Description
Edited by @CarnaViire
Justification
This API allows the users to identify and close idle WebSocket connections without having to rely on the TCP KeepAlive timeout, which can be way too big (e.g. ~15 min on Linux), and can be impossible or very hard to configure.
// see the original issue in the end of the post
API Proposal
namespace System.Net.WebSockets;
// used to create a base WebSocket (ManagedWebSocket) instance
public class WebSocketCreationOptions
{
// existing
//public TimeSpan KeepAliveInterval { get; set; } // default: infinite (off)
// new
public TimeSpan KeepAliveTimeout { get; set; } = Timeout.InfiniteTimeSpan;
// ...
}
// used to configure a ClientWebSocket instance
public class ClientWebSocketOptions
{
// existing
//[UnsupportedOSPlatform("browser")]
//public TimeSpan KeepAliveInterval { get; set; } // default: 30s
// new
[UnsupportedOSPlatform("browser")]
public TimeSpan KeepAliveTimeout { get; set; } = Timeout.InfiniteTimeSpan;
// ...
}
Usage
For ClientWebSocket:
var ws = new ClientWebSocket();
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
await ws.ConnectAsync(uri, cts.Token);
For ManagedWebSocket:
var options = new WebSocketCreationOptions()
{
KeepAliveInterval = WebSocket.DefaultKeepAliveInterval,
KeepAliveTimeout = TimeSpan.FromSeconds(10)
};
var ws = WebSocket.CreateFromStream(stream, options);
Notes on behavior
Timeout.InfiniteTimeSpan
or TimeSpan.Zero
would disable the timeout logic and fall back to the existing "unsolicited" Pong
keep-alive (per understanding in #35473 (comment) we cannot send Ping
s without waiting for the reply)
Any other positive timeout value would turn on Ping-Pong
logic; we will be sending Ping
s instead of Pong
s and track whether we've received Pong
s in time; if not, the connection is treated as stale and aborted. The behavior and implementation should be similar to HTTP/2 KeepAlivePingTimeout
.
Alternative Naming
We could name the new property KeepAlivePingTimeout. This will "break" it's similarity to KeepAliveInterval, but it might help highlight that Ping
s, not Pong
s, are being sent (if we care enough about this distinction).
namespace System.Net.WebSockets;
public class WebSocketCreationOptions
{
//public TimeSpan KeepAliveInterval { get; set; } // existing
public TimeSpan KeepAlivePingTimeout { get; set; } // new
// ...
}
public class ClientWebSocketOptions
{
//public TimeSpan KeepAliveInterval { get; set; } // existing
public TimeSpan KeepAlivePingTimeout { get; set; } // new
// ...
}
Existing API shape for other keep-alives
For comparison:
// HTTP/2 in runtime:
namespace System.Net.Http;
public class SocketsHttpHandler
{
public TimeSpan KeepAlivePingDelay { get; set; } // default: infinite (off)
public TimeSpan KeepAlivePingTimeout { get; set; } // default: 20s (N/A when delay=inf)
public HttpKeepAlivePingPolicy KeepAlivePingPolicy { get; set; } // default: Always
// ...
}
// HTTP/2 in Kestrel:
namespace Microsoft.AspNetCore.Server.Kestrel.Core;
public class Http2Limits
{
public TimeSpan KeepAlivePingDelay { get; set; } // default: infinite (off)
public TimeSpan KeepAlivePingTimeout { get; set; } // default: 20s (N/A when delay=inf)
// ...
}
// Sockets in runtime:
namespace System.Net.Sockets;
public enum SocketOptionName
{
TcpKeepAliveInterval, // between probe retries, if no answer received
TcpKeepAliveRetryCount, // probe retries before deemed idle, if no answer received
TcpKeepAliveTime, // before the first probe, after the last received byte
// ...
}
Original issue by @bjornen77
I think that adding a WebSocketOptions.Timeout property would be a good thing.
Today, the ClientWebSocket is closed when the TCP Retransmission times out(if the connection to the server drops for some reason). This timeout seems to be different on Windows and Linux. By default, on Windows it takes about 21 seconds and on Linux it takes about 15 minutes before the WebSocket is closed. It would be good if we could have a separate Timeout setting, so that we get the same behavior regardless of the platform. And also that this could be configured by the application without having to modify the TCP Timeout settings in the OS... The websocket should still be disconnected if the OS TCP Timeout is reached.
The ClientWebSocket today only sends "Pong" to the server, and therefore it does not wait for a "Pong" response to arrive. I think that the ClientWebSocket shall send a "Ping" instead, and then verify that a "Pong" is received within the Timeout set. If not received, the connection shall be closed. Or at least it should expose the "received "Pong" message in the WebSocketReceiveResult.MessageType. It would be good if an received "Ping" message from a server is also exposed. Then the application could use these Ping/Pong message types to implement its own timeout logic.