Using websocket compression with uWebSockets.js and Websocket-Sharp
Asked Answered
H

1

11

We have a mobile game using websocket for connections. The server is a Node.js app using uWebSockets.js library and the client is a Unity app using Websocket-Sharp library. They both play well together and we didn't have encountered an issue with them.

Recently we wanted to enable websocket compression. Both libraries stated that they support Per-message Compression extension but it seems there's something incompatible with them. Because when we configure to use compression the websocket connection closes immediately on handshake.

We also tested client with ws library and it's provided example for compression with the same result. We tried tinkering with ws compression options and found that when we comment serverMaxWindowBits option (defaults to negotiated value) the connection could be established and sending and receiving messages works without a problem. We also asked about controlling the serverMaxWindowBits in uWebsockets.

The last thing we tried was connecting a minimal uWS server and websocket-sharp client. Here is the code for the server:

const uWS = require('uWebSockets.js');
const port = 5001;

const app = uWS.App({
    }).ws('/*', {
        /* Options */
        compression: 1, // Setting shared compression method
        maxPayloadLength: 4 * 1024,
        idleTimeout: 1000,
        /* Handlers */
        open: (ws, req) => {
            console.log('A WebSocket connected via URL: ' + req.getUrl() + '!');
        },
        message: (ws, message, isBinary) => {
            /* echo every message received */
            let ok = ws.send(message, isBinary);
        },
        drain: (ws) => {
            console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
        },
        close: (ws, code, message) => {
            console.log('WebSocket closed');
        }
    }).any('/*', (res, req) => {
        res.end('Nothing to see here!');
    }).listen(port, (token) => {
        if (token) {
            console.log('Listening to port ' + port);
        } else {
            console.log('Failed to listen to port ' + port);
        }
    });

Here is the client code:

using System;
using WebSocketSharp;

namespace Example
{
  public class Program
  {
    public static void Main (string[] args)
    {
      using (var ws = new WebSocket ("ws://localhost:5001")) {
        ws.OnMessage += (sender, e) =>
            Console.WriteLine ("server says: " + e.Data);

        ws.Compression = CompressionMethod.Deflate; // Turning on compression
        ws.Connect ();

        ws.Send ("{\"comm\":\"example\"}");
        Console.ReadKey (true);
      }
    }
  }
}

When we ran the server and the client, the client emits the following error:

Error|WebSocket.checkHandshakeResponse|The server hasn't sent back 'server_no_context_takeover'. Fatal|WebSocket.doHandshake|Includes an invalid Sec-WebSocket-Extensions header.

It seemed the client expected server_no_context_takeover header and didn't received one. We reviewed uWebsockets source (C++ part of uWebsockets.js module) and found a commented condition for sending back server_no_context_takeover header. So we uncommented the condition and built uWebsockets.js and tested again to encounter the following error in the client:

WebSocketSharp.WebSocketException: The header of a frame cannot be read from the stream.

Any suggestions for making these two libraries work together?

Hereabouts answered 4/12, 2019 at 16:10 Comment(4)
Not sure it helps at all, but socket.io does compress by default. I know the Best HTTP 2 has pretty good support for socket.io - websockets.Kimberleekimberley
@SamuelG Thanks, but using socket.io is not an option because we're currently handling 5k+ concurrent connections with minimum resources.Indwell
@KooroshPasokhi what concluded your reasoning that socket.io would not be able to handle your load or is resource intensive? Would love to hear more about your tests.Kimberleekimberley
@SamuelG Comments are not for discussions, but let's say the main reasons are the focus of socket.io is not performance and we don't need higher layer abstractions in socket.io.Indwell
C
3

Update: Based on my reading of the code in uWebSockets.js, changes would need to be made to enable all the parameters websocket-sharp needs set to enable compression. In Vertx, a high-performance Java server, the following settings work with Unity-compatible websocket-sharp for compression:

vertx.createHttpServer(new HttpServerOptions()
                .setMaxWebsocketFrameSize(65536)
                .setWebsocketAllowServerNoContext(true)
                .setWebsocketPreferredClientNoContext(true)
                .setMaxWebsocketMessageSize(100 * 65536)
                .setPerFrameWebsocketCompressionSupported(true)
                .setPerMessageWebsocketCompressionSupported(true)
                .setCompressionSupported(true));

Previously:

The error is real, websocket-sharp only supports permessage-deflate, use DEDICATED_COMPRESSOR (compression: 2) instead.

Catchfly answered 4/12, 2019 at 18:17 Comment(5)
Unfortunately setting compression method to 2 didn't change error message :(Indwell
Then there is some underlying issue with uWebSockets. I use Java’s vertx websockets with compression with WebSocketSharp in the client which has more configuration fields, and it just works.Catchfly
Maybe try authoring a test directly in uWebSockets.js that reproduces the behavior of WebSocket.cs 's header rejection? DEDICATED_COMPRESSOR really should work!Catchfly
What test do you mean? The architecture of uWebSockets.js is a little complicated. It has three layers written in JS, C++ and C, which makes debugging hard.Indwell
I mean authoring a test in the uWebSockets.js project directory that reproduces the handshake websocket-sharp does, which you could discover maybe by connecting it to a compliant server and seeing what happens?Catchfly

© 2022 - 2024 — McMap. All rights reserved.