0.2.6-dev: WebSocket permessage-deflate for frames from 1 KB (no server context takeover, 2 KB server window, memLevel 7; a client offering a narrower window connects uncompressed) — host bandwidth layer 0.2; golden: unchanged
This commit is contained in:
1 parent
69011d2b54
commit
8edda589f3
6 files changed
+135
-8
No files matched your search
@@ -2,8 +2,8 @@
|
||||
//
|
||||
// * session wiring: SessionRegistry (reconnect tokens) → Lobby (rooms, server/lobby.js) → Network (the socket
|
||||
// protocol, server/net.js), built from the startServer() options (config.js decides which go where);
|
||||
// * WebSocket (ws) at /ws, maxPayload 64 KB, no per-message deflate → Network.handleConnection. Refused at the
|
||||
// upgrade: any other path 404; per-network socket limit for internet clients (maxConnectionsPerAddr, see net.js
|
||||
// * WebSocket (ws) at /ws, maxPayload 64 KB, permessage-deflate (WS_DEFLATE) → Network.handleConnection. Refused at
|
||||
// the upgrade: any other path 404; per-network socket limit for internet clients (maxConnectionsPerAddr, see net.js
|
||||
// clientAddress; local/LAN peers are exempt) 429; server full (maxConnections) or shutting down 503.
|
||||
|
||||
import { WebSocketServer } from 'ws';
|
||||
@@ -15,6 +15,37 @@ import { netOptionsFrom, lobbyOptionsFrom } from './config.js';
|
||||
/** Inbound WebSocket frame limit (DESIGN §8). */
|
||||
export const WS_MAX_PAYLOAD = 64 * 1024;
|
||||
|
||||
/**
|
||||
* permessage-deflate (RFC 7692) for the JSON text frames. A message from 1 KB up (m.public, m.private, b.start, welcome
|
||||
* …) is compressed on its own — no context is kept from one message to the next (`serverNoContextTakeover`) — with a
|
||||
* 2 KB window and zlib memLevel 7, so a connection holds about 72 KB of zlib state; smaller frames (pong, ok) go out
|
||||
* as they are. Measured on a solo session (rounds 3–5, 216 server frames): 169 KB of payload → 63 KB on the wire; a
|
||||
* larger window gains nothing on these messages. Browsers negotiate it by themselves; a client that offers none gets
|
||||
* plain frames. The inflated size of a client frame still counts against WS_MAX_PAYLOAD.
|
||||
*/
|
||||
export const WS_DEFLATE = Object.freeze({
|
||||
threshold: 1024,
|
||||
serverNoContextTakeover: true,
|
||||
serverMaxWindowBits: 11,
|
||||
zlibDeflateOptions: Object.freeze({ memLevel: 7 }),
|
||||
});
|
||||
|
||||
/**
|
||||
* Does every permessage-deflate offer of this `Sec-WebSocket-Extensions` header cap the server window below
|
||||
* WS_DEFLATE.serverMaxWindowBits? ws cannot accept such an offer and refuses the whole handshake (400), so the upgrade
|
||||
* drops the header instead and the connection goes uncompressed. No browser sends `server_max_window_bits`.
|
||||
* @param {string | string[] | undefined} header
|
||||
*/
|
||||
export function narrowDeflateOffer(header) {
|
||||
if (typeof header !== 'string') return false;
|
||||
const offers = header.split(',').map((s) => s.trim()).filter((s) => /^permessage-deflate\b/i.test(s));
|
||||
if (!offers.length) return false;
|
||||
return offers.every((offer) => {
|
||||
const m = /(?:^|;)\s*server_max_window_bits\s*=\s*"?(\d+)"?/i.exec(offer);
|
||||
return !!m && Number(m[1]) < WS_DEFLATE.serverMaxWindowBits;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The session stack of one server.
|
||||
* @param {{ MatchClass?: Function, seedFn?: () => number, [option: string]: any }} opts startServer() options
|
||||
@@ -37,7 +68,7 @@ export function createSessionStack(opts, { data, log }) {
|
||||
* @returns {WebSocketServer}
|
||||
*/
|
||||
export function attachWebSocket(server, { network, log }) {
|
||||
const wss = new WebSocketServer({ noServer: true, maxPayload: WS_MAX_PAYLOAD, perMessageDeflate: false, clientTracking: false });
|
||||
const wss = new WebSocketServer({ noServer: true, maxPayload: WS_MAX_PAYLOAD, perMessageDeflate: { ...WS_DEFLATE }, clientTracking: false });
|
||||
wss.on('connection', (ws, req) => network.handleConnection(ws, req));
|
||||
wss.on('error', (e) => log.error('[ws] server error', e));
|
||||
|
||||
@@ -51,6 +82,7 @@ export function attachWebSocket(server, { network, log }) {
|
||||
const refused = network.admission(req);
|
||||
if (refused === 'per-address') { reject(429, 'Too Many Requests'); return; }
|
||||
if (refused) { reject(503, 'Service Unavailable'); return; }
|
||||
if (narrowDeflateOffer(req.headers['sec-websocket-extensions'])) delete req.headers['sec-websocket-extensions'];
|
||||
try {
|
||||
wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req));
|
||||
} catch (e) {
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
//
|
||||
// http/config.js ROOT, the served directories, the environment (PORT 3000, HOST '::' dual-stack, TRUST_PROXY auto, DEBUG),
|
||||
// which startServer() options go to net.js / lobby.js, the console logger
|
||||
// http/websocket.js session wiring (SessionRegistry → Lobby → Network) and the WebSocket at /ws (maxPayload 64 KB;
|
||||
// refused at upgrade with 404 / 429 per network / 503)
|
||||
// http/websocket.js session wiring (SessionRegistry → Lobby → Network) and the WebSocket at /ws (maxPayload 64 KB,
|
||||
// permessage-deflate from 1 KB; refused at upgrade with 404 / 429 per network / 503)
|
||||
// http/static.js the static mounts (/ → public/, /data/, /shared/, /sim/ `.js` only), the /data.js browser stand-in,
|
||||
// the content packs (/packs/index.json, /packs/<id>/<file> — the registry is packs.js)
|
||||
// http/media.js /media/bgm/act1 → public/assets/audio/bgm/act1.mp3 (audio addressed without its extension)
|
||||
|
||||
Reference in new issue
Block a user