Skip to content

Instantly share code, notes, and snippets.

@tghpereira
Last active March 8, 2025 17:43
Show Gist options
  • Save tghpereira/014d75ac44498fed428e2defa8683c80 to your computer and use it in GitHub Desktop.
Save tghpereira/014d75ac44498fed428e2defa8683c80 to your computer and use it in GitHub Desktop.
Create native api websocket in nodejs (Experimental)

Create native websocket api in node js (Experimental)

Goal

Allow greater control of websockets in the api without relying on libs with socket.io and ws. Looking for the basic implementation of reading and writing data and connection handshake for projects where socket.io and ws do not support the required functionalities in the project.

Issues encountered

It is not possible to receive or send text messages longer than 65522 characters.

Frame Websocket

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

More references https://www.rfc-editor.org/rfc/rfc6455

Chores done

  • Handeshake

  • Simple frame reading (hexadecimal -> 0x1 | typescript -> opcode 0x01) and closed frame detection (hexadecimal -> 0x8 | typescript -> opcode 0x08)

  • Interpret ping frame (hexadecimal -> 0x9 | typescript -> opcode 0x09)

  • Interpret pong frame (hexadecimal -> 0xA | typescript -> opcode 0x0A)

Auxiliary references

[CSE 312] Lecture 20: WebSocket Examples

Auxiliary references in Brazilian Portuguese

O QUE É ENQUADRAMENTO? [Camada de Enlace] [Redes de Computadores]

Erick Wendel

import { createHash } from 'node:crypto';
const opcodes = { text: 0x01, close: 0x08 };
const handshake = (key: string): string => {
return createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
.digest('base64');
};
const unmask = (payload: Buffer, key: number): Buffer => {
const result = Buffer.alloc(payload.byteLength);
for (let i = 0; i < payload.byteLength; ++i) {
const j = i % 4;
const maskingKeyByteShift = j === 3 ? 0 : (3 - j) << 3;
const maskingKeyByte = (maskingKeyByteShift === 0 ? key : key >>> maskingKeyByteShift) & 0b11111111;
const transformedByte = maskingKeyByte ^ payload.readUInt8(i);
result.writeUInt8(transformedByte, i);
}
return result;
};
const readFrame = (buffer: Buffer): string | null | undefined => {
const firstByte = buffer.readUInt8(0);
const opCode = firstByte & 0b00001111; // get last 4 bits of a byte
if (opCode === opcodes.close) {
return null;
} else if (opCode !== opcodes.text) {
return;
}
const secondByte = buffer.readUInt8(1); // start with a payload length
let offset = 2;
let payloadLength = secondByte & 0b01111111; // get last 7 bits of a second byte
if (payloadLength === 126) {
offset += 2;
} else if (payloadLength === 127) {
offset += 8;
}
const isMasked = Boolean((secondByte >>> 7) & 0x1); // get first bit of a second byte
if (isMasked) {
const maskingKey = buffer.readUInt32BE(offset); // read 4-byte mask
offset += 4;
const payload = buffer.subarray(offset);
const result = unmask(payload, maskingKey);
return result.toString('utf-8');
}
return buffer.subarray(offset).toString('utf-8');
};
const createFrame = (data: string) => {
const payloadByteLength = Buffer.byteLength(data);
let payloadBytesOffset = 2;
let payloadLength = payloadByteLength;
if (payloadByteLength > 65535) {
// length value cannot fit in 2 bytes
payloadBytesOffset += 8;
payloadLength = 127;
} else if (payloadByteLength > 125) {
payloadBytesOffset += 2;
payloadLength = 126;
}
const buffer = Buffer.alloc(payloadBytesOffset + payloadByteLength);
// first byte
buffer.writeUInt8(0b10000001, 0); // [FIN (1), RSV1 (0), RSV2 (0), RSV3 (0), Opode (0x01 - text frame)]
buffer[1] = payloadLength; // second byte - actual payload size (if <= 125 bytes) or 126, or 127
if (payloadLength === 126) {
// write actual payload length as a 16-bit unsigned integer
buffer.writeUInt16BE(payloadByteLength, 2);
} else if (payloadByteLength === 127) {
// write actual payload length as a 64-bit unsigned integer
buffer.writeBigUInt64BE(BigInt(payloadByteLength), 2);
}
buffer.write(data, payloadBytesOffset);
return buffer;
};
export { handshake, readFrame, createFrame };
import internal from 'node:stream';
import { createServer } from 'node:http';
import { createFrame, handshake, readFrame } from './base';
const data = 'hello websocket';
const clients = new Map<string, internal.Duplex>();
const server = createServer();
server.on('upgrade', (request, socket) => {
const { 'sec-websocket-key': key } = request.headers;
if (!key) return;
const acceptedConnection = handshake(key);
const responseHeaders = ['HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${acceptedConnection}`];
clients.set(acceptedConnection, socket);
socket.write(responseHeaders.concat('\r\n').join('\r\n'));
socket.on('data', chunk => {
const value = readFrame(chunk);
if (value) console.log(value.length);
});
socket.on('error', error => {
console.error(error);
});
socket.on('close', () => {
clients.delete(acceptedConnection);
console.info('close connection', acceptedConnection, clients.size);
});
});
server.listen(3333, '0.0.0.0', () => console.log('server running'));
server.on('close', () => {
for (const socket of clients.values()) {
socket.destroy();
}
clients.clear();
});
function broadcastSender(message: string) {
if (clients.size > 0) {
for (const socket of clients.values()) {
socket.write(createFrame(message));
}
}
}
setInterval(() => {
broadcastSender(data);
}, 20000);
@hulkish
Copy link

hulkish commented Dec 5, 2023

I'm curious what the performance is like for this? Does this actually work?

@tghpereira
Copy link
Author

@hulkish This implementation is just the base, it works for small message boards. You can finish the implementation if you want, but I advise you to use the Bun runtime, it natively implements websocket

https://bun.sh/

@guest271314
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment