Skip to content
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

WIP: WebSocket Compression #48470

Closed
wants to merge 52 commits into from
Closed
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
2d72286
Initial API changes.
zlatanov Jan 10, 2021
3913164
Added inflater / deflater and retargeted the project for each of the …
zlatanov Feb 12, 2021
1a3f8cf
Websocket sending extracted into a dedicated class so we can implemen…
zlatanov Feb 12, 2021
e8499ff
Fixed a bug in the sender implementation where if a non persisted def…
zlatanov Feb 14, 2021
c4fdf98
Removed tests that were testing RCV1 flag (per message compression) w…
zlatanov Feb 15, 2021
224f88e
Added receiver encoders and implementation for per message compression.
zlatanov Feb 15, 2021
600d0fc
Removed unused namespace.
zlatanov Feb 15, 2021
0890bf8
Fixed decoder state reset to not be called in continuations.
zlatanov Feb 15, 2021
fb56654
Calling dispose for receiver.
zlatanov Feb 15, 2021
7b3b453
Removed unncecessary renting of memory when waiting for close message.
Feb 15, 2021
2584973
Removed unused method.
Feb 15, 2021
7597eb2
Removed unnecessary null check, because the object is never null.
Feb 15, 2021
a8cba93
Updated websockets tests csproj to reflect that now the websockets ha…
zlatanov Feb 16, 2021
e273bf3
Removed socket listener from a test where we are only testing how the…
zlatanov Feb 16, 2021
e26f044
Keep alive interval validated twice when using the existing CreateFro…
zlatanov Feb 16, 2021
80bf74d
The *ContextTakeover properties were interpreted incorrectly.
zlatanov Feb 16, 2021
518740e
Fixed a check where ! (not) was missing.
zlatanov Feb 16, 2021
d517063
Created basic tests for compression using the examples from RFC. Fixe…
zlatanov Feb 16, 2021
63df73a
Simplified the creation of the websocket stream used for testing.
zlatanov Feb 16, 2021
4316bd1
Added duplex end to end test that verifies that client and server com…
zlatanov Feb 16, 2021
398e3d8
Lighting up websocket compression in client.
zlatanov Feb 17, 2021
004c589
Updating the constraints of the MaxWindowBits properties - 8 is no lo…
zlatanov Feb 18, 2021
16353ea
Added more tests and fixed some bugs related to splitting messages in…
zlatanov Feb 18, 2021
ed4b069
Added cancellation token to tests to avoid cases where something is e…
zlatanov Feb 18, 2021
afd23ef
Fixed a bug where the receiver would happily ignore per message defla…
zlatanov Feb 18, 2021
f8d1f0e
Moving ZLibNative to Common so it can be cross compiled alongside Int…
zlatanov Feb 18, 2021
8872025
Removed custom ZLibNative and using the one which now resides in Common.
zlatanov Feb 18, 2021
a0bba92
Small refactoring to make inflate / deflate logic more clear.
zlatanov Feb 18, 2021
7c0c514
Parsing websocket deflate headers allocation free.
zlatanov Feb 19, 2021
148c842
Addressing code style and pr feedback.
zlatanov Feb 19, 2021
f276fbf
A few style improvements based on feedback.
zlatanov Feb 19, 2021
6511982
Stream is never null.
zlatanov Feb 19, 2021
cb38817
Fixed a bug in the deflater when incompressible message is being sent.
zlatanov Feb 19, 2021
a7c049a
Added a test that sends / receives uncompressed messages with differe…
zlatanov Feb 19, 2021
2f11043
Added more tests that test control messages. Fixed a bug in websocket…
zlatanov Feb 19, 2021
7303fe2
Fixed definitions in ref assemblies.
zlatanov Feb 20, 2021
89b7e09
Added links to RFC of each of the websocket deflate properties. Also …
zlatanov Feb 20, 2021
108da58
Addressing PR feedback - always using { } for single line blocks.
zlatanov Feb 20, 2021
e0f98c3
Moving string literals into constants.
zlatanov Feb 20, 2021
c995349
Consistent Common links.
zlatanov Feb 20, 2021
1c2c470
Fully qualified parameter type in ref assembly.
zlatanov Feb 20, 2021
172089a
Fixing failing tests.
zlatanov Feb 21, 2021
624405f
Added comments in code explaining why we use 9 instead of 8 window bi…
zlatanov Feb 21, 2021
383b4c9
Added struct layout auto for readonly struct.
zlatanov Feb 21, 2021
da7d050
Forgot to check _sentCloseFrame flag before trying to wait for server…
zlatanov Feb 21, 2021
8d951fc
Replaced RandomNumberGenerator with Random with seed to remove non-de…
zlatanov Feb 22, 2021
eafbd00
Little refactoring of receiver to reduce complexity and make it more …
zlatanov Feb 22, 2021
75a5c4f
Replaced unused CancellationTokenSource with a method.
zlatanov Feb 22, 2021
82deae3
Removed unneeded (duplicated) check.
zlatanov Feb 22, 2021
2f31feb
More tests.
zlatanov Feb 22, 2021
9ba2536
Created test for deflate options in client websocket.
zlatanov Feb 22, 2021
2099294
Fixed wrong test in http listener with websocket.
zlatanov Feb 22, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<TargetFrameworks>$(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser</TargetFrameworks>
Expand All @@ -25,8 +25,8 @@
<Compile Include="System\IO\Compression\DeflateZLib\DeflateStream.cs" />
<Compile Include="System\IO\Compression\DeflateZLib\Inflater.cs" />
<Compile Include="System\IO\Compression\DeflateZLib\ZLibException.cs" />
<Compile Include="System\IO\Compression\DeflateZLib\ZLibNative.cs" />
<Compile Include="System\IO\Compression\DeflateZLib\ZLibNative.ZStream.cs" />
<Compile Include="$(CommonPath)System\IO\Compression\ZLibNative.cs" LinkBase="System\IO\Compression" />
<Compile Include="$(CommonPath)System\IO\Compression\ZLibNative.ZStream.cs" LinkBase="System\IO\Compression" />
zlatanov marked this conversation as resolved.
Show resolved Hide resolved
<Compile Include="System\IO\Compression\CompressionLevel.cs" />
<Compile Include="System\IO\Compression\CompressionMode.cs" />
<Compile Include="System\IO\Compression\Crc32Helper.ZLib.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ internal ClientWebSocketOptions() { }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.TimeSpan KeepAliveInterval { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public WebSocketDeflateOptions? DeflateOptions { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Net.IWebProxy? Proxy { get { throw null; } set { } }
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")]
public System.Net.Security.RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get { throw null; } set { } }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<root>
<!--
Microsoft ResX Schema

<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema

Version 2.0

The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.

Example:

... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
Expand All @@ -25,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>

There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.

Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.

The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:

Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.

mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.

mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
Expand Down Expand Up @@ -193,8 +194,11 @@
</data>
<data name="net_WebSockets_Connection_Aborted" xml:space="preserve">
<value>Connection was aborted.</value>
</data>
</data>
<data name="net_WebSockets_Invalid_Binary_Type" xml:space="preserve">
<value>WebSocket binary type '{0}' not supported.</value>
</data>
</root>
</data>
<data name="net_WebSockets_WindowBitsNegotiationFailure" xml:space="preserve">
<value>The WebSocket failed to negotiate max {0} window bits. The client requested {1} but the server responded with {2}.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ public TimeSpan KeepAliveInterval
set => throw new PlatformNotSupportedException();
}

[UnsupportedOSPlatform("browser")]
public WebSocketDeflateOptions? DeflateOptions
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}

[UnsupportedOSPlatform("browser")]
public void SetBuffer(int receiveBufferSize, int sendBufferSize)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ public TimeSpan KeepAliveInterval
}
}

[UnsupportedOSPlatform("browser")]
public WebSocketDeflateOptions? DeflateOptions { get; set; }

internal int ReceiveBufferSize => _receiveBufferSize;
internal ArraySegment<byte>? Buffer => _buffer;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
Expand Down Expand Up @@ -175,6 +176,21 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli
}
}

// Because deflate options are negotiated we need a new object
WebSocketDeflateOptions? deflateOptions = null;

if (options.DeflateOptions is not null && response.Headers.TryGetValues(HttpKnownHeaderNames.SecWebSocketExtensions, out var extensions))
{
foreach (var extension in extensions)
{
if (extension.StartsWith("permessage-deflate"))
{
deflateOptions = ParseDeflateOptions(extension, options.DeflateOptions);
break;
}
}
}

if (response.Content is null)
{
throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely);
Expand All @@ -184,11 +200,13 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli
Stream connectedStream = response.Content.ReadAsStream();
Debug.Assert(connectedStream.CanWrite);
Debug.Assert(connectedStream.CanRead);
WebSocket = WebSocket.CreateFromStream(
connectedStream,
isServer: false,
subprotocol,
options.KeepAliveInterval);
WebSocket = WebSocket.CreateFromStream(connectedStream, new WebSocketCreationOptions
{
IsServer = false,
SubProtocol = subprotocol,
KeepAliveInterval = options.KeepAliveInterval,
DeflateOptions = deflateOptions,
});
}
catch (Exception exc)
{
Expand Down Expand Up @@ -218,6 +236,47 @@ public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, Cli
}
}

private static WebSocketDeflateOptions ParseDeflateOptions(string extensions, WebSocketDeflateOptions original)
{
var options = new WebSocketDeflateOptions();

foreach (var value in extensions.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
zlatanov marked this conversation as resolved.
Show resolved Hide resolved
{
if (value == "client_no_context_takeover")
{
options.ClientContextTakeover = false;
}
else if (value == "server_no_context_takeover")
{
options.ServerContextTakeover = false;
}
else if (value.StartsWith("client_max_window_bits="))
{
options.ClientMaxWindowBits = int.Parse(value.Substring("client_max_window_bits=".Length),
NumberFormatInfo.InvariantInfo);
}
else if (value.StartsWith("server_max_window_bits="))
{
options.ServerMaxWindowBits = int.Parse(value.Substring("server_max_window_bits=".Length),
NumberFormatInfo.InvariantInfo);
}
}

if (options.ClientMaxWindowBits > original.ClientMaxWindowBits)
{
throw new WebSocketException(string.Format(SR.net_WebSockets_WindowBitsNegotiationFailure,
"client", original.ClientMaxWindowBits, options.ClientMaxWindowBits));
}

if (options.ServerMaxWindowBits > original.ServerMaxWindowBits)
{
throw new WebSocketException(string.Format(SR.net_WebSockets_WindowBitsNegotiationFailure,
"server", original.ServerMaxWindowBits, options.ServerMaxWindowBits));
}

return options;
}

/// <summary>Adds the necessary headers for the web socket request.</summary>
/// <param name="request">The request to which the headers should be added.</param>
/// <param name="secKey">The generated security key to send in the Sec-WebSocket-Key header.</param>
Expand All @@ -232,6 +291,41 @@ private static void AddWebSocketHeaders(HttpRequestMessage request, string secKe
{
request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.SecWebSocketProtocol, string.Join(", ", options.RequestedSubProtocols));
}
if (options.DeflateOptions is not null)
{
request.Headers.TryAddWithoutValidation(HttpKnownHeaderNames.SecWebSocketExtensions, string.Join("; ", GetDeflateOptions(options.DeflateOptions)));

static IEnumerable<string> GetDeflateOptions(WebSocketDeflateOptions options)
{
yield return "permessage-deflate";

if (options.ClientMaxWindowBits != 15)
{
yield return "client_max_window_bits=" + options.ClientMaxWindowBits;
}
else
{
// Advertise that we support this option
yield return "client_max_window_bits";
}

if (options.ServerMaxWindowBits != 15)
{
yield return "server_max_window_bits=" + options.ServerMaxWindowBits;
}
else
{
// Advertise that we support this option
yield return "server_max_window_bits";
}

if (!options.ServerContextTakeover)
yield return "server_no_context_takeover";

if (!options.ClientContextTakeover)
yield return "client_no_context_takeover";
}
}
}

/// <summary>
Expand Down
18 changes: 18 additions & 0 deletions src/libraries/System.Net.WebSockets/ref/System.Net.WebSockets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ protected WebSocket() { }
public static System.Net.WebSockets.WebSocket CreateClientWebSocket(System.IO.Stream innerStream, string? subProtocol, int receiveBufferSize, int sendBufferSize, System.TimeSpan keepAliveInterval, bool useZeroMaskingKey, System.ArraySegment<byte> internalBuffer) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
public static System.Net.WebSockets.WebSocket CreateFromStream(System.IO.Stream stream, bool isServer, string? subProtocol, System.TimeSpan keepAliveInterval) { throw null; }
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
public static System.Net.WebSockets.WebSocket CreateFromStream(System.IO.Stream stream, WebSocketCreationOptions options) { throw null; }
public static System.ArraySegment<byte> CreateServerBuffer(int receiveBufferSize) { throw null; }
public abstract void Dispose();
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
Expand Down Expand Up @@ -133,4 +135,20 @@ public enum WebSocketState
Closed = 5,
Aborted = 6,
}

public sealed class WebSocketCreationOptions
{
public bool IsServer { get; set; }
public string? SubProtocol { get; set; }
public TimeSpan KeepAliveInterval { get; set; }
public WebSocketDeflateOptions? DeflateOptions { get; set; }
zlatanov marked this conversation as resolved.
Show resolved Hide resolved
}

public sealed class WebSocketDeflateOptions
{
public int ClientMaxWindowBits { get; set; }
public bool ClientContextTakeover { get; set; }
public int ServerMaxWindowBits { get; set; }
public bool ServerContextTakeover { get; set; }
}
}
Loading