mm_websocket_server 2.0.0 → 2.0.2

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 +110 -29
  3. package/package.json +47 -47
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,16 @@ 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) {
139
+ this._assertRunning();
119
140
  const action = params.action;
120
141
  if (action === 'send') return this._actionSend(params);
121
142
  if (action === 'broadcast') return this._actionBroadcast(params);
122
143
  if (action === 'close') return this._actionClose(params);
123
- return this._actionList();
144
+ if (action === 'list') return this._actionList();
145
+ throw new Error(`${this._label()} unsupported action: ${action}`);
124
146
  }
125
147
 
126
148
  /**
@@ -282,36 +304,88 @@ class WebSocketServer extends Mod {
282
304
  path: getPath(req.url || '/'),
283
305
  address: this._remoteAddress(req),
284
306
  ws,
285
- options: null
307
+ options: null,
308
+ queue: Promise.resolve()
286
309
  };
287
310
  this.#clients.set(client_id, client);
288
- ws.on('message', (buf) => this._handleMessage(client_id, buf));
311
+ ws.on('message', (data, is_binary) => this._handleMessage(client_id, data, is_binary));
289
312
  ws.on('close', () => this.#clients.delete(client_id));
290
313
  ws.on('error', (err) => this._emitError(err));
291
314
  }
292
315
 
293
316
  /**
294
- * 处理入站消息:解码后交给事件管理器,返回非空结果时回发来源连接
317
+ * 接收消息并排入该连接的串行队列
295
318
  * @param {string} client_id 客户端 ID
296
- * @param {Object} buf 数据缓冲区
297
- * @returns {Promise<void>} 无返回值
319
+ * @param {Object} data 消息内容,文本帧为 Buffer,二进制帧为 Buffer
320
+ * @param {boolean} is_binary 是否为二进制帧
321
+ * @returns {void}
298
322
  */
299
- async _handleMessage(client_id, buf) {
323
+ _handleMessage(client_id, data, is_binary) {
300
324
  const client = this.#clients.get(client_id);
301
325
  if (!client) return;
302
- const params = { client_id, data: this._decodeData(buf) };
303
- const ctx = { client_id, path: client.path, ws: client.ws };
326
+ this._enqueue(client, () => this._processMessage(client, data, is_binary));
327
+ }
328
+
329
+ /**
330
+ * 把任务追加到连接的串行队列,保证同一连接的消息顺序执行
331
+ * @param {Object} client 连接信息
332
+ * @param {Function} task 待执行任务
333
+ * @returns {void}
334
+ */
335
+ _enqueue(client, task) {
336
+ client.queue = client.queue
337
+ .then(() => task())
338
+ .catch((err) => this._emitError(err));
339
+ }
340
+
341
+ /**
342
+ * 处理入站消息:解码后交给事件管理器,返回非空结果时回发来源连接
343
+ * @param {Object} client 连接信息
344
+ * @param {Object} data 消息内容
345
+ * @param {boolean} is_binary 是否为二进制帧
346
+ * @returns {Promise<void>} 无返回值
347
+ */
348
+ async _processMessage(client, data, is_binary) {
349
+ const params = this._buildParams(client, data, is_binary);
350
+ const ctx = this._buildContext(client, is_binary);
304
351
  const options = this._clientOptions(client);
305
352
  try {
306
353
  const result = await this.#events.run(client.path, params, ctx, options);
307
354
  const error = ctx.error || options.error;
308
355
  if (error) this._emitError(error);
309
- if (result !== undefined && result !== null) this.send(client_id, result);
356
+ if (result !== undefined && result !== null) this.send(client.id, result);
310
357
  } catch (err) {
311
358
  this._emitError(err);
312
359
  }
313
360
  }
314
361
 
362
+ /**
363
+ * 构建事件执行参数
364
+ * @param {Object} client 连接信息
365
+ * @param {Object} data 消息内容
366
+ * @param {boolean} is_binary 是否为二进制帧
367
+ * @returns {Object} 参数对象
368
+ */
369
+ _buildParams(client, data, is_binary) {
370
+ return { client_id: client.id, data: this._decodeData(data, is_binary) };
371
+ }
372
+
373
+ /**
374
+ * 构建事件执行上下文
375
+ * @param {Object} client 连接信息
376
+ * @param {boolean} is_binary 是否为二进制帧
377
+ * @returns {Object} 上下文对象
378
+ */
379
+ _buildContext(client, is_binary) {
380
+ return {
381
+ client_id: client.id,
382
+ path: client.path,
383
+ scope: this.config.scope,
384
+ is_binary,
385
+ ws: client.ws
386
+ };
387
+ }
388
+
315
389
  /**
316
390
  * 获取连接级选项(复制一份,避免事件管理器回写)
317
391
  * @param {Object} client 连接信息
@@ -333,23 +407,26 @@ class WebSocketServer extends Mod {
333
407
 
334
408
  // ---------- 数据编解码 ----------
335
409
  /**
336
- * 编码待发送数据
337
- * @param {string|Object} data 待编码数据
338
- * @returns {string} 编码后的字符串
410
+ * 编码待发送数据,Buffer 原样发送(二进制帧),其余按配置序列化
411
+ * @param {string|Object|Buffer} data 待编码数据
412
+ * @returns {string|Buffer} 编码后的内容
339
413
  */
340
414
  _encodeData(data) {
415
+ if (Buffer.isBuffer(data)) return data;
341
416
  if (this.config.json) return JSON.stringify(data);
342
417
  if (typeof data === 'string') return data;
343
418
  return String(data);
344
419
  }
345
420
 
346
421
  /**
347
- * 解码收到的数据
348
- * @param {Object} buf 数据缓冲区
349
- * @returns {string|Object} 解码后的数据,json 模式下解析失败时返回原文
422
+ * 解码收到的数据,二进制帧保持 Buffer 原样
423
+ * @param {Object} data 消息内容
424
+ * @param {boolean} is_binary 是否为二进制帧
425
+ * @returns {string|Object|Buffer} 解码后的数据,json 模式下解析失败时返回原文
350
426
  */
351
- _decodeData(buf) {
352
- const text = buf.toString('utf8');
427
+ _decodeData(data, is_binary) {
428
+ if (is_binary) return data;
429
+ const text = data.toString('utf8');
353
430
  if (!this.config.json) return text;
354
431
  return this._parseJson(text);
355
432
  }
@@ -414,7 +491,11 @@ class WebSocketServer extends Mod {
414
491
  this.#wss = null;
415
492
  if (!wss) return Promise.resolve();
416
493
  return new Promise((resolve) => {
417
- wss.close(() => resolve());
494
+ try {
495
+ wss.close(() => resolve());
496
+ } catch {
497
+ resolve();
498
+ }
418
499
  });
419
500
  }
420
501
 
package/package.json CHANGED
@@ -1,49 +1,49 @@
1
1
  {
2
- "name": "mm_websocket_server",
3
- "version": "2.0.0",
4
- "description": "最薄的 WebSocket 服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
5
- "keywords": [
6
- "超级美眉",
7
- "入站适配器",
8
- "mm_mod",
9
- "mm_adapter",
10
- "mm_in_adapter",
11
- "mm_receiver",
12
- "websocket",
13
- "ws",
14
- "server"
15
- ],
16
- "layer": "adapter",
17
- "group": "in_adapter",
18
- "license": "ISC",
19
- "author": "qww",
20
- "type": "commonjs",
21
- "main": "index.js",
22
- "files": [
23
- "index.js",
24
- "lib",
25
- "example"
26
- ],
27
- "engines": {
28
- "node": ">=22.5.0"
29
- },
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://gitee.com/qiuwenwu91/mm_websocket_server.git"
33
- },
34
- "homepage": "https://gitee.com/qiuwenwu91/mm_websocket_server",
35
- "bugs": {
36
- "url": "https://gitee.com/qiuwenwu91/mm_websocket_server/issues"
37
- },
38
- "scripts": {
39
- "start": "node example/demo.js",
40
- "demo": "node example/demo.js",
41
- "test": "node --test \"test/*.test.js\""
42
- },
43
- "dependencies": {
44
- "mm_corejs": "^1.1.4",
45
- "mm_event": "^2.2.2",
46
- "mm_mod": "^1.0.1",
47
- "ws": "^8.22.0"
48
- }
2
+ "name": "mm_websocket_server",
3
+ "version": "2.0.2",
4
+ "description": "最薄的 WebSocket 服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
5
+ "keywords": [
6
+ "超级美眉",
7
+ "入站适配器",
8
+ "mm_mod",
9
+ "mm_adapter",
10
+ "mm_in_adapter",
11
+ "mm_receiver",
12
+ "websocket",
13
+ "ws",
14
+ "server"
15
+ ],
16
+ "layer": "adapter",
17
+ "group": "in_adapter",
18
+ "license": "ISC",
19
+ "author": "qww",
20
+ "type": "commonjs",
21
+ "main": "index.js",
22
+ "files": [
23
+ "index.js",
24
+ "lib",
25
+ "example"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22.5.0"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://gitee.com/qiuwenwu91/mm_websocket_server.git"
33
+ },
34
+ "homepage": "https://gitee.com/qiuwenwu91/mm_websocket_server",
35
+ "bugs": {
36
+ "url": "https://gitee.com/qiuwenwu91/mm_websocket_server/issues"
37
+ },
38
+ "scripts": {
39
+ "start": "node example/demo.js",
40
+ "demo": "node example/demo.js",
41
+ "test": "node --test \"test/*.test.js\""
42
+ },
43
+ "dependencies": {
44
+ "mm_corejs": "^1.1.4",
45
+ "mm_event": "^2.2.2",
46
+ "mm_mod": "^1.0.1",
47
+ "ws": "^8.22.0"
48
+ }
49
49
  }