Runtime: Implement WebSocket Compression

Created on 24 Jan 2017  路  28Comments  路  Source: dotnet/runtime

https://tools.ietf.org/html/rfc7692
https://github.com/aspnet/WebSockets/issues/19

Implementation notes:
The System.Net.WebSockets.WebSocket.SendAsync API is poorly suited to compression/extension. Compression is a per-message flag set on the first frame using the RSV1 bit. SendAsync sends frames and only takes the buffer, message type, an EndOfMessage bool, and a cancellationToken. There's no simple way to indicate if the data is or should be compressed / transformed. Possible solutions:

  • Enable compression for all messages, don't provide a per message option
  • Add a new overload of SendAsync with a compression / RSV1 flag. (Requires changing the core WebSocket abstraction)
  • Add a property to WebSocket bool CompressOutput that can be toggled between messages (Requires changing the core WebSocket abstraction)
  • Overload WebSocketMessageType with CompressedBinary and CompressedText. As an enum we may be able to temporarily pass data through here by casting non-standard values. The enum values already do not map directly to the WebSocket frame type values.

ReceiveAsync is easier, it could:

  • Decompress the content internally
  • Add a flag to WebSocketReceiveResult indicating if the content was compressed.
  • Use the same WebSocketMessageType overloads from above indicating if the content is compressed (but only if it does not decompress it internally, don't want to confuse consumers).

The compression extension could not be correctly implemented as wrapper over the current WebSocket API due to the framing requirement. Compression either needs to be built in or the API needs to expose additional frame fields (e.g. RSV1).

api-suggestion area-System.Net

Most helpful comment

@karelz I would like to volunteer.

I've put this off long enough, and now that 3.0 has been released I would like to find time to do it. I know the internals of the WebSocket well, and what needs to be done to efficiently support compression.

All 28 comments

Next step: We need formal API proposal

Any update on this?

Also interested in this. I know that "+1" type comments are generally unhelpful, but remarks on other threads by team members have suggested they are helpful in this cause because this issue is being ignored due to a lack of interest expressed by the community.

I would suggest that people are simply not saying anything because there are third-party libs that implement this. I would also suggest that waiting for community input is a bad way to measure interest because of that. If HttpClient didn't support compression and MS was dragging their feet on it, the community would just make their own compliant client.

The community has done this already, and this effort predates Microsoft's. Have a gander on Nuget. There's over a million downloads of community driven websocket libraries that all address missing features in each other. The community interest has already come and the ship sailed, Microsoft can't gauge interest based on this.

Thanks for additional comments. Upvotes on top post are best way to help us prioritize.
Given the amount of work we have across the board on Networking team (incl. HTTP/2 support and some high-impactful missing APIs in the platform), it is unlikely we will be able to get to this in .NET Core 3.0 timeframe (hence Future milestone).
If there are community alternartives (a nuget package), that is great and relieves a bit pressure - we can focus on things which cannot be easily worked around by 3rd party nuget packages and need to be done in the platform itself.

@karelz Ok that's cool. I agree that if there's a good community alternative, then it can be relied on for now anyway instead. However I'm not sure how this issue applies across the board. Kestrel implements its own websocket, for example, so you're stuck with whatever that implementation has. Anyway thanks for feedback. Is it possible for the community to work on this, or do you think it's something the core team will want control of initially?

All of the ASP.NET Core servers expose the opaque upgrade so it's possible to plug in alternate WebSocket implementations. WebSockets isn't even part of the servers, it's shipped as middleware.
https://github.com/aspnet/WebSockets/blob/64e736173c294ac22028995ee47d56b21aaa3831/src/Microsoft.AspNetCore.WebSockets/WebSocketMiddleware.cs#L57-L64

Thanks bruv

Community can work on almost anything in our repos. It just depends on how deeply invested the contributors are and how much time they are willing to put into it.
For new APIs, an API review has to happen when there is solid proposal - that is something the "core" team has to review & approve. I can help expedite that if there is serious interest from community members to push it forward.

Yes it鈥檚 absolutely possible. @anurse implemented an alternative web socket API a while back before we switched to the corefx version.

Any suggestions for good community alternatives?

@karelz @Tratcher If the task is still up for grabs, I would like to express interest in contributing here. I have already implemented this in a custom library of mine - https://github.com/zlatanov/websockets but my implementation differs from the suggested API.

What I did is introducing the ability to create a send buffer that implements IBufferWriter - see https://github.com/zlatanov/websockets/blob/4d0c4e43e278d3bc6c0e185bb09b828160af2db8/src/Maverick.WebSockets/WebSocketSendBuffer.cs. Based on the websocket capabilities (established during handshake) the send buffer automatically enables compression. It uses array pooling and the last segment being written to is always the uncompressed one. When the segment is full then a new compressed segment is taken from the array pool and the current one is deflated onto it.

Since the IBufferWriter interface is synchronous the disadvantage of this technique is that the message must be fully compressed before we start sending it. In my experience though this is not always a problem because messages sent through websockets are never too big. In a real world example I've had uncompressed text message of 1MB become 40KB.

Currently using my implementation the API usage would look like this:

c# // We would also have socket.TryCreateBinaryBuffer( out var buffer ) if ( socket.TryCreateTextBuffer( out var buffer ) ) // This would fail if socket isn't connected. { // Buffer implements IBufferWriter<Byte> so any API that supports writing // to it will work. (e.g. the new Json implementation ) // .... // When we're done writing await socket.SendAsync( buffer ); // Sending the buffer will consume it and release any resources held // so it's illegal to use after send }

What do you think?

When messages can be of an arbitrary size needing to compress them all in memory isn't great. We're working through a lot of similar issues in AspNetCore where too much needs to happen before we can flush data and it impacts scalability.

I understand. So what if the API looked like this:

c# Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, bool compress, CancellationToken cancellationToken);
In this case though we need to be careful when we really do send. For example the compressed version of the payload (when endOfMessage is false) is too small, there will be negative impact if we flush the data to the underlying connection for every send.

Is it acceptable for this API to return Task.CompletedTask when the underlying compressed buffer isn't full enough (configurable) and endOfMessage isn't true? Thus we really flush only when we reach the underlying buffer capacity or end of message is true.

Avoiding a signature change for SendAsync would be ideal. Adding new WebSocketMessageType enum values looks like the least invasive option.

Is it acceptable for this API to return Task.CompletedTask when the underlying compressed buffer isn't full enough (configurable) and endOfMessage isn't true? Thus we really flush only when we reach the underlying buffer capacity or end of message is true.

Yes that should be fine as long as we flush for message boundaries.

@Tratcher That makes sense.

Can I submit a pull request for this or should we go through a more formal discussion about the changes required?

They're going to want a formal write-up of the the proposed API changes before a PR. E.g. a diff of the API surface including doc comments describing the new items.

@Tratcher do you have a plan to fix in in .net core 5.0 version?
Do you know if any third party implementations like the one from @zlatanov or @tpeczek supports hosting under IIS with kestrel? Or this is not really possible because the work also involves changes to AspNetCoreModule?

No changes have been planned yet.

You're right that hosting with IIS out-of-proc would be problematic because the proxy wouldn't support compression. Hosting with IIS in-proc should allow it.

Thanks for clarification. However, is it possible to host ASP .Net Core application with IIS in-proc?

Is there any other workaround to enable compression with SignalR? Could one use e.g. long-polling?

Thanks for clarification. However, is it possible to host ASP .Net Core application with IIS in-proc?

It's possible to host ASP.NET Core apps in-proc in IIS since 2.2 and it's the default when using IIS in ASP.NET Core 3.0 now. More details can be found in our documentation: https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/?view=aspnetcore-2.2#hosting-models

Is there any other workaround to enable compression with SignalR? Could one use e.g. long-polling?

SignalR actually tries to bypass and disable response compression as much as possible since compression generally requires some level of buffering, which disrupts the real-time nature of SignalR communication.

Can you provide more context on why you want WebSocket compression? You can always compress payloads you are sending yourself and decompress them on the client side.

We have a fairly large and complex application in ASP .NET Core. We use regular http Ajax calls to retrieve page metadata/data and SignalR to push updates to the browser.
We would like to simplify our codebase by moving all communication to SignalR/Websockets. However, some responses may be large and their size could be greatly reduced with compression (it is plain Json).

We could compress and decompress the payloads ourselves, The only disadvantage is that we need to use an external javascript based decompression library. It is also a bit slower then letting a browser do the decompression.

Same reason web-request clients can ask for content to be gzipped, so the applications on both ends don't need to have compression in their app layers. It's transparent.

Understood that it would be more convenient at the websocket layer. In the meantime though you'll need to do the app level compression.

We would like to simplify our codebase by moving all communication to SignalR/Websockets. However, some responses may be large and their size could be greatly reduced with compression (it is plain Json).

Another option may be to switch to using SignalR's binary format (based on MessagePack). That may reduce the overhead enough to help in your situation. If you still need to compress text, then yes, WebSocket compression is the only transparent way to do that.

Triage: We need solid API proposal with use case samples, etc. Any takers?

@karelz I would like to volunteer.

I've put this off long enough, and now that 3.0 has been released I would like to find time to do it. I know the internals of the WebSocket well, and what needs to be done to efficiently support compression.

We'd certainly like to be able to have an end-to-end scenario around WebSocket compression. ASP.NET Core does share some code (via the WebSocket.CreateFromStream API) so we'll be happy to help review APIs and code to ensure we'll be able to adopt this on the server side. SignalR customers have asked us about supporting compressed payloads (for sending large content).

@zlatanov cool, as next step, can you please propose the API shape? See API process for details and examples. Thanks!

Was this page helpful?
0 / 5 - 0 ratings