mm_socket_server 1.0.2 → 3.0.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/README.md +84 -96
- package/example/demo.js +63 -0
- package/index.js +2 -4
- package/lib/socket.js +607 -184
- package/package.json +8 -8
- package/lib/message.js +0 -150
package/lib/socket.js
CHANGED
|
@@ -2,45 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
const net = require('net');
|
|
4
4
|
const dgram = require('dgram');
|
|
5
|
-
const { Mod
|
|
6
|
-
const {
|
|
5
|
+
const { Mod } = require('mm_mod');
|
|
6
|
+
const { eventAdmin } = require('mm_event');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* 长度前缀帧的头部字节数:2 字节 topic 长度 + 4 字节 payload 长度
|
|
10
10
|
*/
|
|
11
|
-
const
|
|
12
|
-
type: 'object',
|
|
13
|
-
properties: {
|
|
14
|
-
protocol: { type: 'string', enum: ['tcp', 'udp'] },
|
|
15
|
-
host: { type: 'string', min_length: 1 },
|
|
16
|
-
port: { type: 'integer', minimum: 1, maximum: 65535 },
|
|
17
|
-
encoding: { type: 'string', enum: ['utf8', 'hex', 'base64'] },
|
|
18
|
-
json: { type: 'boolean' }
|
|
19
|
-
}
|
|
20
|
-
};
|
|
11
|
+
const LENGTH_HEADER_SIZE = 6;
|
|
21
12
|
|
|
22
13
|
/**
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
14
|
+
* topic 长度的字节数
|
|
15
|
+
*/
|
|
16
|
+
const TOPIC_LENGTH_SIZE = 2;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 换行符字节
|
|
20
|
+
*/
|
|
21
|
+
const LINE_FEED = 0x0a;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 回车符字节
|
|
25
|
+
*/
|
|
26
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 空格字节,行协议中分隔 topic 与 payload
|
|
30
|
+
*/
|
|
31
|
+
const SPACE = 0x20;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 支持的协议
|
|
35
|
+
*/
|
|
36
|
+
const PROTOCOLS = ['tcp', 'udp'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 支持的协议配置值
|
|
40
|
+
*/
|
|
41
|
+
const PROTOCOL_MODES = ['tcp', 'udp', 'both'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 支持的帧格式
|
|
45
|
+
*/
|
|
46
|
+
const FRAMINGS = ['line', 'length', 'none'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SocketServer —— TCP/UDP 传输层(入站适配器模块)。
|
|
29
50
|
*
|
|
30
51
|
* 职责:
|
|
31
|
-
* 1.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
52
|
+
* 1. 生命周期:由 Mod 的 init / start / stop 驱动 net.Server / dgram.Socket,
|
|
53
|
+
* protocol 支持 tcp、udp 或 both(同端口同时监听两种传输层);
|
|
54
|
+
* 2. 消息转交:按 framing 把字节流还原成 { topic, payload } 帧,以 topic 为 target
|
|
55
|
+
* 调用 EventManager.run(topic, params, ctx, options),支持 mm_event 的 `*` 通配;
|
|
56
|
+
* 3. 结果回发:事件返回非空结果时按同一帧格式回发来源客户端。
|
|
57
|
+
*
|
|
58
|
+
* 帧格式:
|
|
59
|
+
* line —— `topic<空格>payload\n`,TCP 按换行分帧,payload 不能含换行;
|
|
60
|
+
* length —— `[2字节 topic 长度][4字节 payload 长度][topic][payload]`,二进制安全;
|
|
61
|
+
* none —— 不分帧,target 回落为连接协议名(tcp / udp),保持旧行为。
|
|
37
62
|
*
|
|
38
63
|
* 用法:
|
|
39
|
-
* const socket = new SocketServer({ name: 'socket',
|
|
40
|
-
* socket.
|
|
64
|
+
* const socket = new SocketServer({ name: 'socket', protocol: 'both', port: 9000 });
|
|
65
|
+
* socket.getEventManager().add({ name: 'echo', target: 'chat/*' }).main = async (params) => params.data;
|
|
41
66
|
* await socket.init();
|
|
42
67
|
* await socket.start();
|
|
43
|
-
* await socket.run({}, { action: 'list' });
|
|
44
68
|
*/
|
|
45
69
|
class SocketServer extends Mod {
|
|
46
70
|
/**
|
|
@@ -49,81 +73,144 @@ class SocketServer extends Mod {
|
|
|
49
73
|
static config = {
|
|
50
74
|
name: 'os',
|
|
51
75
|
title: 'Socket服务器模块',
|
|
52
|
-
description: 'TCP/UDP
|
|
53
|
-
version: '
|
|
76
|
+
description: 'TCP/UDP 传输层:按 topic 帧直达事件管理器,不做插件',
|
|
77
|
+
version: '3.0.0',
|
|
54
78
|
protocol: 'tcp',
|
|
55
79
|
host: '0.0.0.0',
|
|
56
80
|
port: 9000,
|
|
57
81
|
encoding: 'utf8',
|
|
58
82
|
json: false,
|
|
59
|
-
|
|
83
|
+
scope: 'socket',
|
|
84
|
+
framing: 'line',
|
|
85
|
+
udp_ttl: 300000,
|
|
60
86
|
params: {
|
|
61
87
|
type: 'object',
|
|
62
88
|
required: ['action'],
|
|
63
89
|
properties: {
|
|
64
90
|
action: { type: 'string', enum: ['send', 'broadcast', 'close', 'list'] },
|
|
65
91
|
client_id: { type: 'string', min_length: 1 },
|
|
66
|
-
|
|
67
|
-
|
|
92
|
+
topic: { type: 'string', min_length: 1 },
|
|
93
|
+
data: {}
|
|
68
94
|
}
|
|
69
95
|
}
|
|
70
96
|
};
|
|
71
97
|
|
|
72
98
|
/**
|
|
73
|
-
*
|
|
99
|
+
* 底层服务器实例,按协议分槽存放
|
|
74
100
|
*/
|
|
75
|
-
#
|
|
101
|
+
#servers = { tcp: null, udp: null };
|
|
76
102
|
|
|
77
103
|
/**
|
|
78
|
-
*
|
|
104
|
+
* 事件管理器(取自 mm_event 的 socket 作用域,与其他实例共享)
|
|
79
105
|
*/
|
|
80
|
-
#
|
|
106
|
+
#events = null;
|
|
81
107
|
|
|
82
108
|
/**
|
|
83
|
-
*
|
|
84
|
-
* 每个实例独立 Handle,避免多实例共享管道导致钩子泄漏
|
|
109
|
+
* 在线连接表:client_id -> 连接信息
|
|
85
110
|
*/
|
|
86
|
-
#
|
|
111
|
+
#clients = new Map();
|
|
87
112
|
|
|
88
113
|
/**
|
|
89
114
|
* 客户端编号自增计数器
|
|
90
115
|
*/
|
|
91
116
|
#counter = 0;
|
|
92
117
|
|
|
118
|
+
/**
|
|
119
|
+
* UDP 过期连接清理定时器
|
|
120
|
+
*/
|
|
121
|
+
#udp_timer = null;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 构造函数
|
|
125
|
+
* @param {Object} config 模块配置
|
|
126
|
+
* @param {Object} server 框架实例
|
|
127
|
+
*/
|
|
128
|
+
constructor(config, server) {
|
|
129
|
+
super(config, server);
|
|
130
|
+
this.#events = eventAdmin(this.config.scope);
|
|
131
|
+
}
|
|
132
|
+
|
|
93
133
|
// ---------- 生命周期钩子 ----------
|
|
94
134
|
/**
|
|
95
|
-
*
|
|
135
|
+
* 初始化钩子:校验协议与帧格式配置
|
|
136
|
+
* @returns {Promise<void>} 无返回值
|
|
137
|
+
* @throws {Error} protocol 或 framing 不受支持时
|
|
96
138
|
*/
|
|
97
139
|
async onInit() {
|
|
98
|
-
|
|
99
|
-
this._initHandle();
|
|
140
|
+
this._validateConfig();
|
|
100
141
|
}
|
|
101
142
|
|
|
102
143
|
/**
|
|
103
|
-
*
|
|
144
|
+
* 校验配置项
|
|
145
|
+
* @returns {void}
|
|
146
|
+
* @throws {Error} protocol 或 framing 不受支持时
|
|
104
147
|
*/
|
|
105
|
-
|
|
148
|
+
_validateConfig() {
|
|
106
149
|
const protocol = this.config.protocol;
|
|
107
|
-
if (protocol
|
|
108
|
-
|
|
109
|
-
|
|
150
|
+
if (!PROTOCOL_MODES.includes(protocol)) {
|
|
151
|
+
throw new Error(`${this._label()} unsupported protocol: ${protocol}`);
|
|
152
|
+
}
|
|
153
|
+
const framing = this.config.framing;
|
|
154
|
+
if (!FRAMINGS.includes(framing)) {
|
|
155
|
+
throw new Error(`${this._label()} unsupported framing: ${framing}`);
|
|
110
156
|
}
|
|
111
|
-
await this._startTcp();
|
|
112
157
|
}
|
|
113
158
|
|
|
114
159
|
/**
|
|
115
|
-
*
|
|
160
|
+
* 启动钩子:按 protocol 逐个创建底层服务器,任一失败则全部回滚
|
|
161
|
+
* @returns {Promise<void>} 无返回值
|
|
116
162
|
*/
|
|
117
|
-
async
|
|
118
|
-
|
|
119
|
-
|
|
163
|
+
async onStart() {
|
|
164
|
+
try {
|
|
165
|
+
for (const protocol of this._enabledProtocols()) {
|
|
166
|
+
await this._startServer(protocol);
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
await this._rollbackStart();
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
120
172
|
}
|
|
121
173
|
|
|
122
174
|
/**
|
|
123
|
-
*
|
|
175
|
+
* 列出本次需要监听的协议
|
|
176
|
+
* @returns {Array<string>} 协议名列表
|
|
124
177
|
*/
|
|
125
|
-
|
|
126
|
-
this
|
|
178
|
+
_enabledProtocols() {
|
|
179
|
+
const protocol = this.config.protocol;
|
|
180
|
+
if (protocol === 'both') return PROTOCOLS.slice();
|
|
181
|
+
return [protocol];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 按协议启动对应的底层服务器
|
|
186
|
+
* @param {string} protocol 协议名,tcp 或 udp
|
|
187
|
+
* @returns {Promise<void>} 无返回值
|
|
188
|
+
*/
|
|
189
|
+
async _startServer(protocol) {
|
|
190
|
+
if (protocol === 'udp') return this._startUdp();
|
|
191
|
+
return this._startTcp();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 启动失败时回滚已创建的资源
|
|
196
|
+
* @returns {Promise<void>} 无返回值
|
|
197
|
+
*/
|
|
198
|
+
async _rollbackStart() {
|
|
199
|
+
try {
|
|
200
|
+
this._closeAllClients();
|
|
201
|
+
await this._closeServers();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
this._emitError(err);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 停止钩子:断开全部连接并关闭服务器(事件管理器与其他实例共享,不清理)
|
|
209
|
+
* @returns {Promise<void>} 无返回值
|
|
210
|
+
*/
|
|
211
|
+
async onStop() {
|
|
212
|
+
this._closeAllClients();
|
|
213
|
+
await this._closeServers();
|
|
127
214
|
}
|
|
128
215
|
|
|
129
216
|
// ---------- 执行入口 ----------
|
|
@@ -133,33 +220,35 @@ class SocketServer extends Mod {
|
|
|
133
220
|
* @param {Object} ctx 执行上下文
|
|
134
221
|
* @param {Object} options 合并后的选项
|
|
135
222
|
* @returns {Promise<Object>} 操作结果
|
|
223
|
+
* @throws {Error} action 不受支持时
|
|
136
224
|
*/
|
|
137
225
|
async main(params, ctx, options) {
|
|
138
226
|
const action = params.action;
|
|
139
227
|
if (action === 'send') return this._actionSend(params);
|
|
140
228
|
if (action === 'broadcast') return this._actionBroadcast(params);
|
|
141
229
|
if (action === 'close') return this._actionClose(params);
|
|
142
|
-
return this._actionList(
|
|
230
|
+
if (action === 'list') return this._actionList();
|
|
231
|
+
throw new Error(`${this._label()} unsupported action: ${action}`);
|
|
143
232
|
}
|
|
144
233
|
|
|
145
234
|
/**
|
|
146
235
|
* 执行发送操作
|
|
147
|
-
* @param {Object} params 调用参数,含 client_id 与
|
|
236
|
+
* @param {Object} params 调用参数,含 client_id、data 与 topic
|
|
148
237
|
* @returns {Object} 操作结果
|
|
149
238
|
*/
|
|
150
239
|
_actionSend(params) {
|
|
151
240
|
this._requireClientId(params);
|
|
152
|
-
const success = this.send(params.client_id, params.data);
|
|
241
|
+
const success = this.send(params.client_id, params.data, params.topic);
|
|
153
242
|
return { action: 'send', client_id: params.client_id, success };
|
|
154
243
|
}
|
|
155
244
|
|
|
156
245
|
/**
|
|
157
246
|
* 执行广播操作
|
|
158
|
-
* @param {Object} params 调用参数,含 data
|
|
247
|
+
* @param {Object} params 调用参数,含 data 与 topic
|
|
159
248
|
* @returns {Object} 操作结果,count 为送达客户端数
|
|
160
249
|
*/
|
|
161
250
|
_actionBroadcast(params) {
|
|
162
|
-
const count = this.broadcast(params.data);
|
|
251
|
+
const count = this.broadcast(params.data, params.topic);
|
|
163
252
|
return { action: 'broadcast', count };
|
|
164
253
|
}
|
|
165
254
|
|
|
@@ -176,62 +265,108 @@ class SocketServer extends Mod {
|
|
|
176
265
|
|
|
177
266
|
/**
|
|
178
267
|
* 执行列出连接操作
|
|
179
|
-
* @param {Object} params 调用参数,可含 query 查询条件(Store 条件/分页)
|
|
180
268
|
* @returns {Object} 操作结果,clients 为在线客户端列表
|
|
181
269
|
*/
|
|
182
|
-
_actionList(
|
|
183
|
-
return { action: 'list', clients: this.listClients(
|
|
270
|
+
_actionList() {
|
|
271
|
+
return { action: 'list', clients: this.listClients() };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------- 事件管理器 ----------
|
|
275
|
+
/**
|
|
276
|
+
* 获取事件管理器,用于注册事件
|
|
277
|
+
* @returns {Object} 事件管理器实例
|
|
278
|
+
*/
|
|
279
|
+
getEventManager() {
|
|
280
|
+
return this.#events;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 注入外部事件管理器
|
|
285
|
+
* @param {Object} manager 事件管理器实例
|
|
286
|
+
* @returns {SocketServer} 当前实例
|
|
287
|
+
* @throws {TypeError} manager 不具备 run 方法时
|
|
288
|
+
*/
|
|
289
|
+
setEventManager(manager) {
|
|
290
|
+
if (!manager || typeof manager.run !== 'function') {
|
|
291
|
+
throw new TypeError(`${this._label()} manager must provide run()`);
|
|
292
|
+
}
|
|
293
|
+
this.#events = manager;
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 获取实际监听地址
|
|
299
|
+
* @param {string} [protocol] 协议名,省略时取首个已启用协议
|
|
300
|
+
* @returns {Object|null} 监听地址信息,未监听时为 null
|
|
301
|
+
*/
|
|
302
|
+
getAddress(protocol) {
|
|
303
|
+
const key = protocol || this._enabledProtocols()[0];
|
|
304
|
+
const server = this.#servers[key];
|
|
305
|
+
return server ? server.address() : null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 获取各协议的监听地址
|
|
310
|
+
* @returns {Object} 形如 { tcp, udp } 的地址表,未监听为 null
|
|
311
|
+
*/
|
|
312
|
+
getAddresses() {
|
|
313
|
+
return { tcp: this.getAddress('tcp'), udp: this.getAddress('udp') };
|
|
184
314
|
}
|
|
185
315
|
|
|
186
316
|
// ---------- 公开操作 ----------
|
|
187
317
|
/**
|
|
188
318
|
* 向指定客户端发送数据
|
|
189
319
|
* @param {string} client_id 客户端 ID
|
|
190
|
-
* @param {string|Object} data 待发送数据
|
|
320
|
+
* @param {string|Object|Buffer} data 待发送数据
|
|
321
|
+
* @param {string} [topic] 目标主题,framing 非 none 时必填
|
|
191
322
|
* @returns {boolean} 是否发送成功
|
|
323
|
+
* @throws {Error} framing 非 none 且缺少 topic 时
|
|
192
324
|
*/
|
|
193
|
-
send(client_id, data) {
|
|
194
|
-
const client = this.#clients.
|
|
325
|
+
send(client_id, data, topic) {
|
|
326
|
+
const client = this.#clients.get(client_id);
|
|
195
327
|
if (!client) return false;
|
|
196
|
-
|
|
197
|
-
return this._sendToClient(client, payload);
|
|
328
|
+
return this._sendToClient(client, this._encodeFrame(topic, data));
|
|
198
329
|
}
|
|
199
330
|
|
|
200
331
|
/**
|
|
201
332
|
* 向全部在线客户端广播数据
|
|
202
|
-
* @param {string|Object} data 待广播数据
|
|
333
|
+
* @param {string|Object|Buffer} data 待广播数据
|
|
334
|
+
* @param {string} [topic] 目标主题,framing 非 none 时必填
|
|
203
335
|
* @returns {number} 送达的客户端数量
|
|
336
|
+
* @throws {Error} framing 非 none 且缺少 topic 时
|
|
204
337
|
*/
|
|
205
|
-
broadcast(data) {
|
|
206
|
-
const payload = this.
|
|
338
|
+
broadcast(data, topic) {
|
|
339
|
+
const payload = this._encodeFrame(topic, data);
|
|
207
340
|
let count = 0;
|
|
208
|
-
for (const client of this.#clients.
|
|
341
|
+
for (const client of this.#clients.values()) {
|
|
209
342
|
if (this._sendToClient(client, payload)) count += 1;
|
|
210
343
|
}
|
|
211
344
|
return count;
|
|
212
345
|
}
|
|
213
346
|
|
|
214
347
|
/**
|
|
215
|
-
*
|
|
348
|
+
* 关闭指定客户端连接(TCP 销毁套接字,UDP 仅移出连接表)
|
|
216
349
|
* @param {string} client_id 客户端 ID
|
|
217
350
|
* @returns {boolean} 是否关闭成功
|
|
218
351
|
*/
|
|
219
352
|
closeClient(client_id) {
|
|
220
|
-
const client = this.#clients.
|
|
353
|
+
const client = this.#clients.get(client_id);
|
|
221
354
|
if (!client) return false;
|
|
355
|
+
this.#clients.delete(client_id);
|
|
222
356
|
if (client.socket) client.socket.destroy();
|
|
223
|
-
this.#clients.delById(client_id);
|
|
224
|
-
this._safeEmit('close', client_id);
|
|
225
357
|
return true;
|
|
226
358
|
}
|
|
227
359
|
|
|
228
360
|
/**
|
|
229
|
-
*
|
|
230
|
-
* @
|
|
231
|
-
* @returns {Array<Object>} 客户端信息列表,含 id 与 address
|
|
361
|
+
* 列出在线客户端
|
|
362
|
+
* @returns {Array<Object>} 客户端信息列表,含 id、protocol 与 address
|
|
232
363
|
*/
|
|
233
|
-
listClients(
|
|
234
|
-
|
|
364
|
+
listClients() {
|
|
365
|
+
const list = [];
|
|
366
|
+
for (const client of this.#clients.values()) {
|
|
367
|
+
list.push({ id: client.id, protocol: client.protocol, address: client.address });
|
|
368
|
+
}
|
|
369
|
+
return list;
|
|
235
370
|
}
|
|
236
371
|
|
|
237
372
|
/**
|
|
@@ -240,64 +375,30 @@ class SocketServer extends Mod {
|
|
|
240
375
|
* @returns {boolean} 是否在线
|
|
241
376
|
*/
|
|
242
377
|
hasClient(client_id) {
|
|
243
|
-
return
|
|
378
|
+
return this.#clients.has(client_id);
|
|
244
379
|
}
|
|
245
380
|
|
|
246
381
|
/**
|
|
247
|
-
*
|
|
382
|
+
* 绑定连接级选项,入站消息将作为 options 透传给事件
|
|
248
383
|
* @param {string} client_id 客户端 ID
|
|
249
384
|
* @param {Object|null} options 选项对象,null 时清空
|
|
250
385
|
* @returns {boolean} 是否设置成功(客户端不在线时失败)
|
|
251
386
|
*/
|
|
252
387
|
setClientOptions(client_id, options) {
|
|
253
|
-
const client = this.#clients.
|
|
388
|
+
const client = this.#clients.get(client_id);
|
|
254
389
|
if (!client) return false;
|
|
255
390
|
client.options = options || null;
|
|
256
391
|
return true;
|
|
257
392
|
}
|
|
258
393
|
|
|
259
394
|
/**
|
|
260
|
-
*
|
|
261
|
-
* @param {
|
|
262
|
-
* @returns {Object}
|
|
263
|
-
*/
|
|
264
|
-
_clientOptions(client_id) {
|
|
265
|
-
const client = this.#clients.getById(client_id);
|
|
266
|
-
if (!client || !client.options) return {};
|
|
267
|
-
return client.options;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* 挂载插件:调用 plugin.bind(this),由插件自行向管道注册钩子;
|
|
272
|
-
* 如 MessagePlugin 挂载后接管 JSON-RPC 请求
|
|
273
|
-
* @param {Object} plugin 具备 bind(server) 方法的插件实例
|
|
274
|
-
* @returns {SocketServer} 当前实例
|
|
275
|
-
* @throws {TypeError} plugin 不具备 bind 方法时
|
|
395
|
+
* 获取连接级选项(复制一份,避免事件管理器回写)
|
|
396
|
+
* @param {Object} client 连接信息
|
|
397
|
+
* @returns {Object} 选项对象
|
|
276
398
|
*/
|
|
277
|
-
|
|
278
|
-
if (!
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
plugin.bind(this);
|
|
282
|
-
return this;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* 获取消息处理管道,供外部注册 before/check/main/render 等钩子;
|
|
287
|
-
* 钩子产出 result 时自动回发来源客户端
|
|
288
|
-
* @returns {Handle} 消息管道实例
|
|
289
|
-
*/
|
|
290
|
-
getHandle() {
|
|
291
|
-
return this.#handle;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* 获取实际监听地址
|
|
296
|
-
* @returns {Object|null} 监听地址信息,未启动时为 null
|
|
297
|
-
*/
|
|
298
|
-
getAddress() {
|
|
299
|
-
const server = this.#server;
|
|
300
|
-
return server ? server.address() : null;
|
|
399
|
+
_clientOptions(client) {
|
|
400
|
+
if (!client.options) return {};
|
|
401
|
+
return { ...client.options };
|
|
301
402
|
}
|
|
302
403
|
|
|
303
404
|
// ---------- TCP ----------
|
|
@@ -307,7 +408,7 @@ class SocketServer extends Mod {
|
|
|
307
408
|
*/
|
|
308
409
|
async _startTcp() {
|
|
309
410
|
const server = net.createServer((socket) => this._handleConnection(socket));
|
|
310
|
-
this.#
|
|
411
|
+
this.#servers.tcp = server;
|
|
311
412
|
await this._listenTcp(server);
|
|
312
413
|
server.on('error', (err) => this._emitError(err));
|
|
313
414
|
}
|
|
@@ -330,7 +431,7 @@ class SocketServer extends Mod {
|
|
|
330
431
|
}
|
|
331
432
|
|
|
332
433
|
/**
|
|
333
|
-
* 登记新的 TCP
|
|
434
|
+
* 登记新的 TCP 连接并绑定回调
|
|
334
435
|
* @param {Object} socket 套接字实例
|
|
335
436
|
*/
|
|
336
437
|
_handleConnection(socket) {
|
|
@@ -338,40 +439,298 @@ class SocketServer extends Mod {
|
|
|
338
439
|
const client_id = `tcp_${this.#counter}`;
|
|
339
440
|
const client = {
|
|
340
441
|
id: client_id,
|
|
341
|
-
|
|
442
|
+
protocol: 'tcp',
|
|
342
443
|
address: this._remoteAddress(socket),
|
|
343
444
|
socket,
|
|
344
|
-
rinfo: null
|
|
445
|
+
rinfo: null,
|
|
446
|
+
options: null,
|
|
447
|
+
buffer: Buffer.alloc(0),
|
|
448
|
+
queue: Promise.resolve()
|
|
345
449
|
};
|
|
346
|
-
this.#clients.
|
|
450
|
+
this.#clients.set(client_id, client);
|
|
347
451
|
socket.on('data', (buf) => this._handleData(client_id, buf));
|
|
348
|
-
socket.on('close', () => this.
|
|
452
|
+
socket.on('close', () => this.#clients.delete(client_id));
|
|
349
453
|
socket.on('error', (err) => this._emitError(err));
|
|
350
|
-
this._safeEmit('connection', client);
|
|
351
454
|
}
|
|
352
455
|
|
|
353
456
|
/**
|
|
354
|
-
*
|
|
457
|
+
* 接收 TCP 数据并排入该连接的串行队列
|
|
355
458
|
* @param {string} client_id 客户端 ID
|
|
356
459
|
* @param {Object} buf 数据缓冲区
|
|
460
|
+
* @returns {void}
|
|
461
|
+
*/
|
|
462
|
+
_handleData(client_id, buf) {
|
|
463
|
+
const client = this.#clients.get(client_id);
|
|
464
|
+
if (!client) return;
|
|
465
|
+
this._enqueue(client, () => this._processTcpData(client, buf));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 把任务追加到连接的串行队列,保证同一连接的消息顺序执行
|
|
470
|
+
* @param {Object} client 连接信息
|
|
471
|
+
* @param {Function} task 待执行任务
|
|
472
|
+
* @returns {void}
|
|
473
|
+
*/
|
|
474
|
+
_enqueue(client, task) {
|
|
475
|
+
client.queue = client.queue
|
|
476
|
+
.then(() => task())
|
|
477
|
+
.catch((err) => this._emitError(err));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 处理 TCP 数据:数据是字节流,需按帧格式累积切分
|
|
482
|
+
* @param {Object} client 连接信息
|
|
483
|
+
* @param {Object} buf 数据缓冲区
|
|
357
484
|
* @returns {Promise<void>} 无返回值
|
|
358
485
|
*/
|
|
359
|
-
async
|
|
360
|
-
|
|
361
|
-
const params = { client_id, data: decoded };
|
|
362
|
-
const ctx = { client_id };
|
|
363
|
-
const done = await this.#handle.run(ctx, params, this._clientOptions(client_id));
|
|
364
|
-
if (done.error) this._emitError(done.error);
|
|
365
|
-
if (done.result !== undefined) this.send(client_id, done.result);
|
|
486
|
+
async _processTcpData(client, buf) {
|
|
487
|
+
await this._processInbound(client, buf, false);
|
|
366
488
|
}
|
|
367
489
|
|
|
490
|
+
// ---------- 分帧与派发 ----------
|
|
368
491
|
/**
|
|
369
|
-
*
|
|
370
|
-
* @param {
|
|
492
|
+
* 统一入站处理:按 framing 还原帧,逐帧派发给事件管理器
|
|
493
|
+
* @param {Object} client 连接信息
|
|
494
|
+
* @param {Object} buf 本次收到的数据
|
|
495
|
+
* @param {boolean} is_complete 数据是否自带边界(UDP 为 true,TCP 为 false)
|
|
496
|
+
* @returns {Promise<void>} 无返回值
|
|
497
|
+
*/
|
|
498
|
+
async _processInbound(client, buf, is_complete) {
|
|
499
|
+
if (this.config.framing === 'none') {
|
|
500
|
+
await this._dispatchMessage(client, client.protocol, this._decodeData(buf));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const buffer = is_complete ? buf : Buffer.concat([client.buffer, buf]);
|
|
504
|
+
const { frames, rest } = this._extractFrames(buffer);
|
|
505
|
+
if (!is_complete) client.buffer = Buffer.from(rest);
|
|
506
|
+
for (const frame of frames) {
|
|
507
|
+
await this._dispatchMessage(client, frame.topic, this._decodeData(frame.payload));
|
|
508
|
+
}
|
|
509
|
+
if (is_complete && rest.length > 0) {
|
|
510
|
+
this._emitError(new Error(`${this._label()} invalid frame: truncated`));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 按当前帧格式切分缓冲区
|
|
516
|
+
* @param {Object} buffer 数据缓冲区
|
|
517
|
+
* @returns {Object} 形如 { frames, rest } 的结果,rest 为未成帧的剩余字节
|
|
518
|
+
*/
|
|
519
|
+
_extractFrames(buffer) {
|
|
520
|
+
if (this.config.framing === 'length') return this._extractLengthFrames(buffer);
|
|
521
|
+
return this._extractLineFrames(buffer);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 按换行切分帧
|
|
526
|
+
* @param {Object} buffer 数据缓冲区
|
|
527
|
+
* @returns {Object} 形如 { frames, rest } 的结果
|
|
528
|
+
*/
|
|
529
|
+
_extractLineFrames(buffer) {
|
|
530
|
+
const frames = [];
|
|
531
|
+
let start = 0;
|
|
532
|
+
let index = buffer.indexOf(LINE_FEED, start);
|
|
533
|
+
while (index !== -1) {
|
|
534
|
+
let end = index;
|
|
535
|
+
if (end > start && buffer[end - 1] === CARRIAGE_RETURN) end -= 1;
|
|
536
|
+
this._appendLineFrame(frames, buffer.subarray(start, end));
|
|
537
|
+
start = index + 1;
|
|
538
|
+
index = buffer.indexOf(LINE_FEED, start);
|
|
539
|
+
}
|
|
540
|
+
return { frames, rest: buffer.subarray(start) };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* 解析单行帧并追加到结果集,缺少 topic 时丢弃并上报错误
|
|
545
|
+
* @param {Array<Object>} frames 帧结果集
|
|
546
|
+
* @param {Object} line 行数据
|
|
547
|
+
* @returns {void}
|
|
548
|
+
*/
|
|
549
|
+
_appendLineFrame(frames, line) {
|
|
550
|
+
if (line.length === 0) return;
|
|
551
|
+
const frame = this._splitLineFrame(line);
|
|
552
|
+
if (!frame) {
|
|
553
|
+
this._emitError(new Error(`${this._label()} invalid frame: missing topic`));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
frames.push(frame);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 把一行拆成 topic 与 payload
|
|
561
|
+
* @param {Object} line 行数据
|
|
562
|
+
* @returns {Object|null} 形如 { topic, payload } 的帧,缺少 topic 时为 null
|
|
563
|
+
*/
|
|
564
|
+
_splitLineFrame(line) {
|
|
565
|
+
const index = line.indexOf(SPACE);
|
|
566
|
+
if (index <= 0) return null;
|
|
567
|
+
const topic = line.subarray(0, index).toString('utf8');
|
|
568
|
+
return { topic, payload: line.subarray(index + 1) };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* 按长度前缀切分帧
|
|
573
|
+
* @param {Object} buffer 数据缓冲区
|
|
574
|
+
* @returns {Object} 形如 { frames, rest } 的结果
|
|
575
|
+
*/
|
|
576
|
+
_extractLengthFrames(buffer) {
|
|
577
|
+
const frames = [];
|
|
578
|
+
let offset = 0;
|
|
579
|
+
while (buffer.length - offset >= LENGTH_HEADER_SIZE) {
|
|
580
|
+
const topic_length = buffer.readUInt16BE(offset);
|
|
581
|
+
const payload_length = buffer.readUInt32BE(offset + TOPIC_LENGTH_SIZE);
|
|
582
|
+
const total = LENGTH_HEADER_SIZE + topic_length + payload_length;
|
|
583
|
+
if (buffer.length - offset < total) break;
|
|
584
|
+
const topic_start = offset + LENGTH_HEADER_SIZE;
|
|
585
|
+
const payload_start = topic_start + topic_length;
|
|
586
|
+
const topic = buffer.subarray(topic_start, payload_start).toString('utf8');
|
|
587
|
+
const payload = buffer.subarray(payload_start, offset + total);
|
|
588
|
+
if (topic_length === 0) {
|
|
589
|
+
this._emitError(new Error(`${this._label()} invalid frame: missing topic`));
|
|
590
|
+
} else {
|
|
591
|
+
frames.push({ topic, payload });
|
|
592
|
+
}
|
|
593
|
+
offset += total;
|
|
594
|
+
}
|
|
595
|
+
return { frames, rest: buffer.subarray(offset) };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 把入站帧派发给事件管理器,以 topic 作为 target
|
|
600
|
+
* @param {Object} client 连接信息
|
|
601
|
+
* @param {string} topic 帧主题
|
|
602
|
+
* @param {string|Object} data 解码后的数据
|
|
603
|
+
* @returns {Promise<void>} 无返回值
|
|
604
|
+
*/
|
|
605
|
+
async _dispatchMessage(client, topic, data) {
|
|
606
|
+
const params = this._buildParams(client, topic, data);
|
|
607
|
+
const ctx = this._buildContext(client, topic);
|
|
608
|
+
const options = this._clientOptions(client);
|
|
609
|
+
try {
|
|
610
|
+
const result = await this.#events.run(topic, params, ctx, options);
|
|
611
|
+
const error = ctx.error || options.error;
|
|
612
|
+
if (error) this._emitError(error);
|
|
613
|
+
this._reply(client, topic, result);
|
|
614
|
+
} catch (err) {
|
|
615
|
+
this._emitError(err);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* 构建事件执行参数
|
|
621
|
+
* @param {Object} client 连接信息
|
|
622
|
+
* @param {string} topic 帧主题
|
|
623
|
+
* @param {string|Object} data 解码后的数据
|
|
624
|
+
* @returns {Object} 参数对象
|
|
625
|
+
*/
|
|
626
|
+
_buildParams(client, topic, data) {
|
|
627
|
+
return { client_id: client.id, topic, data };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 构建事件执行上下文
|
|
632
|
+
* @param {Object} client 连接信息
|
|
633
|
+
* @param {string} topic 帧主题
|
|
634
|
+
* @returns {Object} 上下文对象
|
|
635
|
+
*/
|
|
636
|
+
_buildContext(client, topic) {
|
|
637
|
+
const ctx = {
|
|
638
|
+
client_id: client.id,
|
|
639
|
+
topic,
|
|
640
|
+
protocol: client.protocol,
|
|
641
|
+
address: client.address,
|
|
642
|
+
scope: this.config.scope
|
|
643
|
+
};
|
|
644
|
+
if (client.rinfo) ctx.rinfo = client.rinfo;
|
|
645
|
+
if (client.socket) ctx.socket = client.socket;
|
|
646
|
+
return ctx;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* 按事件返回值回发数据:{ topic, data } 视为重发描述,其余沿用来源 topic
|
|
651
|
+
* @param {Object} client 连接信息
|
|
652
|
+
* @param {string} topic 来源主题
|
|
653
|
+
* @param {*} result 事件返回值
|
|
654
|
+
* @returns {void}
|
|
655
|
+
*/
|
|
656
|
+
_reply(client, topic, result) {
|
|
657
|
+
if (result === undefined || result === null) return;
|
|
658
|
+
if (this._isReplyDescriptor(result)) {
|
|
659
|
+
this.send(client.id, result.data, result.topic);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
this.send(client.id, result, topic);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* 判断事件返回值是否为重发描述
|
|
667
|
+
* @param {*} result 事件返回值
|
|
668
|
+
* @returns {boolean} 是否为形如 { topic, data } 的描述
|
|
669
|
+
*/
|
|
670
|
+
_isReplyDescriptor(result) {
|
|
671
|
+
if (!result || typeof result !== 'object' || Buffer.isBuffer(result)) return false;
|
|
672
|
+
if (typeof result.topic !== 'string' || result.topic.length === 0) return false;
|
|
673
|
+
return Object.prototype.hasOwnProperty.call(result, 'data');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ---------- 建帧与编码 ----------
|
|
677
|
+
/**
|
|
678
|
+
* 按当前帧格式把 topic 与数据打包成可发送的缓冲区
|
|
679
|
+
* @param {string} topic 目标主题,framing 非 none 时必填
|
|
680
|
+
* @param {string|Object|Buffer} data 待发送数据
|
|
681
|
+
* @returns {Object} 待发送缓冲区
|
|
682
|
+
* @throws {Error} framing 非 none 且缺少 topic 时
|
|
683
|
+
*/
|
|
684
|
+
_encodeFrame(topic, data) {
|
|
685
|
+
const payload = this._encodePayload(data);
|
|
686
|
+
if (this.config.framing === 'none') return payload;
|
|
687
|
+
this._requireTopic(topic);
|
|
688
|
+
if (this.config.framing === 'length') return this._buildLengthFrame(topic, payload);
|
|
689
|
+
return this._buildLineFrame(topic, payload);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* 把数据编码成缓冲区,Buffer 原样返回
|
|
694
|
+
* @param {string|Object|Buffer} data 待编码数据
|
|
695
|
+
* @returns {Object} 数据缓冲区
|
|
696
|
+
*/
|
|
697
|
+
_encodePayload(data) {
|
|
698
|
+
if (Buffer.isBuffer(data)) return data;
|
|
699
|
+
return Buffer.from(this._encodeData(data), this.config.encoding);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* 构建行帧:`topic<空格>payload\n`
|
|
704
|
+
* @param {string} topic 主题
|
|
705
|
+
* @param {Object} payload 数据缓冲区
|
|
706
|
+
* @returns {Object} 帧缓冲区
|
|
707
|
+
* @throws {Error} payload 含换行符时(行帧无法表达)
|
|
371
708
|
*/
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
709
|
+
_buildLineFrame(topic, payload) {
|
|
710
|
+
if (payload.indexOf(LINE_FEED) !== -1) {
|
|
711
|
+
throw new Error(`${this._label()} line framing payload cannot contain newline`);
|
|
712
|
+
}
|
|
713
|
+
const head = Buffer.from(`${topic} `, 'utf8');
|
|
714
|
+
const tail = Buffer.from([LINE_FEED]);
|
|
715
|
+
return Buffer.concat([head, payload, tail]);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 构建长度前缀帧:`[2字节 topic 长度][4字节 payload 长度][topic][payload]`
|
|
720
|
+
* @param {string} topic 主题
|
|
721
|
+
* @param {Object} payload 数据缓冲区
|
|
722
|
+
* @returns {Object} 帧缓冲区
|
|
723
|
+
* @throws {Error} topic 超出 65535 字节时
|
|
724
|
+
*/
|
|
725
|
+
_buildLengthFrame(topic, payload) {
|
|
726
|
+
const topic_buf = Buffer.from(topic, 'utf8');
|
|
727
|
+
if (topic_buf.length > 0xffff) {
|
|
728
|
+
throw new Error(`${this._label()} topic too long`);
|
|
729
|
+
}
|
|
730
|
+
const header = Buffer.alloc(LENGTH_HEADER_SIZE);
|
|
731
|
+
header.writeUInt16BE(topic_buf.length, 0);
|
|
732
|
+
header.writeUInt32BE(payload.length, TOPIC_LENGTH_SIZE);
|
|
733
|
+
return Buffer.concat([header, topic_buf, payload]);
|
|
375
734
|
}
|
|
376
735
|
|
|
377
736
|
/**
|
|
@@ -390,10 +749,11 @@ class SocketServer extends Mod {
|
|
|
390
749
|
*/
|
|
391
750
|
async _startUdp() {
|
|
392
751
|
const socket = dgram.createSocket('udp4');
|
|
393
|
-
this.#
|
|
752
|
+
this.#servers.udp = socket;
|
|
394
753
|
await this._bindUdp(socket);
|
|
395
754
|
socket.on('message', (buf, rinfo) => this._handleUdpMessage(buf, rinfo));
|
|
396
755
|
socket.on('error', (err) => this._emitError(err));
|
|
756
|
+
this._startUdpTimer();
|
|
397
757
|
}
|
|
398
758
|
|
|
399
759
|
/**
|
|
@@ -414,31 +774,86 @@ class SocketServer extends Mod {
|
|
|
414
774
|
}
|
|
415
775
|
|
|
416
776
|
/**
|
|
417
|
-
* 处理 UDP
|
|
777
|
+
* 处理 UDP 报文:首见远端地址登记为客户端,之后排入该客户端的串行队列
|
|
418
778
|
* @param {Object} buf 数据缓冲区
|
|
419
779
|
* @param {Object} rinfo 远端地址信息
|
|
780
|
+
* @returns {void}
|
|
420
781
|
*/
|
|
421
782
|
_handleUdpMessage(buf, rinfo) {
|
|
783
|
+
const client = this._ensureUdpClient(rinfo);
|
|
784
|
+
this._enqueue(client, () => this._processUdpData(client, buf));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* 处理 UDP 报文:数据报自带边界,按帧格式解析后派发
|
|
789
|
+
* @param {Object} client 连接信息
|
|
790
|
+
* @param {Object} buf 数据缓冲区
|
|
791
|
+
* @returns {Promise<void>} 无返回值
|
|
792
|
+
*/
|
|
793
|
+
async _processUdpData(client, buf) {
|
|
794
|
+
await this._processInbound(client, buf, true);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* 登记 UDP 客户端(同一远端地址复用同一 client_id),并刷新活跃时间
|
|
799
|
+
* @param {Object} rinfo 远端地址信息
|
|
800
|
+
* @returns {Object} 客户端信息
|
|
801
|
+
*/
|
|
802
|
+
_ensureUdpClient(rinfo) {
|
|
422
803
|
const client_id = `udp_${rinfo.address}:${rinfo.port}`;
|
|
423
|
-
const
|
|
804
|
+
const existed = this.#clients.get(client_id);
|
|
805
|
+
if (existed) {
|
|
806
|
+
existed.last_seen = Date.now();
|
|
807
|
+
return existed;
|
|
808
|
+
}
|
|
424
809
|
const client = {
|
|
425
810
|
id: client_id,
|
|
426
|
-
|
|
811
|
+
protocol: 'udp',
|
|
427
812
|
address: client_id,
|
|
428
813
|
socket: null,
|
|
429
|
-
rinfo
|
|
814
|
+
rinfo,
|
|
815
|
+
options: null,
|
|
816
|
+
last_seen: Date.now(),
|
|
817
|
+
queue: Promise.resolve()
|
|
430
818
|
};
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
819
|
+
this.#clients.set(client_id, client);
|
|
820
|
+
return client;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* 启动 UDP 过期连接清理定时器,udp_ttl <= 0 时关闭清理
|
|
825
|
+
* @returns {void}
|
|
826
|
+
*/
|
|
827
|
+
_startUdpTimer() {
|
|
828
|
+
const ttl = this.config.udp_ttl;
|
|
829
|
+
if (!ttl || ttl <= 0) return;
|
|
830
|
+
const interval = Math.max(1000, Math.min(ttl, 60000));
|
|
831
|
+
this.#udp_timer = setInterval(() => this._pruneUdpClients(), interval);
|
|
832
|
+
if (typeof this.#udp_timer.unref === 'function') this.#udp_timer.unref();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* 清理超过 udp_ttl 未活动的 UDP 客户端
|
|
837
|
+
* @returns {void}
|
|
838
|
+
*/
|
|
839
|
+
_pruneUdpClients() {
|
|
840
|
+
const ttl = this.config.udp_ttl;
|
|
841
|
+
if (!ttl || ttl <= 0) return;
|
|
842
|
+
const deadline = Date.now() - ttl;
|
|
843
|
+
for (const [client_id, client] of this.#clients) {
|
|
844
|
+
if (client.protocol !== 'udp') continue;
|
|
845
|
+
if (client.last_seen <= deadline) this.#clients.delete(client_id);
|
|
434
846
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* 停止 UDP 过期连接清理定时器
|
|
851
|
+
* @returns {void}
|
|
852
|
+
*/
|
|
853
|
+
_clearUdpTimer() {
|
|
854
|
+
if (!this.#udp_timer) return;
|
|
855
|
+
clearInterval(this.#udp_timer);
|
|
856
|
+
this.#udp_timer = null;
|
|
442
857
|
}
|
|
443
858
|
|
|
444
859
|
// ---------- 数据编解码 ----------
|
|
@@ -486,6 +901,7 @@ class SocketServer extends Mod {
|
|
|
486
901
|
*/
|
|
487
902
|
_sendToClient(client, payload) {
|
|
488
903
|
if (client.socket) {
|
|
904
|
+
if (client.socket.destroyed) return false;
|
|
489
905
|
client.socket.write(payload);
|
|
490
906
|
return true;
|
|
491
907
|
}
|
|
@@ -499,54 +915,61 @@ class SocketServer extends Mod {
|
|
|
499
915
|
* @returns {boolean} 是否发送成功
|
|
500
916
|
*/
|
|
501
917
|
_sendUdp(client, payload) {
|
|
502
|
-
const socket = this.#
|
|
918
|
+
const socket = this.#servers.udp;
|
|
503
919
|
if (!socket || !client.rinfo) return false;
|
|
504
920
|
socket.send(payload, client.rinfo.port, client.rinfo.address);
|
|
505
921
|
return true;
|
|
506
922
|
}
|
|
507
923
|
|
|
508
924
|
/**
|
|
509
|
-
*
|
|
925
|
+
* 断开全部客户端连接并清空连接表
|
|
510
926
|
*/
|
|
511
927
|
_closeAllClients() {
|
|
512
|
-
for (const client of this.#clients.
|
|
928
|
+
for (const client of this.#clients.values()) {
|
|
513
929
|
if (client.socket) client.socket.destroy();
|
|
514
930
|
}
|
|
515
931
|
this.#clients.clear();
|
|
516
932
|
}
|
|
517
933
|
|
|
518
934
|
/**
|
|
519
|
-
*
|
|
935
|
+
* 关闭全部底层服务器并停止 UDP 清理定时器
|
|
520
936
|
* @returns {Promise<void>} 无返回值
|
|
521
937
|
*/
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.#
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
});
|
|
938
|
+
async _closeServers() {
|
|
939
|
+
this._clearUdpTimer();
|
|
940
|
+
const servers = this.#servers;
|
|
941
|
+
this.#servers = { tcp: null, udp: null };
|
|
942
|
+
await this._closeOne(servers.tcp);
|
|
943
|
+
await this._closeOne(servers.udp);
|
|
529
944
|
}
|
|
530
945
|
|
|
531
|
-
// ---------- 校验辅助 ----------
|
|
532
946
|
/**
|
|
533
|
-
*
|
|
534
|
-
*
|
|
947
|
+
* 关闭单个底层服务器
|
|
948
|
+
* @param {Object|null} server net.Server 或 dgram.Socket 实例
|
|
949
|
+
* @returns {Promise<void>} 无返回值
|
|
535
950
|
*/
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
951
|
+
_closeOne(server) {
|
|
952
|
+
if (!server) return Promise.resolve();
|
|
953
|
+
return new Promise((resolve) => {
|
|
954
|
+
try {
|
|
955
|
+
server.close(() => resolve());
|
|
956
|
+
} catch {
|
|
957
|
+
resolve();
|
|
958
|
+
}
|
|
959
|
+
});
|
|
540
960
|
}
|
|
541
961
|
|
|
962
|
+
// ---------- 校验辅助 ----------
|
|
542
963
|
/**
|
|
543
|
-
*
|
|
544
|
-
* @
|
|
545
|
-
* @
|
|
964
|
+
* 断言 topic 非空(framing 非 none 时帧必须携带 topic)
|
|
965
|
+
* @param {string} topic 目标主题
|
|
966
|
+
* @returns {void}
|
|
967
|
+
* @throws {Error} topic 为空时
|
|
546
968
|
*/
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
969
|
+
_requireTopic(topic) {
|
|
970
|
+
if (!topic) {
|
|
971
|
+
throw new Error(`${this._label()} topic required when framing is ${this.config.framing}`);
|
|
972
|
+
}
|
|
550
973
|
}
|
|
551
974
|
|
|
552
975
|
/**
|