prettydiff/share-file-systems

Looking for ideal WebSocket parsing guidance

prettydiff opened this issue · 2 comments

WebSocket messages are binary messages with a RFC6455 frame header over messages passed on a TCP stream.

Constraints

  1. Firefox tends to separate frame headers and frame payloads into two messages while other browsers do not
  2. WebSockets allow for message fragmentation where a message may be separated into multiple messages for transport. The first message would receive the actual frame header and the fragments will receive a continuation frame header.
  3. If Node receives messages too quickly the messages will be joined as a single binary input, which must be sliced according to the lengths specified in the various frame headers comprising that input.
  4. The maximum size of a TLS frame is 65536 bytes irrespective of frame fragmentation or frame size noted by the websocket frame header.

What is the most ideal way of solving for those three constraints. Don't worry about the technical requirements of RFC6455 as those are easily solved at this point. I am just looking for guidance on handling the listed items.

The prior solution was super simple, but it has a fatal flaw evident only in perf testing. Incoming messages are about 10x slower to process than outgoing messages. The prior solution just concatenated overflow into a single buffer that is sliced from the front as dictated by lengths specified in the frame headers. That results in a buffer that eventually generates a stack overflow because it grows to fast to process.

The new solution instead uses a buffer array, which is extremely complex. This new approach works for perf testing and in my local testing, but it might be error prone in ways I am not aware of.

Experimental solution:

payload:Buffer = (function terminal_server_transmission_transmitWs_listener_processor_payload():Buffer {
                    if (frame === null) {
                        return null;
                    }
                    const dataLength:number = data.length;
                    let frameSize:number = frame.extended + frame.startByte,
                        index:number = 0,
                        size:number = 0,
                        frameLength:number = socket.frame.length,
                        complete:Buffer = null;

                    // when the data exactly matches the size of payload and frame header
                    if (dataLength === frameSize) {
                        if (buf === null || buf === undefined) {
                            socket.frame[0] = socket.frame[0].slice(frameSize);
                        }
                        return unmask(data.slice(frame.startByte));
                    }

                    if (buf !== null && buf !== undefined) {
                        socket.frame.push(buf);
                        frameLength = frameLength + 1;
                    }
                    if (frameLength < 1) {
                        return null;
                    }
                    do {
                        size = size + socket.frame[index].length;
                        if (size > frameSize) {
                            break;
                        }
                        index = index + 1;
                    } while (index < frameLength);
                    if (size < frameSize) {
                        return null;
                    }
                    if (size > frameSize) {
                        const bulk:Buffer = (index === 0)
                                ? socket.frame[0]
                                : Buffer.concat(socket.frame.slice(0, index + 1)),
                            meta:websocket_meta = extended(bulk);
                        frame.extended = meta.lengthExtended;
                        frame.len = meta.lengthShort;
                        frame.mask = meta.mask;
                        frame.startByte = meta.startByte;
                        frameSize = frame.extended + frame.startByte;
                        if (size < frameSize) {
                            return null;
                        }
                        complete = unmask(bulk.slice(frame.startByte, frameSize));
                        if (index > 0) {
                            socket.frame.splice(1, index);
                        }
                        socket.frame[0] = bulk.slice(frameSize);
                        return complete;
                    }
                    complete = unmask(Buffer.concat(socket.frame.slice(0, index)).slice(frame.startByte));
                    socket.frame.splice(0, index);
                    return complete;
                }());
if (socket.frame.length > 0) {
                terminal_server_transmission_transmitWs_listener_processor(null);
            }

Prior solution:

            if (len > packageSize) {
                // necessary if two frames from unrelated messages are combined into a single packet
                excess = data.slice(packageSize);
                data = data.slice(0, packageSize);
            }
if (excess === null) {
                socket.frame = [];
            } else {
                socket.frame = [excess];
                terminal_server_transmission_transmitWs_listener_processor(null);
            }

So far the learnings from performance testing my approach to websockets.

bun claims that it can send about 740,000 messages per second and the NPM package ws can send about 107,000 messages per second.

  • This package is able to send about 215,000 messages per second, which is twice as fast as ws but only a third as fast as bun.
  • This package is able to process 100,000 messages full life cycle (sending, and receiving) in about 4.4 seconds, which indicates sending is 10x faster than receiving.