Description
Adds support for RFC 7692 "compression extensions for websocket".
API Proposal
// new
public sealed class WebSocketCreationOptions
{
// taken from existing WebSocket.CreateFromStream params.
public bool IsServer { get; set; }
public string? SubProtocol { get; set; }
public TimeSpan KeepAliveInterval { get; set; }
// new
// turns the feature on.
// consider: instead of multiple below properties on options class,
// make a new DeflateCompressionOptions class and if null it is turned off.
public bool UseDeflateCompression { get; set; } = false;
// configures desired client window size (larger window = better compression, more memory usage)
public int ClientMaxDeflateWindowBits { get; set; } = 15;
// controls if the window is re-used between subsequent messages,
// or if each message starts with a blank window.
// memory vs compression tradeoff again.
// might have a better name, "ClientPersistDeflateContext" etc.
public bool ClientDeflateContextTakeover { get; set; } = true;
public int ServerMaxDeflateWindowBits { get; set; } = 15;
public bool ServerDeflateContextTakeover { get; set; } = true;
}
// existing
// this is set in ClientWebSocket.Options before calling Connect() against a URL.
public sealed class ClientWebSocketOptions
{
// new, same as above.
public bool UseDeflateCompression { get; set; } = false;
public int ClientMaxDeflateWindowBits { get; set; } = 15;
public bool ClientDeflateContextTakeover { get; set; } = true;
public int ServerMaxDeflateWindowBits { get; set; } = 15;
public bool ServerDeflateContextTakeover { get; set; } = true;
}
// existing
public abstract class WebSocket
{
// existing
public static WebSocket CreateFromStream(Stream stream, bool isServer, string? subProtocol, TimeSpan keepAliveInterval);
// new
public static WebSocket CreateFromStream(Stream stream, WebSocketCreationOptions options);
}
Original Request
AB#1118550
See discussion here #20004.
At the moment the WebSocket doesn't support per-message deflate (see https://tools.ietf.org/html/rfc7692#section-7). Adding support for it in the BCL will mean that people (myself including) will no longer resort to implementing custom WebSockets in order to use it.
Proposed API
/// <summary>
/// Options to enable per-message deflate compression for <seealso cref="WebSocket" />.
/// </summary>
public sealed class WebSocketCompressionOptions
{
/// <summary>
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the client context.
/// Must be a value between 8 and 15 or -1 indicating no preferences. The default is -1.
/// </summary>
public int ClientMaxWindowBits { get; set; } = -1;
/// <summary>
/// When true, the client informs the peer server of a hint that even if the server doesn't include the
/// "client_no_context_takeover" extension parameter in the corresponding
/// extension negotiation response to the offer, the client is not going to use context takeover. The default is false.
/// </summary>
public bool ClientNoContextTakeover { get; set; }
/// <summary>
/// This parameter indicates the base-2 logarithm of the LZ77 sliding window size of the server context.
/// Must be a value between 8 and 15 or -1 indicating no preferences. The default is -1.
/// </summary>
public int ServerMaxWindowBits { get; set; } = -1;
/// <summary>
/// When true, the client prevents the peer server from using context takeover. If the peer server doesn't use context
/// takeover, the client doesn't need to reserve memory to retain the LZ77 sliding window between messages. The default is false.
/// </summary>
public bool ServerNoContextTakeover { get; set; }
}
public sealed class ClientWebSocketOptions
{
/// <summary>
/// Instructs the <seealso cref="ClientWebSocket" /> to try and negotiate per-message compression.
/// </summary>
public WebSocketCompressionOptions Compression { get; set; }
}
public enum WebSocketOutputCompression
{
/// <summary>
/// Enables output compression if the underlying <seealso cref="WebSocket" /> supports it.
/// </summary>
Default,
/// <summary>
/// Suppresses output compression for the next message.
/// </summary>
SuppressOne,
/// <summary>
/// Suppresses output compression.
/// </summary>
Suppress
}
public abstract class WebSocket
{
/// <summary>
/// Instructs the socket to compress (or not to) the messages being sent when
/// compression has been successfully negotiated.
/// </summary>
public WebSocketOutputCompression OutputCompression { get; set; }
public static WebSocket CreateFromStream(Stream stream, bool isServer, string subProtocol, TimeSpan keepAliveInterval, WebSocketCompressionOptions compression);
}
Rationale and Usage
The main drive behind the API is that we should not introduce any breaking changes. WebSockets already built will work as is. This is why I suggest we add new CreateFromStream method with WebSocketCompressionOptions
parameter instead of adding it to the existing one.
There are a few options built in the WebSocket compression protocol that are considered advance use - controlling the size of the LZ77 sliding window, context takeover. We could easily hide them and choose reasonable defaults, but I think there are good use cases for them and as such we should expose them. See this blog post for good example of their usage: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/.
The usage of the WebSocket would not change at all. By default a WebSocket created with compression options would compress all messages if the connection on the other end supports it. We introduce OutputCompression
property to allow explicit opt out. The property has no effect if compression is not supported for the current connection.
Here is example of how we would disable compression for specific messages:
var socket = GetWebSocket();
// Disable compression for the next message only
socket.OutputCompression = WebSocketCompressionOptions.SuppressOne;
await socket.SendAsync(...);
Here is example for WebSocketClient:
var client = new WebSocketClient();
// Indicate that we want compression if server supports it
client.Options.Compression = new WebSocketCompressionOptions();
await client.ConnectAsync(...);
// Same as before. If compression is enabled it will be performed automatically.
client.SendAsync(...);
// If we need to explicitly disable compression for a time or specific messages
client.OutputCompression = WebSocketOutputCompression.Suppress;
// Send one or more messages
// ...
// Restore default compression state
client.OutputCompression = WebSocketOutputCompression.Default;
Additional work will be required in AspNetCore repository and more specifically WebSocketMiddleware to light up the compression feature.