mm_socket_server 1.0.2 → 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 +84 -96
- package/example/demo.js +63 -0
- package/index.js +2 -4
- package/lib/socket.js +607 -184
- package/package.json +8 -8
- package/lib/message.js +0 -150
package/README.md
CHANGED
|
@@ -1,155 +1,143 @@
|
|
|
1
1
|
# mm_socket_server
|
|
2
2
|
|
|
3
|
-
TCP/UDP
|
|
3
|
+
最薄的 TCP/UDP 套接字服务器入站适配器。继承 `mm_mod` 的 `Mod`,把按 topic 分帧后的报文以 topic 为 `target` 交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。`protocol` 可选 `tcp`、`udp` 或 `both`(同端口同时监听)。
|
|
4
4
|
|
|
5
5
|
- 入口:`index.js`(CommonJS)
|
|
6
|
-
- 导出:`SocketServer
|
|
7
|
-
- 运行时依赖:`mm_corejs`
|
|
8
|
-
- 测试覆盖率:100%(`jest.config.js` 四项阈值)
|
|
6
|
+
- 导出:`SocketServer`、`admin`、`socketServerAdmin`
|
|
7
|
+
- 运行时依赖:`mm_mod`、`mm_event`、`mm_corejs`
|
|
9
8
|
|
|
10
|
-
##
|
|
9
|
+
## 消息路径
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
```
|
|
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
|
+
→ 结果非空时按同一帧格式回发来源客户端
|
|
16
|
+
```
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
事件 `target` 取帧内的 topic(如 `chat/1`),因此可按主题精细分发,也支持 `mm_event` 的 `*` 通配(前缀、后缀、包含)。`params` 为 `{ client_id, topic, data }`,`data` 在 `json: true` 时尝试 `JSON.parse`(失败回落原文)。
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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`,保持旧行为 |
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
topic 一律按 utf8 编解码,`encoding` 只作用于 payload。缺少 topic 的帧会被丢弃并通过 `error` 事件上报 `invalid frame: missing topic`;UDP 长度帧不完整时上报 `invalid frame: truncated`。
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
> **TCP 是字节流,没有天然消息边界。** 模块按 `framing` 做连接级缓冲与拼包/拆包,因此 `line` / `length` 下可安全处理粘包与拆包;UDP 保留数据报边界,一次报文即一帧。
|
|
31
|
+
|
|
32
|
+
同一连接的消息按到达顺序串行执行:事件处理函数返回 Promise 时,后一条消息会等前一条处理完再开始,避免响应乱序。
|
|
29
33
|
|
|
30
34
|
## 快速开始
|
|
31
35
|
|
|
32
36
|
```js
|
|
33
37
|
'use strict';
|
|
34
38
|
|
|
35
|
-
const
|
|
36
|
-
const { SocketServer, MessagePlugin } = require('mm_socket_server');
|
|
37
|
-
|
|
38
|
-
/** 最小 Server 容器:满足 Base 契约即可(见 example/demo.js 完整实现) */
|
|
39
|
-
const server = {
|
|
40
|
-
services: new Map(),
|
|
41
|
-
getService(name) { return this.services.get(name); },
|
|
42
|
-
hasService(name) { return this.services.has(name); },
|
|
43
|
-
setService(name, instance) { this.services.set(name, instance); return this; },
|
|
44
|
-
getManager() { return undefined; },
|
|
45
|
-
hasManager() { return false; }
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const socket = new SocketServer({ name: 'socket', host: '127.0.0.1', port: 9100 }, server);
|
|
39
|
+
const { SocketServer } = require('mm_socket_server');
|
|
49
40
|
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
const socket = new SocketServer({ name: 'socket', protocol: 'both', host: '127.0.0.1', port: 9000 });
|
|
42
|
+
const events = socket.getEventManager();
|
|
52
43
|
|
|
53
|
-
|
|
54
|
-
const rpc = new MessagePlugin();
|
|
55
|
-
socket.use(rpc);
|
|
56
|
-
rpc.addMethod('sum', (params) => params.reduce((a, b) => a + b, 0));
|
|
57
|
-
|
|
58
|
-
// 事件
|
|
59
|
-
socket.on('connection', (client) => socket.setClientOptions(client.id, { user_id: 'u1' }));
|
|
60
|
-
socket.on('message', (client_id, data) => console.log(client_id, data));
|
|
61
|
-
socket.on('close', (client_id) => console.log('断开', client_id));
|
|
62
|
-
socket.on('error', (err) => console.error(err.message));
|
|
44
|
+
events.add({ name: 'echo', target: 'chat/echo', end: true }).main = async (params) => params.data;
|
|
63
45
|
|
|
64
46
|
await socket.init();
|
|
65
47
|
await socket.start();
|
|
66
48
|
|
|
67
|
-
|
|
68
|
-
await socket.run({}, { action: 'list' });
|
|
69
|
-
await socket.run({}, { action: 'broadcast', data: 'hello all' });
|
|
70
|
-
|
|
71
|
-
await socket.stop();
|
|
72
|
-
await socket.dispose();
|
|
49
|
+
console.log(socket.getAddresses());
|
|
73
50
|
```
|
|
74
51
|
|
|
75
|
-
##
|
|
76
|
-
|
|
77
|
-
顶层 `config` 字段即服务器设置,`init` 阶段统一校验。
|
|
52
|
+
## 服务器配置
|
|
78
53
|
|
|
79
54
|
| 字段 | 类型 | 默认值 | 说明 |
|
|
80
55
|
| --- | --- | --- | --- |
|
|
81
|
-
| `name` | string | `'
|
|
82
|
-
| `
|
|
56
|
+
| `name` | string | `'os'` | 模块名称(必填) |
|
|
57
|
+
| `title` | string | `'Socket服务器模块'` | 模块标题 |
|
|
58
|
+
| `description` | string | — | 模块描述 |
|
|
59
|
+
| `version` | string | `'3.0.0'` | 版本号 |
|
|
60
|
+
| `protocol` | string | `'tcp'` | 监听协议,接受 `tcp` / `udp` / `both`,其他值在 `init()` 时抛错 |
|
|
61
|
+
| `framing` | string | `'line'` | 帧格式,接受 `line` / `length` / `none`,其他值在 `init()` 时抛错 |
|
|
83
62
|
| `host` | string | `'0.0.0.0'` | 监听地址 |
|
|
84
|
-
| `port` |
|
|
85
|
-
| `encoding` |
|
|
86
|
-
| `json` | boolean | `false` |
|
|
63
|
+
| `port` | number | `9000` | 监听端口 |
|
|
64
|
+
| `encoding` | string | `'utf8'` | payload 解码字符集(topic 始终 utf8) |
|
|
65
|
+
| `json` | boolean | `false` | 入站尝试 JSON 解析,出站对象自动序列化 |
|
|
66
|
+
| `scope` | string | `'socket'` | 事件管理器作用域,多实例可用不同 scope 隔离事件 |
|
|
67
|
+
| `udp_ttl` | number | `300000` | UDP 客户端空闲多久(毫秒)后移出连接表,`<= 0` 关闭清理 |
|
|
68
|
+
| `params` | object | `{ action }` | `main` 的参数约定(`action` / `client_id` / `topic` / `data`) |
|
|
87
69
|
|
|
88
70
|
## API
|
|
89
71
|
|
|
90
|
-
### SocketServer
|
|
91
|
-
|
|
92
72
|
| 成员 | 说明 |
|
|
93
73
|
| --- | --- |
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
74
|
+
| `init()` / `start()` / `stop()` / `dispose()` | `Mod` 生命周期模板方法 |
|
|
75
|
+
| `onInit()` / `onStart()` / `onStop()` / `onDispose()` | 生命周期钩子,子类可覆盖 |
|
|
76
|
+
| `getEventManager()` | 获取 `mm_event` 上 `scope` 作用域的事件管理器(默认 `socket`,其他实例共享同一作用域),用于注册事件 |
|
|
77
|
+
| `setEventManager(manager)` | 注入外部事件管理器,需提供 `run()` |
|
|
78
|
+
| `getAddress(protocol)` | 指定协议的实际监听地址,未启动为 `null`;省略 `protocol` 时取首个已启用协议 |
|
|
79
|
+
| `getAddresses()` | 返回 `{ tcp, udp }` 两个协议各自的监听地址 |
|
|
80
|
+
| `run(params, ctx, options)` | 主动操作入口,`params.action` 派发 `send` / `broadcast` / `close` / `list` |
|
|
81
|
+
| `send(client_id, data, topic)` | 向指定客户端发送数据,`framing` 非 `none` 时 `topic` 必填,返回是否成功 |
|
|
82
|
+
| `broadcast(data, topic)` | 向全部在线客户端广播,`framing` 非 `none` 时 `topic` 必填,返回送达数 |
|
|
83
|
+
| `closeClient(client_id)` | 关闭指定客户端(TCP 销毁套接字,UDP 仅移出连接表) |
|
|
84
|
+
| `listClients()` | 在线客户端列表 `{ id, protocol, address }` |
|
|
99
85
|
| `hasClient(client_id)` | 是否在线 |
|
|
100
|
-
| `setClientOptions(client_id, options)` |
|
|
101
|
-
| `use(plugin)` | 挂载插件(需实现 `bind(server)`) |
|
|
102
|
-
| `getHandle()` | 消息管道,注册 `before / check / main / render` 等钩子;钩子产出 `result` 自动回发 |
|
|
103
|
-
| `getAddress()` | 实际监听地址,未启动为 `null` |
|
|
86
|
+
| `setClientOptions(client_id, options)` | 绑定连接级选项,随入站消息作为 `options` 透传(`null` 清空) |
|
|
104
87
|
|
|
105
|
-
|
|
88
|
+
## 请求上下文(ctx)
|
|
106
89
|
|
|
107
|
-
|
|
|
108
|
-
| --- | --- |
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
90
|
+
| 字段 | 说明 |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `client_id` | 客户端编号,TCP 形如 `tcp_1`,UDP 形如 `udp_127.0.0.1:5000` |
|
|
93
|
+
| `topic` | 当前帧的 topic,即事件的 `target` |
|
|
94
|
+
| `protocol` | 当前协议,`tcp` 或 `udp` |
|
|
95
|
+
| `address` | 远端地址,形如 `ip:port` |
|
|
96
|
+
| `scope` | 当前实例的事件管理器作用域 |
|
|
97
|
+
| `socket` | TCP 连接(UDP 报文无此字段),事件可直接写出 |
|
|
98
|
+
| `rinfo` | UDP 远端地址信息(TCP 消息无此字段) |
|
|
113
99
|
|
|
114
|
-
|
|
100
|
+
## 事件处理函数
|
|
115
101
|
|
|
116
|
-
|
|
102
|
+
签名固定为 `(params, ctx, options)`:
|
|
117
103
|
|
|
118
|
-
```
|
|
119
|
-
|
|
104
|
+
```js
|
|
105
|
+
events.add({ name: 'echo', target: 'chat/echo', end: true }).main = async (params, ctx, options) => {
|
|
106
|
+
return params.data;
|
|
107
|
+
};
|
|
120
108
|
```
|
|
121
109
|
|
|
122
|
-
|
|
110
|
+
返回非 `undefined` / `null` 的结果会按 `framing` 与原 topic 编码后回发来源客户端;返回 `{ topic, data }` 则改发到指定 topic;返回 `undefined` 则不回发。事件也可直接用 `ctx.socket` 写出。
|
|
123
111
|
|
|
124
|
-
##
|
|
125
|
-
|
|
126
|
-
挂载后接管 `jsonrpc` 形态消息(非 RPC 消息透传给业务钩子):
|
|
112
|
+
## 主动操作
|
|
127
113
|
|
|
128
114
|
```js
|
|
129
|
-
|
|
130
|
-
socket.
|
|
131
|
-
|
|
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' } });
|
|
117
|
+
await socket.run({ action: 'close', client_id: 'tcp_1' });
|
|
118
|
+
await socket.run({ action: 'list' });
|
|
132
119
|
```
|
|
133
120
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
121
|
+
`action` 仅支持上述四种,其他值直接抛 `unsupported action` 错误。`framing` 非 `none` 时 `send` / `broadcast` 必须给出 `topic`。
|
|
122
|
+
|
|
123
|
+
## 生命周期
|
|
124
|
+
|
|
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` 即可。
|
|
140
128
|
|
|
141
|
-
|
|
129
|
+
UDP 没有连接概念,首次收到某远端地址的报文时登记为客户端,之后超过 `udp_ttl` 未再收到报文会被定时清理。
|
|
142
130
|
|
|
143
131
|
## 示例
|
|
144
132
|
|
|
145
133
|
```bash
|
|
146
|
-
npm run demo #
|
|
134
|
+
npm run demo # 启动演示服务器(127.0.0.1:9000),见 example/demo.js
|
|
147
135
|
```
|
|
148
136
|
|
|
149
137
|
## 测试
|
|
150
138
|
|
|
151
139
|
```bash
|
|
152
|
-
npm test #
|
|
140
|
+
npm test # node --test
|
|
153
141
|
```
|
|
154
142
|
|
|
155
143
|
## 许可证
|
package/example/demo.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SocketServer } = require('..');
|
|
4
|
+
|
|
5
|
+
/** 演示监听端口 */
|
|
6
|
+
const DEMO_PORT = 9000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 注册演示事件:帧内 topic 即事件 target
|
|
10
|
+
* @param {Object} events 事件管理器
|
|
11
|
+
* @returns {void}
|
|
12
|
+
*/
|
|
13
|
+
function bindEvents(events) {
|
|
14
|
+
events.add({ name: 'echo', target: 'chat/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: 'chat/time', end: true }).main = async () => ({
|
|
21
|
+
time: new Date().toISOString()
|
|
22
|
+
});
|
|
23
|
+
}
|
|
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
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 演示入口:启动服务器并打印监听地址
|
|
40
|
+
* @returns {Promise<void>} 无返回值
|
|
41
|
+
*/
|
|
42
|
+
async function main() {
|
|
43
|
+
const socket = new SocketServer({
|
|
44
|
+
name: 'demo',
|
|
45
|
+
host: '127.0.0.1',
|
|
46
|
+
port: DEMO_PORT,
|
|
47
|
+
protocol: 'both'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
bindEvents(socket.getEventManager());
|
|
51
|
+
socket.on('error', (err) => console.error(`[demo] 错误: ${err.message}`));
|
|
52
|
+
|
|
53
|
+
await socket.init();
|
|
54
|
+
await socket.start();
|
|
55
|
+
|
|
56
|
+
printAddresses(socket);
|
|
57
|
+
console.log('发送 `chat/echo hello\\n`,事件返回结果会以同一 topic 回发');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
main().catch((err) => {
|
|
61
|
+
console.error(err);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const SocketServer = require('./lib/socket');
|
|
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(SocketServer);
|
|
|
9
8
|
|
|
10
9
|
module.exports = {
|
|
11
10
|
SocketServer,
|
|
12
|
-
MessagePlugin,
|
|
13
11
|
admin,
|
|
14
12
|
/**
|
|
15
13
|
* 获取指定作用域的 Socket 服务器
|
|
16
|
-
* @param {string} [scope='
|
|
14
|
+
* @param {string} [scope='os'] 作用域名称
|
|
17
15
|
* @param {object} [config] 配置参数
|
|
18
16
|
* @returns {SocketServer} Socket 服务器实例
|
|
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
|
};
|