Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
The WebSocket protocol enables two-way communication between a client and a remote host. The System.Net.WebSockets.ClientWebSocket exposes the ability to establish WebSocket connection via an opening handshake, it is created and sent by the ConnectAsync
method.
Differences in HTTP/1.1 and HTTP/2 WebSockets
WebSockets over HTTP/1.1 uses a single TCP connection, therefore it is managed by connection-wide headers, for more information, see RFC 6455. Consider the following example of how to establish WebSocket over HTTP/1.1:
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);
A different approach must be taken with HTTP/2 due to its multiplexing nature. WebSockets are established per stream, for more information, see RFC 8441. With HTTP/2 it is possible to use one connection for multiple web socket streams together with ordinary HTTP streams and extend HTTP/2's more efficient use of the network to WebSockets. There is a special overload of ConnectAsync(Uri, HttpMessageInvoker, CancellationToken) which accepts an HttpMessageInvoker to allow reusing existing pooled connections:
using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);
Set up HTTP version and policy
By default, ClientWebSocket
uses HTTP/1.1 to send an opening handshake and allows downgrade. In .NET 7 web sockets over HTTP/2 are available. It can be changed before calling 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);
Incompatible options
ClientWebSocket
has properties System.Net.WebSockets.ClientWebSocketOptions that the user can set up before the connection is established. However, when HttpMessageInvoker
is provided, it also has these properties. To avoid ambiguity, in that case, properties should be set on HttpMessageInvoker
, and ClientWebSocketOptions
should have default values. Otherwise, if ClientWebSocketOptions
are changed, overload of ConnectAsync
will throw an 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);
Compression
The WebSocket protocol supports per-message deflate as defined in RFC 7692. It is controlled by System.Net.WebSockets.ClientWebSocketOptions.DangerousDeflateOptions. When present, the options are sent to the server during the handshake phase. If the server supports per-message-deflate and the options are accepted, the ClientWebSocket
instance will be created with compression enabled by default for all messages.
using ClientWebSocket ws = new()
{
Options =
{
DangerousDeflateOptions = new WebSocketDeflateOptions()
{
ClientMaxWindowBits = 10,
ServerMaxWindowBits = 10
}
}
};
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 and 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:
- Keep-Alive is OFF, if
KeepAliveInterval
isTimeSpan.Zero
orTimeout.InfiniteTimeSpan
- Unsolicited PONG, if
KeepAliveInterval
is a positive finiteTimeSpan
, -AND-KeepAliveTimeout
isTimeSpan.Zero
orTimeout.InfiniteTimeSpan
- PING/PONG, if
KeepAliveInterval
is a positive finiteTimeSpan
, -AND-KeepAliveTimeout
is a positive finiteTimeSpan
The default KeepAliveTimeout
value is Timeout.InfiniteTimeSpan
, so the default Keep-Alive behavior remains consistent between .NET versions.
If you use ClientWebSocket
, the default ClientWebSocketOptions.KeepAliveInterval value is WebSocket.DefaultKeepAliveInterval (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 ClientWebSocketOptions.KeepAliveTimeout is enough:
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 WebSocketCreationOptions.KeepAliveInterval and WebSocketCreationOptions.KeepAliveTimeout need to be set:
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.
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
:
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.