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 CHANGED
@@ -1,155 +1,143 @@
1
1
  # mm_socket_server
2
2
 
3
- TCP/UDP 套接字服务器入站适配器模块。继承 [mm_corejs](https://gitee.com/qiuwenwu91/mm_corejs) 的 `Mod` 基类,提供连接管理、消息处理管道与 JSON-RPC 2.0 消息插件,可脱离完整框架独立运行。
3
+ 最薄的 TCP/UDP 套接字服务器入站适配器。继承 `mm_mod` 的 `Mod`,把按 topic 分帧后的报文以 topic 为 `target` 交给 `mm_event` 的事件管理器处理,不做路由表、不做插件、不加载脚本。`protocol` 可选 `tcp`、`udp` 或 `both`(同端口同时监听)。
4
4
 
5
5
  - 入口:`index.js`(CommonJS)
6
- - 导出:`SocketServer`(服务器模块)、`MessagePlugin`(JSON-RPC 消息插件)
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
- - **双协议**:`protocol: 'tcp' | 'udp'`,TCP 按连接登记客户端,UDP 按远端地址首见登记。
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 处理函数,适配多用户 / 多设备 / 多智能体场景。
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
- ```bash
22
- npm install mm_socket_server
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
- - Node.js >= 22.5.0
28
- - 模块规范:CommonJS
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 net = require('net');
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
- // 业务钩子:文本消息直接回发 echo
51
- socket.getHandle().on('main', (ctx, params) => `echo: ${params.data}`, 100, 'echo');
41
+ const socket = new SocketServer({ name: 'socket', protocol: 'both', host: '127.0.0.1', port: 9000 });
42
+ const events = socket.getEventManager();
52
43
 
53
- // JSON-RPC 插件:登记方法
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
- ## 服务器配置(config)
76
-
77
- 顶层 `config` 字段即服务器设置,`init` 阶段统一校验。
52
+ ## 服务器配置
78
53
 
79
54
  | 字段 | 类型 | 默认值 | 说明 |
80
55
  | --- | --- | --- | --- |
81
- | `name` | string | `'socket'` | 模块名称(必填) |
82
- | `protocol` | `'tcp' \| 'udp'` | `'tcp'` | 传输协议 |
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` | integer | `9000` | 监听端口(1-65535) |
85
- | `encoding` | `'utf8' \| 'hex' \| 'base64'` | `'utf8'` | 入站数据解码方式 |
86
- | `json` | boolean | `false` | 为 `true` 时入站尝试 JSON 解析(失败回落原文),出站对象自动序列化 |
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
- | `run(ctx, params)` | 主动操作入口,`params.action` 派发:`send`(需 `client_id` / `data`)、`broadcast`(需 `data`)、`close`(需 `client_id`)、`list`(可选 `query` 条件查询) |
95
- | `send(client_id, data)` | 向指定客户端发送数据,返回是否成功 |
96
- | `broadcast(data)` | 向全部在线客户端广播,返回送达数 |
97
- | `closeClient(client_id)` | 关闭指定连接,返回是否发起 |
98
- | `listClients(query)` | 在线客户端列表(`{ id, address }`),支持 Store 条件 / 分页 / keyword |
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)` | 绑定连接级选项(`null` 清空),随入站消息透传 |
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
- | `connection` | `(client)` | 新连接接入(UDP 为首见远端地址) |
110
- | `message` | `(client_id, data)` | 入站消息经管道内置 `emit_message` 钩子分发 |
111
- | `close` | `(client_id)` | 连接关闭 |
112
- | `error` | `(err)` | 底层服务器 / 套接字 / 钩子错误 |
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
- 每条入站消息以 `{ client_id, data }` 为 `params`、连接级 `options` 为第三参进入管道:
102
+ 签名固定为 `(params, ctx, options)`:
117
103
 
118
- ```
119
- before → check → main(业务钩子按 sort 依次执行)→ render
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
- 任一阶段产出非 `undefined` 的 `result` 即回发来源客户端;`check` / `before` 产出时短路后续阶段。
110
+ 返回非 `undefined` / `null` 的结果会按 `framing` 与原 topic 编码后回发来源客户端;返回 `{ topic, data }` 则改发到指定 topic;返回 `undefined` 则不回发。事件也可直接用 `ctx.socket` 写出。
123
111
 
124
- ## MessagePlugin(JSON-RPC 2.0)
125
-
126
- 挂载后接管 `jsonrpc` 形态消息(非 RPC 消息透传给业务钩子):
112
+ ## 主动操作
127
113
 
128
114
  ```js
129
- const rpc = new MessagePlugin();
130
- socket.use(rpc);
131
- rpc.addMethod('echo', (params, extra, options) => params); // extra.client_id,options 为连接级选项
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
- | `addMethod(name, handler)` / `delMethod(name)` / `listMethods(query)` | 方法表管理,处理函数签名 `(params, extra, options)` |
137
- | `push(client_id, data)` | 任意时刻向客户端回推数据(如异步任务完成后) |
138
- | `call(client_id, method, params, options)` | 反向调用客户端方法并等待其反馈,`options.timeout_ms` 缺省 30000 |
139
- | `bind(server)` / `isBound()` | 挂载与状态查询 |
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
- 协议行为:请求(含 `id`)返回响应对象;通知(无 `id`)处理函数有返回值时回推服务器发起的通知 `{ jsonrpc, method, params }`;客户端把响应形态消息(含 `result` / `error`、无 `method`)发回即结算对应的反向调用。
129
+ UDP 没有连接概念,首次收到某远端地址的报文时登记为客户端,之后超过 `udp_ttl` 未再收到报文会被定时清理。
142
130
 
143
131
  ## 示例
144
132
 
145
133
  ```bash
146
- npm run demo # 启动 TCP 演示服务器(127.0.0.1:9100),见 example/demo.js
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 # jest --coverage,四项覆盖率阈值均为 100%
140
+ npm test # node --test
153
141
  ```
154
142
 
155
143
  ## 许可证
@@ -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='sys'] 作用域名称
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, { name: scope, ...config });
22
+ return admin.get(scope, config);
25
23
  }
26
24
  };