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.
- package/README.md +15 -4
- package/lib/websocket.js +109 -29
- 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`
|
|
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`
|
|
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(
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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', (
|
|
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}
|
|
297
|
-
* @
|
|
318
|
+
* @param {Object} data 消息内容,文本帧为 Buffer,二进制帧为 Buffer
|
|
319
|
+
* @param {boolean} is_binary 是否为二进制帧
|
|
320
|
+
* @returns {void}
|
|
298
321
|
*/
|
|
299
|
-
|
|
322
|
+
_handleMessage(client_id, data, is_binary) {
|
|
300
323
|
const client = this.#clients.get(client_id);
|
|
301
324
|
if (!client) return;
|
|
302
|
-
|
|
303
|
-
|
|
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(
|
|
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}
|
|
349
|
-
* @
|
|
421
|
+
* 解码收到的数据,二进制帧保持 Buffer 原样
|
|
422
|
+
* @param {Object} data 消息内容
|
|
423
|
+
* @param {boolean} is_binary 是否为二进制帧
|
|
424
|
+
* @returns {string|Object|Buffer} 解码后的数据,json 模式下解析失败时返回原文
|
|
350
425
|
*/
|
|
351
|
-
_decodeData(
|
|
352
|
-
|
|
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
|
-
|
|
493
|
+
try {
|
|
494
|
+
wss.close(() => resolve());
|
|
495
|
+
} catch {
|
|
496
|
+
resolve();
|
|
497
|
+
}
|
|
418
498
|
});
|
|
419
499
|
}
|
|
420
500
|
|