@encharm/cws 4.6.0 → 4.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CLAUDE.md +5 -3
- package/README.md +13 -1
- package/binding.gyp +8 -2
- package/dist/bindings/cws_darwin_arm64_node115.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node127.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node137.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node147.node +0 -0
- package/dist/bindings/cws_linux_arm64_node115.node +0 -0
- package/dist/bindings/cws_linux_arm64_node127.node +0 -0
- package/dist/bindings/cws_linux_arm64_node137.node +0 -0
- package/dist/bindings/cws_linux_arm64_node147.node +0 -0
- package/dist/bindings/cws_linux_x64_node115.node +0 -0
- package/dist/bindings/cws_linux_x64_node127.node +0 -0
- package/dist/bindings/cws_linux_x64_node137.node +0 -0
- package/dist/bindings/cws_linux_x64_node147.node +0 -0
- package/dist/bindings/cws_win32_x64_node115.node +0 -0
- package/dist/bindings/cws_win32_x64_node127.node +0 -0
- package/dist/bindings/cws_win32_x64_node137.node +0 -0
- package/dist/bindings/cws_win32_x64_node147.node +0 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +20 -4
- package/dist/index.d.ts +6 -1
- package/dist/index.js +2 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +16 -2
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +3 -2
- package/package.json +1 -1
- package/src/Addon.cpp +2 -0
- package/src/Addon.h +10 -1
- package/src/Extensions.cpp +7 -1
- package/src/Extensions.h +2 -1
- package/src/Group.cpp +1 -1
- package/src/Group.h +4 -1
- package/src/HTTPSocket.cpp +2 -2
- package/src/Hub.cpp +11 -76
- package/src/Hub.h +11 -10
- package/src/Socket.h +37 -7
- package/src/WebSocket.cpp +16 -7
- package/src/Zlib.cpp +141 -0
- package/src/Zlib.h +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## Released 4.8.3
|
|
2
|
+
* Corked sends up to 1 KB use the per-loop block pool instead of malloc/free per message; queued messages remember their pool block. Also initialise the `reserved` callback argument, which was passed uninitialised for messages that hit the write queue.
|
|
3
|
+
* JS: resolve the native namespace once per socket instead of on every `send`/`close`/`ping`.
|
|
4
|
+
|
|
5
|
+
## Released 4.8.2
|
|
6
|
+
* Accept compressed messages that end with a BFINAL=1 DEFLATE block (RFC 7692 section 7.2.3.4). Such clients (libdeflate-based and some non-zlib implementations) were disconnected on their first compressed message.
|
|
7
|
+
|
|
8
|
+
## Released 4.8.1
|
|
9
|
+
* Skip the kernel-level cork/uncork (two `setsockopt` calls) around every socket read when write corking is active; those writes leave in the end-of-tick gathered write anyway. Saves 1-2 us of server CPU per read.
|
|
10
|
+
|
|
11
|
+
## Released 4.8.0
|
|
12
|
+
* Prebuilt bindings now compress with zlib-ng 2.2.4 (vendored in `deps/zlib-ng`, statically linked, native `zng_` API so it cannot collide with Node's zlib). Measured on a real RPC stream: level 1 costs 2.2-2.5 us/KB vs 5.6-8 us/KB with zlib. The node-gyp fallback build keeps using Node's zlib through the same `src/Zlib.cpp` wrapper.
|
|
13
|
+
* New `perMessageDeflate.level` option (default 2) and a `zlibBackend` export naming the compiled-in implementation.
|
|
14
|
+
* Fix the node-gyp fallback build (`npm install` on a platform without a prebuilt binding): it lacked the vendored Node header include path, `HAVE_OPENSSL`/`NODE_WANT_INTERNALS`, and a macOS 10.15 deployment target, so it has failed silently since the Node 20 headers were introduced.
|
|
15
|
+
|
|
16
|
+
## Released 4.7.0
|
|
17
|
+
* permessage-deflate: messages are now compressed by default once negotiated (previously only `send(..., { compress: true })` compressed anything). New options `threshold` (minimum size to compress), `windowBits` and `memLevel` (per-socket memory tier for the sliding window, advertised as `server_max_window_bits` when below 15).
|
|
18
|
+
* Remove a leftover debug `console.log` on module load.
|
|
19
|
+
|
|
1
20
|
## Released 4.6.0
|
|
2
21
|
* Add Node 26 support (ABI 147, V8 14.6): vendored `src/headers/26`, `MakeCallback` with explicit `async_context`, tagged internal-field reads via `BaseObject::FromJSObject`.
|
|
3
22
|
* Cork writes per event-loop iteration: sends to the same socket within one tick go out in a single gathered write. Disable with `CWS_CORK=0`.
|
package/CLAUDE.md
CHANGED
|
@@ -21,14 +21,15 @@ Tests bind ports 3000 (ws) and 3001 (wss, certs in `tests/certs/`). The test fil
|
|
|
21
21
|
|
|
22
22
|
### Building the release binaries (multi-ABI)
|
|
23
23
|
|
|
24
|
-
`binding.gyp` is only used by `npm install` for a local build against the running Node. Release binaries are built by the `Makefile` (macOS/Linux) and `make.bat` (Windows), which:
|
|
24
|
+
`binding.gyp` is only used by `npm install` for a local build against the running Node (Node's zlib, no zlib-ng); it must carry the same include path and defines as the Makefile or it fails silently (the install script discards the exit code). Release binaries are built by the `Makefile` (macOS/Linux) and `make.bat` (Windows), which:
|
|
25
25
|
|
|
26
26
|
1. Download official Node header tarballs for one pinned version per supported major into `targets/` (`VER_115`=Node 20, `VER_127`=Node 22, `VER_137`=Node 24, `VER_147`=Node 26; the number is the Node ABI / `process.versions.modules`).
|
|
27
27
|
2. Compile `src/*.cpp` once per ABI with `g++`/`cl` directly, with `-I src/headers/$V` for the matching Node major.
|
|
28
|
-
3.
|
|
28
|
+
3. Build the vendored zlib-ng (`deps/zlib-ng`, native `zng_` API) once per OS/arch: CMake into `deps/zlib-ng/build-<OS>-<arch>/` on macOS/Linux, `nmake -f win32\Makefile.msc` on Windows. The bindings are compiled with `-DCWS_ZLIB_NG` and link it statically. `src/Zlib.cpp` is the only file that includes a zlib header; without the define (node-gyp fallback) it uses Node's zlib.
|
|
29
|
+
4. Emit `dist/bindings/cws_<platform>_<arch>_node<ABI>.node`.
|
|
29
30
|
|
|
30
31
|
```sh
|
|
31
|
-
make # all
|
|
32
|
+
make # all four ABIs for the host OS/arch (needs curl, cmake, g++/clang)
|
|
32
33
|
./build-on-docker.sh linux/arm64 # Linux arm64 (native on Apple Silicon, ~1 min)
|
|
33
34
|
./build-on-docker.sh linux/amd64 # Linux x64 (default; emulated on Apple Silicon, slower)
|
|
34
35
|
make.bat # Windows x64; auto-detects VS 2022 (Build Tools or any edition)
|
|
@@ -78,4 +79,5 @@ The client side never touches Node sockets: `native.connect(clientGroup, url, ws
|
|
|
78
79
|
- Every handler, including `message`, is invoked through `node::MakeCallback`, which establishes an async-context scope and drains nextTick/microtasks after the handler returns. Because that drain happens before `onMessage` detaches the binary `ArrayBuffer`, a continuation that resolves within it still sees the bytes; anything that waits on real I/O sees a detached buffer.
|
|
79
80
|
- `shared.ts` currently has a leftover `console.log('log', ...)` printing the binding path on load.
|
|
80
81
|
- The addon exposes one `Group` per server plus a single shared client group; `ws.external` is the C++ `WebSocket*` and is nulled on disconnect, so every native call in `client.ts` guards on it.
|
|
82
|
+
- permessage-deflate: `Group` carries `deflateWindowBits`/`deflateMemLevel` (tier of the per-socket compressor allocated in the `WebSocket` ctor); the negotiator advertises `server_max_window_bits` when below 15 but ignores a smaller value requested by the client. The JS `send` decides `compress` per message: explicit option, else `compressThreshold` from the server config (undefined when deflate is off). Client-to-server inflate is one shared stream reset per message (`client_no_context_takeover` is always offered).
|
|
81
83
|
- Close code defaults to 1005 when the peer sends none (matches `ws`). App-level ping uses the single byte `'9'` (`APP_PING_CODE`) for browser clients that can't see protocol pings.
|
package/README.md
CHANGED
|
@@ -14,6 +14,11 @@ This table is true if you run ssl directly with `cws` (`Node.js`). In case if yo
|
|
|
14
14
|
|
|
15
15
|
| cWS Version | Node 26 | Node 24 | Node 22 | Node 20 |
|
|
16
16
|
|-------------|---------|----------|---------|----------
|
|
17
|
+
| 4.8.3 | X | X | X | X |
|
|
18
|
+
| 4.8.2 | X | X | X | X |
|
|
19
|
+
| 4.8.1 | X | X | X | X |
|
|
20
|
+
| 4.8.0 | X | X | X | X |
|
|
21
|
+
| 4.7.0 | X | X | X | X |
|
|
17
22
|
| 4.6.0 | X | X | X | X |
|
|
18
23
|
| 4.5.3 | | X | X | X |
|
|
19
24
|
|
|
@@ -174,7 +179,14 @@ const wsServer = new WebSocket.Server({
|
|
|
174
179
|
* noDelay?: boolean (set socket no delay)
|
|
175
180
|
* noServer?: boolean (use this when upgrade done outside of cws)
|
|
176
181
|
* maxPayload?: number
|
|
177
|
-
* perMessageDeflate?: boolean | { serverNoContextTakeover
|
|
182
|
+
* perMessageDeflate?: boolean | { serverNoContextTakeover?: boolean; windowBits?: number; memLevel?: number; threshold?: number }
|
|
183
|
+
* true = shared compressor, no context takeover. serverNoContextTakeover: false = per-socket
|
|
184
|
+
* sliding window (streaming compression; far better on small messages). windowBits 9..15 and
|
|
185
|
+
* memLevel 1..9 pick its memory tier: 15/8 = ~256 KB per socket (default), 12/5 = ~32 KB, 10/3 = ~8 KB.
|
|
186
|
+
* threshold = minimum message size in bytes to compress (default 0); send(..., { compress }) overrides.
|
|
187
|
+
* level 1..9 (default 2): deflate level of the per-socket compressor. The prebuilt bindings use zlib-ng
|
|
188
|
+
* (see `zlibBackend` export): level 1 is its very fast "quick" strategy (~3-4x less CPU than zlib
|
|
189
|
+
* level 1 for ~8% worse ratio), level 2 matches zlib level 1's ratio.
|
|
178
190
|
* verifyClient?: (info: ConnectionInfo, next: VerifyClientNext) => void (use to allow or decline connections)
|
|
179
191
|
**/
|
|
180
192
|
}, () => {
|
package/binding.gyp
CHANGED
|
@@ -12,8 +12,14 @@
|
|
|
12
12
|
'src/cSNode.cpp',
|
|
13
13
|
'src/WebSocket.cpp',
|
|
14
14
|
'src/HTTPSocket.cpp',
|
|
15
|
-
'src/Socket.cpp'
|
|
15
|
+
'src/Socket.cpp',
|
|
16
|
+
'src/Zlib.cpp'
|
|
16
17
|
],
|
|
18
|
+
# Node private headers vendored per major (see src/headers/); the Makefile passes the same -I.
|
|
19
|
+
'include_dirs': [
|
|
20
|
+
'src/headers/<!(node -p "parseInt(process.versions.node)")'
|
|
21
|
+
],
|
|
22
|
+
'defines': ['HAVE_OPENSSL=1', 'NODE_WANT_INTERNALS=1'],
|
|
17
23
|
'conditions': [
|
|
18
24
|
['OS=="linux"', {
|
|
19
25
|
'cflags_cc': ['-std=c++17', '-DUSE_LIBUV'],
|
|
@@ -31,7 +37,7 @@
|
|
|
31
37
|
}],
|
|
32
38
|
['OS=="mac"', {
|
|
33
39
|
'xcode_settings': {
|
|
34
|
-
'MACOSX_DEPLOYMENT_TARGET': '10.
|
|
40
|
+
'MACOSX_DEPLOYMENT_TARGET': '10.15',
|
|
35
41
|
'CLANG_CXX_LANGUAGE_STANDARD': 'c++20',
|
|
36
42
|
'CLANG_CXX_LIBRARY': 'libc++',
|
|
37
43
|
'GCC_GENERATE_DEBUGGING_SYMBOLS': 'NO',
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/client.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare class WebSocket {
|
|
|
12
12
|
registeredEvents: any;
|
|
13
13
|
private external;
|
|
14
14
|
private socketType;
|
|
15
|
+
private compressThreshold;
|
|
16
|
+
private nativeApi;
|
|
15
17
|
constructor(url: string, options?: any);
|
|
16
18
|
get bufferedAmount(): number;
|
|
17
19
|
get _socket(): SocketAddress;
|
package/dist/client.js
CHANGED
|
@@ -4,6 +4,12 @@ exports.WebSocket = void 0;
|
|
|
4
4
|
const server_1 = require("./server");
|
|
5
5
|
const shared_1 = require("./shared");
|
|
6
6
|
const clientGroup = shared_1.native.client.group.create(0, shared_1.DEFAULT_PAYLOAD_LIMIT);
|
|
7
|
+
function messageByteLength(message) {
|
|
8
|
+
if (typeof message === 'string') {
|
|
9
|
+
return Buffer.byteLength(message);
|
|
10
|
+
}
|
|
11
|
+
return message && typeof message.byteLength === 'number' ? message.byteLength : 0;
|
|
12
|
+
}
|
|
7
13
|
shared_1.setupNative(clientGroup, 'client');
|
|
8
14
|
class WebSocket {
|
|
9
15
|
constructor(url, options = {}) {
|
|
@@ -20,9 +26,12 @@ class WebSocket {
|
|
|
20
26
|
message: shared_1.noop
|
|
21
27
|
};
|
|
22
28
|
this.socketType = 'client';
|
|
29
|
+
this.nativeApi = shared_1.native.client;
|
|
23
30
|
if (!this.url && this.options.external) {
|
|
24
31
|
this.socketType = 'server';
|
|
32
|
+
this.nativeApi = shared_1.native.server;
|
|
25
33
|
this.external = this.options.external;
|
|
34
|
+
this.compressThreshold = this.options.compressThreshold;
|
|
26
35
|
}
|
|
27
36
|
else {
|
|
28
37
|
shared_1.native.connect(clientGroup, url, this);
|
|
@@ -76,7 +85,14 @@ class WebSocket {
|
|
|
76
85
|
if (options && options.binary === true) {
|
|
77
86
|
opCode = shared_1.OPCODE_BINARY;
|
|
78
87
|
}
|
|
79
|
-
|
|
88
|
+
let compress = false;
|
|
89
|
+
if (options && options.compress !== undefined) {
|
|
90
|
+
compress = !!options.compress;
|
|
91
|
+
}
|
|
92
|
+
else if (this.compressThreshold !== undefined) {
|
|
93
|
+
compress = messageByteLength(message) >= this.compressThreshold;
|
|
94
|
+
}
|
|
95
|
+
this.nativeApi.send(this.external, message, opCode, cb ? () => process.nextTick(cb) : null, compress);
|
|
80
96
|
}
|
|
81
97
|
else if (cb) {
|
|
82
98
|
cb(new Error('Socket not connected'));
|
|
@@ -84,18 +100,18 @@ class WebSocket {
|
|
|
84
100
|
}
|
|
85
101
|
ping(message) {
|
|
86
102
|
if (this.external) {
|
|
87
|
-
|
|
103
|
+
this.nativeApi.send(this.external, message, shared_1.OPCODE_PING);
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
close(code = 1000, reason) {
|
|
91
107
|
if (this.external) {
|
|
92
|
-
|
|
108
|
+
this.nativeApi.close(this.external, code, reason);
|
|
93
109
|
this.external = null;
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
terminate() {
|
|
97
113
|
if (this.external) {
|
|
98
|
-
|
|
114
|
+
this.nativeApi.terminate(this.external);
|
|
99
115
|
this.external = null;
|
|
100
116
|
}
|
|
101
117
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,10 +21,15 @@ export declare type ServerConfigs = {
|
|
|
21
21
|
noServer?: boolean;
|
|
22
22
|
maxPayload?: number;
|
|
23
23
|
perMessageDeflate?: boolean | {
|
|
24
|
-
serverNoContextTakeover
|
|
24
|
+
serverNoContextTakeover?: boolean;
|
|
25
|
+
windowBits?: number;
|
|
26
|
+
memLevel?: number;
|
|
27
|
+
level?: number;
|
|
28
|
+
threshold?: number;
|
|
25
29
|
};
|
|
26
30
|
verifyClient?: (info: ConnectionInfo, next: VerifyClientNext) => void;
|
|
27
31
|
};
|
|
28
32
|
export { WebSocket } from './client';
|
|
29
33
|
export { WebSocketServer } from './server';
|
|
30
34
|
export declare const secureProtocol: string;
|
|
35
|
+
export { zlibBackend } from './shared';
|
package/dist/index.js
CHANGED
|
@@ -6,3 +6,5 @@ Object.defineProperty(exports, "WebSocket", { enumerable: true, get: function ()
|
|
|
6
6
|
var server_1 = require("./server");
|
|
7
7
|
Object.defineProperty(exports, "WebSocketServer", { enumerable: true, get: function () { return server_1.WebSocketServer; } });
|
|
8
8
|
exports.secureProtocol = 'TLSv1_2_method';
|
|
9
|
+
var shared_1 = require("./shared");
|
|
10
|
+
Object.defineProperty(exports, "zlibBackend", { enumerable: true, get: function () { return shared_1.zlibBackend; } });
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -12,13 +12,27 @@ class WebSocketServer {
|
|
|
12
12
|
connection: shared_1.noop,
|
|
13
13
|
};
|
|
14
14
|
let nativeOptions = 0;
|
|
15
|
+
let windowBits = 15;
|
|
16
|
+
let memLevel = 8;
|
|
17
|
+
let level = 2;
|
|
15
18
|
if (this.options.perMessageDeflate) {
|
|
16
19
|
nativeOptions |= shared_1.PERMESSAGE_DEFLATE;
|
|
17
|
-
|
|
20
|
+
const deflate = typeof this.options.perMessageDeflate === 'object' ? this.options.perMessageDeflate : {};
|
|
21
|
+
if (deflate.serverNoContextTakeover === false) {
|
|
18
22
|
nativeOptions |= shared_1.SLIDING_DEFLATE_WINDOW;
|
|
19
23
|
}
|
|
24
|
+
if (typeof deflate.windowBits === 'number') {
|
|
25
|
+
windowBits = deflate.windowBits;
|
|
26
|
+
}
|
|
27
|
+
if (typeof deflate.memLevel === 'number') {
|
|
28
|
+
memLevel = deflate.memLevel;
|
|
29
|
+
}
|
|
30
|
+
if (typeof deflate.level === 'number') {
|
|
31
|
+
level = deflate.level;
|
|
32
|
+
}
|
|
33
|
+
this.compressThreshold = typeof deflate.threshold === 'number' ? deflate.threshold : 0;
|
|
20
34
|
}
|
|
21
|
-
this.serverGroup = shared_1.native.server.group.create(nativeOptions, this.options.maxPayload || shared_1.DEFAULT_PAYLOAD_LIMIT);
|
|
35
|
+
this.serverGroup = shared_1.native.server.group.create(nativeOptions, this.options.maxPayload || shared_1.DEFAULT_PAYLOAD_LIMIT, windowBits, memLevel, level);
|
|
22
36
|
shared_1.setupNative(this.serverGroup, 'server', this);
|
|
23
37
|
if (this.options.noServer) {
|
|
24
38
|
return;
|
package/dist/shared.d.ts
CHANGED
|
@@ -9,4 +9,5 @@ export declare const PERMESSAGE_DEFLATE: number;
|
|
|
9
9
|
export declare const SLIDING_DEFLATE_WINDOW: number;
|
|
10
10
|
export declare const DEFAULT_PAYLOAD_LIMIT: number;
|
|
11
11
|
export declare const native: any;
|
|
12
|
+
export declare const zlibBackend: string;
|
|
12
13
|
export declare function setupNative(group: any, type: string, wsServer?: WebSocketServer): void;
|
package/dist/shared.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setupNative = exports.native = exports.DEFAULT_PAYLOAD_LIMIT = exports.SLIDING_DEFLATE_WINDOW = exports.PERMESSAGE_DEFLATE = exports.APP_PING_CODE = exports.OPCODE_BINARY = exports.OPCODE_PING = exports.OPCODE_TEXT = exports.noop = void 0;
|
|
3
|
+
exports.setupNative = exports.zlibBackend = exports.native = exports.DEFAULT_PAYLOAD_LIMIT = exports.SLIDING_DEFLATE_WINDOW = exports.PERMESSAGE_DEFLATE = exports.APP_PING_CODE = exports.OPCODE_BINARY = exports.OPCODE_PING = exports.OPCODE_TEXT = exports.noop = void 0;
|
|
4
4
|
const client_1 = require("./client");
|
|
5
5
|
exports.noop = () => { };
|
|
6
6
|
exports.OPCODE_TEXT = 1;
|
|
@@ -19,11 +19,12 @@ exports.native = (() => {
|
|
|
19
19
|
throw err;
|
|
20
20
|
}
|
|
21
21
|
})();
|
|
22
|
+
exports.zlibBackend = exports.native.zlibBackend;
|
|
22
23
|
function setupNative(group, type, wsServer) {
|
|
23
24
|
exports.native.setNoop(exports.noop);
|
|
24
25
|
exports.native[type].group.onConnection(group, (external) => {
|
|
25
26
|
if (type === 'server' && wsServer) {
|
|
26
|
-
const socket = new client_1.WebSocket(null, { external });
|
|
27
|
+
const socket = new client_1.WebSocket(null, { external, compressThreshold: wsServer.compressThreshold });
|
|
27
28
|
exports.native.setUserData(external, socket);
|
|
28
29
|
if (wsServer.upgradeCb) {
|
|
29
30
|
wsServer.upgradeCb(socket);
|
package/package.json
CHANGED
package/src/Addon.cpp
CHANGED
|
@@ -20,6 +20,8 @@ void Initialize(Local<Object> exports) {
|
|
|
20
20
|
NODE_SET_METHOD(exports, "upgrade", upgrade);
|
|
21
21
|
NODE_SET_METHOD(exports, "connect", connect);
|
|
22
22
|
NODE_SET_METHOD(exports, "setNoop", setNoop);
|
|
23
|
+
exports->Set(isolate->GetCurrentContext(), String::NewFromUtf8(isolate, "zlibBackend").ToLocalChecked(),
|
|
24
|
+
String::NewFromUtf8(isolate, cWS::zlib::backend()).ToLocalChecked()).Check();
|
|
23
25
|
hub.getNodeData()->corkState->enabled = corkEnabledFromEnv();
|
|
24
26
|
registerCheck(isolate);
|
|
25
27
|
}
|
package/src/Addon.h
CHANGED
|
@@ -185,8 +185,17 @@ struct GroupData {
|
|
|
185
185
|
|
|
186
186
|
template <bool isServer>
|
|
187
187
|
void createGroup(const FunctionCallbackInfo<Value> &args) {
|
|
188
|
+
int windowBits = args.Length() > 2 && args[2]->IsNumber() ? (int) args[2].As<Integer>()->Value() : 15;
|
|
189
|
+
int memLevel = args.Length() > 3 && args[3]->IsNumber() ? (int) args[3].As<Integer>()->Value() : 8;
|
|
190
|
+
if (windowBits < 9) windowBits = 9;
|
|
191
|
+
if (windowBits > 15) windowBits = 15;
|
|
192
|
+
if (memLevel < 1) memLevel = 1;
|
|
193
|
+
if (memLevel > 9) memLevel = 9;
|
|
194
|
+
int level = args.Length() > 4 && args[4]->IsNumber() ? (int) args[4].As<Integer>()->Value() : 1;
|
|
195
|
+
if (level < 1) level = 1;
|
|
196
|
+
if (level > 9) level = 9;
|
|
188
197
|
cWS::Group<isServer> *group = hub.createGroup<isServer>(
|
|
189
|
-
args[0].As<Integer>()->Value(), args[1].As<Integer>()->Value());
|
|
198
|
+
args[0].As<Integer>()->Value(), args[1].As<Integer>()->Value(), windowBits, memLevel, level);
|
|
190
199
|
group->setUserData(new GroupData);
|
|
191
200
|
args.GetReturnValue().Set(External::New(args.GetIsolate(), group));
|
|
192
201
|
}
|
package/src/Extensions.cpp
CHANGED
|
@@ -76,7 +76,7 @@ ExtensionsParser::ExtensionsParser(const char *data, size_t length) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
template <bool isServer>
|
|
79
|
-
ExtensionsNegotiator<isServer>::ExtensionsNegotiator(int wantedOptions) {
|
|
79
|
+
ExtensionsNegotiator<isServer>::ExtensionsNegotiator(int wantedOptions, int serverMaxWindowBits) : serverMaxWindowBits(serverMaxWindowBits) {
|
|
80
80
|
options = wantedOptions;
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -100,6 +100,12 @@ std::string ExtensionsNegotiator<isServer>::generateOffer() {
|
|
|
100
100
|
if (options & Options::SERVER_NO_CONTEXT_TAKEOVER) {
|
|
101
101
|
//extensionsOffer += "; server_no_context_takeover";
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
// Tell the peer our LZ77 window is smaller than the default so it can
|
|
105
|
+
// size its inflate window accordingly (RFC 7692 section 7.1.2.1).
|
|
106
|
+
if (isServer && serverMaxWindowBits < 15) {
|
|
107
|
+
extensionsOffer += "; server_max_window_bits=" + std::to_string(serverMaxWindowBits);
|
|
108
|
+
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
return extensionsOffer;
|
package/src/Extensions.h
CHANGED
|
@@ -18,8 +18,9 @@ template <bool isServer>
|
|
|
18
18
|
class ExtensionsNegotiator {
|
|
19
19
|
protected:
|
|
20
20
|
int options;
|
|
21
|
+
int serverMaxWindowBits;
|
|
21
22
|
public:
|
|
22
|
-
ExtensionsNegotiator(int wantedOptions);
|
|
23
|
+
ExtensionsNegotiator(int wantedOptions, int serverMaxWindowBits = 15);
|
|
23
24
|
std::string generateOffer();
|
|
24
25
|
void readOffer(std::string offer);
|
|
25
26
|
int getNegotiatedOptions();
|
package/src/Group.cpp
CHANGED
|
@@ -114,7 +114,7 @@ void Group<isServer>::removeWebSocket(WebSocket<isServer> *webSocket) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
template <bool isServer>
|
|
117
|
-
Group<isServer>::Group(int extensionOptions, unsigned int maxPayload, Hub *hub, cS::NodeData *nodeData) : cS::NodeData(*nodeData), maxPayload(maxPayload), hub(hub), extensionOptions(extensionOptions) {
|
|
117
|
+
Group<isServer>::Group(int extensionOptions, unsigned int maxPayload, Hub *hub, cS::NodeData *nodeData, int deflateWindowBits, int deflateMemLevel, int deflateLevel) : cS::NodeData(*nodeData), maxPayload(maxPayload), hub(hub), extensionOptions(extensionOptions), deflateWindowBits(deflateWindowBits), deflateMemLevel(deflateMemLevel), deflateLevel(deflateLevel) {
|
|
118
118
|
connectionHandler = [](WebSocket<isServer> *, HttpRequest) {};
|
|
119
119
|
transferHandler = [](WebSocket<isServer> *) {};
|
|
120
120
|
messageHandler = [](WebSocket<isServer> *, char *, size_t, OpCode) {};
|
package/src/Group.h
CHANGED
|
@@ -41,6 +41,9 @@ protected:
|
|
|
41
41
|
unsigned int maxPayload;
|
|
42
42
|
Hub *hub;
|
|
43
43
|
int extensionOptions;
|
|
44
|
+
int deflateWindowBits = 15;
|
|
45
|
+
int deflateMemLevel = 8;
|
|
46
|
+
int deflateLevel = 1;
|
|
44
47
|
cS::Timer *timer = nullptr, *httpTimer = nullptr;
|
|
45
48
|
const char *userPingMessage;
|
|
46
49
|
size_t userPingMessageLength;
|
|
@@ -61,7 +64,7 @@ protected:
|
|
|
61
64
|
void addHttpSocket(HttpSocket<isServer> *httpSocket);
|
|
62
65
|
void removeHttpSocket(HttpSocket<isServer> *httpSocket);
|
|
63
66
|
|
|
64
|
-
Group(int extensionOptions, unsigned int maxPayload, Hub *hub, cS::NodeData *nodeData);
|
|
67
|
+
Group(int extensionOptions, unsigned int maxPayload, Hub *hub, cS::NodeData *nodeData, int deflateWindowBits = 15, int deflateMemLevel = 8, int deflateLevel = 1);
|
|
65
68
|
void stopListening();
|
|
66
69
|
|
|
67
70
|
public:
|
package/src/HTTPSocket.cpp
CHANGED
|
@@ -210,7 +210,7 @@ void HttpSocket<isServer>::upgrade(const char *secKey, const char *extensions, s
|
|
|
210
210
|
std::string extensionsResponse;
|
|
211
211
|
if (extensionsLength) {
|
|
212
212
|
Group<isServer> *group = Group<isServer>::from(this);
|
|
213
|
-
ExtensionsNegotiator<cWS::SERVER> extensionsNegotiator(group->extensionOptions);
|
|
213
|
+
ExtensionsNegotiator<cWS::SERVER> extensionsNegotiator(group->extensionOptions, group->deflateWindowBits);
|
|
214
214
|
extensionsNegotiator.readOffer(std::string(extensions, extensionsLength));
|
|
215
215
|
extensionsResponse = extensionsNegotiator.generateOffer();
|
|
216
216
|
if (extensionsNegotiator.getNegotiatedOptions() & PERMESSAGE_DEFLATE) {
|
|
@@ -289,7 +289,7 @@ void HttpSocket<isServer>::onEnd(cS::Socket *s) {
|
|
|
289
289
|
if (message->callback) {
|
|
290
290
|
message->callback(nullptr, message->callbackData, true, nullptr);
|
|
291
291
|
}
|
|
292
|
-
httpSocket->messageQueue.pop();
|
|
292
|
+
httpSocket->messageQueue.pop(httpSocket->nodeData);
|
|
293
293
|
}
|
|
294
294
|
|
|
295
295
|
while (httpSocket->outstandingResponsesHead) {
|
package/src/Hub.cpp
CHANGED
|
@@ -5,87 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
namespace cWS {
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
// windowBits 9..15 and memLevel 1..9 select the per-socket memory tier (zlib uses
|
|
9
|
+
// about 4x the window plus a 2^(memLevel+9) byte hash table): 15/8 = ~256 KB,
|
|
10
|
+
// 12/5 = ~32 KB, 10/3 = ~8 KB, 9/2 = ~4 KB. Smaller tiers trade ratio for memory.
|
|
11
|
+
// level 1..9: with zlib-ng level 1 is the very fast "quick" strategy (~8% worse
|
|
12
|
+
// ratio than zlib level 1), level 2 matches zlib level 1's ratio at higher speed.
|
|
13
|
+
zlib::Stream *Hub::allocateDefaultCompressor(int level, int windowBits, int memLevel) {
|
|
14
|
+
return zlib::createDeflate(level, windowBits, memLevel);
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
char *Hub::deflate(char *data, size_t &length,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
z_stream *compressor = slidingDeflateWindow ? slidingDeflateWindow : &deflationStream;
|
|
17
|
-
|
|
18
|
-
compressor->next_in = (Bytef *) data;
|
|
19
|
-
compressor->avail_in = (unsigned int) length;
|
|
20
|
-
|
|
21
|
-
// note: zlib requires more than 6 bytes with Z_SYNC_FLUSH
|
|
22
|
-
const int DEFLATE_OUTPUT_CHUNK = LARGE_BUFFER_SIZE;
|
|
23
|
-
|
|
24
|
-
int err;
|
|
25
|
-
do {
|
|
26
|
-
compressor->next_out = (Bytef *) zlibBuffer;
|
|
27
|
-
compressor->avail_out = DEFLATE_OUTPUT_CHUNK;
|
|
28
|
-
|
|
29
|
-
err = ::deflate(compressor, Z_SYNC_FLUSH);
|
|
30
|
-
if (Z_OK == err && compressor->avail_out == 0) {
|
|
31
|
-
dynamicZlibBuffer.append(zlibBuffer, DEFLATE_OUTPUT_CHUNK - compressor->avail_out);
|
|
32
|
-
continue;
|
|
33
|
-
} else {
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
} while (true);
|
|
37
|
-
|
|
38
|
-
// note: should not change avail_out
|
|
39
|
-
if (!slidingDeflateWindow) {
|
|
40
|
-
deflateReset(compressor);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (dynamicZlibBuffer.length()) {
|
|
44
|
-
dynamicZlibBuffer.append(zlibBuffer, DEFLATE_OUTPUT_CHUNK - compressor->avail_out);
|
|
45
|
-
|
|
46
|
-
length = dynamicZlibBuffer.length() - 4;
|
|
47
|
-
return (char *) dynamicZlibBuffer.data();
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
length = DEFLATE_OUTPUT_CHUNK - compressor->avail_out - 4;
|
|
51
|
-
return zlibBuffer;
|
|
17
|
+
char *Hub::deflate(char *data, size_t &length, zlib::Stream *slidingDeflateWindow) {
|
|
18
|
+
zlib::Stream *compressor = slidingDeflateWindow ? slidingDeflateWindow : deflationStream;
|
|
19
|
+
return zlib::deflate(compressor, data, length, zlibBuffer, LARGE_BUFFER_SIZE, dynamicZlibBuffer, !slidingDeflateWindow);
|
|
52
20
|
}
|
|
53
21
|
|
|
54
|
-
// todo: let's go through this code once more some time!
|
|
55
22
|
char *Hub::inflate(char *data, size_t &length, size_t maxPayload) {
|
|
56
|
-
dynamicZlibBuffer
|
|
57
|
-
|
|
58
|
-
inflationStream.next_in = (Bytef *) data;
|
|
59
|
-
inflationStream.avail_in = (unsigned int) length;
|
|
60
|
-
|
|
61
|
-
int err;
|
|
62
|
-
do {
|
|
63
|
-
inflationStream.next_out = (Bytef *) zlibBuffer;
|
|
64
|
-
inflationStream.avail_out = LARGE_BUFFER_SIZE;
|
|
65
|
-
err = ::inflate(&inflationStream, Z_FINISH);
|
|
66
|
-
if (!inflationStream.avail_in) {
|
|
67
|
-
break;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
dynamicZlibBuffer.append(zlibBuffer, LARGE_BUFFER_SIZE - inflationStream.avail_out);
|
|
71
|
-
} while (err == Z_BUF_ERROR && dynamicZlibBuffer.length() <= maxPayload);
|
|
72
|
-
|
|
73
|
-
inflateReset(&inflationStream);
|
|
74
|
-
|
|
75
|
-
if ((err != Z_BUF_ERROR && err != Z_OK) || dynamicZlibBuffer.length() > maxPayload) {
|
|
76
|
-
length = 0;
|
|
77
|
-
return nullptr;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (dynamicZlibBuffer.length()) {
|
|
81
|
-
dynamicZlibBuffer.append(zlibBuffer, LARGE_BUFFER_SIZE - inflationStream.avail_out);
|
|
82
|
-
|
|
83
|
-
length = dynamicZlibBuffer.length();
|
|
84
|
-
return (char *) dynamicZlibBuffer.data();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
length = LARGE_BUFFER_SIZE - inflationStream.avail_out;
|
|
88
|
-
return zlibBuffer;
|
|
23
|
+
return zlib::inflate(inflationStream, data, length, maxPayload, zlibBuffer, LARGE_BUFFER_SIZE, dynamicZlibBuffer);
|
|
89
24
|
}
|
|
90
25
|
|
|
91
26
|
void Hub::onServerAccept(cS::Socket *s) {
|
package/src/Hub.h
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
#include "Group.h"
|
|
5
5
|
#include "cSNode.h"
|
|
6
6
|
#include <string>
|
|
7
|
-
#include
|
|
7
|
+
#include "Zlib.h"
|
|
8
8
|
#include <mutex>
|
|
9
9
|
#include <map>
|
|
10
10
|
|
|
@@ -18,10 +18,10 @@ protected:
|
|
|
18
18
|
Group<CLIENT> *group;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
static
|
|
21
|
+
static zlib::Stream *allocateDefaultCompressor(int level = 1, int windowBits = 15, int memLevel = 8);
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
char *deflate(char *data, size_t &length,
|
|
23
|
+
zlib::Stream *inflationStream = nullptr, *deflationStream = nullptr;
|
|
24
|
+
char *deflate(char *data, size_t &length, zlib::Stream *slidingDeflateWindow);
|
|
25
25
|
char *inflate(char *data, size_t &length, size_t maxPayload);
|
|
26
26
|
char *zlibBuffer;
|
|
27
27
|
std::string dynamicZlibBuffer;
|
|
@@ -32,8 +32,8 @@ protected:
|
|
|
32
32
|
|
|
33
33
|
public:
|
|
34
34
|
template <bool isServer>
|
|
35
|
-
Group<isServer> *createGroup(int extensionOptions = 0, unsigned int maxPayload = 16777216) {
|
|
36
|
-
return new Group<isServer>(extensionOptions, maxPayload, this, nodeData);
|
|
35
|
+
Group<isServer> *createGroup(int extensionOptions = 0, unsigned int maxPayload = 16777216, int deflateWindowBits = 15, int deflateMemLevel = 8, int deflateLevel = 1) {
|
|
36
|
+
return new Group<isServer>(extensionOptions, maxPayload, this, nodeData, deflateWindowBits, deflateMemLevel, deflateLevel);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
template <bool isServer>
|
|
@@ -48,10 +48,11 @@ public:
|
|
|
48
48
|
|
|
49
49
|
Hub(int extensionOptions = 0, bool useDefaultLoop = false, unsigned int maxPayload = 16777216) : cS::Node(LARGE_BUFFER_SIZE, WebSocketProtocol<SERVER, WebSocket<SERVER>>::CONSUME_PRE_PADDING, WebSocketProtocol<SERVER, WebSocket<SERVER>>::CONSUME_POST_PADDING, useDefaultLoop),
|
|
50
50
|
Group<SERVER>(extensionOptions, maxPayload, this, nodeData), Group<CLIENT>(0, maxPayload, this, nodeData) {
|
|
51
|
-
|
|
51
|
+
inflationStream = zlib::createInflate(15);
|
|
52
52
|
zlibBuffer = new char[LARGE_BUFFER_SIZE];
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
// shared compressor for groups without a per-socket sliding window
|
|
55
|
+
deflationStream = allocateDefaultCompressor();
|
|
55
56
|
|
|
56
57
|
#ifdef CWS_THREADSAFE
|
|
57
58
|
getLoop()->preCbData = nodeData;
|
|
@@ -67,8 +68,8 @@ public:
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
~Hub() {
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
zlib::destroy(inflationStream);
|
|
72
|
+
zlib::destroy(deflationStream);
|
|
72
73
|
delete [] zlibBuffer;
|
|
73
74
|
}
|
|
74
75
|
|
package/src/Socket.h
CHANGED
|
@@ -50,20 +50,32 @@ protected:
|
|
|
50
50
|
Message *nextMessage = nullptr;
|
|
51
51
|
void (*callback)(void *socket, void *data, bool cancelled, void *reserved) = nullptr;
|
|
52
52
|
void *callbackData = nullptr, *reserved = nullptr;
|
|
53
|
+
// >= 0: block came from NodeData's small-block pool (index for freeSmallMemoryBlock);
|
|
54
|
+
// -1: heap (new char[]). Messages are allocated as raw char arrays, so every
|
|
55
|
+
// allocation site must set this explicitly (the initializers above never run).
|
|
56
|
+
int poolIndex = -1;
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
Message *head = nullptr, *tail = nullptr;
|
|
56
60
|
size_t totalLength = 0;
|
|
57
61
|
|
|
58
|
-
void
|
|
62
|
+
static void release(NodeData *nodeData, Message *message) {
|
|
63
|
+
if (message->poolIndex >= 0) {
|
|
64
|
+
nodeData->freeSmallMemoryBlock((char *) message, message->poolIndex);
|
|
65
|
+
} else {
|
|
66
|
+
delete [] (char *) message;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
void pop(NodeData *nodeData)
|
|
59
71
|
{
|
|
60
72
|
Message *nextMessage;
|
|
61
73
|
if ((nextMessage = head->nextMessage)) {
|
|
62
74
|
totalLength -= head->length;
|
|
63
|
-
|
|
75
|
+
release(nodeData, head);
|
|
64
76
|
head = nextMessage;
|
|
65
77
|
} else {
|
|
66
|
-
|
|
78
|
+
release(nodeData, head);
|
|
67
79
|
head = tail = nullptr;
|
|
68
80
|
totalLength = 0;
|
|
69
81
|
}
|
|
@@ -174,7 +186,7 @@ protected:
|
|
|
174
186
|
// which frees the queue (use-after-free otherwise).
|
|
175
187
|
auto callback = messagePtr->callback;
|
|
176
188
|
void *callbackData = messagePtr->callbackData, *reserved = messagePtr->reserved;
|
|
177
|
-
socket->messageQueue.pop();
|
|
189
|
+
socket->messageQueue.pop(socket->nodeData);
|
|
178
190
|
if (callback) {
|
|
179
191
|
callback(p, callbackData, false, reserved);
|
|
180
192
|
if (socket->isClosed()) {
|
|
@@ -256,7 +268,7 @@ protected:
|
|
|
256
268
|
// which frees the queue (use-after-free otherwise).
|
|
257
269
|
auto callback = messagePtr->callback;
|
|
258
270
|
void *callbackData = messagePtr->callbackData, *reserved = messagePtr->reserved;
|
|
259
|
-
socket->messageQueue.pop();
|
|
271
|
+
socket->messageQueue.pop(socket->nodeData);
|
|
260
272
|
if (callback) {
|
|
261
273
|
callback(p, callbackData, false, reserved);
|
|
262
274
|
if (socket->isClosed()) {
|
|
@@ -318,6 +330,10 @@ protected:
|
|
|
318
330
|
messagePtr->length = length;
|
|
319
331
|
messagePtr->data = ((char *) messagePtr) + sizeof(Queue::Message);
|
|
320
332
|
messagePtr->nextMessage = nullptr;
|
|
333
|
+
messagePtr->callback = nullptr;
|
|
334
|
+
messagePtr->callbackData = nullptr;
|
|
335
|
+
messagePtr->reserved = nullptr;
|
|
336
|
+
messagePtr->poolIndex = -1;
|
|
321
337
|
|
|
322
338
|
if (data) {
|
|
323
339
|
memcpy((char *) messagePtr->data, data, messagePtr->length);
|
|
@@ -385,10 +401,21 @@ protected:
|
|
|
385
401
|
if (corkActive()) {
|
|
386
402
|
// Frame now, write later: everything sent to this socket during the
|
|
387
403
|
// current loop iteration goes out in one gathered write (uncork()).
|
|
388
|
-
|
|
404
|
+
// Small messages come from the per-loop block pool (pop() returns them).
|
|
405
|
+
Queue::Message *messagePtr;
|
|
406
|
+
if (estimatedLength <= cS::NodeData::preAllocMaxSize) {
|
|
407
|
+
int memoryIndex = nodeData->getMemoryBlockIndex((int) estimatedLength);
|
|
408
|
+
messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
|
|
409
|
+
messagePtr->data = ((char *) messagePtr) + sizeof(Queue::Message);
|
|
410
|
+
messagePtr->poolIndex = memoryIndex;
|
|
411
|
+
} else {
|
|
412
|
+
messagePtr = allocMessage(estimatedLength - sizeof(Queue::Message));
|
|
413
|
+
}
|
|
389
414
|
messagePtr->length = T::transform(message, (char *) messagePtr->data, length, transformData);
|
|
415
|
+
messagePtr->nextMessage = nullptr;
|
|
390
416
|
messagePtr->callback = callback;
|
|
391
417
|
messagePtr->callbackData = callbackData;
|
|
418
|
+
messagePtr->reserved = nullptr;
|
|
392
419
|
enqueue(messagePtr);
|
|
393
420
|
if (!corkPending) {
|
|
394
421
|
corkPending = true;
|
|
@@ -405,6 +432,9 @@ protected:
|
|
|
405
432
|
Queue::Message *messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
|
|
406
433
|
messagePtr->data = ((char *) messagePtr) + sizeof(Queue::Message);
|
|
407
434
|
messagePtr->length = T::transform(message, (char *) messagePtr->data, length, transformData);
|
|
435
|
+
messagePtr->nextMessage = nullptr;
|
|
436
|
+
messagePtr->reserved = nullptr;
|
|
437
|
+
messagePtr->poolIndex = memoryIndex;
|
|
408
438
|
|
|
409
439
|
bool wasTransferred;
|
|
410
440
|
if (write(messagePtr, wasTransferred)) {
|
|
@@ -589,7 +619,7 @@ public:
|
|
|
589
619
|
if (callbacks && m->callback) {
|
|
590
620
|
callbacks->push_back({m->callback, m->callbackData, m->reserved});
|
|
591
621
|
}
|
|
592
|
-
messageQueue.pop();
|
|
622
|
+
messageQueue.pop(nodeData);
|
|
593
623
|
} else {
|
|
594
624
|
m->length -= sent;
|
|
595
625
|
m->data += sent;
|
package/src/WebSocket.cpp
CHANGED
|
@@ -11,7 +11,8 @@ WebSocket<isServer>::WebSocket(bool perMessageDeflate, cS::Socket *socket) : cS:
|
|
|
11
11
|
|
|
12
12
|
// if we are created in a group with sliding deflate window allocate it here
|
|
13
13
|
if (Group<isServer>::from(this)->extensionOptions & SLIDING_DEFLATE_WINDOW) {
|
|
14
|
-
|
|
14
|
+
Group<isServer> *group = Group<isServer>::from(this);
|
|
15
|
+
slidingDeflateWindow = Hub::allocateDefaultCompressor(group->deflateLevel, group->deflateWindowBits, group->deflateMemLevel);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -52,7 +53,7 @@ void WebSocket<isServer>::send(const char *message, size_t length, OpCode opCode
|
|
|
52
53
|
|
|
53
54
|
static size_t transform(const char *src, char *dst, size_t length, TransformData transformData) {
|
|
54
55
|
if (transformData.compress) {
|
|
55
|
-
char *deflated = Group<isServer>::from(transformData.s)->hub->deflate((char *) src, length, (
|
|
56
|
+
char *deflated = Group<isServer>::from(transformData.s)->hub->deflate((char *) src, length, (zlib::Stream *) transformData.s->slidingDeflateWindow);
|
|
56
57
|
return WebSocketProtocol<isServer, WebSocket<isServer>>::formatMessage(dst, deflated, length, transformData.opCode, length, true);
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -150,6 +151,9 @@ void WebSocket<isServer>::sendPrepared(typename WebSocket<isServer>::PreparedMes
|
|
|
150
151
|
Queue::Message *messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
|
|
151
152
|
messagePtr->data = preparedMessage->buffer;
|
|
152
153
|
messagePtr->length = preparedMessage->length;
|
|
154
|
+
messagePtr->nextMessage = nullptr;
|
|
155
|
+
messagePtr->reserved = nullptr;
|
|
156
|
+
messagePtr->poolIndex = memoryIndex;
|
|
153
157
|
|
|
154
158
|
bool wasTransferred;
|
|
155
159
|
if (write(messagePtr, wasTransferred)) {
|
|
@@ -194,9 +198,15 @@ cS::Socket *WebSocket<isServer>::onData(cS::Socket *s, char *data, size_t length
|
|
|
194
198
|
|
|
195
199
|
webSocket->hasOutstandingPong = false;
|
|
196
200
|
if (!webSocket->isShuttingDown()) {
|
|
197
|
-
|
|
201
|
+
// The kernel-level cork (TCP_CORK/TCP_NOPUSH) around a read only helps when
|
|
202
|
+
// replies are written immediately; with write corking they leave in one gathered
|
|
203
|
+
// write at the end of the tick, so skip the two setsockopt calls per read.
|
|
204
|
+
bool kernelCork = !webSocket->corkActive();
|
|
205
|
+
if (kernelCork) {
|
|
206
|
+
webSocket->cork(true);
|
|
207
|
+
}
|
|
198
208
|
WebSocketProtocol<isServer, WebSocket<isServer>>::consume(data, (unsigned int) length, webSocket);
|
|
199
|
-
if (!webSocket->isClosed()) {
|
|
209
|
+
if (kernelCork && !webSocket->isClosed()) {
|
|
200
210
|
webSocket->cork(false);
|
|
201
211
|
}
|
|
202
212
|
}
|
|
@@ -305,7 +315,7 @@ void WebSocket<isServer>::onEnd(cS::Socket *s) {
|
|
|
305
315
|
if (message->callback) {
|
|
306
316
|
message->callback(nullptr, message->callbackData, true, nullptr);
|
|
307
317
|
}
|
|
308
|
-
webSocket->messageQueue.pop();
|
|
318
|
+
webSocket->messageQueue.pop(webSocket->nodeData);
|
|
309
319
|
}
|
|
310
320
|
|
|
311
321
|
webSocket->nodeData->clearPendingPollChanges(webSocket);
|
|
@@ -313,8 +323,7 @@ void WebSocket<isServer>::onEnd(cS::Socket *s) {
|
|
|
313
323
|
// remove any per-websocket zlib memory
|
|
314
324
|
if (webSocket->slidingDeflateWindow) {
|
|
315
325
|
// this relates to Hub::allocateDefaultCompressor
|
|
316
|
-
|
|
317
|
-
delete (z_stream *) webSocket->slidingDeflateWindow;
|
|
326
|
+
zlib::destroy((zlib::Stream *) webSocket->slidingDeflateWindow);
|
|
318
327
|
webSocket->slidingDeflateWindow = nullptr;
|
|
319
328
|
}
|
|
320
329
|
}
|
package/src/Zlib.cpp
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#include "Zlib.h"
|
|
2
|
+
|
|
3
|
+
#ifdef CWS_ZLIB_NG
|
|
4
|
+
#include "zlib-ng.h"
|
|
5
|
+
typedef zng_stream ZStream;
|
|
6
|
+
#define ZFN(name) zng_##name
|
|
7
|
+
#define CWS_ZLIB_BACKEND "zlib-ng " ZLIBNG_VERSION
|
|
8
|
+
#else
|
|
9
|
+
#include <zlib.h>
|
|
10
|
+
typedef z_stream ZStream;
|
|
11
|
+
#define ZFN(name) name
|
|
12
|
+
#define CWS_ZLIB_BACKEND "zlib " ZLIB_VERSION
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
namespace cWS {
|
|
16
|
+
namespace zlib {
|
|
17
|
+
|
|
18
|
+
struct Stream {
|
|
19
|
+
ZStream zs;
|
|
20
|
+
bool isDeflate;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
typedef decltype(ZStream::next_in) InPtr;
|
|
24
|
+
typedef decltype(ZStream::next_out) OutPtr;
|
|
25
|
+
|
|
26
|
+
Stream *createDeflate(int level, int windowBits, int memLevel) {
|
|
27
|
+
Stream *stream = new Stream();
|
|
28
|
+
stream->isDeflate = true;
|
|
29
|
+
ZFN(deflateInit2)(&stream->zs, level, Z_DEFLATED, -windowBits, memLevel, Z_DEFAULT_STRATEGY);
|
|
30
|
+
return stream;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
Stream *createInflate(int windowBits) {
|
|
34
|
+
Stream *stream = new Stream();
|
|
35
|
+
stream->isDeflate = false;
|
|
36
|
+
ZFN(inflateInit2)(&stream->zs, -windowBits);
|
|
37
|
+
return stream;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
void reset(Stream *stream) {
|
|
41
|
+
if (stream->isDeflate) {
|
|
42
|
+
ZFN(deflateReset)(&stream->zs);
|
|
43
|
+
} else {
|
|
44
|
+
ZFN(inflateReset)(&stream->zs);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
void destroy(Stream *stream) {
|
|
49
|
+
if (!stream) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (stream->isDeflate) {
|
|
53
|
+
ZFN(deflateEnd)(&stream->zs);
|
|
54
|
+
} else {
|
|
55
|
+
ZFN(inflateEnd)(&stream->zs);
|
|
56
|
+
}
|
|
57
|
+
delete stream;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
char *deflate(Stream *stream, char *data, size_t &length, char *buffer, size_t bufferSize, std::string &dynamic, bool resetAfter) {
|
|
61
|
+
ZStream &zs = stream->zs;
|
|
62
|
+
dynamic.clear();
|
|
63
|
+
|
|
64
|
+
zs.next_in = (InPtr) data;
|
|
65
|
+
zs.avail_in = (unsigned int) length;
|
|
66
|
+
|
|
67
|
+
// note: zlib requires more than 6 bytes of output space with Z_SYNC_FLUSH
|
|
68
|
+
int err;
|
|
69
|
+
do {
|
|
70
|
+
zs.next_out = (OutPtr) buffer;
|
|
71
|
+
zs.avail_out = (unsigned int) bufferSize;
|
|
72
|
+
|
|
73
|
+
err = ZFN(deflate)(&zs, Z_SYNC_FLUSH);
|
|
74
|
+
if (Z_OK == err && zs.avail_out == 0) {
|
|
75
|
+
dynamic.append(buffer, bufferSize - zs.avail_out);
|
|
76
|
+
continue;
|
|
77
|
+
} else {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
} while (true);
|
|
81
|
+
|
|
82
|
+
// note: must not change avail_out
|
|
83
|
+
if (resetAfter) {
|
|
84
|
+
ZFN(deflateReset)(&zs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (dynamic.length()) {
|
|
88
|
+
dynamic.append(buffer, bufferSize - zs.avail_out);
|
|
89
|
+
length = dynamic.length() - 4;
|
|
90
|
+
return (char *) dynamic.data();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
length = bufferSize - zs.avail_out - 4;
|
|
94
|
+
return buffer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
char *inflate(Stream *stream, char *data, size_t &length, size_t maxPayload, char *buffer, size_t bufferSize, std::string &dynamic) {
|
|
98
|
+
ZStream &zs = stream->zs;
|
|
99
|
+
dynamic.clear();
|
|
100
|
+
|
|
101
|
+
zs.next_in = (InPtr) data;
|
|
102
|
+
zs.avail_in = (unsigned int) length;
|
|
103
|
+
|
|
104
|
+
int err;
|
|
105
|
+
do {
|
|
106
|
+
zs.next_out = (OutPtr) buffer;
|
|
107
|
+
zs.avail_out = (unsigned int) bufferSize;
|
|
108
|
+
err = ZFN(inflate)(&zs, Z_FINISH);
|
|
109
|
+
// Z_STREAM_END: the sender ended the message with a BFINAL=1 block, which
|
|
110
|
+
// RFC 7692 section 7.2.3.4 permits (libdeflate and some non-zlib clients do
|
|
111
|
+
// this); anything left in the input is the 4-byte tail and is ignored.
|
|
112
|
+
if (!zs.avail_in || err == Z_STREAM_END) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
dynamic.append(buffer, bufferSize - zs.avail_out);
|
|
117
|
+
} while (err == Z_BUF_ERROR && dynamic.length() <= maxPayload);
|
|
118
|
+
|
|
119
|
+
ZFN(inflateReset)(&zs);
|
|
120
|
+
|
|
121
|
+
if ((err != Z_BUF_ERROR && err != Z_OK && err != Z_STREAM_END) || dynamic.length() > maxPayload) {
|
|
122
|
+
length = 0;
|
|
123
|
+
return nullptr;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (dynamic.length()) {
|
|
127
|
+
dynamic.append(buffer, bufferSize - zs.avail_out);
|
|
128
|
+
length = dynamic.length();
|
|
129
|
+
return (char *) dynamic.data();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
length = bufferSize - zs.avail_out;
|
|
133
|
+
return buffer;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const char *backend() {
|
|
137
|
+
return CWS_ZLIB_BACKEND;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/Zlib.h
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#ifndef CWS_ZLIB_H
|
|
2
|
+
#define CWS_ZLIB_H
|
|
3
|
+
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
#include <string>
|
|
6
|
+
|
|
7
|
+
// Thin backend-neutral wrapper around the DEFLATE library. Built with
|
|
8
|
+
// -DCWS_ZLIB_NG (the prebuilt bindings) it uses the vendored zlib-ng through
|
|
9
|
+
// its native zng_ API, so nothing collides with the zlib inside Node; without
|
|
10
|
+
// the define (node-gyp fallback) it uses zlib.h from the Node headers. Zlib.cpp
|
|
11
|
+
// is the only translation unit that includes either header.
|
|
12
|
+
namespace cWS {
|
|
13
|
+
namespace zlib {
|
|
14
|
+
|
|
15
|
+
struct Stream;
|
|
16
|
+
|
|
17
|
+
Stream *createDeflate(int level, int windowBits, int memLevel);
|
|
18
|
+
Stream *createInflate(int windowBits = 15);
|
|
19
|
+
void reset(Stream *stream);
|
|
20
|
+
void destroy(Stream *stream);
|
|
21
|
+
|
|
22
|
+
// Deflates one message with a sync flush. Output lands in `buffer` (bufferSize
|
|
23
|
+
// bytes) or, when it does not fit, is accumulated in `dynamic`. Returns the output
|
|
24
|
+
// pointer and sets `length` to its size minus the 4-byte sync-flush tail
|
|
25
|
+
// (RFC 7692). With `resetAfter` the stream forgets the message (no context takeover).
|
|
26
|
+
char *deflate(Stream *stream, char *data, size_t &length, char *buffer, size_t bufferSize, std::string &dynamic, bool resetAfter);
|
|
27
|
+
|
|
28
|
+
// Inflates one message (the 4-byte tail must already be appended). Returns nullptr
|
|
29
|
+
// and length 0 on error or when the output would exceed maxPayload. Always resets.
|
|
30
|
+
char *inflate(Stream *stream, char *data, size_t &length, size_t maxPayload, char *buffer, size_t bufferSize, std::string &dynamic);
|
|
31
|
+
|
|
32
|
+
// e.g. "zlib-ng 2.2.4" or "zlib 1.3.1"
|
|
33
|
+
const char *backend();
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#endif // CWS_ZLIB_H
|