mm_websocket_server 2.0.0 → 2.0.1

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.
Files changed (3) hide show
  1. package/README.md +15 -4
  2. package/lib/websocket.js +109 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,13 +9,17 @@
9
9
  ## 消息路径
10
10
 
11
11
  ```
12
- WebSocket.Server → connection → ctx = { client_id, path, ws }
13
- → eventManager.run(path, params, ctx, options)
12
+ WebSocket.Server → connection → ctx = { client_id, path, scope, ws }
13
+ → eventManager.run(path, params, ctx, options)(同一连接的消息串行执行)
14
14
  → 结果非空时回发来源连接
15
15
  ```
16
16
 
17
17
  事件 `target` 取握手请求路径(第一个 `?` 之前)。`params` 为 `{ client_id, data }`,`data` 在 `json: true` 时尝试 `JSON.parse`(失败回落原文)。
18
18
 
19
+ 同一连接的消息按到达顺序串行执行:事件处理函数返回 Promise 时,后一条消息会等前一条处理完再开始,避免响应乱序。
20
+
21
+ 二进制帧保持 `Buffer` 原样传给事件(`ctx.is_binary` 为 `true`),不参与 `json` 解析;事件返回 `Buffer` 时也按二进制帧回发。文本帧走 `json` / UTF-8 文本逻辑。
22
+
19
23
  ## 快速开始
20
24
 
21
25
  ```js
@@ -48,6 +52,7 @@ console.log(websocket.getAddress());
48
52
  | `host` | string | `'0.0.0.0'` | 监听地址 |
49
53
  | `port` | number | `8090` | 监听端口 |
50
54
  | `json` | boolean | `true` | 入站尝试 JSON 解析,出站对象自动序列化 |
55
+ | `scope` | string | `'websocket'` | 事件管理器作用域,多实例可用不同 scope 隔离事件 |
51
56
  | `params` | object | `{ action }` | `main` 的参数约定 |
52
57
 
53
58
  不限制升级路径,任何路径都可连接,路径即事件 `target`。
@@ -58,7 +63,7 @@ console.log(websocket.getAddress());
58
63
  | --- | --- |
59
64
  | `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
60
65
  | `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
61
- | `getEventManager()` | 获取 `mm_event` 的 `websocket` 作用域事件管理器(与其他实例共享),用于注册事件 |
66
+ | `getEventManager()` | 获取 `mm_event` 上 `scope` 作用域的事件管理器(默认 `websocket`,其他实例共享同一作用域),用于注册事件 |
62
67
  | `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
63
68
  | `getAddress()` | 实际监听地址,未启动为 `null` |
64
69
  | `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `send` / `broadcast` / `close` / `list` |
@@ -75,6 +80,8 @@ console.log(websocket.getAddress());
75
80
  | --- | --- |
76
81
  | `client_id` | 连接编号,形如 `ws_1` |
77
82
  | `path` | 握手请求路径,`?` 之后已截断 |
83
+ | `scope` | 当前实例的事件管理器作用域 |
84
+ | `is_binary` | 是否为二进制帧 |
78
85
  | `ws` | 原生 ws 连接,事件可直接写出 |
79
86
 
80
87
  ## 事件处理函数
@@ -98,9 +105,13 @@ await websocket.run({ action: 'close', client_id: 'ws_1' });
98
105
  await websocket.run({ action: 'list' });
99
106
  ```
100
107
 
108
+ `action` 仅支持上述四种,其他值直接抛 `unsupported action` 错误。
109
+
101
110
  ## 生命周期
102
111
 
103
- `onStart` 创建并监听 `WebSocket.Server`,`onStop` 断开全部连接并关闭服务器。事件管理器取自 `mm_event` 的 `websocket` 作用域,由全局 `Admin` 共享,停止时不清理。端口被占用时 `start()` 抛错,模块进入 `error` 状态,可 `dispose()` 释放。
112
+ `onStart` 先挂 `error` / `connection` 监听再等待 `listening`,创建并监听 `WebSocket.Server`;`onStop` 断开全部连接并关闭服务器。启动失败(如端口被占用)时会先回滚已创建的服务器与连接表再抛错,模块进入 `error` 状态,可 `dispose()` 释放。
113
+
114
+ 事件管理器取自 `mm_event` 上 `scope` 指定的作用域(默认 `websocket`),由全局 `Admin` 共享,停止时不清理;多个实例需要不同作用域时,配置不同的 `scope` 即可。
104
115
 
105
116
  ## 示例
106
117
 
package/lib/websocket.js CHANGED
@@ -41,6 +41,7 @@ class WebSocketServer extends Mod {
41
41
  host: '0.0.0.0',
42
42
  port: 8090,
43
43
  json: true,
44
+ scope: 'websocket',
44
45
  params: {
45
46
  type: 'object',
46
47
  required: ['action'],
@@ -79,23 +80,41 @@ class WebSocketServer extends Mod {
79
80
  */
80
81
  constructor(config, server) {
81
82
  super(config, server);
82
- this.#events = eventAdmin('websocket');
83
+ this.#events = eventAdmin(this.config.scope);
83
84
  }
84
85
 
85
86
  // ---------- 生命周期钩子 ----------
86
87
  /**
87
- * 启动钩子:创建 ws 服务器并开始监听
88
+ * 启动钩子:创建 ws 服务器并开始监听,失败时回滚已创建的资源
88
89
  * @returns {Promise<void>} 无返回值
89
90
  */
90
91
  async onStart() {
91
- const wss = new WebSocket.Server({
92
- host: this.config.host,
93
- port: this.config.port
94
- });
95
- this.#wss = wss;
96
- await this._listen(wss);
97
- wss.on('error', (err) => this._emitError(err));
98
- wss.on('connection', (ws, req) => this._handleConnection(ws, req));
92
+ try {
93
+ const wss = new WebSocket.Server({
94
+ host: this.config.host,
95
+ port: this.config.port
96
+ });
97
+ this.#wss = wss;
98
+ wss.on('error', (err) => this._emitError(err));
99
+ wss.on('connection', (ws, req) => this._handleConnection(ws, req));
100
+ await this._listen(wss);
101
+ } catch (err) {
102
+ await this._rollbackStart();
103
+ throw err;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * 启动失败时回滚已创建的资源
109
+ * @returns {Promise<void>} 无返回值
110
+ */
111
+ async _rollbackStart() {
112
+ try {
113
+ this._closeAllClients();
114
+ await this._closeServer();
115
+ } catch (err) {
116
+ this._emitError(err);
117
+ }
99
118
  }
100
119
 
101
120
  /**
@@ -114,13 +133,15 @@ class WebSocketServer extends Mod {
114
133
  * @param {Object} ctx 执行上下文
115
134
  * @param {Object} options 合并后的选项
116
135
  * @returns {Promise<Object>} 操作结果
136
+ * @throws {Error} action 不受支持时
117
137
  */
118
138
  async main(params, ctx, options) {
119
139
  const action = params.action;
120
140
  if (action === 'send') return this._actionSend(params);
121
141
  if (action === 'broadcast') return this._actionBroadcast(params);
122
142
  if (action === 'close') return this._actionClose(params);
123
- return this._actionList();
143
+ if (action === 'list') return this._actionList();
144
+ throw new Error(`${this._label()} unsupported action: ${action}`);
124
145
  }
125
146
 
126
147
  /**
@@ -282,36 +303,88 @@ class WebSocketServer extends Mod {
282
303
  path: getPath(req.url || '/'),
283
304
  address: this._remoteAddress(req),
284
305
  ws,
285
- options: null
306
+ options: null,
307
+ queue: Promise.resolve()
286
308
  };
287
309
  this.#clients.set(client_id, client);
288
- ws.on('message', (buf) => this._handleMessage(client_id, buf));
310
+ ws.on('message', (data, is_binary) => this._handleMessage(client_id, data, is_binary));
289
311
  ws.on('close', () => this.#clients.delete(client_id));
290
312
  ws.on('error', (err) => this._emitError(err));
291
313
  }
292
314
 
293
315
  /**
294
- * 处理入站消息:解码后交给事件管理器,返回非空结果时回发来源连接
316
+ * 接收消息并排入该连接的串行队列
295
317
  * @param {string} client_id 客户端 ID
296
- * @param {Object} buf 数据缓冲区
297
- * @returns {Promise<void>} 无返回值
318
+ * @param {Object} data 消息内容,文本帧为 Buffer,二进制帧为 Buffer
319
+ * @param {boolean} is_binary 是否为二进制帧
320
+ * @returns {void}
298
321
  */
299
- async _handleMessage(client_id, buf) {
322
+ _handleMessage(client_id, data, is_binary) {
300
323
  const client = this.#clients.get(client_id);
301
324
  if (!client) return;
302
- const params = { client_id, data: this._decodeData(buf) };
303
- const ctx = { client_id, path: client.path, ws: client.ws };
325
+ this._enqueue(client, () => this._processMessage(client, data, is_binary));
326
+ }
327
+
328
+ /**
329
+ * 把任务追加到连接的串行队列,保证同一连接的消息顺序执行
330
+ * @param {Object} client 连接信息
331
+ * @param {Function} task 待执行任务
332
+ * @returns {void}
333
+ */
334
+ _enqueue(client, task) {
335
+ client.queue = client.queue
336
+ .then(() => task())
337
+ .catch((err) => this._emitError(err));
338
+ }
339
+
340
+ /**
341
+ * 处理入站消息:解码后交给事件管理器,返回非空结果时回发来源连接
342
+ * @param {Object} client 连接信息
343
+ * @param {Object} data 消息内容
344
+ * @param {boolean} is_binary 是否为二进制帧
345
+ * @returns {Promise<void>} 无返回值
346
+ */
347
+ async _processMessage(client, data, is_binary) {
348
+ const params = this._buildParams(client, data, is_binary);
349
+ const ctx = this._buildContext(client, is_binary);
304
350
  const options = this._clientOptions(client);
305
351
  try {
306
352
  const result = await this.#events.run(client.path, params, ctx, options);
307
353
  const error = ctx.error || options.error;
308
354
  if (error) this._emitError(error);
309
- if (result !== undefined && result !== null) this.send(client_id, result);
355
+ if (result !== undefined && result !== null) this.send(client.id, result);
310
356
  } catch (err) {
311
357
  this._emitError(err);
312
358
  }
313
359
  }
314
360
 
361
+ /**
362
+ * 构建事件执行参数
363
+ * @param {Object} client 连接信息
364
+ * @param {Object} data 消息内容
365
+ * @param {boolean} is_binary 是否为二进制帧
366
+ * @returns {Object} 参数对象
367
+ */
368
+ _buildParams(client, data, is_binary) {
369
+ return { client_id: client.id, data: this._decodeData(data, is_binary) };
370
+ }
371
+
372
+ /**
373
+ * 构建事件执行上下文
374
+ * @param {Object} client 连接信息
375
+ * @param {boolean} is_binary 是否为二进制帧
376
+ * @returns {Object} 上下文对象
377
+ */
378
+ _buildContext(client, is_binary) {
379
+ return {
380
+ client_id: client.id,
381
+ path: client.path,
382
+ scope: this.config.scope,
383
+ is_binary,
384
+ ws: client.ws
385
+ };
386
+ }
387
+
315
388
  /**
316
389
  * 获取连接级选项(复制一份,避免事件管理器回写)
317
390
  * @param {Object} client 连接信息
@@ -333,23 +406,26 @@ class WebSocketServer extends Mod {
333
406
 
334
407
  // ---------- 数据编解码 ----------
335
408
  /**
336
- * 编码待发送数据
337
- * @param {string|Object} data 待编码数据
338
- * @returns {string} 编码后的字符串
409
+ * 编码待发送数据,Buffer 原样发送(二进制帧),其余按配置序列化
410
+ * @param {string|Object|Buffer} data 待编码数据
411
+ * @returns {string|Buffer} 编码后的内容
339
412
  */
340
413
  _encodeData(data) {
414
+ if (Buffer.isBuffer(data)) return data;
341
415
  if (this.config.json) return JSON.stringify(data);
342
416
  if (typeof data === 'string') return data;
343
417
  return String(data);
344
418
  }
345
419
 
346
420
  /**
347
- * 解码收到的数据
348
- * @param {Object} buf 数据缓冲区
349
- * @returns {string|Object} 解码后的数据,json 模式下解析失败时返回原文
421
+ * 解码收到的数据,二进制帧保持 Buffer 原样
422
+ * @param {Object} data 消息内容
423
+ * @param {boolean} is_binary 是否为二进制帧
424
+ * @returns {string|Object|Buffer} 解码后的数据,json 模式下解析失败时返回原文
350
425
  */
351
- _decodeData(buf) {
352
- const text = buf.toString('utf8');
426
+ _decodeData(data, is_binary) {
427
+ if (is_binary) return data;
428
+ const text = data.toString('utf8');
353
429
  if (!this.config.json) return text;
354
430
  return this._parseJson(text);
355
431
  }
@@ -414,7 +490,11 @@ class WebSocketServer extends Mod {
414
490
  this.#wss = null;
415
491
  if (!wss) return Promise.resolve();
416
492
  return new Promise((resolve) => {
417
- wss.close(() => resolve());
493
+ try {
494
+ wss.close(() => resolve());
495
+ } catch {
496
+ resolve();
497
+ }
418
498
  });
419
499
  }
420
500
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mm_websocket_server",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "最薄的 WebSocket 服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
5
5
  "keywords": [
6
6
  "超级美眉",