mm_socket_server 2.0.0 → 3.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/README.md +49 -25
- package/example/demo.js +19 -7
- package/lib/socket.js +527 -76
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mm_socket_server
|
|
2
2
|
|
|
3
|
-
最薄的 TCP/UDP 套接字服务器入站适配器。继承 `mm_mod` 的 `Mod
|
|
3
|
+
最薄的 TCP/UDP 套接字服务器入站适配器。继承 `mm_mod` 的 `Mod`,把按 topic 分帧后的报文以 topic 为 `target` 交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。`protocol` 可选 `tcp`、`udp` 或 `both`(同端口同时监听)。
|
|
4
4
|
|
|
5
5
|
- 入口:`index.js`(CommonJS)
|
|
6
6
|
- 导出:`SocketServer`、`admin`、`socketServerAdmin`
|
|
@@ -9,12 +9,27 @@
|
|
|
9
9
|
## 消息路径
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
net / dgram → connection / message →
|
|
13
|
-
→
|
|
14
|
-
→
|
|
12
|
+
net / dgram → connection / data / message → 按 framing 切出 { topic, payload }
|
|
13
|
+
→ ctx = { client_id, topic, protocol, address, scope, socket | rinfo }
|
|
14
|
+
→ eventManager.run(topic, params, ctx, options)(同一连接的消息串行执行)
|
|
15
|
+
→ 结果非空时按同一帧格式回发来源客户端
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
事件 `target`
|
|
18
|
+
事件 `target` 取帧内的 topic(如 `chat/1`),因此可按主题精细分发,也支持 `mm_event` 的 `*` 通配(前缀、后缀、包含)。`params` 为 `{ client_id, topic, data }`,`data` 在 `json: true` 时尝试 `JSON.parse`(失败回落原文)。
|
|
19
|
+
|
|
20
|
+
### 帧格式
|
|
21
|
+
|
|
22
|
+
| `framing` | 帧格式 | 说明 |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `line`(默认) | `topic<空格>payload\n` | 以 `0x0a` 分帧,兼容剥除 `\r\n`;payload 不能含换行,否则发送时报错 |
|
|
25
|
+
| `length` | `[uint16BE topic 长度][uint32BE payload 长度][topic][payload]` | 二进制安全,TCP 按长度拼包,UDP 用于校验完整性 |
|
|
26
|
+
| `none` | 不分帧 | `target` 回落为协议名 `tcp` / `udp`,保持旧行为 |
|
|
27
|
+
|
|
28
|
+
topic 一律按 utf8 编解码,`encoding` 只作用于 payload。缺少 topic 的帧会被丢弃并通过 `error` 事件上报 `invalid frame: missing topic`;UDP 长度帧不完整时上报 `invalid frame: truncated`。
|
|
29
|
+
|
|
30
|
+
> **TCP 是字节流,没有天然消息边界。** 模块按 `framing` 做连接级缓冲与拼包/拆包,因此 `line` / `length` 下可安全处理粘包与拆包;UDP 保留数据报边界,一次报文即一帧。
|
|
31
|
+
|
|
32
|
+
同一连接的消息按到达顺序串行执行:事件处理函数返回 Promise 时,后一条消息会等前一条处理完再开始,避免响应乱序。
|
|
18
33
|
|
|
19
34
|
## 快速开始
|
|
20
35
|
|
|
@@ -23,18 +38,15 @@ net / dgram → connection / message → ctx = { client_id, protocol, address, s
|
|
|
23
38
|
|
|
24
39
|
const { SocketServer } = require('mm_socket_server');
|
|
25
40
|
|
|
26
|
-
const socket = new SocketServer({ name: 'socket', host: '127.0.0.1', port: 9000 });
|
|
41
|
+
const socket = new SocketServer({ name: 'socket', protocol: 'both', host: '127.0.0.1', port: 9000 });
|
|
27
42
|
const events = socket.getEventManager();
|
|
28
43
|
|
|
29
|
-
events.add({ name: 'echo', target: '
|
|
30
|
-
client_id: ctx.client_id,
|
|
31
|
-
data: params.data
|
|
32
|
-
});
|
|
44
|
+
events.add({ name: 'echo', target: 'chat/echo', end: true }).main = async (params) => params.data;
|
|
33
45
|
|
|
34
46
|
await socket.init();
|
|
35
47
|
await socket.start();
|
|
36
48
|
|
|
37
|
-
console.log(socket.
|
|
49
|
+
console.log(socket.getAddresses());
|
|
38
50
|
```
|
|
39
51
|
|
|
40
52
|
## 服务器配置
|
|
@@ -44,13 +56,16 @@ console.log(socket.getAddress());
|
|
|
44
56
|
| `name` | string | `'os'` | 模块名称(必填) |
|
|
45
57
|
| `title` | string | `'Socket服务器模块'` | 模块标题 |
|
|
46
58
|
| `description` | string | — | 模块描述 |
|
|
47
|
-
| `version` | string | `'
|
|
48
|
-
| `protocol` | string | `'tcp'` |
|
|
59
|
+
| `version` | string | `'3.0.0'` | 版本号 |
|
|
60
|
+
| `protocol` | string | `'tcp'` | 监听协议,接受 `tcp` / `udp` / `both`,其他值在 `init()` 时抛错 |
|
|
61
|
+
| `framing` | string | `'line'` | 帧格式,接受 `line` / `length` / `none`,其他值在 `init()` 时抛错 |
|
|
49
62
|
| `host` | string | `'0.0.0.0'` | 监听地址 |
|
|
50
63
|
| `port` | number | `9000` | 监听端口 |
|
|
51
|
-
| `encoding` | string | `'utf8'` |
|
|
64
|
+
| `encoding` | string | `'utf8'` | payload 解码字符集(topic 始终 utf8) |
|
|
52
65
|
| `json` | boolean | `false` | 入站尝试 JSON 解析,出站对象自动序列化 |
|
|
53
|
-
| `
|
|
66
|
+
| `scope` | string | `'socket'` | 事件管理器作用域,多实例可用不同 scope 隔离事件 |
|
|
67
|
+
| `udp_ttl` | number | `300000` | UDP 客户端空闲多久(毫秒)后移出连接表,`<= 0` 关闭清理 |
|
|
68
|
+
| `params` | object | `{ action }` | `main` 的参数约定(`action` / `client_id` / `topic` / `data`) |
|
|
54
69
|
|
|
55
70
|
## API
|
|
56
71
|
|
|
@@ -58,12 +73,13 @@ console.log(socket.getAddress());
|
|
|
58
73
|
| --- | --- |
|
|
59
74
|
| `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
|
|
60
75
|
| `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
|
|
61
|
-
| `getEventManager()` | 获取 `mm_event`
|
|
76
|
+
| `getEventManager()` | 获取 `mm_event` 上 `scope` 作用域的事件管理器(默认 `socket`,其他实例共享同一作用域),用于注册事件 |
|
|
62
77
|
| `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
|
|
63
|
-
| `getAddress()` |
|
|
78
|
+
| `getAddress(protocol)` | 指定协议的实际监听地址,未启动为 `null`;省略 `protocol` 时取首个已启用协议 |
|
|
79
|
+
| `getAddresses()` | 返回 `{ tcp, udp }` 两个协议各自的监听地址 |
|
|
64
80
|
| `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `send` / `broadcast` / `close` / `list` |
|
|
65
|
-
| `send(client_id, data)` |
|
|
66
|
-
| `broadcast(data)` |
|
|
81
|
+
| `send(client_id, data, topic)` | 向指定客户端发送数据,`framing` 非 `none` 时 `topic` 必填,返回是否成功 |
|
|
82
|
+
| `broadcast(data, topic)` | 向全部在线客户端广播,`framing` 非 `none` 时 `topic` 必填,返回送达数 |
|
|
67
83
|
| `closeClient(client_id)` | 关闭指定客户端(TCP 销毁套接字,UDP 仅移出连接表) |
|
|
68
84
|
| `listClients()` | 在线客户端列表 `{ id, protocol, address }` |
|
|
69
85
|
| `hasClient(client_id)` | 是否在线 |
|
|
@@ -74,8 +90,10 @@ console.log(socket.getAddress());
|
|
|
74
90
|
| 字段 | 说明 |
|
|
75
91
|
| --- | --- |
|
|
76
92
|
| `client_id` | 客户端编号,TCP 形如 `tcp_1`,UDP 形如 `udp_127.0.0.1:5000` |
|
|
93
|
+
| `topic` | 当前帧的 topic,即事件的 `target` |
|
|
77
94
|
| `protocol` | 当前协议,`tcp` 或 `udp` |
|
|
78
95
|
| `address` | 远端地址,形如 `ip:port` |
|
|
96
|
+
| `scope` | 当前实例的事件管理器作用域 |
|
|
79
97
|
| `socket` | TCP 连接(UDP 报文无此字段),事件可直接写出 |
|
|
80
98
|
| `rinfo` | UDP 远端地址信息(TCP 消息无此字段) |
|
|
81
99
|
|
|
@@ -84,25 +102,31 @@ console.log(socket.getAddress());
|
|
|
84
102
|
签名固定为 `(params, ctx, options)`:
|
|
85
103
|
|
|
86
104
|
```js
|
|
87
|
-
events.add({ name: 'echo', target: '
|
|
88
|
-
return
|
|
105
|
+
events.add({ name: 'echo', target: 'chat/echo', end: true }).main = async (params, ctx, options) => {
|
|
106
|
+
return params.data;
|
|
89
107
|
};
|
|
90
108
|
```
|
|
91
109
|
|
|
92
|
-
返回非 `undefined` / `null` 的结果会按 `
|
|
110
|
+
返回非 `undefined` / `null` 的结果会按 `framing` 与原 topic 编码后回发来源客户端;返回 `{ topic, data }` 则改发到指定 topic;返回 `undefined` 则不回发。事件也可直接用 `ctx.socket` 写出。
|
|
93
111
|
|
|
94
112
|
## 主动操作
|
|
95
113
|
|
|
96
114
|
```js
|
|
97
|
-
await socket.run({ action: 'send', client_id: 'tcp_1', data: 'hi' });
|
|
98
|
-
await socket.run({ action: 'broadcast', data: { type: 'notice' } });
|
|
115
|
+
await socket.run({ action: 'send', client_id: 'tcp_1', topic: 'chat/echo', data: 'hi' });
|
|
116
|
+
await socket.run({ action: 'broadcast', topic: 'notice', data: { type: 'notice' } });
|
|
99
117
|
await socket.run({ action: 'close', client_id: 'tcp_1' });
|
|
100
118
|
await socket.run({ action: 'list' });
|
|
101
119
|
```
|
|
102
120
|
|
|
121
|
+
`action` 仅支持上述四种,其他值直接抛 `unsupported action` 错误。`framing` 非 `none` 时 `send` / `broadcast` 必须给出 `topic`。
|
|
122
|
+
|
|
103
123
|
## 生命周期
|
|
104
124
|
|
|
105
|
-
`
|
|
125
|
+
`onInit` 校验 `protocol` 只能是 `tcp` / `udp` / `both`、`framing` 只能是 `line` / `length` / `none`;`onStart` 按 `protocol` 创建并监听 `net.Server` 或 `dgram.Socket`(`both` 时两者同端口并行,TCP 与 UDP 端口空间互不影响),`onStop` 断开全部连接并关闭服务器。启动失败(如端口被占用)时会先回滚已创建的服务器与连接表再抛错,模块进入 `error` 状态,可 `dispose()` 释放;`both` 模式下任一协议启动失败也会回滚另一协议。
|
|
126
|
+
|
|
127
|
+
事件管理器取自 `mm_event` 上 `scope` 指定的作用域(默认 `socket`),由全局 `Admin` 共享,停止时不清理;多个实例需要不同作用域时,配置不同的 `scope` 即可。
|
|
128
|
+
|
|
129
|
+
UDP 没有连接概念,首次收到某远端地址的报文时登记为客户端,之后超过 `udp_ttl` 未再收到报文会被定时清理。
|
|
106
130
|
|
|
107
131
|
## 示例
|
|
108
132
|
|
package/example/demo.js
CHANGED
|
@@ -6,22 +6,35 @@ const { SocketServer } = require('..');
|
|
|
6
6
|
const DEMO_PORT = 9000;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* 注册演示事件:帧内 topic 即事件 target
|
|
10
10
|
* @param {Object} events 事件管理器
|
|
11
11
|
* @returns {void}
|
|
12
12
|
*/
|
|
13
13
|
function bindEvents(events) {
|
|
14
|
-
events.add({ name: 'echo', target: '
|
|
14
|
+
events.add({ name: 'echo', target: 'chat/echo', end: true }).main = async (params, ctx) => ({
|
|
15
15
|
type: 'echo',
|
|
16
16
|
client_id: ctx.client_id,
|
|
17
17
|
data: params.data
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
events.add({ name: 'time', target: '
|
|
20
|
+
events.add({ name: 'time', target: 'chat/time', end: true }).main = async () => ({
|
|
21
21
|
time: new Date().toISOString()
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 打印两种协议的监听地址
|
|
27
|
+
* @param {Object} socket 套接字服务器实例
|
|
28
|
+
* @returns {void}
|
|
29
|
+
*/
|
|
30
|
+
function printAddresses(socket) {
|
|
31
|
+
const addresses = socket.getAddresses();
|
|
32
|
+
for (const protocol of Object.keys(addresses)) {
|
|
33
|
+
const address = addresses[protocol];
|
|
34
|
+
if (address) console.log(`${protocol}://127.0.0.1:${address.port}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* 演示入口:启动服务器并打印监听地址
|
|
27
40
|
* @returns {Promise<void>} 无返回值
|
|
@@ -31,7 +44,7 @@ async function main() {
|
|
|
31
44
|
name: 'demo',
|
|
32
45
|
host: '127.0.0.1',
|
|
33
46
|
port: DEMO_PORT,
|
|
34
|
-
protocol: '
|
|
47
|
+
protocol: 'both'
|
|
35
48
|
});
|
|
36
49
|
|
|
37
50
|
bindEvents(socket.getEventManager());
|
|
@@ -40,9 +53,8 @@ async function main() {
|
|
|
40
53
|
await socket.init();
|
|
41
54
|
await socket.start();
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
console.log(`
|
|
45
|
-
console.log('发送任意内容,结果由 target 为 tcp 的事件返回并自动回发');
|
|
56
|
+
printAddresses(socket);
|
|
57
|
+
console.log('发送 `chat/echo hello\\n`,事件返回结果会以同一 topic 回发');
|
|
46
58
|
}
|
|
47
59
|
|
|
48
60
|
main().catch((err) => {
|
package/lib/socket.js
CHANGED
|
@@ -6,17 +6,63 @@ const { Mod } = require('mm_mod');
|
|
|
6
6
|
const { eventAdmin } = require('mm_event');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* 长度前缀帧的头部字节数:2 字节 topic 长度 + 4 字节 payload 长度
|
|
10
|
+
*/
|
|
11
|
+
const LENGTH_HEADER_SIZE = 6;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* topic 长度的字节数
|
|
15
|
+
*/
|
|
16
|
+
const TOPIC_LENGTH_SIZE = 2;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 换行符字节
|
|
20
|
+
*/
|
|
21
|
+
const LINE_FEED = 0x0a;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 回车符字节
|
|
25
|
+
*/
|
|
26
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 空格字节,行协议中分隔 topic 与 payload
|
|
30
|
+
*/
|
|
31
|
+
const SPACE = 0x20;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 支持的协议
|
|
35
|
+
*/
|
|
36
|
+
const PROTOCOLS = ['tcp', 'udp'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 支持的协议配置值
|
|
40
|
+
*/
|
|
41
|
+
const PROTOCOL_MODES = ['tcp', 'udp', 'both'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 支持的帧格式
|
|
45
|
+
*/
|
|
46
|
+
const FRAMINGS = ['line', 'length', 'none'];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SocketServer —— TCP/UDP 传输层(入站适配器模块)。
|
|
50
|
+
*
|
|
51
|
+
* 职责:
|
|
52
|
+
* 1. 生命周期:由 Mod 的 init / start / stop 驱动 net.Server / dgram.Socket,
|
|
53
|
+
* protocol 支持 tcp、udp 或 both(同端口同时监听两种传输层);
|
|
54
|
+
* 2. 消息转交:按 framing 把字节流还原成 { topic, payload } 帧,以 topic 为 target
|
|
55
|
+
* 调用 EventManager.run(topic, params, ctx, options),支持 mm_event 的 `*` 通配;
|
|
56
|
+
* 3. 结果回发:事件返回非空结果时按同一帧格式回发来源客户端。
|
|
10
57
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 2
|
|
14
|
-
*
|
|
15
|
-
* 3. 结果回发:事件返回非空结果时回发来源客户端,事件也可用 ctx.socket 自行写出。
|
|
58
|
+
* 帧格式:
|
|
59
|
+
* line —— `topic<空格>payload\n`,TCP 按换行分帧,payload 不能含换行;
|
|
60
|
+
* length —— `[2字节 topic 长度][4字节 payload 长度][topic][payload]`,二进制安全;
|
|
61
|
+
* none —— 不分帧,target 回落为连接协议名(tcp / udp),保持旧行为。
|
|
16
62
|
*
|
|
17
63
|
* 用法:
|
|
18
|
-
* const socket = new SocketServer({ name: 'socket', protocol: '
|
|
19
|
-
* socket.getEventManager().add({ name: 'echo', target: '
|
|
64
|
+
* const socket = new SocketServer({ name: 'socket', protocol: 'both', port: 9000 });
|
|
65
|
+
* socket.getEventManager().add({ name: 'echo', target: 'chat/*' }).main = async (params) => params.data;
|
|
20
66
|
* await socket.init();
|
|
21
67
|
* await socket.start();
|
|
22
68
|
*/
|
|
@@ -27,28 +73,32 @@ class SocketServer extends Mod {
|
|
|
27
73
|
static config = {
|
|
28
74
|
name: 'os',
|
|
29
75
|
title: 'Socket服务器模块',
|
|
30
|
-
description: 'TCP/UDP
|
|
31
|
-
version: '
|
|
76
|
+
description: 'TCP/UDP 传输层:按 topic 帧直达事件管理器,不做插件',
|
|
77
|
+
version: '3.0.0',
|
|
32
78
|
protocol: 'tcp',
|
|
33
79
|
host: '0.0.0.0',
|
|
34
80
|
port: 9000,
|
|
35
81
|
encoding: 'utf8',
|
|
36
82
|
json: false,
|
|
83
|
+
scope: 'socket',
|
|
84
|
+
framing: 'line',
|
|
85
|
+
udp_ttl: 300000,
|
|
37
86
|
params: {
|
|
38
87
|
type: 'object',
|
|
39
88
|
required: ['action'],
|
|
40
89
|
properties: {
|
|
41
90
|
action: { type: 'string', enum: ['send', 'broadcast', 'close', 'list'] },
|
|
42
91
|
client_id: { type: 'string', min_length: 1 },
|
|
92
|
+
topic: { type: 'string', min_length: 1 },
|
|
43
93
|
data: {}
|
|
44
94
|
}
|
|
45
95
|
}
|
|
46
96
|
};
|
|
47
97
|
|
|
48
98
|
/**
|
|
49
|
-
*
|
|
99
|
+
* 底层服务器实例,按协议分槽存放
|
|
50
100
|
*/
|
|
51
|
-
#
|
|
101
|
+
#servers = { tcp: null, udp: null };
|
|
52
102
|
|
|
53
103
|
/**
|
|
54
104
|
* 事件管理器(取自 mm_event 的 socket 作用域,与其他实例共享)
|
|
@@ -65,6 +115,11 @@ class SocketServer extends Mod {
|
|
|
65
115
|
*/
|
|
66
116
|
#counter = 0;
|
|
67
117
|
|
|
118
|
+
/**
|
|
119
|
+
* UDP 过期连接清理定时器
|
|
120
|
+
*/
|
|
121
|
+
#udp_timer = null;
|
|
122
|
+
|
|
68
123
|
/**
|
|
69
124
|
* 构造函数
|
|
70
125
|
* @param {Object} config 模块配置
|
|
@@ -72,20 +127,81 @@ class SocketServer extends Mod {
|
|
|
72
127
|
*/
|
|
73
128
|
constructor(config, server) {
|
|
74
129
|
super(config, server);
|
|
75
|
-
this.#events = eventAdmin(
|
|
130
|
+
this.#events = eventAdmin(this.config.scope);
|
|
76
131
|
}
|
|
77
132
|
|
|
78
133
|
// ---------- 生命周期钩子 ----------
|
|
79
134
|
/**
|
|
80
|
-
*
|
|
135
|
+
* 初始化钩子:校验协议与帧格式配置
|
|
136
|
+
* @returns {Promise<void>} 无返回值
|
|
137
|
+
* @throws {Error} protocol 或 framing 不受支持时
|
|
138
|
+
*/
|
|
139
|
+
async onInit() {
|
|
140
|
+
this._validateConfig();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 校验配置项
|
|
145
|
+
* @returns {void}
|
|
146
|
+
* @throws {Error} protocol 或 framing 不受支持时
|
|
147
|
+
*/
|
|
148
|
+
_validateConfig() {
|
|
149
|
+
const protocol = this.config.protocol;
|
|
150
|
+
if (!PROTOCOL_MODES.includes(protocol)) {
|
|
151
|
+
throw new Error(`${this._label()} unsupported protocol: ${protocol}`);
|
|
152
|
+
}
|
|
153
|
+
const framing = this.config.framing;
|
|
154
|
+
if (!FRAMINGS.includes(framing)) {
|
|
155
|
+
throw new Error(`${this._label()} unsupported framing: ${framing}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 启动钩子:按 protocol 逐个创建底层服务器,任一失败则全部回滚
|
|
81
161
|
* @returns {Promise<void>} 无返回值
|
|
82
162
|
*/
|
|
83
163
|
async onStart() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
164
|
+
try {
|
|
165
|
+
for (const protocol of this._enabledProtocols()) {
|
|
166
|
+
await this._startServer(protocol);
|
|
167
|
+
}
|
|
168
|
+
} catch (err) {
|
|
169
|
+
await this._rollbackStart();
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 列出本次需要监听的协议
|
|
176
|
+
* @returns {Array<string>} 协议名列表
|
|
177
|
+
*/
|
|
178
|
+
_enabledProtocols() {
|
|
179
|
+
const protocol = this.config.protocol;
|
|
180
|
+
if (protocol === 'both') return PROTOCOLS.slice();
|
|
181
|
+
return [protocol];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 按协议启动对应的底层服务器
|
|
186
|
+
* @param {string} protocol 协议名,tcp 或 udp
|
|
187
|
+
* @returns {Promise<void>} 无返回值
|
|
188
|
+
*/
|
|
189
|
+
async _startServer(protocol) {
|
|
190
|
+
if (protocol === 'udp') return this._startUdp();
|
|
191
|
+
return this._startTcp();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 启动失败时回滚已创建的资源
|
|
196
|
+
* @returns {Promise<void>} 无返回值
|
|
197
|
+
*/
|
|
198
|
+
async _rollbackStart() {
|
|
199
|
+
try {
|
|
200
|
+
this._closeAllClients();
|
|
201
|
+
await this._closeServers();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
this._emitError(err);
|
|
87
204
|
}
|
|
88
|
-
await this._startTcp();
|
|
89
205
|
}
|
|
90
206
|
|
|
91
207
|
/**
|
|
@@ -94,7 +210,7 @@ class SocketServer extends Mod {
|
|
|
94
210
|
*/
|
|
95
211
|
async onStop() {
|
|
96
212
|
this._closeAllClients();
|
|
97
|
-
await this.
|
|
213
|
+
await this._closeServers();
|
|
98
214
|
}
|
|
99
215
|
|
|
100
216
|
// ---------- 执行入口 ----------
|
|
@@ -104,33 +220,35 @@ class SocketServer extends Mod {
|
|
|
104
220
|
* @param {Object} ctx 执行上下文
|
|
105
221
|
* @param {Object} options 合并后的选项
|
|
106
222
|
* @returns {Promise<Object>} 操作结果
|
|
223
|
+
* @throws {Error} action 不受支持时
|
|
107
224
|
*/
|
|
108
225
|
async main(params, ctx, options) {
|
|
109
226
|
const action = params.action;
|
|
110
227
|
if (action === 'send') return this._actionSend(params);
|
|
111
228
|
if (action === 'broadcast') return this._actionBroadcast(params);
|
|
112
229
|
if (action === 'close') return this._actionClose(params);
|
|
113
|
-
return this._actionList();
|
|
230
|
+
if (action === 'list') return this._actionList();
|
|
231
|
+
throw new Error(`${this._label()} unsupported action: ${action}`);
|
|
114
232
|
}
|
|
115
233
|
|
|
116
234
|
/**
|
|
117
235
|
* 执行发送操作
|
|
118
|
-
* @param {Object} params 调用参数,含 client_id 与
|
|
236
|
+
* @param {Object} params 调用参数,含 client_id、data 与 topic
|
|
119
237
|
* @returns {Object} 操作结果
|
|
120
238
|
*/
|
|
121
239
|
_actionSend(params) {
|
|
122
240
|
this._requireClientId(params);
|
|
123
|
-
const success = this.send(params.client_id, params.data);
|
|
241
|
+
const success = this.send(params.client_id, params.data, params.topic);
|
|
124
242
|
return { action: 'send', client_id: params.client_id, success };
|
|
125
243
|
}
|
|
126
244
|
|
|
127
245
|
/**
|
|
128
246
|
* 执行广播操作
|
|
129
|
-
* @param {Object} params 调用参数,含 data
|
|
247
|
+
* @param {Object} params 调用参数,含 data 与 topic
|
|
130
248
|
* @returns {Object} 操作结果,count 为送达客户端数
|
|
131
249
|
*/
|
|
132
250
|
_actionBroadcast(params) {
|
|
133
|
-
const count = this.broadcast(params.data);
|
|
251
|
+
const count = this.broadcast(params.data, params.topic);
|
|
134
252
|
return { action: 'broadcast', count };
|
|
135
253
|
}
|
|
136
254
|
|
|
@@ -178,33 +296,47 @@ class SocketServer extends Mod {
|
|
|
178
296
|
|
|
179
297
|
/**
|
|
180
298
|
* 获取实际监听地址
|
|
181
|
-
* @
|
|
299
|
+
* @param {string} [protocol] 协议名,省略时取首个已启用协议
|
|
300
|
+
* @returns {Object|null} 监听地址信息,未监听时为 null
|
|
182
301
|
*/
|
|
183
|
-
getAddress() {
|
|
184
|
-
const
|
|
302
|
+
getAddress(protocol) {
|
|
303
|
+
const key = protocol || this._enabledProtocols()[0];
|
|
304
|
+
const server = this.#servers[key];
|
|
185
305
|
return server ? server.address() : null;
|
|
186
306
|
}
|
|
187
307
|
|
|
308
|
+
/**
|
|
309
|
+
* 获取各协议的监听地址
|
|
310
|
+
* @returns {Object} 形如 { tcp, udp } 的地址表,未监听为 null
|
|
311
|
+
*/
|
|
312
|
+
getAddresses() {
|
|
313
|
+
return { tcp: this.getAddress('tcp'), udp: this.getAddress('udp') };
|
|
314
|
+
}
|
|
315
|
+
|
|
188
316
|
// ---------- 公开操作 ----------
|
|
189
317
|
/**
|
|
190
318
|
* 向指定客户端发送数据
|
|
191
319
|
* @param {string} client_id 客户端 ID
|
|
192
|
-
* @param {string|Object} data 待发送数据
|
|
320
|
+
* @param {string|Object|Buffer} data 待发送数据
|
|
321
|
+
* @param {string} [topic] 目标主题,framing 非 none 时必填
|
|
193
322
|
* @returns {boolean} 是否发送成功
|
|
323
|
+
* @throws {Error} framing 非 none 且缺少 topic 时
|
|
194
324
|
*/
|
|
195
|
-
send(client_id, data) {
|
|
325
|
+
send(client_id, data, topic) {
|
|
196
326
|
const client = this.#clients.get(client_id);
|
|
197
327
|
if (!client) return false;
|
|
198
|
-
return this._sendToClient(client, this.
|
|
328
|
+
return this._sendToClient(client, this._encodeFrame(topic, data));
|
|
199
329
|
}
|
|
200
330
|
|
|
201
331
|
/**
|
|
202
332
|
* 向全部在线客户端广播数据
|
|
203
|
-
* @param {string|Object} data 待广播数据
|
|
333
|
+
* @param {string|Object|Buffer} data 待广播数据
|
|
334
|
+
* @param {string} [topic] 目标主题,framing 非 none 时必填
|
|
204
335
|
* @returns {number} 送达的客户端数量
|
|
336
|
+
* @throws {Error} framing 非 none 且缺少 topic 时
|
|
205
337
|
*/
|
|
206
|
-
broadcast(data) {
|
|
207
|
-
const payload = this.
|
|
338
|
+
broadcast(data, topic) {
|
|
339
|
+
const payload = this._encodeFrame(topic, data);
|
|
208
340
|
let count = 0;
|
|
209
341
|
for (const client of this.#clients.values()) {
|
|
210
342
|
if (this._sendToClient(client, payload)) count += 1;
|
|
@@ -276,7 +408,7 @@ class SocketServer extends Mod {
|
|
|
276
408
|
*/
|
|
277
409
|
async _startTcp() {
|
|
278
410
|
const server = net.createServer((socket) => this._handleConnection(socket));
|
|
279
|
-
this.#
|
|
411
|
+
this.#servers.tcp = server;
|
|
280
412
|
await this._listenTcp(server);
|
|
281
413
|
server.on('error', (err) => this._emitError(err));
|
|
282
414
|
}
|
|
@@ -311,7 +443,9 @@ class SocketServer extends Mod {
|
|
|
311
443
|
address: this._remoteAddress(socket),
|
|
312
444
|
socket,
|
|
313
445
|
rinfo: null,
|
|
314
|
-
options: null
|
|
446
|
+
options: null,
|
|
447
|
+
buffer: Buffer.alloc(0),
|
|
448
|
+
queue: Promise.resolve()
|
|
315
449
|
};
|
|
316
450
|
this.#clients.set(client_id, client);
|
|
317
451
|
socket.on('data', (buf) => this._handleData(client_id, buf));
|
|
@@ -320,32 +454,285 @@ class SocketServer extends Mod {
|
|
|
320
454
|
}
|
|
321
455
|
|
|
322
456
|
/**
|
|
323
|
-
*
|
|
457
|
+
* 接收 TCP 数据并排入该连接的串行队列
|
|
324
458
|
* @param {string} client_id 客户端 ID
|
|
325
459
|
* @param {Object} buf 数据缓冲区
|
|
326
|
-
* @returns {
|
|
460
|
+
* @returns {void}
|
|
327
461
|
*/
|
|
328
|
-
|
|
462
|
+
_handleData(client_id, buf) {
|
|
329
463
|
const client = this.#clients.get(client_id);
|
|
330
464
|
if (!client) return;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
465
|
+
this._enqueue(client, () => this._processTcpData(client, buf));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* 把任务追加到连接的串行队列,保证同一连接的消息顺序执行
|
|
470
|
+
* @param {Object} client 连接信息
|
|
471
|
+
* @param {Function} task 待执行任务
|
|
472
|
+
* @returns {void}
|
|
473
|
+
*/
|
|
474
|
+
_enqueue(client, task) {
|
|
475
|
+
client.queue = client.queue
|
|
476
|
+
.then(() => task())
|
|
477
|
+
.catch((err) => this._emitError(err));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 处理 TCP 数据:数据是字节流,需按帧格式累积切分
|
|
482
|
+
* @param {Object} client 连接信息
|
|
483
|
+
* @param {Object} buf 数据缓冲区
|
|
484
|
+
* @returns {Promise<void>} 无返回值
|
|
485
|
+
*/
|
|
486
|
+
async _processTcpData(client, buf) {
|
|
487
|
+
await this._processInbound(client, buf, false);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ---------- 分帧与派发 ----------
|
|
491
|
+
/**
|
|
492
|
+
* 统一入站处理:按 framing 还原帧,逐帧派发给事件管理器
|
|
493
|
+
* @param {Object} client 连接信息
|
|
494
|
+
* @param {Object} buf 本次收到的数据
|
|
495
|
+
* @param {boolean} is_complete 数据是否自带边界(UDP 为 true,TCP 为 false)
|
|
496
|
+
* @returns {Promise<void>} 无返回值
|
|
497
|
+
*/
|
|
498
|
+
async _processInbound(client, buf, is_complete) {
|
|
499
|
+
if (this.config.framing === 'none') {
|
|
500
|
+
await this._dispatchMessage(client, client.protocol, this._decodeData(buf));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const buffer = is_complete ? buf : Buffer.concat([client.buffer, buf]);
|
|
504
|
+
const { frames, rest } = this._extractFrames(buffer);
|
|
505
|
+
if (!is_complete) client.buffer = Buffer.from(rest);
|
|
506
|
+
for (const frame of frames) {
|
|
507
|
+
await this._dispatchMessage(client, frame.topic, this._decodeData(frame.payload));
|
|
508
|
+
}
|
|
509
|
+
if (is_complete && rest.length > 0) {
|
|
510
|
+
this._emitError(new Error(`${this._label()} invalid frame: truncated`));
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 按当前帧格式切分缓冲区
|
|
516
|
+
* @param {Object} buffer 数据缓冲区
|
|
517
|
+
* @returns {Object} 形如 { frames, rest } 的结果,rest 为未成帧的剩余字节
|
|
518
|
+
*/
|
|
519
|
+
_extractFrames(buffer) {
|
|
520
|
+
if (this.config.framing === 'length') return this._extractLengthFrames(buffer);
|
|
521
|
+
return this._extractLineFrames(buffer);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 按换行切分帧
|
|
526
|
+
* @param {Object} buffer 数据缓冲区
|
|
527
|
+
* @returns {Object} 形如 { frames, rest } 的结果
|
|
528
|
+
*/
|
|
529
|
+
_extractLineFrames(buffer) {
|
|
530
|
+
const frames = [];
|
|
531
|
+
let start = 0;
|
|
532
|
+
let index = buffer.indexOf(LINE_FEED, start);
|
|
533
|
+
while (index !== -1) {
|
|
534
|
+
let end = index;
|
|
535
|
+
if (end > start && buffer[end - 1] === CARRIAGE_RETURN) end -= 1;
|
|
536
|
+
this._appendLineFrame(frames, buffer.subarray(start, end));
|
|
537
|
+
start = index + 1;
|
|
538
|
+
index = buffer.indexOf(LINE_FEED, start);
|
|
539
|
+
}
|
|
540
|
+
return { frames, rest: buffer.subarray(start) };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* 解析单行帧并追加到结果集,缺少 topic 时丢弃并上报错误
|
|
545
|
+
* @param {Array<Object>} frames 帧结果集
|
|
546
|
+
* @param {Object} line 行数据
|
|
547
|
+
* @returns {void}
|
|
548
|
+
*/
|
|
549
|
+
_appendLineFrame(frames, line) {
|
|
550
|
+
if (line.length === 0) return;
|
|
551
|
+
const frame = this._splitLineFrame(line);
|
|
552
|
+
if (!frame) {
|
|
553
|
+
this._emitError(new Error(`${this._label()} invalid frame: missing topic`));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
frames.push(frame);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 把一行拆成 topic 与 payload
|
|
561
|
+
* @param {Object} line 行数据
|
|
562
|
+
* @returns {Object|null} 形如 { topic, payload } 的帧,缺少 topic 时为 null
|
|
563
|
+
*/
|
|
564
|
+
_splitLineFrame(line) {
|
|
565
|
+
const index = line.indexOf(SPACE);
|
|
566
|
+
if (index <= 0) return null;
|
|
567
|
+
const topic = line.subarray(0, index).toString('utf8');
|
|
568
|
+
return { topic, payload: line.subarray(index + 1) };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* 按长度前缀切分帧
|
|
573
|
+
* @param {Object} buffer 数据缓冲区
|
|
574
|
+
* @returns {Object} 形如 { frames, rest } 的结果
|
|
575
|
+
*/
|
|
576
|
+
_extractLengthFrames(buffer) {
|
|
577
|
+
const frames = [];
|
|
578
|
+
let offset = 0;
|
|
579
|
+
while (buffer.length - offset >= LENGTH_HEADER_SIZE) {
|
|
580
|
+
const topic_length = buffer.readUInt16BE(offset);
|
|
581
|
+
const payload_length = buffer.readUInt32BE(offset + TOPIC_LENGTH_SIZE);
|
|
582
|
+
const total = LENGTH_HEADER_SIZE + topic_length + payload_length;
|
|
583
|
+
if (buffer.length - offset < total) break;
|
|
584
|
+
const topic_start = offset + LENGTH_HEADER_SIZE;
|
|
585
|
+
const payload_start = topic_start + topic_length;
|
|
586
|
+
const topic = buffer.subarray(topic_start, payload_start).toString('utf8');
|
|
587
|
+
const payload = buffer.subarray(payload_start, offset + total);
|
|
588
|
+
if (topic_length === 0) {
|
|
589
|
+
this._emitError(new Error(`${this._label()} invalid frame: missing topic`));
|
|
590
|
+
} else {
|
|
591
|
+
frames.push({ topic, payload });
|
|
592
|
+
}
|
|
593
|
+
offset += total;
|
|
594
|
+
}
|
|
595
|
+
return { frames, rest: buffer.subarray(offset) };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* 把入站帧派发给事件管理器,以 topic 作为 target
|
|
600
|
+
* @param {Object} client 连接信息
|
|
601
|
+
* @param {string} topic 帧主题
|
|
602
|
+
* @param {string|Object} data 解码后的数据
|
|
603
|
+
* @returns {Promise<void>} 无返回值
|
|
604
|
+
*/
|
|
605
|
+
async _dispatchMessage(client, topic, data) {
|
|
606
|
+
const params = this._buildParams(client, topic, data);
|
|
607
|
+
const ctx = this._buildContext(client, topic);
|
|
338
608
|
const options = this._clientOptions(client);
|
|
339
609
|
try {
|
|
340
|
-
const result = await this.#events.run(
|
|
610
|
+
const result = await this.#events.run(topic, params, ctx, options);
|
|
341
611
|
const error = ctx.error || options.error;
|
|
342
612
|
if (error) this._emitError(error);
|
|
343
|
-
|
|
613
|
+
this._reply(client, topic, result);
|
|
344
614
|
} catch (err) {
|
|
345
615
|
this._emitError(err);
|
|
346
616
|
}
|
|
347
617
|
}
|
|
348
618
|
|
|
619
|
+
/**
|
|
620
|
+
* 构建事件执行参数
|
|
621
|
+
* @param {Object} client 连接信息
|
|
622
|
+
* @param {string} topic 帧主题
|
|
623
|
+
* @param {string|Object} data 解码后的数据
|
|
624
|
+
* @returns {Object} 参数对象
|
|
625
|
+
*/
|
|
626
|
+
_buildParams(client, topic, data) {
|
|
627
|
+
return { client_id: client.id, topic, data };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* 构建事件执行上下文
|
|
632
|
+
* @param {Object} client 连接信息
|
|
633
|
+
* @param {string} topic 帧主题
|
|
634
|
+
* @returns {Object} 上下文对象
|
|
635
|
+
*/
|
|
636
|
+
_buildContext(client, topic) {
|
|
637
|
+
const ctx = {
|
|
638
|
+
client_id: client.id,
|
|
639
|
+
topic,
|
|
640
|
+
protocol: client.protocol,
|
|
641
|
+
address: client.address,
|
|
642
|
+
scope: this.config.scope
|
|
643
|
+
};
|
|
644
|
+
if (client.rinfo) ctx.rinfo = client.rinfo;
|
|
645
|
+
if (client.socket) ctx.socket = client.socket;
|
|
646
|
+
return ctx;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* 按事件返回值回发数据:{ topic, data } 视为重发描述,其余沿用来源 topic
|
|
651
|
+
* @param {Object} client 连接信息
|
|
652
|
+
* @param {string} topic 来源主题
|
|
653
|
+
* @param {*} result 事件返回值
|
|
654
|
+
* @returns {void}
|
|
655
|
+
*/
|
|
656
|
+
_reply(client, topic, result) {
|
|
657
|
+
if (result === undefined || result === null) return;
|
|
658
|
+
if (this._isReplyDescriptor(result)) {
|
|
659
|
+
this.send(client.id, result.data, result.topic);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
this.send(client.id, result, topic);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* 判断事件返回值是否为重发描述
|
|
667
|
+
* @param {*} result 事件返回值
|
|
668
|
+
* @returns {boolean} 是否为形如 { topic, data } 的描述
|
|
669
|
+
*/
|
|
670
|
+
_isReplyDescriptor(result) {
|
|
671
|
+
if (!result || typeof result !== 'object' || Buffer.isBuffer(result)) return false;
|
|
672
|
+
if (typeof result.topic !== 'string' || result.topic.length === 0) return false;
|
|
673
|
+
return Object.prototype.hasOwnProperty.call(result, 'data');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ---------- 建帧与编码 ----------
|
|
677
|
+
/**
|
|
678
|
+
* 按当前帧格式把 topic 与数据打包成可发送的缓冲区
|
|
679
|
+
* @param {string} topic 目标主题,framing 非 none 时必填
|
|
680
|
+
* @param {string|Object|Buffer} data 待发送数据
|
|
681
|
+
* @returns {Object} 待发送缓冲区
|
|
682
|
+
* @throws {Error} framing 非 none 且缺少 topic 时
|
|
683
|
+
*/
|
|
684
|
+
_encodeFrame(topic, data) {
|
|
685
|
+
const payload = this._encodePayload(data);
|
|
686
|
+
if (this.config.framing === 'none') return payload;
|
|
687
|
+
this._requireTopic(topic);
|
|
688
|
+
if (this.config.framing === 'length') return this._buildLengthFrame(topic, payload);
|
|
689
|
+
return this._buildLineFrame(topic, payload);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* 把数据编码成缓冲区,Buffer 原样返回
|
|
694
|
+
* @param {string|Object|Buffer} data 待编码数据
|
|
695
|
+
* @returns {Object} 数据缓冲区
|
|
696
|
+
*/
|
|
697
|
+
_encodePayload(data) {
|
|
698
|
+
if (Buffer.isBuffer(data)) return data;
|
|
699
|
+
return Buffer.from(this._encodeData(data), this.config.encoding);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* 构建行帧:`topic<空格>payload\n`
|
|
704
|
+
* @param {string} topic 主题
|
|
705
|
+
* @param {Object} payload 数据缓冲区
|
|
706
|
+
* @returns {Object} 帧缓冲区
|
|
707
|
+
* @throws {Error} payload 含换行符时(行帧无法表达)
|
|
708
|
+
*/
|
|
709
|
+
_buildLineFrame(topic, payload) {
|
|
710
|
+
if (payload.indexOf(LINE_FEED) !== -1) {
|
|
711
|
+
throw new Error(`${this._label()} line framing payload cannot contain newline`);
|
|
712
|
+
}
|
|
713
|
+
const head = Buffer.from(`${topic} `, 'utf8');
|
|
714
|
+
const tail = Buffer.from([LINE_FEED]);
|
|
715
|
+
return Buffer.concat([head, payload, tail]);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 构建长度前缀帧:`[2字节 topic 长度][4字节 payload 长度][topic][payload]`
|
|
720
|
+
* @param {string} topic 主题
|
|
721
|
+
* @param {Object} payload 数据缓冲区
|
|
722
|
+
* @returns {Object} 帧缓冲区
|
|
723
|
+
* @throws {Error} topic 超出 65535 字节时
|
|
724
|
+
*/
|
|
725
|
+
_buildLengthFrame(topic, payload) {
|
|
726
|
+
const topic_buf = Buffer.from(topic, 'utf8');
|
|
727
|
+
if (topic_buf.length > 0xffff) {
|
|
728
|
+
throw new Error(`${this._label()} topic too long`);
|
|
729
|
+
}
|
|
730
|
+
const header = Buffer.alloc(LENGTH_HEADER_SIZE);
|
|
731
|
+
header.writeUInt16BE(topic_buf.length, 0);
|
|
732
|
+
header.writeUInt32BE(payload.length, TOPIC_LENGTH_SIZE);
|
|
733
|
+
return Buffer.concat([header, topic_buf, payload]);
|
|
734
|
+
}
|
|
735
|
+
|
|
349
736
|
/**
|
|
350
737
|
* 获取远端地址字符串
|
|
351
738
|
* @param {Object} socket 套接字实例
|
|
@@ -362,10 +749,11 @@ class SocketServer extends Mod {
|
|
|
362
749
|
*/
|
|
363
750
|
async _startUdp() {
|
|
364
751
|
const socket = dgram.createSocket('udp4');
|
|
365
|
-
this.#
|
|
752
|
+
this.#servers.udp = socket;
|
|
366
753
|
await this._bindUdp(socket);
|
|
367
754
|
socket.on('message', (buf, rinfo) => this._handleUdpMessage(buf, rinfo));
|
|
368
755
|
socket.on('error', (err) => this._emitError(err));
|
|
756
|
+
this._startUdpTimer();
|
|
369
757
|
}
|
|
370
758
|
|
|
371
759
|
/**
|
|
@@ -386,52 +774,88 @@ class SocketServer extends Mod {
|
|
|
386
774
|
}
|
|
387
775
|
|
|
388
776
|
/**
|
|
389
|
-
* 处理 UDP
|
|
777
|
+
* 处理 UDP 报文:首见远端地址登记为客户端,之后排入该客户端的串行队列
|
|
390
778
|
* @param {Object} buf 数据缓冲区
|
|
391
779
|
* @param {Object} rinfo 远端地址信息
|
|
392
|
-
* @returns {
|
|
780
|
+
* @returns {void}
|
|
393
781
|
*/
|
|
394
|
-
|
|
782
|
+
_handleUdpMessage(buf, rinfo) {
|
|
395
783
|
const client = this._ensureUdpClient(rinfo);
|
|
396
|
-
|
|
397
|
-
const ctx = {
|
|
398
|
-
client_id: client.id,
|
|
399
|
-
protocol: client.protocol,
|
|
400
|
-
address: client.address,
|
|
401
|
-
rinfo
|
|
402
|
-
};
|
|
403
|
-
const options = this._clientOptions(client);
|
|
404
|
-
try {
|
|
405
|
-
const result = await this.#events.run(this.config.protocol, params, ctx, options);
|
|
406
|
-
const error = ctx.error || options.error;
|
|
407
|
-
if (error) this._emitError(error);
|
|
408
|
-
if (result !== undefined && result !== null) this.send(client.id, result);
|
|
409
|
-
} catch (err) {
|
|
410
|
-
this._emitError(err);
|
|
411
|
-
}
|
|
784
|
+
this._enqueue(client, () => this._processUdpData(client, buf));
|
|
412
785
|
}
|
|
413
786
|
|
|
414
787
|
/**
|
|
415
|
-
*
|
|
788
|
+
* 处理 UDP 报文:数据报自带边界,按帧格式解析后派发
|
|
789
|
+
* @param {Object} client 连接信息
|
|
790
|
+
* @param {Object} buf 数据缓冲区
|
|
791
|
+
* @returns {Promise<void>} 无返回值
|
|
792
|
+
*/
|
|
793
|
+
async _processUdpData(client, buf) {
|
|
794
|
+
await this._processInbound(client, buf, true);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* 登记 UDP 客户端(同一远端地址复用同一 client_id),并刷新活跃时间
|
|
416
799
|
* @param {Object} rinfo 远端地址信息
|
|
417
800
|
* @returns {Object} 客户端信息
|
|
418
801
|
*/
|
|
419
802
|
_ensureUdpClient(rinfo) {
|
|
420
803
|
const client_id = `udp_${rinfo.address}:${rinfo.port}`;
|
|
421
804
|
const existed = this.#clients.get(client_id);
|
|
422
|
-
if (existed)
|
|
805
|
+
if (existed) {
|
|
806
|
+
existed.last_seen = Date.now();
|
|
807
|
+
return existed;
|
|
808
|
+
}
|
|
423
809
|
const client = {
|
|
424
810
|
id: client_id,
|
|
425
811
|
protocol: 'udp',
|
|
426
812
|
address: client_id,
|
|
427
813
|
socket: null,
|
|
428
814
|
rinfo,
|
|
429
|
-
options: null
|
|
815
|
+
options: null,
|
|
816
|
+
last_seen: Date.now(),
|
|
817
|
+
queue: Promise.resolve()
|
|
430
818
|
};
|
|
431
819
|
this.#clients.set(client_id, client);
|
|
432
820
|
return client;
|
|
433
821
|
}
|
|
434
822
|
|
|
823
|
+
/**
|
|
824
|
+
* 启动 UDP 过期连接清理定时器,udp_ttl <= 0 时关闭清理
|
|
825
|
+
* @returns {void}
|
|
826
|
+
*/
|
|
827
|
+
_startUdpTimer() {
|
|
828
|
+
const ttl = this.config.udp_ttl;
|
|
829
|
+
if (!ttl || ttl <= 0) return;
|
|
830
|
+
const interval = Math.max(1000, Math.min(ttl, 60000));
|
|
831
|
+
this.#udp_timer = setInterval(() => this._pruneUdpClients(), interval);
|
|
832
|
+
if (typeof this.#udp_timer.unref === 'function') this.#udp_timer.unref();
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* 清理超过 udp_ttl 未活动的 UDP 客户端
|
|
837
|
+
* @returns {void}
|
|
838
|
+
*/
|
|
839
|
+
_pruneUdpClients() {
|
|
840
|
+
const ttl = this.config.udp_ttl;
|
|
841
|
+
if (!ttl || ttl <= 0) return;
|
|
842
|
+
const deadline = Date.now() - ttl;
|
|
843
|
+
for (const [client_id, client] of this.#clients) {
|
|
844
|
+
if (client.protocol !== 'udp') continue;
|
|
845
|
+
if (client.last_seen <= deadline) this.#clients.delete(client_id);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* 停止 UDP 过期连接清理定时器
|
|
851
|
+
* @returns {void}
|
|
852
|
+
*/
|
|
853
|
+
_clearUdpTimer() {
|
|
854
|
+
if (!this.#udp_timer) return;
|
|
855
|
+
clearInterval(this.#udp_timer);
|
|
856
|
+
this.#udp_timer = null;
|
|
857
|
+
}
|
|
858
|
+
|
|
435
859
|
// ---------- 数据编解码 ----------
|
|
436
860
|
/**
|
|
437
861
|
* 编码待发送数据
|
|
@@ -491,7 +915,7 @@ class SocketServer extends Mod {
|
|
|
491
915
|
* @returns {boolean} 是否发送成功
|
|
492
916
|
*/
|
|
493
917
|
_sendUdp(client, payload) {
|
|
494
|
-
const socket = this.#
|
|
918
|
+
const socket = this.#servers.udp;
|
|
495
919
|
if (!socket || !client.rinfo) return false;
|
|
496
920
|
socket.send(payload, client.rinfo.port, client.rinfo.address);
|
|
497
921
|
return true;
|
|
@@ -508,19 +932,46 @@ class SocketServer extends Mod {
|
|
|
508
932
|
}
|
|
509
933
|
|
|
510
934
|
/**
|
|
511
|
-
*
|
|
935
|
+
* 关闭全部底层服务器并停止 UDP 清理定时器
|
|
512
936
|
* @returns {Promise<void>} 无返回值
|
|
513
937
|
*/
|
|
514
|
-
|
|
515
|
-
|
|
938
|
+
async _closeServers() {
|
|
939
|
+
this._clearUdpTimer();
|
|
940
|
+
const servers = this.#servers;
|
|
941
|
+
this.#servers = { tcp: null, udp: null };
|
|
942
|
+
await this._closeOne(servers.tcp);
|
|
943
|
+
await this._closeOne(servers.udp);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* 关闭单个底层服务器
|
|
948
|
+
* @param {Object|null} server net.Server 或 dgram.Socket 实例
|
|
949
|
+
* @returns {Promise<void>} 无返回值
|
|
950
|
+
*/
|
|
951
|
+
_closeOne(server) {
|
|
516
952
|
if (!server) return Promise.resolve();
|
|
517
|
-
this.#server = null;
|
|
518
953
|
return new Promise((resolve) => {
|
|
519
|
-
|
|
954
|
+
try {
|
|
955
|
+
server.close(() => resolve());
|
|
956
|
+
} catch {
|
|
957
|
+
resolve();
|
|
958
|
+
}
|
|
520
959
|
});
|
|
521
960
|
}
|
|
522
961
|
|
|
523
962
|
// ---------- 校验辅助 ----------
|
|
963
|
+
/**
|
|
964
|
+
* 断言 topic 非空(framing 非 none 时帧必须携带 topic)
|
|
965
|
+
* @param {string} topic 目标主题
|
|
966
|
+
* @returns {void}
|
|
967
|
+
* @throws {Error} topic 为空时
|
|
968
|
+
*/
|
|
969
|
+
_requireTopic(topic) {
|
|
970
|
+
if (!topic) {
|
|
971
|
+
throw new Error(`${this._label()} topic required when framing is ${this.config.framing}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
524
975
|
/**
|
|
525
976
|
* 断言调用参数含 client_id
|
|
526
977
|
* @param {Object} params 调用参数
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mm_socket_server",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "最薄的 TCP/UDP
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "最薄的 TCP/UDP 套接字服务器入站适配器:按 topic 分帧后直达 mm_event 事件管理器,不做路由与插件。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"超级美眉",
|
|
7
7
|
"入站适配器",
|