@encharm/cws 4.10.0 → 4.11.0
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 +4 -0
- package/CLAUDE.md +4 -0
- package/README.md +15 -0
- 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 +3 -1
- package/dist/client.js +10 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/prepared.d.ts +6 -0
- package/dist/prepared.js +17 -0
- package/package.json +1 -1
- package/src/Addon.h +41 -0
- package/src/WebSocket.cpp +149 -0
- package/src/WebSocket.h +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## Released 4.11.0
|
|
2
|
+
* Prepared messages for fan-out: `new PreparedMessage(bytes)` copies a payload into native memory once; `ws.send(prepared, { prefix })` sends `prefix + payload` as one frame without copying or compressing the payload per socket. Uncompressed, the payload goes out as a second gather buffer. Compressed (shared compressor mode), the prefix is emitted as a DEFLATE stored block ahead of the payload's deflate blocks, which are built once on the first compressed send and cached on the handle; standard inflaters accept the result unchanged. Sockets with context takeover and client sockets take the regular copying path. Measured with 300 subscribers and a 3 KB payload: 0.9 -> 0.2 µs of JS-thread time per socket, and no per-socket compression at all.
|
|
3
|
+
* `send()` accepts any `ArrayBufferView` (typed arrays are sent from their own offset, no `Buffer.from` copy needed).
|
|
4
|
+
|
|
1
5
|
## Released 4.10.0
|
|
2
6
|
* microdeflate: a built-in ~150-line raw-DEFLATE encoder (fixed Huffman, greedy LZ77, no stream state, no per-message hash reset) now compresses independent messages, i.e. the shared-compressor mode. Measured on a real BSON RPC stream: same ratio as zlib-ng level 1 (2.84 vs 2.85) at ~1.7x its speed; a compressed 2 KB message costs 4.6 µs of worker CPU instead of 6.0. Output is standard DEFLATE (round-trip tested against zlib's inflater). `CWS_MICRO_DEFLATE=0` falls back to zlib-ng; `zlibBackend` reports `+ microdeflate` when active. Dedicated windows (context takeover) and inflate stay on zlib-ng.
|
|
3
7
|
* Compression moved to the send worker: `send()` of a compressed message queues the raw payload and the worker deflates + frames it. Main-thread cost of a compressed 2 KB RPC message drops from ~6 µs to ~0.7 µs (Linux, per-thread measurement); wire output is byte-identical. Main-thread write paths (same-tick terminate, full worker queue, drain loop after a short write) deflate pending messages themselves first.
|
package/CLAUDE.md
CHANGED
|
@@ -76,6 +76,10 @@ The client side never touches Node sockets: `native.connect(clientGroup, url, ws
|
|
|
76
76
|
|
|
77
77
|
`SendWorker` (`src/SendWorker.cpp`) starts one `std::thread` at addon load when corking is enabled. `Socket::uncork` moves up to 512 queued frames into a `Socket::SendOp` (ownership moves with them) and hands it over through `deps/readerwriterqueue` (blocking SPSC main→worker, plain SPSC worker→main plus a `uv_async`). The worker only calls `sendmsg`/`WSASend` and fills `result`/`error`; `Socket::sendComplete` on the main thread pops what was sent, requeues the rest at the head, resubmits if more queued, arms `UV_WRITABLE` for the classic drain loop on a short write, runs send callbacks, and calls `endCb` (= `STATE::onEnd`, set in `setState`) on a hard error. While an op is in flight `write()` appends to the queue and the drain loop stays out. `closeSocket` orphans an in-flight op (`socket = nullptr`, `closeFd = true`) and lets the completion close the fd, so a reused fd number can never receive the old socket's bytes. SSL sockets never use the worker. `CWS_SEND_THREAD=0` disables. Compressed sends: `WebSocket::send` queues the raw payload as a `compressPending` message (`enqueueCompressPending`); `performSend` deflates + frames it on the worker with the socket's window (`op->deflateWindow`) or the worker's own shared compressor, then builds the iovecs. Any main-thread write path calls `materializePending()` first (`WebSocket::materialize` via `materializeCb`, using the hub's compressor). A socket closed mid-flight hands its window to the op (`destroyWindow`, `workerOwnsWindow`) so `onEnd` does not free it under the worker.
|
|
78
78
|
|
|
79
|
+
### Prepared (shared) messages
|
|
80
|
+
|
|
81
|
+
`cWS::SharedPayload` (`src/WebSocket.h`) holds one payload for many recipients: the raw bytes plus, after the first compressed send, their deflate blocks (`Hub::deflate` with no window, i.e. the shared compressor; sync-flush tail stripped like every compressed message). `WebSocket::sendShared(prefix, payload, ...)` queues one frame as two `Queue::Message`s: an owned one with the frame header and the prefix, and a borrowed one (`ownsData = false`) pointing into the payload whose completion callback (`sharedSent`) drops the payload reference and then runs the user's callback. Compressed, the prefix becomes DEFLATE stored blocks (`00 LEN NLEN`, BFINAL=0) ahead of the cached blocks; that is only valid without context takeover, so a socket with a `slidingDeflateWindow`, the client role (masking) and an empty payload fall back to a contiguous `send()`. JS: `PreparedMessage` (`lib/prepared.ts`, native handle released through a `FinalizationRegistry`) and `WebSocket.send` routes a `PreparedMessage` (with the optional `prefix` option) to `native.sendShared`.
|
|
82
|
+
|
|
79
83
|
### Things that are easy to get wrong
|
|
80
84
|
|
|
81
85
|
- `dist/*.js` and `dist/bindings/*.node` are committed. Rebuild TS with `npm run build-ts` and commit the output; binaries must be rebuilt on each platform when C++ changes.
|
package/README.md
CHANGED
|
@@ -294,6 +294,21 @@ With permessage-deflate enabled, compression of outgoing messages also runs on t
|
|
|
294
294
|
|
|
295
295
|
Set `CWS_SEND_THREAD=0` to disable it (sends and compression then happen on the main thread at the end of the tick). The `sendThread` export reports `'active'` or the reason it is not. TLS sockets always send on the main thread.
|
|
296
296
|
|
|
297
|
+
### Prepared messages (fan-out)
|
|
298
|
+
|
|
299
|
+
When one payload goes to many sockets with only a small per-socket prefix in front of it (an RPC header, a subscription id), prepare it once:
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
const { PreparedMessage } = require('@encharm/cws');
|
|
303
|
+
|
|
304
|
+
const prepared = new PreparedMessage(payloadBytes); // copied into native memory once
|
|
305
|
+
for (const ws of subscribers) {
|
|
306
|
+
ws.send(prepared, { prefix: headerFor(ws) }); // one frame: header + payload
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`send` accepts a `PreparedMessage` wherever it accepts a buffer, plus a `prefix` option (a per-socket header, string or bytes) spliced in front of the payload inside the same frame. `binary` defaults to `true` for prepared messages; `compress` defaults to the server's `threshold` applied to the total length. The payload is never copied or compressed per socket: without deflate it is a second gather buffer in the end-of-tick write; with the shared compressor it is deflated once, on the first compressed send, and the prefix is spliced in front as a DEFLATE stored block. Sockets negotiated with context takeover (`serverNoContextTakeover: false`) and client sockets fall back to the regular path. Handles are released when garbage collected; sends in flight keep their own reference.
|
|
311
|
+
|
|
297
312
|
### Secure WebSocket
|
|
298
313
|
You can use `wss://` with `cws` by providing `https` server to `cws` and setting `secureProtocol` on https options:
|
|
299
314
|
|
|
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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { WebSocketServer } from './server';
|
|
3
3
|
import { SocketAddress, ServerConfigs } from './index';
|
|
4
|
+
import { PreparedMessage } from './prepared';
|
|
4
5
|
export declare class WebSocket {
|
|
5
6
|
url: string;
|
|
6
7
|
private options;
|
|
@@ -28,9 +29,10 @@ export declare class WebSocket {
|
|
|
28
29
|
on(event: 'error', listener: (err: Error) => void): void;
|
|
29
30
|
on(event: 'message', listener: (message: string | any) => void): void;
|
|
30
31
|
on(event: 'close', listener: (code?: number, reason?: string) => void): void;
|
|
31
|
-
send(message: string | Buffer, options?: {
|
|
32
|
+
send(message: string | Buffer | ArrayBufferView | PreparedMessage, options?: {
|
|
32
33
|
binary?: boolean;
|
|
33
34
|
compress?: boolean;
|
|
35
|
+
prefix?: string | Buffer | ArrayBufferView;
|
|
34
36
|
}, cb?: (err?: Error) => void): void;
|
|
35
37
|
ping(message?: string | Buffer): void;
|
|
36
38
|
close(code?: number, reason?: string): void;
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WebSocket = void 0;
|
|
4
4
|
const server_1 = require("./server");
|
|
5
5
|
const shared_1 = require("./shared");
|
|
6
|
+
const prepared_1 = require("./prepared");
|
|
6
7
|
const clientGroup = shared_1.native.client.group.create(0, shared_1.DEFAULT_PAYLOAD_LIMIT);
|
|
7
8
|
function messageByteLength(message) {
|
|
8
9
|
if (typeof message === 'string') {
|
|
@@ -78,6 +79,7 @@ class WebSocket {
|
|
|
78
79
|
}
|
|
79
80
|
send(message, options, cb) {
|
|
80
81
|
if (this.external) {
|
|
82
|
+
const prepared = message instanceof prepared_1.PreparedMessage;
|
|
81
83
|
let opCode = typeof message === 'string' ? shared_1.OPCODE_TEXT : shared_1.OPCODE_BINARY;
|
|
82
84
|
if (options && options.binary === false) {
|
|
83
85
|
opCode = shared_1.OPCODE_TEXT;
|
|
@@ -90,9 +92,15 @@ class WebSocket {
|
|
|
90
92
|
compress = !!options.compress;
|
|
91
93
|
}
|
|
92
94
|
else if (this.compressThreshold !== undefined) {
|
|
93
|
-
compress = messageByteLength(message) >= this.compressThreshold;
|
|
95
|
+
compress = messageByteLength(message) + (options && options.prefix ? messageByteLength(options.prefix) : 0) >= this.compressThreshold;
|
|
96
|
+
}
|
|
97
|
+
const callback = cb ? () => process.nextTick(cb) : null;
|
|
98
|
+
if (prepared) {
|
|
99
|
+
this.nativeApi.sendShared(this.external, options && options.prefix, message.external, opCode, callback, compress);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.nativeApi.send(this.external, message, opCode, callback, compress);
|
|
94
103
|
}
|
|
95
|
-
this.nativeApi.send(this.external, message, opCode, cb ? () => process.nextTick(cb) : null, compress);
|
|
96
104
|
}
|
|
97
105
|
else if (cb) {
|
|
98
106
|
cb(new Error('Socket not connected'));
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ export declare type ServerConfigs = {
|
|
|
30
30
|
verifyClient?: (info: ConnectionInfo, next: VerifyClientNext) => void;
|
|
31
31
|
};
|
|
32
32
|
export { WebSocket } from './client';
|
|
33
|
+
export { PreparedMessage } from './prepared';
|
|
33
34
|
export { WebSocketServer } from './server';
|
|
34
35
|
export declare const secureProtocol: string;
|
|
35
36
|
export { zlibBackend, sendThread } from './shared';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.secureProtocol = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "WebSocket", { enumerable: true, get: function () { return client_1.WebSocket; } });
|
|
6
|
+
var prepared_1 = require("./prepared");
|
|
7
|
+
Object.defineProperty(exports, "PreparedMessage", { enumerable: true, get: function () { return prepared_1.PreparedMessage; } });
|
|
6
8
|
var server_1 = require("./server");
|
|
7
9
|
Object.defineProperty(exports, "WebSocketServer", { enumerable: true, get: function () { return server_1.WebSocketServer; } });
|
|
8
10
|
exports.secureProtocol = 'TLSv1_2_method';
|
package/dist/prepared.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PreparedMessage = void 0;
|
|
4
|
+
const shared_1 = require("./shared");
|
|
5
|
+
const preparedRegistry = typeof global.FinalizationRegistry === 'function'
|
|
6
|
+
? new global.FinalizationRegistry((external) => shared_1.native.server.releaseShared(external))
|
|
7
|
+
: undefined;
|
|
8
|
+
class PreparedMessage {
|
|
9
|
+
constructor(data) {
|
|
10
|
+
this.byteLength = data.byteLength;
|
|
11
|
+
this.external = shared_1.native.server.prepareShared(data);
|
|
12
|
+
if (preparedRegistry) {
|
|
13
|
+
preparedRegistry.register(this, this.external);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.PreparedMessage = PreparedMessage;
|
package/package.json
CHANGED
package/src/Addon.h
CHANGED
|
@@ -644,6 +644,44 @@ void sendPrepared(const FunctionCallbackInfo<Value> &args) {
|
|
|
644
644
|
->Value());
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
+
template <bool isServer>
|
|
648
|
+
void prepareShared(const FunctionCallbackInfo<Value> &args) {
|
|
649
|
+
NativeString nativeString(args.GetIsolate(), args[0]);
|
|
650
|
+
args.GetReturnValue().Set(External::New(
|
|
651
|
+
args.GetIsolate(),
|
|
652
|
+
cWS::WebSocket<isServer>::prepareShared(nativeString.getData(),
|
|
653
|
+
nativeString.getLength())));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
template <bool isServer>
|
|
657
|
+
void releaseShared(const FunctionCallbackInfo<Value> &args) {
|
|
658
|
+
cWS::SharedPayload::unref(
|
|
659
|
+
(cWS::SharedPayload *)args[0].As<External>()->Value());
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// (socket, prefix, payload, opCode, callback, compress)
|
|
663
|
+
template <bool isServer>
|
|
664
|
+
void sendShared(const FunctionCallbackInfo<Value> &args) {
|
|
665
|
+
NativeString prefix(args.GetIsolate(), args[1]);
|
|
666
|
+
cWS::SharedPayload *payload =
|
|
667
|
+
(cWS::SharedPayload *)args[2].As<External>()->Value();
|
|
668
|
+
cWS::OpCode opCode = (cWS::OpCode)args[3].As<Integer>()->Value();
|
|
669
|
+
|
|
670
|
+
SendCallbackData *sc = nullptr;
|
|
671
|
+
void (*callback)(cWS::WebSocket<isServer> *, void *, bool, void *) = nullptr;
|
|
672
|
+
if (args[4]->IsFunction()) {
|
|
673
|
+
callback = sendCallback;
|
|
674
|
+
sc = new SendCallbackData;
|
|
675
|
+
sc->jsCallback.Reset(args.GetIsolate(), Local<Function>::Cast(args[4]));
|
|
676
|
+
sc->isolate = args.GetIsolate();
|
|
677
|
+
}
|
|
678
|
+
bool compress = args[5].As<Boolean>()->Value();
|
|
679
|
+
|
|
680
|
+
unwrapSocket<isServer>(args[0].As<External>())
|
|
681
|
+
->sendShared(prefix.getData(), prefix.getLength(), payload, opCode,
|
|
682
|
+
compress, callback, sc);
|
|
683
|
+
}
|
|
684
|
+
|
|
647
685
|
template <bool isServer>
|
|
648
686
|
void finalizeMessage(const FunctionCallbackInfo<Value> &args) {
|
|
649
687
|
cWS::WebSocket<isServer>::finalizeMessage(
|
|
@@ -723,6 +761,9 @@ struct Namespace {
|
|
|
723
761
|
NODE_SET_METHOD(object, "prepareMessage", prepareMessage<isServer>);
|
|
724
762
|
NODE_SET_METHOD(object, "sendPrepared", sendPrepared<isServer>);
|
|
725
763
|
NODE_SET_METHOD(object, "finalizeMessage", finalizeMessage<isServer>);
|
|
764
|
+
NODE_SET_METHOD(object, "prepareShared", prepareShared<isServer>);
|
|
765
|
+
NODE_SET_METHOD(object, "sendShared", sendShared<isServer>);
|
|
766
|
+
NODE_SET_METHOD(object, "releaseShared", releaseShared<isServer>);
|
|
726
767
|
|
|
727
768
|
Local<Object> group = Object::New(isolate);
|
|
728
769
|
NODE_SET_METHOD(group, "onConnection", onConnection<isServer>);
|
package/src/WebSocket.cpp
CHANGED
|
@@ -82,6 +82,155 @@ void WebSocket<isServer>::materialize(cS::Socket *s, cS::Socket::Queue::Message
|
|
|
82
82
|
cS::Socket::materializeOnMain(s, m, stream, !webSocket->slidingDeflateWindow, hub->zlibBuffer, Hub::LARGE_BUFFER_SIZE, hub->dynamicZlibBuffer);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
template <bool isServer>
|
|
86
|
+
SharedPayload *WebSocket<isServer>::prepareShared(const char *data, size_t length) {
|
|
87
|
+
SharedPayload *payload = new SharedPayload;
|
|
88
|
+
payload->raw.assign(data, length);
|
|
89
|
+
return payload;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
namespace {
|
|
93
|
+
struct SharedCallback {
|
|
94
|
+
void (*callback)(void *socket, void *data, bool cancelled, void *reserved);
|
|
95
|
+
void *data;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Completion of the shared part of a sendShared() frame: drops the payload reference,
|
|
99
|
+
// then runs the caller's callback. `socket` is null when the send was orphaned by a close.
|
|
100
|
+
void sharedSent(void *socket, void *data, bool cancelled, void *reserved) {
|
|
101
|
+
SharedPayload::unref((SharedPayload *) data);
|
|
102
|
+
if (reserved) {
|
|
103
|
+
SharedCallback *c = (SharedCallback *) reserved;
|
|
104
|
+
c->callback(socket, c->data, cancelled, nullptr);
|
|
105
|
+
delete c;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Sends prefix + payload as one frame. The frame is queued as two messages: an owned
|
|
112
|
+
* one with the header and the prefix, and a borrowed one pointing into the payload, so
|
|
113
|
+
* nothing is copied or compressed per recipient.
|
|
114
|
+
*
|
|
115
|
+
* Compressed: the prefix goes out as DEFLATE stored blocks in front of the payload's
|
|
116
|
+
* pre-built blocks. That is valid because those blocks were produced from the payload
|
|
117
|
+
* alone, so no match reaches back before their start. It needs a socket without context
|
|
118
|
+
* takeover (no sliding window): with takeover the per-socket window would not contain
|
|
119
|
+
* these bytes, so that case, like the client role (masking rewrites every byte), takes
|
|
120
|
+
* the regular copying path.
|
|
121
|
+
*/
|
|
122
|
+
template <bool isServer>
|
|
123
|
+
void WebSocket<isServer>::sendShared(const char *prefix, size_t prefixLength, SharedPayload *payload, OpCode opCode, bool compress,
|
|
124
|
+
void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved), void *callbackData) {
|
|
125
|
+
bool deflate = compress && compressionStatus == WebSocket<isServer>::CompressionStatus::ENABLED && opCode < 3;
|
|
126
|
+
if (!isServer || (deflate && slidingDeflateWindow) || payload->raw.empty()) {
|
|
127
|
+
std::string whole;
|
|
128
|
+
whole.reserve(prefixLength + payload->raw.size());
|
|
129
|
+
whole.append(prefix, prefixLength).append(payload->raw);
|
|
130
|
+
send(whole.data(), whole.size(), opCode, callback, callbackData, compress);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (isClosed()) {
|
|
134
|
+
if (callback) {
|
|
135
|
+
callback(this, callbackData, true, nullptr);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const char *body;
|
|
141
|
+
size_t bodyLength;
|
|
142
|
+
if (deflate) {
|
|
143
|
+
if (!payload->deflatedReady) {
|
|
144
|
+
size_t length = payload->raw.size();
|
|
145
|
+
char *out = Group<isServer>::from(this)->hub->deflate((char *) payload->raw.data(), length, nullptr);
|
|
146
|
+
payload->deflated.assign(out, length);
|
|
147
|
+
payload->deflatedReady = true;
|
|
148
|
+
}
|
|
149
|
+
body = payload->deflated.data();
|
|
150
|
+
bodyLength = payload->deflated.size();
|
|
151
|
+
} else {
|
|
152
|
+
body = payload->raw.data();
|
|
153
|
+
bodyLength = payload->raw.size();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// a stored block carries at most 65535 bytes: 1 byte header, LEN, NLEN
|
|
157
|
+
const size_t STORED_MAX = 65535;
|
|
158
|
+
size_t storedBlocks = deflate ? (prefixLength + STORED_MAX - 1) / STORED_MAX : 0;
|
|
159
|
+
size_t prefixPart = prefixLength + storedBlocks * 5;
|
|
160
|
+
const size_t MAX_HEADER = 14;
|
|
161
|
+
|
|
162
|
+
Queue::Message *first = allocMessage(MAX_HEADER + prefixPart);
|
|
163
|
+
char *dst = (char *) first->data;
|
|
164
|
+
char *p = dst + WebSocketProtocol<isServer, WebSocket<isServer>>::formatMessage(dst, dst, 0, opCode, prefixPart + bodyLength, deflate);
|
|
165
|
+
if (deflate) {
|
|
166
|
+
const char *src = prefix;
|
|
167
|
+
for (size_t remaining = prefixLength; remaining; ) {
|
|
168
|
+
size_t n = remaining > STORED_MAX ? STORED_MAX : remaining;
|
|
169
|
+
*p++ = 0; // BFINAL=0, BTYPE=00, already byte aligned
|
|
170
|
+
*p++ = (char) (n & 0xff);
|
|
171
|
+
*p++ = (char) (n >> 8);
|
|
172
|
+
*p++ = (char) (~n & 0xff);
|
|
173
|
+
*p++ = (char) ((~n >> 8) & 0xff);
|
|
174
|
+
memcpy(p, src, n);
|
|
175
|
+
p += n;
|
|
176
|
+
src += n;
|
|
177
|
+
remaining -= n;
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
memcpy(p, prefix, prefixLength);
|
|
181
|
+
p += prefixLength;
|
|
182
|
+
}
|
|
183
|
+
first->length = p - dst;
|
|
184
|
+
|
|
185
|
+
int memoryIndex = nodeData->getMemoryBlockIndex(sizeof(Queue::Message));
|
|
186
|
+
Queue::Message *second = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
|
|
187
|
+
second->data = body;
|
|
188
|
+
second->length = bodyLength;
|
|
189
|
+
second->nextMessage = nullptr;
|
|
190
|
+
second->poolIndex = memoryIndex;
|
|
191
|
+
second->ownsData = false;
|
|
192
|
+
second->compressPending = false;
|
|
193
|
+
second->opCode = 0;
|
|
194
|
+
second->callback = sharedSent;
|
|
195
|
+
second->callbackData = payload;
|
|
196
|
+
second->reserved = callback ? new SharedCallback{(void(*)(void *, void *, bool, void *)) callback, callbackData} : nullptr;
|
|
197
|
+
payload->references++;
|
|
198
|
+
|
|
199
|
+
if (corkActive()) {
|
|
200
|
+
enqueue(first);
|
|
201
|
+
enqueue(second);
|
|
202
|
+
if (!corkPending) {
|
|
203
|
+
corkPending = true;
|
|
204
|
+
nodeData->corkState->pending.push_back(this);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// no write corking (CWS_CORK=0, TLS): write now, queue what does not fit, keep order
|
|
210
|
+
bool wasTransferred;
|
|
211
|
+
if (!write(first, wasTransferred)) {
|
|
212
|
+
freeMessage(first);
|
|
213
|
+
void *reserved = second->reserved;
|
|
214
|
+
nodeData->freeSmallMemoryBlock((char *) second, memoryIndex);
|
|
215
|
+
sharedSent(this, payload, true, reserved);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!wasTransferred) {
|
|
219
|
+
freeMessage(first);
|
|
220
|
+
}
|
|
221
|
+
if (!write(second, wasTransferred)) {
|
|
222
|
+
void *reserved = second->reserved;
|
|
223
|
+
nodeData->freeSmallMemoryBlock((char *) second, memoryIndex);
|
|
224
|
+
sharedSent(this, payload, true, reserved);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (!wasTransferred) {
|
|
228
|
+
void *reserved = second->reserved;
|
|
229
|
+
nodeData->freeSmallMemoryBlock((char *) second, memoryIndex);
|
|
230
|
+
sharedSent(this, payload, false, reserved);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
85
234
|
/*
|
|
86
235
|
* Prepares a single message for use with sendPrepared.
|
|
87
236
|
*
|
package/src/WebSocket.h
CHANGED
|
@@ -12,6 +12,23 @@ struct Group;
|
|
|
12
12
|
template <bool isServer>
|
|
13
13
|
struct HttpSocket;
|
|
14
14
|
|
|
15
|
+
// A payload prepared once and sent to many sockets with sendShared() (fan-out). Holds the
|
|
16
|
+
// raw bytes and, after the first compressed send, their DEFLATE blocks: the same bytes a
|
|
17
|
+
// regular compressed send would carry (sync-flush tail stripped). Reference counted: the
|
|
18
|
+
// creator holds one reference, every queued message holds one until it completes.
|
|
19
|
+
struct SharedPayload {
|
|
20
|
+
std::string raw;
|
|
21
|
+
std::string deflated;
|
|
22
|
+
bool deflatedReady = false;
|
|
23
|
+
int references = 1;
|
|
24
|
+
|
|
25
|
+
static void unref(SharedPayload *payload) {
|
|
26
|
+
if (!--payload->references) {
|
|
27
|
+
delete payload;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
15
32
|
template <const bool isServer>
|
|
16
33
|
struct WIN32_EXPORT WebSocket : cS::Socket, WebSocketState<isServer> {
|
|
17
34
|
protected:
|
|
@@ -74,6 +91,10 @@ public:
|
|
|
74
91
|
void ping(const char *message) {send(message, OpCode::PING);}
|
|
75
92
|
void send(const char *message, OpCode opCode = OpCode::TEXT) {send(message, strlen(message), opCode);}
|
|
76
93
|
void send(const char *message, size_t length, OpCode opCode, void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr, void *callbackData = nullptr, bool compress = false);
|
|
94
|
+
static SharedPayload *prepareShared(const char *data, size_t length);
|
|
95
|
+
// One frame carrying prefix + payload without copying or re-compressing the payload. Not thread safe.
|
|
96
|
+
void sendShared(const char *prefix, size_t prefixLength, SharedPayload *payload, OpCode opCode, bool compress,
|
|
97
|
+
void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr, void *callbackData = nullptr);
|
|
77
98
|
static PreparedMessage *prepareMessage(char *data, size_t length, OpCode opCode, bool compressed, void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr);
|
|
78
99
|
static PreparedMessage *prepareMessageBatch(std::vector<std::string> &messages, std::vector<int> &excludedMessages,
|
|
79
100
|
OpCode opCode, bool compressed, void(*callback)(WebSocket<isServer> *webSocket, void *data, bool cancelled, void *reserved) = nullptr);
|