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/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, Store } = require('mm_corejs');
6
- const { Handle } = require('mm_handle');
5
+ const { Mod } = require('mm_mod');
6
+ const { eventAdmin } = require('mm_event');
7
7
 
8
8
  /**
9
- * 服务器设置校验 schema(校验对象为顶层 config,而非 run 时选项)
9
+ * 长度前缀帧的头部字节数:2 字节 topic 长度 + 4 字节 payload 长度
10
10
  */
11
- const SERVER_SCHEMA = {
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
- * SocketServer —— TCP/UDP 套接字服务器(入站适配器模块)。
24
- *
25
- * 继承 mm_corejs 的 Mod:生命周期由 Lifecycle 模板方法驱动,
26
- * 调用参数校验由 config.params 声明式定义;
27
- * 服务器设置(protocol / host / port / encoding / json)为顶层 config 字段,
28
- * 在 onInit 阶段按 SERVER_SCHEMA 统一校验。
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. 监听:onStart 按 protocol 创建 net.Server 或 dgram.Socket 并监听;
32
- * 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
33
- * 3. 消息管道:收到的消息经 Handle 的 before/check/main/render 阶段处理,
34
- * 管道产出 result 时自动回发来源客户端;getHandle() 可注册自定义钩子;
35
- * 4. 事件分发:connection / message / close / error 经 EventEmitter 向外广播;
36
- * 5. 主动操作:run(ctx, params) 按 params.action 派发 send / broadcast / close / list。
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', host: '127.0.0.1', port: 9000 }, server);
40
- * socket.getHandle().on('main', (ctx, params) => `echo: ${params.data}`, 100, 'echo');
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: '1.0.0',
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
- options: {},
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
- data: {},
67
- query: { type: 'object' }
92
+ topic: { type: 'string', min_length: 1 },
93
+ data: {}
68
94
  }
69
95
  }
70
96
  };
71
97
 
72
98
  /**
73
- * 底层服务器实例(net.Server 或 dgram.Socket)
99
+ * 底层服务器实例,按协议分槽存放
74
100
  */
75
- #server = null;
101
+ #servers = { tcp: null, udp: null };
76
102
 
77
103
  /**
78
- * 连接注册表:Store 存储,主键 id、名称字段 name
104
+ * 事件管理器(取自 mm_event 的 socket 作用域,与其他实例共享)
79
105
  */
80
- #clients = new Store({ key: 'id', name: 'name' });
106
+ #events = null;
81
107
 
82
108
  /**
83
- * 消息处理管道(before/check/main/render/error/success/after)
84
- * 每个实例独立 Handle,避免多实例共享管道导致钩子泄漏
109
+ * 在线连接表:client_id -> 连接信息
85
110
  */
86
- #handle = new Handle();
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
- await this._validateConfig();
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
- async onStart() {
148
+ _validateConfig() {
106
149
  const protocol = this.config.protocol;
107
- if (protocol === 'udp') {
108
- await this._startUdp();
109
- return;
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 onStop() {
118
- this._closeAllClients();
119
- await this._closeServer();
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
- async onDispose() {
126
- this.#clients.clear();
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(params);
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 与 data
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(params) {
183
- return { action: 'list', clients: this.listClients(params.query) };
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.getById(client_id);
325
+ send(client_id, data, topic) {
326
+ const client = this.#clients.get(client_id);
195
327
  if (!client) return false;
196
- const payload = this._encodeData(data);
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._encodeData(data);
338
+ broadcast(data, topic) {
339
+ const payload = this._encodeFrame(topic, data);
207
340
  let count = 0;
208
- for (const client of this.#clients.list()) {
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.getById(client_id);
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
- * 列出在线客户端(支持 Store 条件查询与分页)
230
- * @param {Object} query 查询条件,可含 page_now / page_size / keyword
231
- * @returns {Array<Object>} 客户端信息列表,含 id 与 address
361
+ * 列出在线客户端
362
+ * @returns {Array<Object>} 客户端信息列表,含 id、protocol 与 address
232
363
  */
233
- listClients(query) {
234
- return this.#clients.list(query).map((client) => ({ id: client.id, address: client.address }));
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 !!this.#clients.getById(client_id);
378
+ return this.#clients.has(client_id);
244
379
  }
245
380
 
246
381
  /**
247
- * 绑定连接级选项(如 user_id / device_id / agent_id),入站消息将作为管道 options 透传给钩子与 RPC 处理函数
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.getById(client_id);
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 {string} client_id 客户端 ID
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
- use(plugin) {
278
- if (!plugin || typeof plugin.bind !== 'function') {
279
- throw new TypeError(`${this._label()} plugin must provide bind()`);
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.#server = server;
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
- name: client_id,
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.add(client);
450
+ this.#clients.set(client_id, client);
347
451
  socket.on('data', (buf) => this._handleData(client_id, buf));
348
- socket.on('close', () => this._handleClose(client_id));
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
- * 处理 TCP 收到的数据:解码后进入消息管道,产出 result 时自动回发
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 _handleData(client_id, buf) {
360
- const decoded = this._decodeData(buf);
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
- * 处理 TCP 连接关闭
370
- * @param {string} client_id 客户端 ID
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
- _handleClose(client_id) {
373
- const existed = this.#clients.delById(client_id);
374
- if (existed) this._safeEmit('close', client_id);
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.#server = socket;
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 报文:首见远端地址登记为客户端并触发 connection
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 is_new = !this.#clients.getById(client_id);
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
- name: client_id,
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
- if (is_new) {
432
- this.#clients.add(client);
433
- this._safeEmit('connection', client);
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
- const decoded = this._decodeData(buf);
436
- const params = { client_id, data: decoded };
437
- const ctx = { client_id };
438
- this.#handle.run(ctx, params, this._clientOptions(client_id)).then((done) => {
439
- if (done.error) this._emitError(done.error);
440
- if (done.result !== undefined) this.send(client_id, done.result);
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.#server;
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.list()) {
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
- _closeServer() {
523
- const server = this.#server;
524
- if (!server) return Promise.resolve();
525
- this.#server = null;
526
- return new Promise((resolve) => {
527
- server.close(resolve);
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
- * 装配消息管道:内置 main 钩子负责分发 message 事件(sort 置后,
534
- * 不返回值,业务钩子产出的 result 由 _handleData 统一回发)
947
+ * 关闭单个底层服务器
948
+ * @param {Object|null} server net.Server 或 dgram.Socket 实例
949
+ * @returns {Promise<void>} 无返回值
535
950
  */
536
- _initHandle() {
537
- this.#handle.on('main', (params, ctx) => {
538
- this._safeEmit('message', params.client_id, params.data);
539
- }, 1000, 'emit_message');
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
- * 校验顶层 config 中的服务器设置
544
- * @returns {Promise<void>} 无返回值
545
- * @throws {Error} 校验不通过时
964
+ * 断言 topic 非空(framing 非 none 时帧必须携带 topic)
965
+ * @param {string} topic 目标主题
966
+ * @returns {void}
967
+ * @throws {Error} topic 为空时
546
968
  */
547
- async _validateConfig() {
548
- const tip = await this._validate(this.config, SERVER_SCHEMA);
549
- if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
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
  /**