mm_websocket_server 0.0.0-stage → 2.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/LICENSE +15 -0
- package/README.md +118 -2
- package/example/demo.js +51 -0
- package/index.js +24 -0
- package/lib/websocket.js +434 -0
- package/package.json +47 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Admin
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# mm_websocket_server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
最薄的 WebSocket 服务器入站适配器。继承 `mm_mod` 的 `Mod`,把 `ws` 的每条消息直接交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。
|
|
4
|
+
|
|
5
|
+
- 入口:`index.js`(CommonJS)
|
|
6
|
+
- 导出:`WebSocketServer`、`admin`、`websocketServerAdmin`
|
|
7
|
+
- 运行时依赖:`mm_mod`、`mm_event`、`mm_corejs`、`ws`
|
|
8
|
+
|
|
9
|
+
## 消息路径
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
WebSocket.Server → connection → ctx = { client_id, path, ws }
|
|
13
|
+
→ eventManager.run(path, params, ctx, options)
|
|
14
|
+
→ 结果非空时回发来源连接
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
事件 `target` 取握手请求路径(第一个 `?` 之前)。`params` 为 `{ client_id, data }`,`data` 在 `json: true` 时尝试 `JSON.parse`(失败回落原文)。
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
const { WebSocketServer } = require('mm_websocket_server');
|
|
25
|
+
|
|
26
|
+
const websocket = new WebSocketServer({ name: 'websocket', host: '127.0.0.1', port: 8090 });
|
|
27
|
+
const events = websocket.getEventManager();
|
|
28
|
+
|
|
29
|
+
events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx) => ({
|
|
30
|
+
client_id: ctx.client_id,
|
|
31
|
+
data: params.data
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await websocket.init();
|
|
35
|
+
await websocket.start();
|
|
36
|
+
|
|
37
|
+
console.log(websocket.getAddress());
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 服务器配置
|
|
41
|
+
|
|
42
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| `name` | string | `'os'` | 模块名称(必填) |
|
|
45
|
+
| `title` | string | `'WebSocket服务器模块'` | 模块标题 |
|
|
46
|
+
| `description` | string | — | 模块描述 |
|
|
47
|
+
| `version` | string | `'2.0.0'` | 版本号 |
|
|
48
|
+
| `host` | string | `'0.0.0.0'` | 监听地址 |
|
|
49
|
+
| `port` | number | `8090` | 监听端口 |
|
|
50
|
+
| `json` | boolean | `true` | 入站尝试 JSON 解析,出站对象自动序列化 |
|
|
51
|
+
| `params` | object | `{ action }` | `main` 的参数约定 |
|
|
52
|
+
|
|
53
|
+
不限制升级路径,任何路径都可连接,路径即事件 `target`。
|
|
54
|
+
|
|
55
|
+
## API
|
|
56
|
+
|
|
57
|
+
| 成员 | 说明 |
|
|
58
|
+
| --- | --- |
|
|
59
|
+
| `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
|
|
60
|
+
| `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
|
|
61
|
+
| `getEventManager()` | 获取 `mm_event` 的 `websocket` 作用域事件管理器(与其他实例共享),用于注册事件 |
|
|
62
|
+
| `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
|
|
63
|
+
| `getAddress()` | 实际监听地址,未启动为 `null` |
|
|
64
|
+
| `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `send` / `broadcast` / `close` / `list` |
|
|
65
|
+
| `send(client_id, data)` | 向指定连接发送数据,返回是否成功 |
|
|
66
|
+
| `broadcast(data)` | 向全部在线连接广播,返回送达数 |
|
|
67
|
+
| `closeClient(client_id)` | 关闭指定连接,返回是否发起 |
|
|
68
|
+
| `listClients()` | 在线连接列表 `{ id, path, address }` |
|
|
69
|
+
| `hasClient(client_id)` | 是否在线 |
|
|
70
|
+
| `setClientOptions(client_id, options)` | 绑定连接级选项,随入站消息作为 `options` 透传(`null` 清空) |
|
|
71
|
+
|
|
72
|
+
## 请求上下文(ctx)
|
|
73
|
+
|
|
74
|
+
| 字段 | 说明 |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `client_id` | 连接编号,形如 `ws_1` |
|
|
77
|
+
| `path` | 握手请求路径,`?` 之后已截断 |
|
|
78
|
+
| `ws` | 原生 ws 连接,事件可直接写出 |
|
|
79
|
+
|
|
80
|
+
## 事件处理函数
|
|
81
|
+
|
|
82
|
+
签名固定为 `(params, ctx, options)`:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
events.add({ name: 'echo', target: '/echo', end: true }).main = async (params, ctx, options) => {
|
|
86
|
+
return { data: params.data };
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
返回非 `undefined` / `null` 的结果会按 `json` 配置编码后回发来源连接;返回 `undefined` 则不回发。事件也可直接用 `ctx.ws` 写出。
|
|
91
|
+
|
|
92
|
+
## 主动操作
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
await websocket.run({ action: 'send', client_id: 'ws_1', data: 'hi' });
|
|
96
|
+
await websocket.run({ action: 'broadcast', data: { type: 'notice' } });
|
|
97
|
+
await websocket.run({ action: 'close', client_id: 'ws_1' });
|
|
98
|
+
await websocket.run({ action: 'list' });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 生命周期
|
|
102
|
+
|
|
103
|
+
`onStart` 创建并监听 `WebSocket.Server`,`onStop` 断开全部连接并关闭服务器。事件管理器取自 `mm_event` 的 `websocket` 作用域,由全局 `Admin` 共享,停止时不清理。端口被占用时 `start()` 抛错,模块进入 `error` 状态,可 `dispose()` 释放。
|
|
104
|
+
|
|
105
|
+
## 示例
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run demo # 启动演示服务器(127.0.0.1:8090),见 example/demo.js
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 测试
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm test # node --test
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 许可证
|
|
118
|
+
|
|
119
|
+
ISC
|
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
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WebSocketServer = require('./lib/websocket');
|
|
4
|
+
const Admin = require('mm_corejs').Admin;
|
|
5
|
+
|
|
6
|
+
let admin = new Admin();
|
|
7
|
+
admin.setClass(WebSocketServer);
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
WebSocketServer,
|
|
11
|
+
admin,
|
|
12
|
+
/**
|
|
13
|
+
* 获取指定作用域的 WebSocket 服务器
|
|
14
|
+
* @param {string} [scope='os'] 作用域名称
|
|
15
|
+
* @param {object} [config] 配置参数
|
|
16
|
+
* @returns {WebSocketServer} WebSocket 服务器实例
|
|
17
|
+
*/
|
|
18
|
+
websocketServerAdmin(scope, config) {
|
|
19
|
+
if (!scope) {
|
|
20
|
+
scope = 'os';
|
|
21
|
+
}
|
|
22
|
+
return admin.get(scope, config);
|
|
23
|
+
}
|
|
24
|
+
};
|
package/lib/websocket.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WebSocket = require('ws');
|
|
4
|
+
const { Mod } = require('mm_mod');
|
|
5
|
+
const { eventAdmin } = require('mm_event');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 获取请求路径(在 ? 处截断,不解析查询串)
|
|
9
|
+
* @param {string} url 原始请求地址
|
|
10
|
+
* @returns {string} 请求路径
|
|
11
|
+
*/
|
|
12
|
+
function getPath(url) {
|
|
13
|
+
const index = url.indexOf('?');
|
|
14
|
+
return index === -1 ? url : url.slice(0, index);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WebSocketServer —— 最薄的 WebSocket 传输层(入站适配器模块)。
|
|
19
|
+
*
|
|
20
|
+
* 只做三件事:
|
|
21
|
+
* 1. 生命周期:由 Mod 的 init / start / stop 驱动 WebSocket.Server;
|
|
22
|
+
* 2. 消息转交:以握手请求路径为 target,直接调用
|
|
23
|
+
* EventManager.run(path, params, ctx, options),不做路由、不做插件;
|
|
24
|
+
* 3. 结果回发:事件返回非空结果时回发来源客户端,事件也可用 ctx.ws 自行写出。
|
|
25
|
+
*
|
|
26
|
+
* 用法:
|
|
27
|
+
* const wss = new WebSocketServer({ name: 'websocket', port: 8090 });
|
|
28
|
+
* wss.getEventManager().add({ name: 'echo', target: '*' }).main = async (params) => params.data;
|
|
29
|
+
* await wss.init();
|
|
30
|
+
* await wss.start();
|
|
31
|
+
*/
|
|
32
|
+
class WebSocketServer extends Mod {
|
|
33
|
+
/**
|
|
34
|
+
* 模块默认配置
|
|
35
|
+
*/
|
|
36
|
+
static config = {
|
|
37
|
+
name: 'os',
|
|
38
|
+
title: 'WebSocket服务器模块',
|
|
39
|
+
description: 'WebSocket 传输层:消息直达事件管理器,不做路由与插件',
|
|
40
|
+
version: '2.0.0',
|
|
41
|
+
host: '0.0.0.0',
|
|
42
|
+
port: 8090,
|
|
43
|
+
json: true,
|
|
44
|
+
params: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
required: ['action'],
|
|
47
|
+
properties: {
|
|
48
|
+
action: { type: 'string', enum: ['send', 'broadcast', 'close', 'list'] },
|
|
49
|
+
client_id: { type: 'string', min_length: 1 },
|
|
50
|
+
data: {}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* ws 服务器实例
|
|
57
|
+
*/
|
|
58
|
+
#wss = null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 事件管理器(取自 mm_event 的 websocket 作用域,与其他实例共享)
|
|
62
|
+
*/
|
|
63
|
+
#events = null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 在线连接表:client_id -> 连接信息
|
|
67
|
+
*/
|
|
68
|
+
#clients = new Map();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 客户端编号自增计数器
|
|
72
|
+
*/
|
|
73
|
+
#counter = 0;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 构造函数
|
|
77
|
+
* @param {Object} config 模块配置
|
|
78
|
+
* @param {Object} server 框架实例
|
|
79
|
+
*/
|
|
80
|
+
constructor(config, server) {
|
|
81
|
+
super(config, server);
|
|
82
|
+
this.#events = eventAdmin('websocket');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------- 生命周期钩子 ----------
|
|
86
|
+
/**
|
|
87
|
+
* 启动钩子:创建 ws 服务器并开始监听
|
|
88
|
+
* @returns {Promise<void>} 无返回值
|
|
89
|
+
*/
|
|
90
|
+
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));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 停止钩子:断开全部连接并关闭服务器(事件管理器与其他实例共享,不清理)
|
|
103
|
+
* @returns {Promise<void>} 无返回值
|
|
104
|
+
*/
|
|
105
|
+
async onStop() {
|
|
106
|
+
this._closeAllClients();
|
|
107
|
+
await this._closeServer();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------- 执行入口 ----------
|
|
111
|
+
/**
|
|
112
|
+
* 模块主逻辑:按 action 派发操作
|
|
113
|
+
* @param {Object} params 调用参数,含 action 及操作所需字段
|
|
114
|
+
* @param {Object} ctx 执行上下文
|
|
115
|
+
* @param {Object} options 合并后的选项
|
|
116
|
+
* @returns {Promise<Object>} 操作结果
|
|
117
|
+
*/
|
|
118
|
+
async main(params, ctx, options) {
|
|
119
|
+
const action = params.action;
|
|
120
|
+
if (action === 'send') return this._actionSend(params);
|
|
121
|
+
if (action === 'broadcast') return this._actionBroadcast(params);
|
|
122
|
+
if (action === 'close') return this._actionClose(params);
|
|
123
|
+
return this._actionList();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 执行发送操作
|
|
128
|
+
* @param {Object} params 调用参数,含 client_id 与 data
|
|
129
|
+
* @returns {Object} 操作结果
|
|
130
|
+
*/
|
|
131
|
+
_actionSend(params) {
|
|
132
|
+
this._requireClientId(params);
|
|
133
|
+
const success = this.send(params.client_id, params.data);
|
|
134
|
+
return { action: 'send', client_id: params.client_id, success };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 执行广播操作
|
|
139
|
+
* @param {Object} params 调用参数,含 data
|
|
140
|
+
* @returns {Object} 操作结果,count 为送达连接数
|
|
141
|
+
*/
|
|
142
|
+
_actionBroadcast(params) {
|
|
143
|
+
const count = this.broadcast(params.data);
|
|
144
|
+
return { action: 'broadcast', count };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 执行关闭连接操作
|
|
149
|
+
* @param {Object} params 调用参数,含 client_id
|
|
150
|
+
* @returns {Object} 操作结果
|
|
151
|
+
*/
|
|
152
|
+
_actionClose(params) {
|
|
153
|
+
this._requireClientId(params);
|
|
154
|
+
const success = this.closeClient(params.client_id);
|
|
155
|
+
return { action: 'close', client_id: params.client_id, success };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 执行列出连接操作
|
|
160
|
+
* @returns {Object} 操作结果,clients 为在线连接列表
|
|
161
|
+
*/
|
|
162
|
+
_actionList() {
|
|
163
|
+
return { action: 'list', clients: this.listClients() };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---------- 事件管理器 ----------
|
|
167
|
+
/**
|
|
168
|
+
* 获取事件管理器,用于注册事件
|
|
169
|
+
* @returns {Object} 事件管理器实例
|
|
170
|
+
*/
|
|
171
|
+
getEventManager() {
|
|
172
|
+
return this.#events;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 注入外部事件管理器
|
|
177
|
+
* @param {Object} manager 事件管理器实例
|
|
178
|
+
* @returns {WebSocketServer} 当前实例
|
|
179
|
+
* @throws {TypeError} manager 不具备 run 方法时
|
|
180
|
+
*/
|
|
181
|
+
setEventManager(manager) {
|
|
182
|
+
if (!manager || typeof manager.run !== 'function') {
|
|
183
|
+
throw new TypeError(`${this._label()} manager must provide run()`);
|
|
184
|
+
}
|
|
185
|
+
this.#events = manager;
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 获取实际监听地址
|
|
191
|
+
* @returns {Object|null} 监听地址信息,未启动时为 null
|
|
192
|
+
*/
|
|
193
|
+
getAddress() {
|
|
194
|
+
const wss = this.#wss;
|
|
195
|
+
return wss ? wss.address() : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---------- 公开操作 ----------
|
|
199
|
+
/**
|
|
200
|
+
* 向指定连接发送数据
|
|
201
|
+
* @param {string} client_id 客户端 ID
|
|
202
|
+
* @param {string|Object} data 待发送数据
|
|
203
|
+
* @returns {boolean} 是否发送成功
|
|
204
|
+
*/
|
|
205
|
+
send(client_id, data) {
|
|
206
|
+
const client = this.#clients.get(client_id);
|
|
207
|
+
if (!client) return false;
|
|
208
|
+
return this._sendToClient(client, this._encodeData(data));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 向全部在线连接广播数据
|
|
213
|
+
* @param {string|Object} data 待广播数据
|
|
214
|
+
* @returns {number} 送达的连接数量
|
|
215
|
+
*/
|
|
216
|
+
broadcast(data) {
|
|
217
|
+
const payload = this._encodeData(data);
|
|
218
|
+
let count = 0;
|
|
219
|
+
for (const client of this.#clients.values()) {
|
|
220
|
+
if (this._sendToClient(client, payload)) count += 1;
|
|
221
|
+
}
|
|
222
|
+
return count;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 关闭指定连接(连接表由关闭回调清理)
|
|
227
|
+
* @param {string} client_id 客户端 ID
|
|
228
|
+
* @returns {boolean} 是否发起关闭
|
|
229
|
+
*/
|
|
230
|
+
closeClient(client_id) {
|
|
231
|
+
const client = this.#clients.get(client_id);
|
|
232
|
+
if (!client) return false;
|
|
233
|
+
client.ws.close();
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 列出在线连接
|
|
239
|
+
* @returns {Array<Object>} 连接信息列表,含 id、path 与 address
|
|
240
|
+
*/
|
|
241
|
+
listClients() {
|
|
242
|
+
const list = [];
|
|
243
|
+
for (const client of this.#clients.values()) {
|
|
244
|
+
list.push({ id: client.id, path: client.path, address: client.address });
|
|
245
|
+
}
|
|
246
|
+
return list;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 判断连接是否在线
|
|
251
|
+
* @param {string} client_id 客户端 ID
|
|
252
|
+
* @returns {boolean} 是否在线
|
|
253
|
+
*/
|
|
254
|
+
hasClient(client_id) {
|
|
255
|
+
return this.#clients.has(client_id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 绑定连接级选项,入站消息将作为 options 透传给事件
|
|
260
|
+
* @param {string} client_id 客户端 ID
|
|
261
|
+
* @param {Object|null} options 选项对象,null 时清空
|
|
262
|
+
* @returns {boolean} 是否设置成功(连接不在线时失败)
|
|
263
|
+
*/
|
|
264
|
+
setClientOptions(client_id, options) {
|
|
265
|
+
const client = this.#clients.get(client_id);
|
|
266
|
+
if (!client) return false;
|
|
267
|
+
client.options = options || null;
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------- 连接处理 ----------
|
|
272
|
+
/**
|
|
273
|
+
* 登记新的连接并绑定回调
|
|
274
|
+
* @param {Object} ws ws 连接实例
|
|
275
|
+
* @param {Object} req 握手请求对象
|
|
276
|
+
*/
|
|
277
|
+
_handleConnection(ws, req) {
|
|
278
|
+
this.#counter += 1;
|
|
279
|
+
const client_id = `ws_${this.#counter}`;
|
|
280
|
+
const client = {
|
|
281
|
+
id: client_id,
|
|
282
|
+
path: getPath(req.url || '/'),
|
|
283
|
+
address: this._remoteAddress(req),
|
|
284
|
+
ws,
|
|
285
|
+
options: null
|
|
286
|
+
};
|
|
287
|
+
this.#clients.set(client_id, client);
|
|
288
|
+
ws.on('message', (buf) => this._handleMessage(client_id, buf));
|
|
289
|
+
ws.on('close', () => this.#clients.delete(client_id));
|
|
290
|
+
ws.on('error', (err) => this._emitError(err));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 处理入站消息:解码后交给事件管理器,返回非空结果时回发来源连接
|
|
295
|
+
* @param {string} client_id 客户端 ID
|
|
296
|
+
* @param {Object} buf 数据缓冲区
|
|
297
|
+
* @returns {Promise<void>} 无返回值
|
|
298
|
+
*/
|
|
299
|
+
async _handleMessage(client_id, buf) {
|
|
300
|
+
const client = this.#clients.get(client_id);
|
|
301
|
+
if (!client) return;
|
|
302
|
+
const params = { client_id, data: this._decodeData(buf) };
|
|
303
|
+
const ctx = { client_id, path: client.path, ws: client.ws };
|
|
304
|
+
const options = this._clientOptions(client);
|
|
305
|
+
try {
|
|
306
|
+
const result = await this.#events.run(client.path, params, ctx, options);
|
|
307
|
+
const error = ctx.error || options.error;
|
|
308
|
+
if (error) this._emitError(error);
|
|
309
|
+
if (result !== undefined && result !== null) this.send(client_id, result);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
this._emitError(err);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 获取连接级选项(复制一份,避免事件管理器回写)
|
|
317
|
+
* @param {Object} client 连接信息
|
|
318
|
+
* @returns {Object} 选项对象
|
|
319
|
+
*/
|
|
320
|
+
_clientOptions(client) {
|
|
321
|
+
if (!client.options) return {};
|
|
322
|
+
return { ...client.options };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* 获取远端地址字符串
|
|
327
|
+
* @param {Object} req 握手请求对象
|
|
328
|
+
* @returns {string} 形如 ip:port 的地址
|
|
329
|
+
*/
|
|
330
|
+
_remoteAddress(req) {
|
|
331
|
+
return `${req.socket.remoteAddress}:${req.socket.remotePort}`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ---------- 数据编解码 ----------
|
|
335
|
+
/**
|
|
336
|
+
* 编码待发送数据
|
|
337
|
+
* @param {string|Object} data 待编码数据
|
|
338
|
+
* @returns {string} 编码后的字符串
|
|
339
|
+
*/
|
|
340
|
+
_encodeData(data) {
|
|
341
|
+
if (this.config.json) return JSON.stringify(data);
|
|
342
|
+
if (typeof data === 'string') return data;
|
|
343
|
+
return String(data);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 解码收到的数据
|
|
348
|
+
* @param {Object} buf 数据缓冲区
|
|
349
|
+
* @returns {string|Object} 解码后的数据,json 模式下解析失败时返回原文
|
|
350
|
+
*/
|
|
351
|
+
_decodeData(buf) {
|
|
352
|
+
const text = buf.toString('utf8');
|
|
353
|
+
if (!this.config.json) return text;
|
|
354
|
+
return this._parseJson(text);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* 尝试将文本解析为 JSON
|
|
359
|
+
* @param {string} text 文本内容
|
|
360
|
+
* @returns {Object|string} 解析结果,失败时返回原文
|
|
361
|
+
*/
|
|
362
|
+
_parseJson(text) {
|
|
363
|
+
try {
|
|
364
|
+
return JSON.parse(text);
|
|
365
|
+
} catch {
|
|
366
|
+
return text;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---------- 发送与清理 ----------
|
|
371
|
+
/**
|
|
372
|
+
* 向单个连接发送已编码数据
|
|
373
|
+
* @param {Object} client 连接信息
|
|
374
|
+
* @param {string} payload 已编码数据
|
|
375
|
+
* @returns {boolean} 是否发送成功
|
|
376
|
+
*/
|
|
377
|
+
_sendToClient(client, payload) {
|
|
378
|
+
if (client.ws.readyState !== WebSocket.OPEN) return false;
|
|
379
|
+
client.ws.send(payload);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 断开全部连接并清空连接表
|
|
385
|
+
*/
|
|
386
|
+
_closeAllClients() {
|
|
387
|
+
for (const client of this.#clients.values()) {
|
|
388
|
+
client.ws.terminate();
|
|
389
|
+
}
|
|
390
|
+
this.#clients.clear();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 等待 ws 服务器监听成功
|
|
395
|
+
* @param {Object} wss WebSocket.Server 实例
|
|
396
|
+
* @returns {Promise<void>} 无返回值
|
|
397
|
+
*/
|
|
398
|
+
_listen(wss) {
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
wss.once('error', reject);
|
|
401
|
+
wss.once('listening', () => {
|
|
402
|
+
wss.removeListener('error', reject);
|
|
403
|
+
resolve();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 关闭 ws 服务器(连接已在之前全部终止)
|
|
410
|
+
* @returns {Promise<void>} 无返回值
|
|
411
|
+
*/
|
|
412
|
+
_closeServer() {
|
|
413
|
+
const wss = this.#wss;
|
|
414
|
+
this.#wss = null;
|
|
415
|
+
if (!wss) return Promise.resolve();
|
|
416
|
+
return new Promise((resolve) => {
|
|
417
|
+
wss.close(() => resolve());
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ---------- 校验辅助 ----------
|
|
422
|
+
/**
|
|
423
|
+
* 断言调用参数含 client_id
|
|
424
|
+
* @param {Object} params 调用参数
|
|
425
|
+
* @throws {Error} 缺少 client_id 时
|
|
426
|
+
*/
|
|
427
|
+
_requireClientId(params) {
|
|
428
|
+
if (!params.client_id) {
|
|
429
|
+
throw new Error(`${this._label()} params.client_id required`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = WebSocketServer;
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_websocket_server",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
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
|
+
}
|
|
49
|
+
}
|