@encharm/cws 4.8.3 → 4.10.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 +10 -0
- package/CLAUDE.md +6 -2
- package/README.md +13 -0
- package/binding.gyp +4 -2
- package/deps/readerwriterqueue/LICENSE.md +28 -0
- package/deps/readerwriterqueue/README.md +186 -0
- package/deps/readerwriterqueue/atomicops.h +761 -0
- package/deps/readerwriterqueue/readerwriterqueue.h +979 -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/index.d.ts +1 -1
- package/dist/index.js +1 -0
- package/dist/shared.d.ts +1 -0
- package/dist/shared.js +2 -1
- package/package.json +2 -2
- package/scripts/install.js +21 -0
- package/src/Addon.cpp +2 -0
- package/src/Addon.h +6 -0
- package/src/Hub.cpp +4 -2
- package/src/MicroDeflate.h +109 -0
- package/src/Networking.h +5 -2
- package/src/SendWorker.cpp +229 -0
- package/src/SendWorker.h +25 -0
- package/src/Socket.h +195 -4
- package/src/WebSocket.cpp +20 -2
- package/src/WebSocket.h +1 -0
- package/src/Zlib.cpp +33 -1
- package/src/Zlib.h +6 -0
|
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/index.d.ts
CHANGED
|
@@ -32,4 +32,4 @@ export declare type ServerConfigs = {
|
|
|
32
32
|
export { WebSocket } from './client';
|
|
33
33
|
export { WebSocketServer } from './server';
|
|
34
34
|
export declare const secureProtocol: string;
|
|
35
|
-
export { zlibBackend } from './shared';
|
|
35
|
+
export { zlibBackend, sendThread } from './shared';
|
package/dist/index.js
CHANGED
|
@@ -8,3 +8,4 @@ Object.defineProperty(exports, "WebSocketServer", { enumerable: true, get: funct
|
|
|
8
8
|
exports.secureProtocol = 'TLSv1_2_method';
|
|
9
9
|
var shared_1 = require("./shared");
|
|
10
10
|
Object.defineProperty(exports, "zlibBackend", { enumerable: true, get: function () { return shared_1.zlibBackend; } });
|
|
11
|
+
Object.defineProperty(exports, "sendThread", { enumerable: true, get: function () { return shared_1.sendThread; } });
|
package/dist/shared.d.ts
CHANGED
|
@@ -10,4 +10,5 @@ export declare const SLIDING_DEFLATE_WINDOW: number;
|
|
|
10
10
|
export declare const DEFAULT_PAYLOAD_LIMIT: number;
|
|
11
11
|
export declare const native: any;
|
|
12
12
|
export declare const zlibBackend: string;
|
|
13
|
+
export declare const sendThread: string;
|
|
13
14
|
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.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;
|
|
3
|
+
exports.setupNative = exports.sendThread = 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;
|
|
@@ -20,6 +20,7 @@ exports.native = (() => {
|
|
|
20
20
|
}
|
|
21
21
|
})();
|
|
22
22
|
exports.zlibBackend = exports.native.zlibBackend;
|
|
23
|
+
exports.sendThread = exports.native.sendThread;
|
|
23
24
|
function setupNative(group, type, wsServer) {
|
|
24
25
|
exports.native.setNoop(exports.noop);
|
|
25
26
|
exports.native[type].group.onConnection(group, (external) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@encharm/cws",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.10.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"description": "cWS - fast C++ WebSocket implementation for Node.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"lint": "tslint -c tslint.json 'lib/**/*.ts'",
|
|
9
9
|
"test": "nyc mocha './tests/**/*.test.ts' --exit",
|
|
10
10
|
"clean": "rimraf dist/bindings/*",
|
|
11
|
-
"install": "node
|
|
11
|
+
"install": "node scripts/install.js",
|
|
12
12
|
"build-ts": "rimraf dist/*.js && rimraf dist/*.ts && npm run lint && tsc",
|
|
13
13
|
"build-cpp": "node-gyp rebuild > build_log.txt 2>&1 || exit 0"
|
|
14
14
|
},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// npm/yarn `install` hook. The package ships prebuilt bindings under dist/bindings; only
|
|
2
|
+
// fall back to a node-gyp source build (against Node's own zlib, no zlib-ng) when none
|
|
3
|
+
// matches this platform and Node ABI. Set CWS_FORCE_BUILD=1 to build regardless.
|
|
4
|
+
// The build is best effort: failures are logged to build_log.txt and never fail the install.
|
|
5
|
+
'use strict';
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const root = path.join(__dirname, '..');
|
|
11
|
+
const binding = path.join(root, 'dist', 'bindings', `cws_${process.platform}_${process.arch}_node${process.versions.modules}.node`);
|
|
12
|
+
|
|
13
|
+
if (fs.existsSync(binding) && !process.env.CWS_FORCE_BUILD) {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const log = fs.openSync(path.join(root, 'build_log.txt'), 'w');
|
|
18
|
+
fs.writeSync(log, process.env.CWS_FORCE_BUILD ? `CWS_FORCE_BUILD set, building from source (prebuilt: ${binding})\n` : `no prebuilt binding at ${binding}, building from source\n`);
|
|
19
|
+
spawnSync('node-gyp', ['rebuild'], { cwd: root, stdio: ['ignore', log, log], shell: true });
|
|
20
|
+
fs.closeSync(log);
|
|
21
|
+
process.exit(0);
|
package/src/Addon.cpp
CHANGED
|
@@ -24,6 +24,8 @@ void Initialize(Local<Object> exports) {
|
|
|
24
24
|
String::NewFromUtf8(isolate, cWS::zlib::backend()).ToLocalChecked()).Check();
|
|
25
25
|
hub.getNodeData()->corkState->enabled = corkEnabledFromEnv();
|
|
26
26
|
registerCheck(isolate);
|
|
27
|
+
exports->Set(isolate->GetCurrentContext(), String::NewFromUtf8(isolate, "sendThread").ToLocalChecked(),
|
|
28
|
+
String::NewFromUtf8(isolate, cS::SendWorker::status()).ToLocalChecked()).Check();
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
NODE_MODULE(addon, Initialize)
|
package/src/Addon.h
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <openssl/ssl.h>
|
|
7
7
|
#include <uv.h>
|
|
8
8
|
#include <cstring>
|
|
9
|
+
#include "SendWorker.h"
|
|
9
10
|
|
|
10
11
|
#define NODE_WANT_INTERNALS 1
|
|
11
12
|
|
|
@@ -97,6 +98,11 @@ void registerCheck(Isolate *isolate) {
|
|
|
97
98
|
});
|
|
98
99
|
uv_unref((uv_handle_t *)&check);
|
|
99
100
|
|
|
101
|
+
// Send worker thread: only meaningful with corking, whose end-of-tick flush feeds it.
|
|
102
|
+
if (hub.getNodeData()->corkState->enabled) {
|
|
103
|
+
cS::SendWorker::init((uv_loop_t *)hub.getLoop());
|
|
104
|
+
}
|
|
105
|
+
|
|
100
106
|
uv_prepare_init((uv_loop_t *)hub.getLoop(), &corkPrepare);
|
|
101
107
|
uv_prepare_start(&corkPrepare, [](uv_prepare_t *) {
|
|
102
108
|
cS::Socket::flushCorked(hub.getNodeData());
|
package/src/Hub.cpp
CHANGED
|
@@ -15,8 +15,10 @@ zlib::Stream *Hub::allocateDefaultCompressor(int level, int windowBits, int memL
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
char *Hub::deflate(char *data, size_t &length, zlib::Stream *slidingDeflateWindow) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (!slidingDeflateWindow) {
|
|
19
|
+
return zlib::deflateIndependent(deflationStream, data, length, zlibBuffer, LARGE_BUFFER_SIZE, dynamicZlibBuffer);
|
|
20
|
+
}
|
|
21
|
+
return zlib::deflate(slidingDeflateWindow, data, length, zlibBuffer, LARGE_BUFFER_SIZE, dynamicZlibBuffer, false);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
char *Hub::inflate(char *data, size_t &length, size_t maxPayload) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#ifndef CWS_MICRODEFLATE_H
|
|
2
|
+
#define CWS_MICRODEFLATE_H
|
|
3
|
+
|
|
4
|
+
// Minimal raw-DEFLATE encoder for independent messages (permessage-deflate without
|
|
5
|
+
// context takeover, i.e. the shared-compressor mode). One fixed-Huffman block, greedy
|
|
6
|
+
// LZ77 over a 4-byte hash, no lazy matching, no stream state, and no per-message table
|
|
7
|
+
// clearing: stale entries are validated by position and content. Output ends the way
|
|
8
|
+
// Z_SYNC_FLUSH ends (empty stored block); the returned length already excludes the
|
|
9
|
+
// trailing 00 00 ff ff, as RFC 7692 requires. Measured on a real BSON RPC stream: same
|
|
10
|
+
// ratio as zlib-ng level 1 at ~1.7x its speed, mostly by skipping the 64 KB hash reset
|
|
11
|
+
// zlib performs per message. Decodes with any inflater.
|
|
12
|
+
//
|
|
13
|
+
// Not for context takeover: matches never reference earlier messages.
|
|
14
|
+
|
|
15
|
+
#include <cstdint>
|
|
16
|
+
#include <cstring>
|
|
17
|
+
#include <cstddef>
|
|
18
|
+
|
|
19
|
+
namespace cWS {
|
|
20
|
+
namespace microdeflate {
|
|
21
|
+
|
|
22
|
+
// Worst case output for `length` input bytes (all literals at 9 bits + block overhead).
|
|
23
|
+
inline size_t bound(size_t length) {
|
|
24
|
+
return length + length / 8 + 32;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class Encoder {
|
|
28
|
+
static const int HASH_BITS = 13;
|
|
29
|
+
uint32_t table[1 << HASH_BITS];
|
|
30
|
+
|
|
31
|
+
static const uint16_t *lenBase() { static const uint16_t t[29] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}; return t; }
|
|
32
|
+
static const uint8_t *lenExtra() { static const uint8_t t[29] = {0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0}; return t; }
|
|
33
|
+
static const uint16_t *distBase() { static const uint16_t t[30] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}; return t; }
|
|
34
|
+
static const uint8_t *distExtra() { static const uint8_t t[30] = {0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; return t; }
|
|
35
|
+
|
|
36
|
+
struct Tables {
|
|
37
|
+
uint16_t litCode[288]; uint8_t litBits[288];
|
|
38
|
+
uint8_t lenCode[259];
|
|
39
|
+
uint8_t distCode[32769];
|
|
40
|
+
uint8_t distCodeRev[30];
|
|
41
|
+
static uint32_t rev(uint32_t v, int n) { uint32_t r = 0; for (int i = 0; i < n; i++) { r = (r << 1) | (v & 1); v >>= 1; } return r; }
|
|
42
|
+
Tables() {
|
|
43
|
+
for (int i = 0; i < 288; i++) {
|
|
44
|
+
int bits, code;
|
|
45
|
+
if (i < 144) { bits = 8; code = 0x30 + i; }
|
|
46
|
+
else if (i < 256) { bits = 9; code = 0x190 + (i - 144); }
|
|
47
|
+
else if (i < 280) { bits = 7; code = i - 256; }
|
|
48
|
+
else { bits = 8; code = 0xC0 + (i - 280); }
|
|
49
|
+
litCode[i] = (uint16_t) rev(code, bits); litBits[i] = (uint8_t) bits;
|
|
50
|
+
}
|
|
51
|
+
for (int c = 0; c < 29; c++) for (int l = lenBase()[c]; l < (c < 28 ? lenBase()[c + 1] : 259); l++) lenCode[l] = (uint8_t) c;
|
|
52
|
+
for (int c = 0; c < 30; c++) { for (int d = distBase()[c]; d < (c < 29 ? distBase()[c + 1] : 32769); d++) distCode[d] = (uint8_t) c; distCodeRev[c] = (uint8_t) rev(c, 5); }
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
static const Tables &tables() { static const Tables t; return t; }
|
|
56
|
+
|
|
57
|
+
struct BitWriter {
|
|
58
|
+
uint8_t *out; size_t pos = 0; uint64_t acc = 0; int n = 0;
|
|
59
|
+
inline void put(uint32_t v, int bits) { acc |= (uint64_t) v << n; n += bits; while (n >= 8) { out[pos++] = (uint8_t) acc; acc >>= 8; n -= 8; } }
|
|
60
|
+
inline void flushByte() { if (n) { out[pos++] = (uint8_t) acc; acc = 0; n = 0; } }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
static inline uint32_t hash4(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return (v * 2654435761u) >> (32 - HASH_BITS); }
|
|
64
|
+
|
|
65
|
+
public:
|
|
66
|
+
Encoder() { memset(table, 0xff, sizeof(table)); tables(); }
|
|
67
|
+
|
|
68
|
+
// `out` must have room for bound(length) bytes. Returns the compressed length
|
|
69
|
+
// (without the 4-byte sync-flush tail).
|
|
70
|
+
size_t compress(const uint8_t *in, size_t length, uint8_t *out) {
|
|
71
|
+
const Tables &t = tables();
|
|
72
|
+
BitWriter w{out};
|
|
73
|
+
w.put(0, 1); w.put(1, 2); // BFINAL=0, BTYPE=01 (fixed Huffman)
|
|
74
|
+
size_t i = 0;
|
|
75
|
+
const size_t limit = length >= 4 ? length - 4 : 0;
|
|
76
|
+
while (i < limit) {
|
|
77
|
+
uint32_t h = hash4(in + i);
|
|
78
|
+
uint32_t cand = table[h];
|
|
79
|
+
table[h] = (uint32_t) i;
|
|
80
|
+
if (cand < i && i - cand <= 32768 && memcmp(in + cand, in + i, 4) == 0) {
|
|
81
|
+
size_t maxLen = length - i; if (maxLen > 258) maxLen = 258;
|
|
82
|
+
size_t m = 4;
|
|
83
|
+
while (m < maxLen && in[cand + m] == in[i + m]) m++;
|
|
84
|
+
uint32_t dist = (uint32_t) (i - cand);
|
|
85
|
+
int lc = t.lenCode[m];
|
|
86
|
+
w.put(t.litCode[257 + lc], t.litBits[257 + lc]);
|
|
87
|
+
if (lenExtra()[lc]) w.put((uint32_t) (m - lenBase()[lc]), lenExtra()[lc]);
|
|
88
|
+
int dc = t.distCode[dist];
|
|
89
|
+
w.put(t.distCodeRev[dc], 5);
|
|
90
|
+
if (distExtra()[dc]) w.put(dist - distBase()[dc], distExtra()[dc]);
|
|
91
|
+
if (i + 1 < limit) table[hash4(in + i + 1)] = (uint32_t) (i + 1);
|
|
92
|
+
i += m;
|
|
93
|
+
} else {
|
|
94
|
+
w.put(t.litCode[in[i]], t.litBits[in[i]]);
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (; i < length; i++) w.put(t.litCode[in[i]], t.litBits[in[i]]);
|
|
99
|
+
w.put(t.litCode[256], t.litBits[256]); // end of block
|
|
100
|
+
w.put(0, 1); w.put(0, 2); w.flushByte(); // empty stored block: BFINAL=0, BTYPE=00, then byte-align
|
|
101
|
+
out[w.pos++] = 0; out[w.pos++] = 0; out[w.pos++] = 0xff; out[w.pos++] = 0xff;
|
|
102
|
+
return w.pos - 4;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#endif // CWS_MICRODEFLATE_H
|
package/src/Networking.h
CHANGED
|
@@ -34,8 +34,11 @@
|
|
|
34
34
|
#define be64toh(x) __builtin_bswap64(x)
|
|
35
35
|
#else
|
|
36
36
|
#define __thread __declspec(thread)
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Windows is little-endian; the intrinsic avoids depending on WinSock's htonll/ntohll,
|
|
38
|
+
// whose declaration depends on SDK version macros that not every translation unit sets.
|
|
39
|
+
#include <stdlib.h>
|
|
40
|
+
#define htobe64(x) _byteswap_uint64(x)
|
|
41
|
+
#define be64toh(x) _byteswap_uint64(x)
|
|
39
42
|
#define pthread_t DWORD
|
|
40
43
|
#define pthread_self GetCurrentThreadId
|
|
41
44
|
#endif
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#include "SendWorker.h"
|
|
2
|
+
#include "Socket.h"
|
|
3
|
+
#include "Zlib.h"
|
|
4
|
+
#include "cWS.h"
|
|
5
|
+
#include "readerwriterqueue.h"
|
|
6
|
+
#include <thread>
|
|
7
|
+
#include <cstdlib>
|
|
8
|
+
#include <cstring>
|
|
9
|
+
#include <cerrno>
|
|
10
|
+
#include <cctype>
|
|
11
|
+
#include <string>
|
|
12
|
+
|
|
13
|
+
namespace cS {
|
|
14
|
+
|
|
15
|
+
namespace {
|
|
16
|
+
bool workerActive = false;
|
|
17
|
+
const char *workerStatus = "not initialised";
|
|
18
|
+
moodycamel::BlockingReaderWriterQueue<Socket::SendOp *> *toWorker = nullptr;
|
|
19
|
+
moodycamel::ReaderWriterQueue<Socket::SendOp *> *toMain = nullptr;
|
|
20
|
+
uv_async_t mainWake;
|
|
21
|
+
|
|
22
|
+
void onMainWake(uv_async_t *) {
|
|
23
|
+
Socket::SendOp *op;
|
|
24
|
+
while (toMain->try_dequeue(op)) {
|
|
25
|
+
Socket::sendComplete(op);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
void workerLoop() {
|
|
30
|
+
Socket::SendOp *op;
|
|
31
|
+
for (;;) {
|
|
32
|
+
toWorker->wait_dequeue(op);
|
|
33
|
+
Socket::performSend(op);
|
|
34
|
+
toMain->enqueue(op);
|
|
35
|
+
uv_async_send(&mainWake);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
bool SendWorker::init(uv_loop_t *loop) {
|
|
41
|
+
const char *disabled = getenv("CWS_SEND_THREAD");
|
|
42
|
+
std::string value = disabled ? disabled : "";
|
|
43
|
+
for (char &c : value) c = (char) tolower((unsigned char) c);
|
|
44
|
+
if (value == "0" || value == "false" || value == "off" || value == "no") {
|
|
45
|
+
workerStatus = "disabled by CWS_SEND_THREAD";
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
toWorker = new moodycamel::BlockingReaderWriterQueue<Socket::SendOp *>(4096);
|
|
49
|
+
toMain = new moodycamel::ReaderWriterQueue<Socket::SendOp *>(4096);
|
|
50
|
+
uv_async_init(loop, &mainWake, onMainWake);
|
|
51
|
+
uv_unref((uv_handle_t *) &mainWake); // completions alone must not keep the process alive
|
|
52
|
+
std::thread(workerLoop).detach();
|
|
53
|
+
workerActive = true;
|
|
54
|
+
workerStatus = "active";
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
bool SendWorker::active() { return workerActive; }
|
|
59
|
+
const char *SendWorker::status() { return workerStatus; }
|
|
60
|
+
|
|
61
|
+
bool SendWorker::submit(void *op) {
|
|
62
|
+
// try_enqueue never allocates; a full queue means "send it yourself this tick".
|
|
63
|
+
return toWorker->try_enqueue((Socket::SendOp *) op);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- Socket side (needs Socket internals) ----
|
|
67
|
+
|
|
68
|
+
// Deflates a pending message with the given stream and replaces its raw payload with a
|
|
69
|
+
// framed compressed message. Shared between the worker (its own compressor) and the
|
|
70
|
+
// main-thread materialize path (the hub's).
|
|
71
|
+
static void deflateAndFrame(Socket::Queue::Message *m, cWS::zlib::Stream *stream, bool resetAfter, char *buffer, size_t bufferSize, std::string &dynamic) {
|
|
72
|
+
size_t compressedLength = m->length;
|
|
73
|
+
// resetAfter == independent message (no context takeover): microdeflate; else the socket's window
|
|
74
|
+
char *deflated = resetAfter ? cWS::zlib::deflateIndependent(stream, (char *) m->data, compressedLength, buffer, bufferSize, dynamic)
|
|
75
|
+
: cWS::zlib::deflate(stream, (char *) m->data, compressedLength, buffer, bufferSize, dynamic, false);
|
|
76
|
+
const size_t HEADER = cWS::WebSocketProtocol<true, cWS::WebSocket<true>>::LONG_MESSAGE_HEADER;
|
|
77
|
+
char *frame = new char[compressedLength + HEADER];
|
|
78
|
+
size_t frameLength = cWS::WebSocketProtocol<true, cWS::WebSocket<true>>::formatMessage(frame, deflated, compressedLength, (cWS::OpCode) m->opCode, compressedLength, true);
|
|
79
|
+
delete [] (char *) m->data;
|
|
80
|
+
m->data = frame;
|
|
81
|
+
m->length = frameLength;
|
|
82
|
+
m->ownsData = true;
|
|
83
|
+
m->compressPending = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
namespace {
|
|
87
|
+
// worker-thread compression state: its own shared compressor and scratch buffers
|
|
88
|
+
cWS::zlib::Stream *workerDeflate = nullptr;
|
|
89
|
+
const size_t WORKER_BUFFER = 300 * 1024;
|
|
90
|
+
char *workerBuffer = nullptr;
|
|
91
|
+
std::string workerDynamic;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void Socket::materializeOnMain(Socket *s, Queue::Message *m, cWS::zlib::Stream *stream, bool resetAfter, char *buffer, size_t bufferSize, std::string &dynamic) {
|
|
95
|
+
deflateAndFrame(m, stream, resetAfter, buffer, bufferSize, dynamic);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
void Socket::performSend(SendOp *op) {
|
|
99
|
+
if (!workerDeflate) {
|
|
100
|
+
workerDeflate = cWS::zlib::createDeflate(1, 15, 8);
|
|
101
|
+
workerBuffer = new char[WORKER_BUFFER];
|
|
102
|
+
}
|
|
103
|
+
// 1. deflate + frame anything still pending, with this socket's window or the shared one
|
|
104
|
+
cWS::zlib::Stream *stream = op->deflateWindow ? (cWS::zlib::Stream *) op->deflateWindow : workerDeflate;
|
|
105
|
+
for (Queue::Message *m = op->head; m; m = m->nextMessage) {
|
|
106
|
+
if (m->compressPending) {
|
|
107
|
+
deflateAndFrame(m, stream, !op->deflateWindow, workerBuffer, WORKER_BUFFER, workerDynamic);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 2. gather
|
|
111
|
+
op->count = 0;
|
|
112
|
+
op->bytes = 0;
|
|
113
|
+
for (Queue::Message *m = op->head; m; m = m->nextMessage) {
|
|
114
|
+
#ifdef _WIN32
|
|
115
|
+
op->bufs[op->count].buf = (CHAR *) m->data;
|
|
116
|
+
op->bufs[op->count].len = (ULONG) m->length;
|
|
117
|
+
#else
|
|
118
|
+
op->iov[op->count].iov_base = (void *) m->data;
|
|
119
|
+
op->iov[op->count].iov_len = m->length;
|
|
120
|
+
#endif
|
|
121
|
+
op->bytes += m->length;
|
|
122
|
+
op->count++;
|
|
123
|
+
}
|
|
124
|
+
#ifdef _WIN32
|
|
125
|
+
DWORD sent = 0;
|
|
126
|
+
if (WSASend(op->fd, op->bufs, (DWORD) op->count, &sent, 0, nullptr, nullptr) == SOCKET_ERROR) {
|
|
127
|
+
op->result = -1;
|
|
128
|
+
op->error = WSAGetLastError();
|
|
129
|
+
} else {
|
|
130
|
+
op->result = (ssize_t) sent;
|
|
131
|
+
op->error = 0;
|
|
132
|
+
}
|
|
133
|
+
#else
|
|
134
|
+
msghdr msg = {};
|
|
135
|
+
msg.msg_iov = op->iov;
|
|
136
|
+
msg.msg_iovlen = op->count;
|
|
137
|
+
op->result = ::sendmsg(op->fd, &msg, MSG_NOSIGNAL);
|
|
138
|
+
op->error = op->result < 0 ? errno : 0;
|
|
139
|
+
#endif
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
static bool sendWouldBlock(int error) {
|
|
143
|
+
#ifdef _WIN32
|
|
144
|
+
return error == WSAEWOULDBLOCK;
|
|
145
|
+
#else
|
|
146
|
+
return error == EWOULDBLOCK || error == EAGAIN;
|
|
147
|
+
#endif
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
void Socket::sendComplete(SendOp *op) {
|
|
151
|
+
Socket *s = op->socket;
|
|
152
|
+
if (!s) {
|
|
153
|
+
// socket closed while the op was in flight: release its messages, then the fd we kept open
|
|
154
|
+
for (Queue::Message *m = op->head; m;) {
|
|
155
|
+
Queue::Message *next = m->nextMessage;
|
|
156
|
+
if (m->callback) {
|
|
157
|
+
m->callback(nullptr, m->callbackData, true, m->reserved);
|
|
158
|
+
}
|
|
159
|
+
Queue::release(op->nodeData, m);
|
|
160
|
+
m = next;
|
|
161
|
+
}
|
|
162
|
+
if (op->closeFd) {
|
|
163
|
+
op->nodeData->netContext->closeSocket(op->fd);
|
|
164
|
+
}
|
|
165
|
+
if (op->destroyWindow) {
|
|
166
|
+
cWS::zlib::destroy((cWS::zlib::Stream *) op->destroyWindow);
|
|
167
|
+
}
|
|
168
|
+
delete op;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
s->sendOp = nullptr;
|
|
172
|
+
|
|
173
|
+
ssize_t res = op->result;
|
|
174
|
+
if (res < 0) {
|
|
175
|
+
if (sendWouldBlock(op->error)) {
|
|
176
|
+
res = 0;
|
|
177
|
+
} else {
|
|
178
|
+
s->requeueFront(op);
|
|
179
|
+
delete op;
|
|
180
|
+
if (s->endCb) {
|
|
181
|
+
s->endCb(s);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
std::vector<PendingCallback> callbacks;
|
|
188
|
+
size_t sent = (size_t) res, opBytes = op->bytes;
|
|
189
|
+
while (sent > 0 && op->head) {
|
|
190
|
+
Queue::Message *m = op->head;
|
|
191
|
+
if (sent >= m->length) {
|
|
192
|
+
sent -= m->length;
|
|
193
|
+
op->bytes -= m->length;
|
|
194
|
+
if (m->callback) {
|
|
195
|
+
callbacks.push_back({m->callback, m->callbackData, m->reserved});
|
|
196
|
+
}
|
|
197
|
+
op->head = m->nextMessage;
|
|
198
|
+
Queue::release(op->nodeData, m);
|
|
199
|
+
} else {
|
|
200
|
+
m->length -= sent;
|
|
201
|
+
m->data += sent;
|
|
202
|
+
op->bytes -= sent;
|
|
203
|
+
sent = 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!op->head) {
|
|
207
|
+
op->tail = nullptr;
|
|
208
|
+
}
|
|
209
|
+
bool complete = (size_t) res == opBytes;
|
|
210
|
+
s->requeueFront(op);
|
|
211
|
+
delete op;
|
|
212
|
+
|
|
213
|
+
if (!s->messageQueue.empty()) {
|
|
214
|
+
if (complete) {
|
|
215
|
+
s->submitToWorker(); // more arrived while in flight
|
|
216
|
+
} else if ((s->getPoll() & UV_WRITABLE) == 0) {
|
|
217
|
+
s->setPoll(s->getPoll() | UV_WRITABLE); // kernel buffer full: the drain loop resumes when writable
|
|
218
|
+
s->changePoll(s);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (PendingCallback &c : callbacks) {
|
|
222
|
+
c.callback(s, c.callbackData, false, c.reserved);
|
|
223
|
+
if (s->isClosed()) {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
}
|
package/src/SendWorker.h
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#ifndef CWS_SEND_WORKER_H
|
|
2
|
+
#define CWS_SEND_WORKER_H
|
|
3
|
+
|
|
4
|
+
// Send worker thread. The end-of-tick flush hands each socket's queued frames to
|
|
5
|
+
// one worker thread as an op; the worker does the gathered send syscall (the
|
|
6
|
+
// kernel's TCP work is charged to it, not to the JS thread) and posts the op back.
|
|
7
|
+
// Both directions are single-producer/single-consumer lock-free queues
|
|
8
|
+
// (deps/readerwriterqueue); the worker parks on a semaphore when idle and the main
|
|
9
|
+
// thread is woken through a uv_async. Disabled with CWS_SEND_THREAD=0.
|
|
10
|
+
|
|
11
|
+
#include <uv.h>
|
|
12
|
+
|
|
13
|
+
namespace cS {
|
|
14
|
+
|
|
15
|
+
struct SendWorker {
|
|
16
|
+
static bool init(uv_loop_t *loop);
|
|
17
|
+
static bool active();
|
|
18
|
+
static const char *status(); // "active", or why not
|
|
19
|
+
// Main thread only. Returns false if the queue is full; the caller then sends synchronously.
|
|
20
|
+
static bool submit(void *op);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#endif // CWS_SEND_WORKER_H
|