mm_websocket_server 1.0.2 → 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 +74 -105
- package/example/demo.js +51 -0
- package/index.js +2 -4
- package/lib/websocket.js +221 -189
- package/package.json +7 -7
- package/lib/message.js +0 -150
package/README.md
CHANGED
|
@@ -1,159 +1,128 @@
|
|
|
1
1
|
# mm_websocket_server
|
|
2
2
|
|
|
3
|
-
WebSocket
|
|
3
|
+
最薄的 WebSocket 服务器入站适配器。继承 `mm_mod` 的 `Mod`,把 `ws` 的每条消息直接交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。
|
|
4
4
|
|
|
5
5
|
- 入口:`index.js`(CommonJS)
|
|
6
|
-
- 导出:`WebSocketServer
|
|
7
|
-
- 运行时依赖:`mm_corejs`、`ws`
|
|
8
|
-
- 测试覆盖率:100%(`jest.config.js` 四项阈值)
|
|
6
|
+
- 导出:`WebSocketServer`、`admin`、`websocketServerAdmin`
|
|
7
|
+
- 运行时依赖:`mm_mod`、`mm_event`、`mm_corejs`、`ws`
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## 消息路径
|
|
11
10
|
|
|
12
|
-
- **独立监听**:`WebSocket.Server` 按 `host / port / path` 直接监听,仅处理升级握手,不提供普通 HTTP 服务。
|
|
13
|
-
- **生命周期**:`init`(校验服务器设置)→ `start`(监听)→ `stop`(终止全部连接并关闭服务)→ `dispose`(清空注册表),状态机由 `mm_corejs` 驱动。
|
|
14
|
-
- **消息管道**:入站消息经 `Handle` 的 `before / check / main / render` 阶段处理,管道产出 `result` 时自动回发来源客户端;`getHandle()` 可注册业务钩子(echo、鉴权、改写等)。
|
|
15
|
-
- **连接管理**:`Store` 登记在线客户端,支持 `send / broadcast / closeClient / listClients(条件查询与分页)/ hasClient / setClientOptions`。
|
|
16
|
-
- **JSON-RPC 2.0 插件**:`MessagePlugin` 挂载后接管 RPC 请求,支持请求-响应、通知回推、主动推送与服务器反向调用客户端方法。
|
|
17
|
-
- **选项透传**:连接级 `options`(user_id / device_id / agent_id 等)随每条入站消息透传给管道钩子与 RPC 处理函数,适配多用户 / 多设备 / 多智能体场景。
|
|
18
|
-
|
|
19
|
-
## 安装
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install mm_websocket_server
|
|
23
11
|
```
|
|
12
|
+
WebSocket.Server → connection → ctx = { client_id, path, scope, ws }
|
|
13
|
+
→ eventManager.run(path, params, ctx, options)(同一连接的消息串行执行)
|
|
14
|
+
→ 结果非空时回发来源连接
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
事件 `target` 取握手请求路径(第一个 `?` 之前)。`params` 为 `{ client_id, data }`,`data` 在 `json: true` 时尝试 `JSON.parse`(失败回落原文)。
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
同一连接的消息按到达顺序串行执行:事件处理函数返回 Promise 时,后一条消息会等前一条处理完再开始,避免响应乱序。
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
- 模块规范:CommonJS
|
|
21
|
+
二进制帧保持 `Buffer` 原样传给事件(`ctx.is_binary` 为 `true`),不参与 `json` 解析;事件返回 `Buffer` 时也按二进制帧回发。文本帧走 `json` / UTF-8 文本逻辑。
|
|
29
22
|
|
|
30
23
|
## 快速开始
|
|
31
24
|
|
|
32
25
|
```js
|
|
33
26
|
'use strict';
|
|
34
27
|
|
|
35
|
-
const { WebSocketServer
|
|
28
|
+
const { WebSocketServer } = require('mm_websocket_server');
|
|
36
29
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
services: new Map(),
|
|
40
|
-
getService(name) { return this.services.get(name); },
|
|
41
|
-
hasService(name) { return this.services.has(name); },
|
|
42
|
-
setService(name, instance) { this.services.set(name, instance); return this; },
|
|
43
|
-
getManager() { return undefined; },
|
|
44
|
-
hasManager() { return false; }
|
|
45
|
-
};
|
|
30
|
+
const websocket = new WebSocketServer({ name: 'websocket', host: '127.0.0.1', port: 8090 });
|
|
31
|
+
const events = websocket.getEventManager();
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// 业务钩子:非 RPC 消息直接回发 echo
|
|
56
|
-
wss.getHandle().on('main', (ctx, params) => ({ type: 'echo', data: params.data }), 200, 'echo');
|
|
57
|
-
|
|
58
|
-
// JSON-RPC 插件:登记方法
|
|
59
|
-
const rpc = new MessagePlugin();
|
|
60
|
-
wss.use(rpc);
|
|
61
|
-
rpc.addMethod('sum', (params) => params.reduce((a, b) => a + b, 0));
|
|
62
|
-
|
|
63
|
-
// 事件
|
|
64
|
-
wss.on('connection', (client) => wss.setClientOptions(client.id, { user_id: 'u1' }));
|
|
65
|
-
wss.on('message', (client_id, data) => console.log(client_id, data));
|
|
66
|
-
wss.on('close', (client_id) => console.log('断开', client_id));
|
|
67
|
-
wss.on('error', (err) => console.error(err.message));
|
|
68
|
-
|
|
69
|
-
await wss.init();
|
|
70
|
-
await wss.start();
|
|
71
|
-
|
|
72
|
-
// 主动操作
|
|
73
|
-
await wss.run({}, { action: 'list' });
|
|
74
|
-
await wss.run({}, { action: 'broadcast', data: { type: 'notice', text: 'hello all' } });
|
|
75
|
-
|
|
76
|
-
await wss.stop();
|
|
77
|
-
await wss.dispose();
|
|
78
|
-
```
|
|
33
|
+
events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx) => ({
|
|
34
|
+
client_id: ctx.client_id,
|
|
35
|
+
data: params.data
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await websocket.init();
|
|
39
|
+
await websocket.start();
|
|
79
40
|
|
|
80
|
-
|
|
41
|
+
console.log(websocket.getAddress());
|
|
42
|
+
```
|
|
81
43
|
|
|
82
|
-
|
|
44
|
+
## 服务器配置
|
|
83
45
|
|
|
84
46
|
| 字段 | 类型 | 默认值 | 说明 |
|
|
85
47
|
| --- | --- | --- | --- |
|
|
86
|
-
| `name` | string | `'
|
|
48
|
+
| `name` | string | `'os'` | 模块名称(必填) |
|
|
49
|
+
| `title` | string | `'WebSocket服务器模块'` | 模块标题 |
|
|
50
|
+
| `description` | string | — | 模块描述 |
|
|
51
|
+
| `version` | string | `'2.0.0'` | 版本号 |
|
|
87
52
|
| `host` | string | `'0.0.0.0'` | 监听地址 |
|
|
88
|
-
| `port` |
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
53
|
+
| `port` | number | `8090` | 监听端口 |
|
|
54
|
+
| `json` | boolean | `true` | 入站尝试 JSON 解析,出站对象自动序列化 |
|
|
55
|
+
| `scope` | string | `'websocket'` | 事件管理器作用域,多实例可用不同 scope 隔离事件 |
|
|
56
|
+
| `params` | object | `{ action }` | `main` 的参数约定 |
|
|
91
57
|
|
|
92
|
-
|
|
58
|
+
不限制升级路径,任何路径都可连接,路径即事件 `target`。
|
|
93
59
|
|
|
94
|
-
|
|
60
|
+
## API
|
|
95
61
|
|
|
96
62
|
| 成员 | 说明 |
|
|
97
63
|
| --- | --- |
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `listClients(query)` | 在线客户端列表(`{ id, address }`),支持 Store 条件 / 分页 / keyword |
|
|
103
|
-
| `hasClient(client_id)` | 是否在线 |
|
|
104
|
-
| `setClientOptions(client_id, options)` | 绑定连接级选项(`null` 清空),随入站消息透传 |
|
|
105
|
-
| `use(plugin)` | 挂载插件(需实现 `bind(server)`) |
|
|
106
|
-
| `getHandle()` | 消息管道,注册 `before / check / main / render` 等钩子;钩子产出 `result` 自动回发 |
|
|
64
|
+
| `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
|
|
65
|
+
| `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
|
|
66
|
+
| `getEventManager()` | 获取 `mm_event` 上 `scope` 作用域的事件管理器(默认 `websocket`,其他实例共享同一作用域),用于注册事件 |
|
|
67
|
+
| `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
|
|
107
68
|
| `getAddress()` | 实际监听地址,未启动为 `null` |
|
|
69
|
+
| `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `send` / `broadcast` / `close` / `list` |
|
|
70
|
+
| `send(client_id, data)` | 向指定连接发送数据,返回是否成功 |
|
|
71
|
+
| `broadcast(data)` | 向全部在线连接广播,返回送达数 |
|
|
72
|
+
| `closeClient(client_id)` | 关闭指定连接,返回是否发起 |
|
|
73
|
+
| `listClients()` | 在线连接列表 `{ id, path, address }` |
|
|
74
|
+
| `hasClient(client_id)` | 是否在线 |
|
|
75
|
+
| `setClientOptions(client_id, options)` | 绑定连接级选项,随入站消息作为 `options` 透传(`null` 清空) |
|
|
108
76
|
|
|
109
|
-
|
|
77
|
+
## 请求上下文(ctx)
|
|
110
78
|
|
|
111
|
-
|
|
|
112
|
-
| --- | --- |
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
79
|
+
| 字段 | 说明 |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| `client_id` | 连接编号,形如 `ws_1` |
|
|
82
|
+
| `path` | 握手请求路径,`?` 之后已截断 |
|
|
83
|
+
| `scope` | 当前实例的事件管理器作用域 |
|
|
84
|
+
| `is_binary` | 是否为二进制帧 |
|
|
85
|
+
| `ws` | 原生 ws 连接,事件可直接写出 |
|
|
117
86
|
|
|
118
|
-
|
|
87
|
+
## 事件处理函数
|
|
119
88
|
|
|
120
|
-
|
|
89
|
+
签名固定为 `(params, ctx, options)`:
|
|
121
90
|
|
|
122
|
-
```
|
|
123
|
-
|
|
91
|
+
```js
|
|
92
|
+
events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx, options) => {
|
|
93
|
+
return { data: params.data };
|
|
94
|
+
};
|
|
124
95
|
```
|
|
125
96
|
|
|
126
|
-
|
|
97
|
+
返回非 `undefined` / `null` 的结果会按 `json` 配置编码后回发来源连接;返回 `undefined` 则不回发。事件也可直接用 `ctx.ws` 写出。
|
|
127
98
|
|
|
128
|
-
##
|
|
129
|
-
|
|
130
|
-
挂载后接管 `jsonrpc` 形态消息(非 RPC 消息透传给业务钩子):
|
|
99
|
+
## 主动操作
|
|
131
100
|
|
|
132
101
|
```js
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
102
|
+
await websocket.run({ action: 'send', client_id: 'ws_1', data: 'hi' });
|
|
103
|
+
await websocket.run({ action: 'broadcast', data: { type: 'notice' } });
|
|
104
|
+
await websocket.run({ action: 'close', client_id: 'ws_1' });
|
|
105
|
+
await websocket.run({ action: 'list' });
|
|
136
106
|
```
|
|
137
107
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
| `bind(server)` / `isBound()` | 挂载与状态查询 |
|
|
108
|
+
`action` 仅支持上述四种,其他值直接抛 `unsupported action` 错误。
|
|
109
|
+
|
|
110
|
+
## 生命周期
|
|
111
|
+
|
|
112
|
+
`onStart` 先挂 `error` / `connection` 监听再等待 `listening`,创建并监听 `WebSocket.Server`;`onStop` 断开全部连接并关闭服务器。启动失败(如端口被占用)时会先回滚已创建的服务器与连接表再抛错,模块进入 `error` 状态,可 `dispose()` 释放。
|
|
144
113
|
|
|
145
|
-
|
|
114
|
+
事件管理器取自 `mm_event` 上 `scope` 指定的作用域(默认 `websocket`),由全局 `Admin` 共享,停止时不清理;多个实例需要不同作用域时,配置不同的 `scope` 即可。
|
|
146
115
|
|
|
147
116
|
## 示例
|
|
148
117
|
|
|
149
118
|
```bash
|
|
150
|
-
npm run demo #
|
|
119
|
+
npm run demo # 启动演示服务器(127.0.0.1:8090),见 example/demo.js
|
|
151
120
|
```
|
|
152
121
|
|
|
153
122
|
## 测试
|
|
154
123
|
|
|
155
124
|
```bash
|
|
156
|
-
npm test #
|
|
125
|
+
npm test # node --test
|
|
157
126
|
```
|
|
158
127
|
|
|
159
128
|
## 许可证
|
package/example/demo.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { WebSocketServer } = require('..');
|
|
4
|
+
|
|
5
|
+
/** 演示监听端口 */
|
|
6
|
+
const DEMO_PORT = 8090;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 注册演示事件:握手路径即事件 target
|
|
10
|
+
* @param {Object} events 事件管理器
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*/
|
|
13
|
+
function bindEvents(events) {
|
|
14
|
+
events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx) => ({
|
|
15
|
+
type: 'echo',
|
|
16
|
+
client_id: ctx.client_id,
|
|
17
|
+
data: params.data
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
events.add({ name: 'time', target: '/time', end: true }).main = async () => ({
|
|
21
|
+
time: new Date().toISOString()
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
events.add({ name: 'error', target: '/error', end: true }).main = async () => {
|
|
25
|
+
throw new Error('boom');
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 演示入口:启动服务器并打印监听地址
|
|
31
|
+
* @returns {Promise<void>} 无返回值
|
|
32
|
+
*/
|
|
33
|
+
async function main() {
|
|
34
|
+
const websocket = new WebSocketServer({ name: 'demo', host: '127.0.0.1', port: DEMO_PORT });
|
|
35
|
+
|
|
36
|
+
bindEvents(websocket.getEventManager());
|
|
37
|
+
websocket.on('error', (err) => console.error(`[demo] 错误: ${err.message}`));
|
|
38
|
+
|
|
39
|
+
await websocket.init();
|
|
40
|
+
await websocket.start();
|
|
41
|
+
|
|
42
|
+
const address = websocket.getAddress();
|
|
43
|
+
console.log(`ws://127.0.0.1:${address.port}/echo`);
|
|
44
|
+
console.log(`ws://127.0.0.1:${address.port}/time`);
|
|
45
|
+
console.log('连接后发送任意内容,结果由事件返回并自动回发');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
main().catch((err) => {
|
|
49
|
+
console.error(err);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const WebSocketServer = require('./lib/websocket');
|
|
4
|
-
const MessagePlugin = require('./lib/message');
|
|
5
4
|
const Admin = require('mm_corejs').Admin;
|
|
6
5
|
|
|
7
6
|
let admin = new Admin();
|
|
@@ -9,11 +8,10 @@ admin.setClass(WebSocketServer);
|
|
|
9
8
|
|
|
10
9
|
module.exports = {
|
|
11
10
|
WebSocketServer,
|
|
12
|
-
MessagePlugin,
|
|
13
11
|
admin,
|
|
14
12
|
/**
|
|
15
13
|
* 获取指定作用域的 WebSocket 服务器
|
|
16
|
-
* @param {string} [scope='
|
|
14
|
+
* @param {string} [scope='os'] 作用域名称
|
|
17
15
|
* @param {object} [config] 配置参数
|
|
18
16
|
* @returns {WebSocketServer} WebSocket 服务器实例
|
|
19
17
|
*/
|
|
@@ -21,6 +19,6 @@ module.exports = {
|
|
|
21
19
|
if (!scope) {
|
|
22
20
|
scope = 'os';
|
|
23
21
|
}
|
|
24
|
-
return admin.get(scope,
|
|
22
|
+
return admin.get(scope, config);
|
|
25
23
|
}
|
|
26
24
|
};
|
package/lib/websocket.js
CHANGED
|
@@ -1,47 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const WebSocket = require('ws');
|
|
4
|
-
const { Mod
|
|
5
|
-
const {
|
|
4
|
+
const { Mod } = require('mm_mod');
|
|
5
|
+
const { eventAdmin } = require('mm_event');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* 获取请求路径(在 ? 处截断,不解析查询串)
|
|
9
|
+
* @param {string} url 原始请求地址
|
|
10
|
+
* @returns {string} 请求路径
|
|
9
11
|
*/
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
port: { type: 'integer', minimum: 1, maximum: 65535 },
|
|
15
|
-
path: { type: 'string', pattern: '^/' },
|
|
16
|
-
json: { type: 'boolean' }
|
|
17
|
-
}
|
|
18
|
-
};
|
|
12
|
+
function getPath(url) {
|
|
13
|
+
const index = url.indexOf('?');
|
|
14
|
+
return index === -1 ? url : url.slice(0, index);
|
|
15
|
+
}
|
|
19
16
|
|
|
20
17
|
/**
|
|
21
|
-
* WebSocketServer ——
|
|
22
|
-
*
|
|
23
|
-
* 继承 mm_corejs 的 Mod:生命周期由 Lifecycle 模板方法驱动,
|
|
24
|
-
* 调用参数校验由 config.params 声明式定义;
|
|
25
|
-
* 服务器设置(host / port / path / json)为顶层 config 字段,
|
|
26
|
-
* 在 onInit 阶段按 SERVER_SCHEMA 统一校验。
|
|
18
|
+
* WebSocketServer —— 最薄的 WebSocket 传输层(入站适配器模块)。
|
|
27
19
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
|
|
34
|
-
* 3. 消息管道:收到的消息经 Handle 的 before/check/main/render 阶段处理,
|
|
35
|
-
* 管道产出 result 时自动回发来源客户端;getHandle() 可注册自定义钩子;
|
|
36
|
-
* 4. 事件分发:connection / message / close / error 经 EventEmitter 向外广播;
|
|
37
|
-
* 5. 主动操作:run(ctx, params) 按 params.action 派发 send / broadcast / close / list。
|
|
20
|
+
* 只做三件事:
|
|
21
|
+
* 1. 生命周期:由 Mod 的 init / start / stop 驱动 WebSocket.Server;
|
|
22
|
+
* 2. 消息转交:以握手请求路径为 target,直接调用
|
|
23
|
+
* EventManager.run(path, params, ctx, options),不做路由、不做插件;
|
|
24
|
+
* 3. 结果回发:事件返回非空结果时回发来源客户端,事件也可用 ctx.ws 自行写出。
|
|
38
25
|
*
|
|
39
26
|
* 用法:
|
|
40
|
-
* const wss = new WebSocketServer({ name: 'websocket',
|
|
41
|
-
* wss.
|
|
27
|
+
* const wss = new WebSocketServer({ name: 'websocket', port: 8090 });
|
|
28
|
+
* wss.getEventManager().add({ name: 'echo', target: '*' }).main = async (params) => params.data;
|
|
42
29
|
* await wss.init();
|
|
43
30
|
* await wss.start();
|
|
44
|
-
* await wss.run({}, { action: 'list' });
|
|
45
31
|
*/
|
|
46
32
|
class WebSocketServer extends Mod {
|
|
47
33
|
/**
|
|
@@ -50,82 +36,94 @@ class WebSocketServer extends Mod {
|
|
|
50
36
|
static config = {
|
|
51
37
|
name: 'os',
|
|
52
38
|
title: 'WebSocket服务器模块',
|
|
53
|
-
description: 'WebSocket
|
|
54
|
-
version: '
|
|
39
|
+
description: 'WebSocket 传输层:消息直达事件管理器,不做路由与插件',
|
|
40
|
+
version: '2.0.0',
|
|
55
41
|
host: '0.0.0.0',
|
|
56
42
|
port: 8090,
|
|
57
|
-
path: '/',
|
|
58
43
|
json: true,
|
|
59
|
-
|
|
44
|
+
scope: 'websocket',
|
|
60
45
|
params: {
|
|
61
46
|
type: 'object',
|
|
62
47
|
required: ['action'],
|
|
63
48
|
properties: {
|
|
64
49
|
action: { type: 'string', enum: ['send', 'broadcast', 'close', 'list'] },
|
|
65
50
|
client_id: { type: 'string', min_length: 1 },
|
|
66
|
-
data: {}
|
|
67
|
-
query: { type: 'object' }
|
|
51
|
+
data: {}
|
|
68
52
|
}
|
|
69
53
|
}
|
|
70
54
|
};
|
|
71
55
|
|
|
72
56
|
/**
|
|
73
|
-
* ws
|
|
57
|
+
* ws 服务器实例
|
|
74
58
|
*/
|
|
75
59
|
#wss = null;
|
|
76
60
|
|
|
77
61
|
/**
|
|
78
|
-
*
|
|
62
|
+
* 事件管理器(取自 mm_event 的 websocket 作用域,与其他实例共享)
|
|
79
63
|
*/
|
|
80
|
-
#
|
|
64
|
+
#events = null;
|
|
81
65
|
|
|
82
66
|
/**
|
|
83
|
-
*
|
|
84
|
-
* 每个实例独立 Handle,避免多实例共享管道导致钩子泄漏
|
|
67
|
+
* 在线连接表:client_id -> 连接信息
|
|
85
68
|
*/
|
|
86
|
-
#
|
|
69
|
+
#clients = new Map();
|
|
87
70
|
|
|
88
71
|
/**
|
|
89
72
|
* 客户端编号自增计数器
|
|
90
73
|
*/
|
|
91
74
|
#counter = 0;
|
|
92
75
|
|
|
93
|
-
// ---------- 生命周期钩子 ----------
|
|
94
76
|
/**
|
|
95
|
-
*
|
|
77
|
+
* 构造函数
|
|
78
|
+
* @param {Object} config 模块配置
|
|
79
|
+
* @param {Object} server 框架实例
|
|
96
80
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
this.
|
|
81
|
+
constructor(config, server) {
|
|
82
|
+
super(config, server);
|
|
83
|
+
this.#events = eventAdmin(this.config.scope);
|
|
100
84
|
}
|
|
101
85
|
|
|
86
|
+
// ---------- 生命周期钩子 ----------
|
|
102
87
|
/**
|
|
103
|
-
* 启动钩子:创建 ws
|
|
88
|
+
* 启动钩子:创建 ws 服务器并开始监听,失败时回滚已创建的资源
|
|
89
|
+
* @returns {Promise<void>} 无返回值
|
|
104
90
|
*/
|
|
105
91
|
async onStart() {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
114
105
|
}
|
|
115
106
|
|
|
116
107
|
/**
|
|
117
|
-
*
|
|
108
|
+
* 启动失败时回滚已创建的资源
|
|
109
|
+
* @returns {Promise<void>} 无返回值
|
|
118
110
|
*/
|
|
119
|
-
async
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
async _rollbackStart() {
|
|
112
|
+
try {
|
|
113
|
+
this._closeAllClients();
|
|
114
|
+
await this._closeServer();
|
|
115
|
+
} catch (err) {
|
|
116
|
+
this._emitError(err);
|
|
117
|
+
}
|
|
122
118
|
}
|
|
123
119
|
|
|
124
120
|
/**
|
|
125
|
-
*
|
|
121
|
+
* 停止钩子:断开全部连接并关闭服务器(事件管理器与其他实例共享,不清理)
|
|
122
|
+
* @returns {Promise<void>} 无返回值
|
|
126
123
|
*/
|
|
127
|
-
async
|
|
128
|
-
this
|
|
124
|
+
async onStop() {
|
|
125
|
+
this._closeAllClients();
|
|
126
|
+
await this._closeServer();
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
// ---------- 执行入口 ----------
|
|
@@ -135,13 +133,15 @@ class WebSocketServer extends Mod {
|
|
|
135
133
|
* @param {Object} ctx 执行上下文
|
|
136
134
|
* @param {Object} options 合并后的选项
|
|
137
135
|
* @returns {Promise<Object>} 操作结果
|
|
136
|
+
* @throws {Error} action 不受支持时
|
|
138
137
|
*/
|
|
139
138
|
async main(params, ctx, options) {
|
|
140
139
|
const action = params.action;
|
|
141
140
|
if (action === 'send') return this._actionSend(params);
|
|
142
141
|
if (action === 'broadcast') return this._actionBroadcast(params);
|
|
143
142
|
if (action === 'close') return this._actionClose(params);
|
|
144
|
-
return this._actionList(
|
|
143
|
+
if (action === 'list') return this._actionList();
|
|
144
|
+
throw new Error(`${this._label()} unsupported action: ${action}`);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
/**
|
|
@@ -158,7 +158,7 @@ class WebSocketServer extends Mod {
|
|
|
158
158
|
/**
|
|
159
159
|
* 执行广播操作
|
|
160
160
|
* @param {Object} params 调用参数,含 data
|
|
161
|
-
* @returns {Object} 操作结果,count
|
|
161
|
+
* @returns {Object} 操作结果,count 为送达连接数
|
|
162
162
|
*/
|
|
163
163
|
_actionBroadcast(params) {
|
|
164
164
|
const count = this.broadcast(params.data);
|
|
@@ -178,177 +178,226 @@ class WebSocketServer extends Mod {
|
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* 执行列出连接操作
|
|
181
|
-
* @
|
|
182
|
-
|
|
181
|
+
* @returns {Object} 操作结果,clients 为在线连接列表
|
|
182
|
+
*/
|
|
183
|
+
_actionList() {
|
|
184
|
+
return { action: 'list', clients: this.listClients() };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------- 事件管理器 ----------
|
|
188
|
+
/**
|
|
189
|
+
* 获取事件管理器,用于注册事件
|
|
190
|
+
* @returns {Object} 事件管理器实例
|
|
191
|
+
*/
|
|
192
|
+
getEventManager() {
|
|
193
|
+
return this.#events;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 注入外部事件管理器
|
|
198
|
+
* @param {Object} manager 事件管理器实例
|
|
199
|
+
* @returns {WebSocketServer} 当前实例
|
|
200
|
+
* @throws {TypeError} manager 不具备 run 方法时
|
|
201
|
+
*/
|
|
202
|
+
setEventManager(manager) {
|
|
203
|
+
if (!manager || typeof manager.run !== 'function') {
|
|
204
|
+
throw new TypeError(`${this._label()} manager must provide run()`);
|
|
205
|
+
}
|
|
206
|
+
this.#events = manager;
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 获取实际监听地址
|
|
212
|
+
* @returns {Object|null} 监听地址信息,未启动时为 null
|
|
183
213
|
*/
|
|
184
|
-
|
|
185
|
-
|
|
214
|
+
getAddress() {
|
|
215
|
+
const wss = this.#wss;
|
|
216
|
+
return wss ? wss.address() : null;
|
|
186
217
|
}
|
|
187
218
|
|
|
188
219
|
// ---------- 公开操作 ----------
|
|
189
220
|
/**
|
|
190
|
-
*
|
|
221
|
+
* 向指定连接发送数据
|
|
191
222
|
* @param {string} client_id 客户端 ID
|
|
192
223
|
* @param {string|Object} data 待发送数据
|
|
193
224
|
* @returns {boolean} 是否发送成功
|
|
194
225
|
*/
|
|
195
226
|
send(client_id, data) {
|
|
196
|
-
const client = this.#clients.
|
|
227
|
+
const client = this.#clients.get(client_id);
|
|
197
228
|
if (!client) return false;
|
|
198
|
-
|
|
199
|
-
return this._sendToClient(client, payload);
|
|
229
|
+
return this._sendToClient(client, this._encodeData(data));
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
/**
|
|
203
|
-
*
|
|
233
|
+
* 向全部在线连接广播数据
|
|
204
234
|
* @param {string|Object} data 待广播数据
|
|
205
|
-
* @returns {number}
|
|
235
|
+
* @returns {number} 送达的连接数量
|
|
206
236
|
*/
|
|
207
237
|
broadcast(data) {
|
|
208
238
|
const payload = this._encodeData(data);
|
|
209
239
|
let count = 0;
|
|
210
|
-
for (const client of this.#clients.
|
|
240
|
+
for (const client of this.#clients.values()) {
|
|
211
241
|
if (this._sendToClient(client, payload)) count += 1;
|
|
212
242
|
}
|
|
213
243
|
return count;
|
|
214
244
|
}
|
|
215
245
|
|
|
216
246
|
/**
|
|
217
|
-
*
|
|
247
|
+
* 关闭指定连接(连接表由关闭回调清理)
|
|
218
248
|
* @param {string} client_id 客户端 ID
|
|
219
249
|
* @returns {boolean} 是否发起关闭
|
|
220
250
|
*/
|
|
221
251
|
closeClient(client_id) {
|
|
222
|
-
const client = this.#clients.
|
|
252
|
+
const client = this.#clients.get(client_id);
|
|
223
253
|
if (!client) return false;
|
|
224
254
|
client.ws.close();
|
|
225
255
|
return true;
|
|
226
256
|
}
|
|
227
257
|
|
|
228
258
|
/**
|
|
229
|
-
*
|
|
230
|
-
* @
|
|
231
|
-
* @returns {Array<Object>} 客户端信息列表,含 id 与 address
|
|
259
|
+
* 列出在线连接
|
|
260
|
+
* @returns {Array<Object>} 连接信息列表,含 id、path 与 address
|
|
232
261
|
*/
|
|
233
|
-
listClients(
|
|
234
|
-
|
|
262
|
+
listClients() {
|
|
263
|
+
const list = [];
|
|
264
|
+
for (const client of this.#clients.values()) {
|
|
265
|
+
list.push({ id: client.id, path: client.path, address: client.address });
|
|
266
|
+
}
|
|
267
|
+
return list;
|
|
235
268
|
}
|
|
236
269
|
|
|
237
270
|
/**
|
|
238
|
-
*
|
|
271
|
+
* 判断连接是否在线
|
|
239
272
|
* @param {string} client_id 客户端 ID
|
|
240
273
|
* @returns {boolean} 是否在线
|
|
241
274
|
*/
|
|
242
275
|
hasClient(client_id) {
|
|
243
|
-
return
|
|
276
|
+
return this.#clients.has(client_id);
|
|
244
277
|
}
|
|
245
278
|
|
|
246
279
|
/**
|
|
247
|
-
*
|
|
280
|
+
* 绑定连接级选项,入站消息将作为 options 透传给事件
|
|
248
281
|
* @param {string} client_id 客户端 ID
|
|
249
282
|
* @param {Object|null} options 选项对象,null 时清空
|
|
250
|
-
* @returns {boolean}
|
|
283
|
+
* @returns {boolean} 是否设置成功(连接不在线时失败)
|
|
251
284
|
*/
|
|
252
285
|
setClientOptions(client_id, options) {
|
|
253
|
-
const client = this.#clients.
|
|
286
|
+
const client = this.#clients.get(client_id);
|
|
254
287
|
if (!client) return false;
|
|
255
288
|
client.options = options || null;
|
|
256
289
|
return true;
|
|
257
290
|
}
|
|
258
291
|
|
|
292
|
+
// ---------- 连接处理 ----------
|
|
259
293
|
/**
|
|
260
|
-
*
|
|
261
|
-
* @param {
|
|
262
|
-
* @
|
|
294
|
+
* 登记新的连接并绑定回调
|
|
295
|
+
* @param {Object} ws ws 连接实例
|
|
296
|
+
* @param {Object} req 握手请求对象
|
|
263
297
|
*/
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
298
|
+
_handleConnection(ws, req) {
|
|
299
|
+
this.#counter += 1;
|
|
300
|
+
const client_id = `ws_${this.#counter}`;
|
|
301
|
+
const client = {
|
|
302
|
+
id: client_id,
|
|
303
|
+
path: getPath(req.url || '/'),
|
|
304
|
+
address: this._remoteAddress(req),
|
|
305
|
+
ws,
|
|
306
|
+
options: null,
|
|
307
|
+
queue: Promise.resolve()
|
|
308
|
+
};
|
|
309
|
+
this.#clients.set(client_id, client);
|
|
310
|
+
ws.on('message', (data, is_binary) => this._handleMessage(client_id, data, is_binary));
|
|
311
|
+
ws.on('close', () => this.#clients.delete(client_id));
|
|
312
|
+
ws.on('error', (err) => this._emitError(err));
|
|
268
313
|
}
|
|
269
314
|
|
|
270
315
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* @param {Object}
|
|
274
|
-
* @
|
|
275
|
-
* @
|
|
316
|
+
* 接收消息并排入该连接的串行队列
|
|
317
|
+
* @param {string} client_id 客户端 ID
|
|
318
|
+
* @param {Object} data 消息内容,文本帧为 Buffer,二进制帧为 Buffer
|
|
319
|
+
* @param {boolean} is_binary 是否为二进制帧
|
|
320
|
+
* @returns {void}
|
|
276
321
|
*/
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
plugin.bind(this);
|
|
282
|
-
return this;
|
|
322
|
+
_handleMessage(client_id, data, is_binary) {
|
|
323
|
+
const client = this.#clients.get(client_id);
|
|
324
|
+
if (!client) return;
|
|
325
|
+
this._enqueue(client, () => this._processMessage(client, data, is_binary));
|
|
283
326
|
}
|
|
284
327
|
|
|
285
328
|
/**
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* @
|
|
329
|
+
* 把任务追加到连接的串行队列,保证同一连接的消息顺序执行
|
|
330
|
+
* @param {Object} client 连接信息
|
|
331
|
+
* @param {Function} task 待执行任务
|
|
332
|
+
* @returns {void}
|
|
289
333
|
*/
|
|
290
|
-
|
|
291
|
-
|
|
334
|
+
_enqueue(client, task) {
|
|
335
|
+
client.queue = client.queue
|
|
336
|
+
.then(() => task())
|
|
337
|
+
.catch((err) => this._emitError(err));
|
|
292
338
|
}
|
|
293
339
|
|
|
294
340
|
/**
|
|
295
|
-
*
|
|
296
|
-
* @
|
|
341
|
+
* 处理入站消息:解码后交给事件管理器,返回非空结果时回发来源连接
|
|
342
|
+
* @param {Object} client 连接信息
|
|
343
|
+
* @param {Object} data 消息内容
|
|
344
|
+
* @param {boolean} is_binary 是否为二进制帧
|
|
345
|
+
* @returns {Promise<void>} 无返回值
|
|
297
346
|
*/
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
347
|
+
async _processMessage(client, data, is_binary) {
|
|
348
|
+
const params = this._buildParams(client, data, is_binary);
|
|
349
|
+
const ctx = this._buildContext(client, is_binary);
|
|
350
|
+
const options = this._clientOptions(client);
|
|
351
|
+
try {
|
|
352
|
+
const result = await this.#events.run(client.path, params, ctx, options);
|
|
353
|
+
const error = ctx.error || options.error;
|
|
354
|
+
if (error) this._emitError(error);
|
|
355
|
+
if (result !== undefined && result !== null) this.send(client.id, result);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
this._emitError(err);
|
|
358
|
+
}
|
|
301
359
|
}
|
|
302
360
|
|
|
303
|
-
// ---------- 连接处理 ----------
|
|
304
361
|
/**
|
|
305
|
-
*
|
|
306
|
-
* @param {Object}
|
|
307
|
-
* @param {Object}
|
|
362
|
+
* 构建事件执行参数
|
|
363
|
+
* @param {Object} client 连接信息
|
|
364
|
+
* @param {Object} data 消息内容
|
|
365
|
+
* @param {boolean} is_binary 是否为二进制帧
|
|
366
|
+
* @returns {Object} 参数对象
|
|
308
367
|
*/
|
|
309
|
-
|
|
310
|
-
this
|
|
311
|
-
const client_id = `ws_${this.#counter}`;
|
|
312
|
-
const client = {
|
|
313
|
-
id: client_id,
|
|
314
|
-
name: client_id,
|
|
315
|
-
address: this._remoteAddress(req),
|
|
316
|
-
ws
|
|
317
|
-
};
|
|
318
|
-
this.#clients.add(client);
|
|
319
|
-
ws.on('message', (buf) => this._handleMessage(client_id, buf));
|
|
320
|
-
ws.on('close', () => this._handleClose(client_id));
|
|
321
|
-
ws.on('error', (err) => this._emitError(err));
|
|
322
|
-
this._safeEmit('connection', client);
|
|
368
|
+
_buildParams(client, data, is_binary) {
|
|
369
|
+
return { client_id: client.id, data: this._decodeData(data, is_binary) };
|
|
323
370
|
}
|
|
324
371
|
|
|
325
372
|
/**
|
|
326
|
-
*
|
|
327
|
-
* @param {
|
|
328
|
-
* @param {
|
|
329
|
-
* @returns {
|
|
373
|
+
* 构建事件执行上下文
|
|
374
|
+
* @param {Object} client 连接信息
|
|
375
|
+
* @param {boolean} is_binary 是否为二进制帧
|
|
376
|
+
* @returns {Object} 上下文对象
|
|
330
377
|
*/
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
};
|
|
338
386
|
}
|
|
339
387
|
|
|
340
388
|
/**
|
|
341
|
-
*
|
|
342
|
-
* @param {
|
|
389
|
+
* 获取连接级选项(复制一份,避免事件管理器回写)
|
|
390
|
+
* @param {Object} client 连接信息
|
|
391
|
+
* @returns {Object} 选项对象
|
|
343
392
|
*/
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
393
|
+
_clientOptions(client) {
|
|
394
|
+
if (!client.options) return {};
|
|
395
|
+
return { ...client.options };
|
|
347
396
|
}
|
|
348
397
|
|
|
349
398
|
/**
|
|
350
399
|
* 获取远端地址字符串
|
|
351
|
-
* @param {Object} req
|
|
400
|
+
* @param {Object} req 握手请求对象
|
|
352
401
|
* @returns {string} 形如 ip:port 的地址
|
|
353
402
|
*/
|
|
354
403
|
_remoteAddress(req) {
|
|
@@ -357,23 +406,26 @@ class WebSocketServer extends Mod {
|
|
|
357
406
|
|
|
358
407
|
// ---------- 数据编解码 ----------
|
|
359
408
|
/**
|
|
360
|
-
*
|
|
361
|
-
* @param {string|Object} data 待编码数据
|
|
362
|
-
* @returns {string}
|
|
409
|
+
* 编码待发送数据,Buffer 原样发送(二进制帧),其余按配置序列化
|
|
410
|
+
* @param {string|Object|Buffer} data 待编码数据
|
|
411
|
+
* @returns {string|Buffer} 编码后的内容
|
|
363
412
|
*/
|
|
364
413
|
_encodeData(data) {
|
|
414
|
+
if (Buffer.isBuffer(data)) return data;
|
|
365
415
|
if (this.config.json) return JSON.stringify(data);
|
|
366
416
|
if (typeof data === 'string') return data;
|
|
367
417
|
return String(data);
|
|
368
418
|
}
|
|
369
419
|
|
|
370
420
|
/**
|
|
371
|
-
*
|
|
372
|
-
* @param {Object}
|
|
373
|
-
* @
|
|
421
|
+
* 解码收到的数据,二进制帧保持 Buffer 原样
|
|
422
|
+
* @param {Object} data 消息内容
|
|
423
|
+
* @param {boolean} is_binary 是否为二进制帧
|
|
424
|
+
* @returns {string|Object|Buffer} 解码后的数据,json 模式下解析失败时返回原文
|
|
374
425
|
*/
|
|
375
|
-
_decodeData(
|
|
376
|
-
|
|
426
|
+
_decodeData(data, is_binary) {
|
|
427
|
+
if (is_binary) return data;
|
|
428
|
+
const text = data.toString('utf8');
|
|
377
429
|
if (!this.config.json) return text;
|
|
378
430
|
return this._parseJson(text);
|
|
379
431
|
}
|
|
@@ -393,8 +445,8 @@ class WebSocketServer extends Mod {
|
|
|
393
445
|
|
|
394
446
|
// ---------- 发送与清理 ----------
|
|
395
447
|
/**
|
|
396
|
-
*
|
|
397
|
-
* @param {Object} client
|
|
448
|
+
* 向单个连接发送已编码数据
|
|
449
|
+
* @param {Object} client 连接信息
|
|
398
450
|
* @param {string} payload 已编码数据
|
|
399
451
|
* @returns {boolean} 是否发送成功
|
|
400
452
|
*/
|
|
@@ -405,10 +457,10 @@ class WebSocketServer extends Mod {
|
|
|
405
457
|
}
|
|
406
458
|
|
|
407
459
|
/**
|
|
408
|
-
*
|
|
460
|
+
* 断开全部连接并清空连接表
|
|
409
461
|
*/
|
|
410
462
|
_closeAllClients() {
|
|
411
|
-
for (const client of this.#clients.
|
|
463
|
+
for (const client of this.#clients.values()) {
|
|
412
464
|
client.ws.terminate();
|
|
413
465
|
}
|
|
414
466
|
this.#clients.clear();
|
|
@@ -430,7 +482,7 @@ class WebSocketServer extends Mod {
|
|
|
430
482
|
}
|
|
431
483
|
|
|
432
484
|
/**
|
|
433
|
-
* 关闭 ws
|
|
485
|
+
* 关闭 ws 服务器(连接已在之前全部终止)
|
|
434
486
|
* @returns {Promise<void>} 无返回值
|
|
435
487
|
*/
|
|
436
488
|
_closeServer() {
|
|
@@ -438,35 +490,15 @@ class WebSocketServer extends Mod {
|
|
|
438
490
|
this.#wss = null;
|
|
439
491
|
if (!wss) return Promise.resolve();
|
|
440
492
|
return new Promise((resolve) => {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
493
|
+
try {
|
|
494
|
+
wss.close(() => resolve());
|
|
495
|
+
} catch {
|
|
496
|
+
resolve();
|
|
444
497
|
}
|
|
445
|
-
wss.close(resolve);
|
|
446
498
|
});
|
|
447
499
|
}
|
|
448
500
|
|
|
449
501
|
// ---------- 校验辅助 ----------
|
|
450
|
-
/**
|
|
451
|
-
* 装配消息管道:内置 main 钩子负责分发 message 事件(sort 置后,
|
|
452
|
-
* 不返回值,业务钩子产出的 result 由 _handleMessage 统一回发)
|
|
453
|
-
*/
|
|
454
|
-
_initHandle() {
|
|
455
|
-
this.#handle.on('main', (params, ctx) => {
|
|
456
|
-
this._safeEmit('message', params.client_id, params.data);
|
|
457
|
-
}, 1000, 'emit_message');
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* 校验顶层 config 中的服务器设置
|
|
462
|
-
* @returns {Promise<void>} 无返回值
|
|
463
|
-
* @throws {Error} 校验不通过时
|
|
464
|
-
*/
|
|
465
|
-
async _validateConfig() {
|
|
466
|
-
const tip = await this._validate(this.config, SERVER_SCHEMA);
|
|
467
|
-
if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
502
|
/**
|
|
471
503
|
* 断言调用参数含 client_id
|
|
472
504
|
* @param {Object} params 调用参数
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_websocket_server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "WebSocket
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "最薄的 WebSocket 服务器入站适配器:消息直达 mm_event 事件管理器,不做路由与插件。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"超级美眉",
|
|
7
7
|
"入站适配器",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"main": "index.js",
|
|
22
22
|
"files": [
|
|
23
23
|
"index.js",
|
|
24
|
-
"lib"
|
|
24
|
+
"lib",
|
|
25
|
+
"example"
|
|
25
26
|
],
|
|
26
27
|
"engines": {
|
|
27
28
|
"node": ">=22.5.0"
|
|
@@ -37,13 +38,12 @@
|
|
|
37
38
|
"scripts": {
|
|
38
39
|
"start": "node example/demo.js",
|
|
39
40
|
"demo": "node example/demo.js",
|
|
40
|
-
"test": "
|
|
41
|
+
"test": "node --test \"test/*.test.js\""
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"mm_corejs": "^1.1.4",
|
|
45
|
+
"mm_event": "^2.2.2",
|
|
46
|
+
"mm_mod": "^1.0.1",
|
|
44
47
|
"ws": "^8.22.0"
|
|
45
|
-
},
|
|
46
|
-
"devDependencies": {
|
|
47
|
-
"jest": "^30.5.2"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/lib/message.js
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { JsonRpc } = require('mm_corejs');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* MessagePlugin —— JSON-RPC 2.0 消息插件(mm_websocket_server 的可选业务组件)。
|
|
7
|
-
*
|
|
8
|
-
* 职责:
|
|
9
|
-
* 1. 方法表:内部持有 JsonRpc 核心,addMethod / delMethod / listMethods 登记可调用方法;
|
|
10
|
-
* 2. 管道挂载:bind(websocket) 后向消息管道注册 main 钩子 "json_rpc"(sort 100,
|
|
11
|
-
* 先于内置 emit_message 执行),命中 JSON-RPC 请求时以响应对象作为管道 result,
|
|
12
|
-
* 由 WebSocketServer 自动回发来源客户端;
|
|
13
|
-
* 3. 通知回推:通知(无 id)处理函数返回非 undefined 结果时,产出服务器发起的
|
|
14
|
-
* 通知对象 { jsonrpc, method, params: result } 回推来源客户端;无结果或出错时
|
|
15
|
-
* 不产出;非 JSON-RPC 消息(解析失败、缺 jsonrpc 字段)透传,控制权交还管道;
|
|
16
|
-
* 4. 主动推送:push(client_id, data) 委托 WebSocketServer.send,处理函数或服务器代码
|
|
17
|
-
* 可在任意时刻(如异步任务完成后)向指定客户端回推结果;
|
|
18
|
-
* 5. 反向调用:call(client_id, method, params, options) 向客户端发起 JSON-RPC 请求
|
|
19
|
-
* 并等待其反馈;入站消息为响应形态(含 result/error、无 method)时被优先识别
|
|
20
|
-
* 并结算待响应调用,实现服务器实时调用客户端方法;
|
|
21
|
-
* 6. 编码适配:json 模式产出对象(由 send 序列化),文本模式字符串原样、其余 JSON 序列化。
|
|
22
|
-
*
|
|
23
|
-
* 用法:
|
|
24
|
-
* const ws = new WebSocketServer({ name: 'websocket', port: 9200, json: true }, server);
|
|
25
|
-
* const rpc = new MessagePlugin();
|
|
26
|
-
* ws.use(rpc);
|
|
27
|
-
* rpc.addMethod('echo', (params) => params);
|
|
28
|
-
* await ws.init();
|
|
29
|
-
* await ws.start();
|
|
30
|
-
*/
|
|
31
|
-
class MessagePlugin {
|
|
32
|
-
/**
|
|
33
|
-
* JSON-RPC 协议核心
|
|
34
|
-
*/
|
|
35
|
-
#rpc = new JsonRpc();
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* 已挂载的 WebSocketServer 实例,未挂载时为 null
|
|
39
|
-
*/
|
|
40
|
-
#server = null;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 挂载到 WebSocketServer 的消息管道,注册内置 main 钩子
|
|
44
|
-
* @param {Object} server 具备 getHandle() 的 WebSocketServer 实例
|
|
45
|
-
* @returns {MessagePlugin} 当前实例
|
|
46
|
-
* @throws {TypeError} server 不具备 getHandle 方法时
|
|
47
|
-
*/
|
|
48
|
-
bind(server) {
|
|
49
|
-
if (!server || typeof server.getHandle !== 'function') {
|
|
50
|
-
throw new TypeError('[MessagePlugin] server must provide getHandle()');
|
|
51
|
-
}
|
|
52
|
-
this.#server = server;
|
|
53
|
-
server.getHandle().on('main', (params, ctx, options) => {
|
|
54
|
-
return this._execute(params, ctx, options);
|
|
55
|
-
}, 100, 'json_rpc');
|
|
56
|
-
return this;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 判断是否已挂载
|
|
61
|
-
* @returns {boolean} 是否已挂载到 WebSocketServer
|
|
62
|
-
*/
|
|
63
|
-
isBound() {
|
|
64
|
-
return this.#server !== null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 登记 RPC 方法
|
|
69
|
-
* @param {string} name 方法名
|
|
70
|
-
* @param {Function} handler 处理函数,签名 handler(params, extra, options),extra 含 client_id,options 为管道请求选项
|
|
71
|
-
* @returns {MessagePlugin} 当前实例
|
|
72
|
-
*/
|
|
73
|
-
addMethod(name, handler) {
|
|
74
|
-
this.#rpc.addMethod(name, handler);
|
|
75
|
-
return this;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 删除 RPC 方法
|
|
80
|
-
* @param {string} name 方法名
|
|
81
|
-
* @returns {boolean} 是否删除成功
|
|
82
|
-
*/
|
|
83
|
-
delMethod(name) {
|
|
84
|
-
return this.#rpc.delMethod(name);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* 列出 RPC 方法(支持 Store 条件查询与分页)
|
|
89
|
-
* @param {Object} [query] 查询条件,缺省时列出全部
|
|
90
|
-
* @returns {Array<Object>} 方法列表,含 name
|
|
91
|
-
*/
|
|
92
|
-
listMethods(query) {
|
|
93
|
-
return this.#rpc.listMethods(query);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 主动推送:向指定客户端回推数据(可用于异步任务完成后通知)
|
|
98
|
-
* @param {string} client_id 目标客户端 ID
|
|
99
|
-
* @param {string|Object} data 待推送数据,对象时按编码模式序列化
|
|
100
|
-
* @returns {boolean} 是否发送成功
|
|
101
|
-
* @throws {Error} 插件尚未挂载到服务器时
|
|
102
|
-
*/
|
|
103
|
-
push(client_id, data) {
|
|
104
|
-
if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
|
|
105
|
-
const payload = this._format(data);
|
|
106
|
-
return this.#server.send(client_id, payload);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 实时调用客户端方法:发送 JSON-RPC 请求并等待客户端反馈 result
|
|
111
|
-
* @param {string} client_id 目标客户端 ID
|
|
112
|
-
* @param {string} method 客户端方法名
|
|
113
|
-
* @param {*} [params] 调用参数
|
|
114
|
-
* @param {Object} [options] 选项,含 timeout_ms(缺省 30000,0 为不超时)
|
|
115
|
-
* @returns {Promise<*>} 客户端反馈的 result;错误响应、送达失败或超时时 reject
|
|
116
|
-
* @throws {Error} 插件尚未挂载到服务器时
|
|
117
|
-
*/
|
|
118
|
-
call(client_id, method, params, options = {}) {
|
|
119
|
-
if (!this.#server) throw new Error('[MessagePlugin] not bound to a server');
|
|
120
|
-
return this.#rpc.call((request) => this.#server.send(client_id, this._format(request)), method, params, options);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* 内置 main 钩子逻辑:优先识别客户端反馈,其次执行 RPC 请求并格式化响应
|
|
125
|
-
* @param {Object} params 消息参数,含 client_id 与 data
|
|
126
|
-
* @param {Object} ctx 执行上下文
|
|
127
|
-
* @param {Object} [options] 管道请求选项(连接级,如 user_id / device_id / agent_id),透传给处理函数
|
|
128
|
-
* @returns {Promise<Object|string|undefined>} 响应对象或 JSON 字符串;反馈、透传与无结果通知时为 undefined
|
|
129
|
-
*/
|
|
130
|
-
async _execute(params, ctx, options) {
|
|
131
|
-
if (this.#rpc.acceptResponse(params.data)) return undefined;
|
|
132
|
-
const extra = { client_id: params.client_id };
|
|
133
|
-
const response = await this.#rpc.handle(params.data, extra, options);
|
|
134
|
-
if (response === undefined) return undefined;
|
|
135
|
-
return this._format(response);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* 按服务器 json 配置格式化数据
|
|
140
|
-
* @param {Object|string} data 响应对象或待推送数据
|
|
141
|
-
* @returns {Object|string} json 模式返回原值,文本模式字符串原样、其余 JSON 序列化
|
|
142
|
-
*/
|
|
143
|
-
_format(data) {
|
|
144
|
-
if (this.#server.config.json) return data;
|
|
145
|
-
if (typeof data === 'string') return data;
|
|
146
|
-
return JSON.stringify(data);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
module.exports = MessagePlugin;
|