mm_socket_server 1.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 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 ADDED
@@ -0,0 +1,157 @@
1
+ # mm_socket
2
+
3
+ TCP/UDP 套接字服务器入站端口模块。继承 [mm_corejs](https://gitee.com/qiuwenwu91/mm_corejs) 的 `Mod` 基类,提供连接管理、消息处理管道与 JSON-RPC 2.0 消息插件,可脱离完整框架独立运行。
4
+
5
+ - 入口:`index.js`(CommonJS)
6
+ - 导出:`SocketServer`(服务器模块)、`MessagePlugin`(JSON-RPC 消息插件)
7
+ - 运行时依赖:`mm_corejs`
8
+ - 测试覆盖率:100%(`jest.config.js` 四项阈值)
9
+
10
+ ## 特性
11
+
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 处理函数,适配多用户 / 多设备 / 多智能体场景。
18
+
19
+ ## 安装
20
+
21
+ ```bash
22
+ npm install mm_socket_server
23
+ ```
24
+
25
+ ## 环境要求
26
+
27
+ - Node.js >= 22.5.0
28
+ - 模块规范:CommonJS
29
+
30
+ ## 快速开始
31
+
32
+ ```js
33
+ 'use strict';
34
+
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(server, { name: 'socket', host: '127.0.0.1', port: 9100 });
49
+
50
+ // 业务钩子:文本消息直接回发 echo
51
+ socket.getHandle().on('main', (ctx, params) => `echo: ${params.data}`, 100, 'echo');
52
+
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));
63
+
64
+ await socket.init();
65
+ await socket.start();
66
+
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();
73
+ ```
74
+
75
+ ## 服务器配置(config)
76
+
77
+ 顶层 `config` 字段即服务器设置,`init` 阶段统一校验。
78
+
79
+ | 字段 | 类型 | 默认值 | 说明 |
80
+ | --- | --- | --- | --- |
81
+ | `name` | string | `'socket'` | 模块名称(必填) |
82
+ | `protocol` | `'tcp' \| 'udp'` | `'tcp'` | 传输协议 |
83
+ | `host` | string | `'0.0.0.0'` | 监听地址 |
84
+ | `port` | integer | `9000` | 监听端口(1-65535) |
85
+ | `encoding` | `'utf8' \| 'hex' \| 'base64'` | `'utf8'` | 入站数据解码方式 |
86
+ | `json` | boolean | `false` | 为 `true` 时入站尝试 JSON 解析(失败回落原文),出站对象自动序列化 |
87
+
88
+ ## API
89
+
90
+ ### SocketServer
91
+
92
+ | 成员 | 说明 |
93
+ | --- | --- |
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 |
99
+ | `hasClient(client_id)` | 是否在线 |
100
+ | `setClientOptions(client_id, options)` | 绑定连接级选项(`null` 清空),随入站消息透传 |
101
+ | `use(plugin)` | 挂载插件(需实现 `bind(server)`) |
102
+ | `getHandle()` | 消息管道,注册 `before / check / main / render` 等钩子;钩子产出 `result` 自动回发 |
103
+ | `getAddress()` | 实际监听地址,未启动为 `null` |
104
+
105
+ ### 事件
106
+
107
+ | 事件 | 参数 | 触发时机 |
108
+ | --- | --- | --- |
109
+ | `connection` | `(client)` | 新连接接入(UDP 为首见远端地址) |
110
+ | `message` | `(client_id, data)` | 入站消息经管道内置 `emit_message` 钩子分发 |
111
+ | `close` | `(client_id)` | 连接关闭 |
112
+ | `error` | `(err)` | 底层服务器 / 套接字 / 钩子错误 |
113
+
114
+ ### 消息管道
115
+
116
+ 每条入站消息以 `{ client_id, data }` 为 `params`、连接级 `options` 为第三参进入管道:
117
+
118
+ ```
119
+ before → check → main(业务钩子按 sort 依次执行)→ render
120
+ ```
121
+
122
+ 任一阶段产出非 `undefined` 的 `result` 即回发来源客户端;`check` / `before` 产出时短路后续阶段。
123
+
124
+ ## MessagePlugin(JSON-RPC 2.0)
125
+
126
+ 挂载后接管 `jsonrpc` 形态消息(非 RPC 消息透传给业务钩子):
127
+
128
+ ```js
129
+ const rpc = new MessagePlugin();
130
+ socket.use(rpc);
131
+ rpc.addMethod('echo', (params, extra, options) => params); // extra.client_id,options 为连接级选项
132
+ ```
133
+
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()` | 挂载与状态查询 |
140
+
141
+ 协议行为:请求(含 `id`)返回响应对象;通知(无 `id`)处理函数有返回值时回推服务器发起的通知 `{ jsonrpc, method, params }`;客户端把响应形态消息(含 `result` / `error`、无 `method`)发回即结算对应的反向调用。
142
+
143
+ ## 示例
144
+
145
+ ```bash
146
+ npm run demo # 启动 TCP 演示服务器(127.0.0.1:9100),见 example/demo.js
147
+ ```
148
+
149
+ ## 测试
150
+
151
+ ```bash
152
+ npm test # jest --coverage,四项覆盖率阈值均为 100%
153
+ ```
154
+
155
+ ## 许可证
156
+
157
+ ISC
package/index.js ADDED
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const SocketServer = require('./lib/socket');
4
+ const MessagePlugin = require('./lib/message');
5
+
6
+ module.exports = {
7
+ SocketServer,
8
+ MessagePlugin
9
+ };
package/lib/message.js ADDED
@@ -0,0 +1,150 @@
1
+ 'use strict';
2
+
3
+ const { JsonRpc } = require('mm_corejs');
4
+
5
+ /**
6
+ * MessagePlugin —— JSON-RPC 2.0 消息插件(mm_socket 的可选业务组件)。
7
+ *
8
+ * 职责:
9
+ * 1. 方法表:内部持有 JsonRpc 核心,addMethod / delMethod / listMethods 登记可调用方法;
10
+ * 2. 管道挂载:bind(socket) 后向消息管道注册 main 钩子 "json_rpc"(sort 100,
11
+ * 先于内置 emit_message 执行),命中 JSON-RPC 请求时以响应对象作为管道 result,
12
+ * 由 SocketServer 自动回发来源客户端;
13
+ * 3. 通知回推:通知(无 id)处理函数返回非 undefined 结果时,产出服务器发起的
14
+ * 通知对象 { jsonrpc, method, params: result } 回推来源客户端;无结果或出错时
15
+ * 不产出;非 JSON-RPC 消息(解析失败、缺 jsonrpc 字段)透传,控制权交还管道;
16
+ * 4. 主动推送:push(client_id, data) 委托 SocketServer.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 socket = new SocketServer(server, { name: 'socket', port: 9100, json: true });
25
+ * const rpc = new MessagePlugin();
26
+ * socket.use(rpc);
27
+ * rpc.addMethod('echo', (params) => params);
28
+ * await socket.init();
29
+ * await socket.start();
30
+ */
31
+ class MessagePlugin {
32
+ /**
33
+ * JSON-RPC 协议核心
34
+ */
35
+ #rpc = new JsonRpc();
36
+
37
+ /**
38
+ * 已挂载的 SocketServer 实例,未挂载时为 null
39
+ */
40
+ #server = null;
41
+
42
+ /**
43
+ * 挂载到 SocketServer 的消息管道,注册内置 main 钩子
44
+ * @param {Object} server 具备 getHandle() 的 SocketServer 实例
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', (ctx, params, options) => {
54
+ return this._execute(ctx, params, options);
55
+ }, 100, 'json_rpc');
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * 判断是否已挂载
61
+ * @returns {boolean} 是否已挂载到 SocketServer
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} ctx 执行上下文
126
+ * @param {Object} params 消息参数,含 client_id 与 data
127
+ * @param {Object} [options] 管道请求选项(连接级,如 user_id / device_id / agent_id),透传给处理函数
128
+ * @returns {Promise<Object|string|undefined>} 响应对象或 JSON 字符串;反馈、透传与无结果通知时为 undefined
129
+ */
130
+ async _execute(ctx, params, 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;
package/lib/socket.js ADDED
@@ -0,0 +1,562 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+ const dgram = require('dgram');
5
+ const { Mod, Handle, Store } = require('mm_corejs');
6
+
7
+ /**
8
+ * 服务器设置校验 schema(校验对象为顶层 config,而非 run 时选项)
9
+ */
10
+ const SERVER_SCHEMA = {
11
+ type: 'object',
12
+ properties: {
13
+ protocol: { type: 'string', enum: ['tcp', 'udp'] },
14
+ host: { type: 'string', min_length: 1 },
15
+ port: { type: 'integer', minimum: 1, maximum: 65535 },
16
+ encoding: { type: 'string', enum: ['utf8', 'hex', 'base64'] },
17
+ json: { type: 'boolean' }
18
+ }
19
+ };
20
+
21
+ /**
22
+ * SocketServer —— TCP/UDP 套接字服务器(入站端口模块)。
23
+ *
24
+ * 继承 mm_corejs 的 Mod:生命周期由 Lifecycle 模板方法驱动,
25
+ * 调用参数校验由 config.params 声明式定义;
26
+ * 服务器设置(protocol / host / port / encoding / json)为顶层 config 字段,
27
+ * 在 onInit 阶段按 SERVER_SCHEMA 统一校验。
28
+ *
29
+ * 职责:
30
+ * 1. 监听:onStart 按 protocol 创建 net.Server 或 dgram.Socket 并监听;
31
+ * 2. 连接管理:#clients 使用 Store 登记在线客户端(主键 id,支持条件查询与分页);
32
+ * 3. 消息管道:收到的消息经 Handle 的 before/check/main/render 阶段处理,
33
+ * 管道产出 result 时自动回发来源客户端;getHandle() 可注册自定义钩子;
34
+ * 4. 事件分发:connection / message / close / error 经 EventEmitter 向外广播;
35
+ * 5. 主动操作:run(ctx, params) 按 params.action 派发 send / broadcast / close / list。
36
+ *
37
+ * 用法:
38
+ * const socket = new SocketServer(server, { name: 'socket', host: '127.0.0.1', port: 9000 });
39
+ * socket.getHandle().on('main', (ctx, params) => `echo: ${params.data}`, 100, 'echo');
40
+ * await socket.init();
41
+ * await socket.start();
42
+ * await socket.run({}, { action: 'list' });
43
+ */
44
+ class SocketServer extends Mod {
45
+ /**
46
+ * 模块默认配置
47
+ */
48
+ static config = {
49
+ name: 'socket',
50
+ title: 'Socket服务器模块',
51
+ description: 'TCP/UDP 套接字服务器,管理连接并分发消息',
52
+ version: '1.0.0',
53
+ protocol: 'tcp',
54
+ host: '0.0.0.0',
55
+ port: 9000,
56
+ encoding: 'utf8',
57
+ json: false,
58
+ options: {},
59
+ params: {
60
+ type: 'object',
61
+ required: ['action'],
62
+ properties: {
63
+ action: { type: 'string', enum: ['send', 'broadcast', 'close', 'list'] },
64
+ client_id: { type: 'string', min_length: 1 },
65
+ data: {},
66
+ query: { type: 'object' }
67
+ }
68
+ }
69
+ };
70
+
71
+ /**
72
+ * 底层服务器实例(net.Server 或 dgram.Socket)
73
+ */
74
+ #server = null;
75
+
76
+ /**
77
+ * 连接注册表:Store 存储,主键 id、名称字段 name
78
+ */
79
+ #clients = new Store({ key: 'id', name: 'name' });
80
+
81
+ /**
82
+ * 消息处理管道(before/check/main/render/error/success/after)
83
+ */
84
+ #handle = new Handle();
85
+
86
+ /**
87
+ * 客户端编号自增计数器
88
+ */
89
+ #counter = 0;
90
+
91
+ // ---------- 生命周期钩子 ----------
92
+ /**
93
+ * 初始化钩子:校验服务器设置并装配消息管道
94
+ */
95
+ async onInit() {
96
+ await this._validateConfig();
97
+ this._initHandle();
98
+ }
99
+
100
+ /**
101
+ * 启动钩子:按协议创建底层服务器并开始监听
102
+ */
103
+ async onStart() {
104
+ const protocol = this.config.protocol;
105
+ if (protocol === 'udp') {
106
+ await this._startUdp();
107
+ return;
108
+ }
109
+ await this._startTcp();
110
+ }
111
+
112
+ /**
113
+ * 停止钩子:断开全部连接并关闭底层服务器
114
+ */
115
+ async onStop() {
116
+ this._closeAllClients();
117
+ await this._closeServer();
118
+ }
119
+
120
+ /**
121
+ * 释放钩子:清空连接注册表
122
+ */
123
+ async onDispose() {
124
+ this.#clients.clear();
125
+ }
126
+
127
+ // ---------- 执行入口 ----------
128
+ /**
129
+ * 模块主逻辑:按 action 派发操作
130
+ * @param {Object} ctx 执行上下文
131
+ * @param {Object} params 调用参数,含 action 及操作所需字段
132
+ * @param {Object} options 合并后的选项
133
+ * @returns {Promise<Object>} 操作结果
134
+ */
135
+ async main(ctx, params, options) {
136
+ const action = params.action;
137
+ if (action === 'send') return this._actionSend(params);
138
+ if (action === 'broadcast') return this._actionBroadcast(params);
139
+ if (action === 'close') return this._actionClose(params);
140
+ return this._actionList(params);
141
+ }
142
+
143
+ /**
144
+ * 执行发送操作
145
+ * @param {Object} params 调用参数,含 client_id 与 data
146
+ * @returns {Object} 操作结果
147
+ */
148
+ _actionSend(params) {
149
+ this._requireClientId(params);
150
+ const success = this.send(params.client_id, params.data);
151
+ return { action: 'send', client_id: params.client_id, success };
152
+ }
153
+
154
+ /**
155
+ * 执行广播操作
156
+ * @param {Object} params 调用参数,含 data
157
+ * @returns {Object} 操作结果,count 为送达客户端数
158
+ */
159
+ _actionBroadcast(params) {
160
+ const count = this.broadcast(params.data);
161
+ return { action: 'broadcast', count };
162
+ }
163
+
164
+ /**
165
+ * 执行关闭连接操作
166
+ * @param {Object} params 调用参数,含 client_id
167
+ * @returns {Object} 操作结果
168
+ */
169
+ _actionClose(params) {
170
+ this._requireClientId(params);
171
+ const success = this.closeClient(params.client_id);
172
+ return { action: 'close', client_id: params.client_id, success };
173
+ }
174
+
175
+ /**
176
+ * 执行列出连接操作
177
+ * @param {Object} params 调用参数,可含 query 查询条件(Store 条件/分页)
178
+ * @returns {Object} 操作结果,clients 为在线客户端列表
179
+ */
180
+ _actionList(params) {
181
+ return { action: 'list', clients: this.listClients(params.query) };
182
+ }
183
+
184
+ // ---------- 公开操作 ----------
185
+ /**
186
+ * 向指定客户端发送数据
187
+ * @param {string} client_id 客户端 ID
188
+ * @param {string|Object} data 待发送数据
189
+ * @returns {boolean} 是否发送成功
190
+ */
191
+ send(client_id, data) {
192
+ const client = this.#clients.getById(client_id);
193
+ if (!client) return false;
194
+ const payload = this._encodeData(data);
195
+ return this._sendToClient(client, payload);
196
+ }
197
+
198
+ /**
199
+ * 向全部在线客户端广播数据
200
+ * @param {string|Object} data 待广播数据
201
+ * @returns {number} 送达的客户端数量
202
+ */
203
+ broadcast(data) {
204
+ const payload = this._encodeData(data);
205
+ let count = 0;
206
+ for (const client of this.#clients.list()) {
207
+ if (this._sendToClient(client, payload)) count += 1;
208
+ }
209
+ return count;
210
+ }
211
+
212
+ /**
213
+ * 关闭指定客户端连接
214
+ * @param {string} client_id 客户端 ID
215
+ * @returns {boolean} 是否关闭成功
216
+ */
217
+ closeClient(client_id) {
218
+ const client = this.#clients.getById(client_id);
219
+ if (!client) return false;
220
+ if (client.socket) client.socket.destroy();
221
+ this.#clients.delById(client_id);
222
+ this._safeEmit('close', client_id);
223
+ return true;
224
+ }
225
+
226
+ /**
227
+ * 列出在线客户端(支持 Store 条件查询与分页)
228
+ * @param {Object} query 查询条件,可含 page_now / page_size / keyword
229
+ * @returns {Array<Object>} 客户端信息列表,含 id 与 address
230
+ */
231
+ listClients(query) {
232
+ return this.#clients.list(query).map((client) => ({ id: client.id, address: client.address }));
233
+ }
234
+
235
+ /**
236
+ * 判断客户端是否在线
237
+ * @param {string} client_id 客户端 ID
238
+ * @returns {boolean} 是否在线
239
+ */
240
+ hasClient(client_id) {
241
+ return !!this.#clients.getById(client_id);
242
+ }
243
+
244
+ /**
245
+ * 绑定连接级选项(如 user_id / device_id / agent_id),入站消息将作为管道 options 透传给钩子与 RPC 处理函数
246
+ * @param {string} client_id 客户端 ID
247
+ * @param {Object|null} options 选项对象,null 时清空
248
+ * @returns {boolean} 是否设置成功(客户端不在线时失败)
249
+ */
250
+ setClientOptions(client_id, options) {
251
+ const client = this.#clients.getById(client_id);
252
+ if (!client) return false;
253
+ client.options = options || null;
254
+ return true;
255
+ }
256
+
257
+ /**
258
+ * 读取连接级选项,未绑定时返回空对象
259
+ * @param {string} client_id 客户端 ID
260
+ * @returns {Object} 连接级选项
261
+ */
262
+ _clientOptions(client_id) {
263
+ const client = this.#clients.getById(client_id);
264
+ if (!client || !client.options) return {};
265
+ return client.options;
266
+ }
267
+
268
+ /**
269
+ * 挂载插件:调用 plugin.bind(this),由插件自行向管道注册钩子;
270
+ * 如 MessagePlugin 挂载后接管 JSON-RPC 请求
271
+ * @param {Object} plugin 具备 bind(server) 方法的插件实例
272
+ * @returns {SocketServer} 当前实例
273
+ * @throws {TypeError} plugin 不具备 bind 方法时
274
+ */
275
+ use(plugin) {
276
+ if (!plugin || typeof plugin.bind !== 'function') {
277
+ throw new TypeError(`${this._label()} plugin must provide bind()`);
278
+ }
279
+ plugin.bind(this);
280
+ return this;
281
+ }
282
+
283
+ /**
284
+ * 获取消息处理管道,供外部注册 before/check/main/render 等钩子;
285
+ * 钩子产出 result 时自动回发来源客户端
286
+ * @returns {Handle} 消息管道实例
287
+ */
288
+ getHandle() {
289
+ return this.#handle;
290
+ }
291
+
292
+ /**
293
+ * 获取实际监听地址
294
+ * @returns {Object|null} 监听地址信息,未启动时为 null
295
+ */
296
+ getAddress() {
297
+ const server = this.#server;
298
+ return server ? server.address() : null;
299
+ }
300
+
301
+ // ---------- TCP ----------
302
+ /**
303
+ * 启动 TCP 服务器
304
+ * @returns {Promise<void>} 无返回值
305
+ */
306
+ async _startTcp() {
307
+ const server = net.createServer((socket) => this._handleConnection(socket));
308
+ this.#server = server;
309
+ await this._listenTcp(server);
310
+ server.on('error', (err) => this._emitError(err));
311
+ }
312
+
313
+ /**
314
+ * 等待 TCP 服务器监听成功
315
+ * @param {Object} server net.Server 实例
316
+ * @returns {Promise<void>} 无返回值
317
+ */
318
+ _listenTcp(server) {
319
+ const host = this.config.host;
320
+ const port = this.config.port;
321
+ return new Promise((resolve, reject) => {
322
+ server.once('error', reject);
323
+ server.listen({ host, port }, () => {
324
+ server.removeListener('error', reject);
325
+ resolve();
326
+ });
327
+ });
328
+ }
329
+
330
+ /**
331
+ * 登记新的 TCP 连接并绑定事件
332
+ * @param {Object} socket 套接字实例
333
+ */
334
+ _handleConnection(socket) {
335
+ this.#counter += 1;
336
+ const client_id = `tcp_${this.#counter}`;
337
+ const client = {
338
+ id: client_id,
339
+ name: client_id,
340
+ address: this._remoteAddress(socket),
341
+ socket,
342
+ rinfo: null
343
+ };
344
+ this.#clients.add(client);
345
+ socket.on('data', (buf) => this._handleData(client_id, buf));
346
+ socket.on('close', () => this._handleClose(client_id));
347
+ socket.on('error', (err) => this._emitError(err));
348
+ this._safeEmit('connection', client);
349
+ }
350
+
351
+ /**
352
+ * 处理 TCP 收到的数据:解码后进入消息管道,产出 result 时自动回发
353
+ * @param {string} client_id 客户端 ID
354
+ * @param {Object} buf 数据缓冲区
355
+ * @returns {Promise<void>} 无返回值
356
+ */
357
+ async _handleData(client_id, buf) {
358
+ const decoded = this._decodeData(buf);
359
+ const params = { client_id, data: decoded };
360
+ const ctx = { client_id };
361
+ const done = await this.#handle.run(ctx, params, this._clientOptions(client_id));
362
+ if (done.error) this._emitError(done.error);
363
+ if (done.result !== undefined) this.send(client_id, done.result);
364
+ }
365
+
366
+ /**
367
+ * 处理 TCP 连接关闭
368
+ * @param {string} client_id 客户端 ID
369
+ */
370
+ _handleClose(client_id) {
371
+ const existed = this.#clients.delById(client_id);
372
+ if (existed) this._safeEmit('close', client_id);
373
+ }
374
+
375
+ /**
376
+ * 获取远端地址字符串
377
+ * @param {Object} socket 套接字实例
378
+ * @returns {string} 形如 ip:port 的地址
379
+ */
380
+ _remoteAddress(socket) {
381
+ return `${socket.remoteAddress}:${socket.remotePort}`;
382
+ }
383
+
384
+ // ---------- UDP ----------
385
+ /**
386
+ * 启动 UDP 服务器
387
+ * @returns {Promise<void>} 无返回值
388
+ */
389
+ async _startUdp() {
390
+ const socket = dgram.createSocket('udp4');
391
+ this.#server = socket;
392
+ await this._bindUdp(socket);
393
+ socket.on('message', (buf, rinfo) => this._handleUdpMessage(buf, rinfo));
394
+ socket.on('error', (err) => this._emitError(err));
395
+ }
396
+
397
+ /**
398
+ * 等待 UDP 套接字绑定成功
399
+ * @param {Object} socket dgram.Socket 实例
400
+ * @returns {Promise<void>} 无返回值
401
+ */
402
+ _bindUdp(socket) {
403
+ const host = this.config.host;
404
+ const port = this.config.port;
405
+ return new Promise((resolve, reject) => {
406
+ socket.once('error', reject);
407
+ socket.bind({ address: host, port }, () => {
408
+ socket.removeListener('error', reject);
409
+ resolve();
410
+ });
411
+ });
412
+ }
413
+
414
+ /**
415
+ * 处理 UDP 报文:首见远端地址登记为客户端并触发 connection
416
+ * @param {Object} buf 数据缓冲区
417
+ * @param {Object} rinfo 远端地址信息
418
+ */
419
+ _handleUdpMessage(buf, rinfo) {
420
+ const client_id = `udp_${rinfo.address}:${rinfo.port}`;
421
+ const is_new = !this.#clients.getById(client_id);
422
+ const client = {
423
+ id: client_id,
424
+ name: client_id,
425
+ address: client_id,
426
+ socket: null,
427
+ rinfo
428
+ };
429
+ if (is_new) {
430
+ this.#clients.add(client);
431
+ this._safeEmit('connection', client);
432
+ }
433
+ const decoded = this._decodeData(buf);
434
+ const params = { client_id, data: decoded };
435
+ const ctx = { client_id };
436
+ this.#handle.run(ctx, params, this._clientOptions(client_id)).then((done) => {
437
+ if (done.error) this._emitError(done.error);
438
+ if (done.result !== undefined) this.send(client_id, done.result);
439
+ });
440
+ }
441
+
442
+ // ---------- 数据编解码 ----------
443
+ /**
444
+ * 编码待发送数据
445
+ * @param {string|Object} data 待编码数据
446
+ * @returns {string} 编码后的字符串
447
+ */
448
+ _encodeData(data) {
449
+ if (this.config.json) return JSON.stringify(data);
450
+ if (typeof data === 'string') return data;
451
+ return String(data);
452
+ }
453
+
454
+ /**
455
+ * 解码收到的数据
456
+ * @param {Object} buf 数据缓冲区
457
+ * @returns {string|Object} 解码后的数据,json 模式下解析失败时返回原文
458
+ */
459
+ _decodeData(buf) {
460
+ const text = buf.toString(this.config.encoding);
461
+ if (!this.config.json) return text;
462
+ return this._parseJson(text);
463
+ }
464
+
465
+ /**
466
+ * 尝试将文本解析为 JSON
467
+ * @param {string} text 文本内容
468
+ * @returns {Object|string} 解析结果,失败时返回原文
469
+ */
470
+ _parseJson(text) {
471
+ try {
472
+ return JSON.parse(text);
473
+ } catch {
474
+ return text;
475
+ }
476
+ }
477
+
478
+ // ---------- 发送与清理 ----------
479
+ /**
480
+ * 向单个客户端发送已编码数据
481
+ * @param {Object} client 客户端信息
482
+ * @param {string} payload 已编码数据
483
+ * @returns {boolean} 是否发送成功
484
+ */
485
+ _sendToClient(client, payload) {
486
+ if (client.socket) {
487
+ client.socket.write(payload);
488
+ return true;
489
+ }
490
+ return this._sendUdp(client, payload);
491
+ }
492
+
493
+ /**
494
+ * 向 UDP 客户端发送数据
495
+ * @param {Object} client 客户端信息,含 rinfo
496
+ * @param {string} payload 已编码数据
497
+ * @returns {boolean} 是否发送成功
498
+ */
499
+ _sendUdp(client, payload) {
500
+ const socket = this.#server;
501
+ if (!socket || !client.rinfo) return false;
502
+ socket.send(payload, client.rinfo.port, client.rinfo.address);
503
+ return true;
504
+ }
505
+
506
+ /**
507
+ * 断开全部客户端连接并清空注册表
508
+ */
509
+ _closeAllClients() {
510
+ for (const client of this.#clients.list()) {
511
+ if (client.socket) client.socket.destroy();
512
+ }
513
+ this.#clients.clear();
514
+ }
515
+
516
+ /**
517
+ * 关闭底层服务器
518
+ * @returns {Promise<void>} 无返回值
519
+ */
520
+ _closeServer() {
521
+ const server = this.#server;
522
+ if (!server) return Promise.resolve();
523
+ this.#server = null;
524
+ return new Promise((resolve) => {
525
+ server.close(resolve);
526
+ });
527
+ }
528
+
529
+ // ---------- 校验辅助 ----------
530
+ /**
531
+ * 装配消息管道:内置 main 钩子负责分发 message 事件(sort 置后,
532
+ * 不返回值,业务钩子产出的 result 由 _handleData 统一回发)
533
+ */
534
+ _initHandle() {
535
+ this.#handle.on('main', (ctx, params) => {
536
+ this._safeEmit('message', params.client_id, params.data);
537
+ }, 1000, 'emit_message');
538
+ }
539
+
540
+ /**
541
+ * 校验顶层 config 中的服务器设置
542
+ * @returns {Promise<void>} 无返回值
543
+ * @throws {Error} 校验不通过时
544
+ */
545
+ async _validateConfig() {
546
+ const tip = await this._validate(this.config, SERVER_SCHEMA);
547
+ if (tip) throw new Error(`${this._label()} invalid config: ${tip}`);
548
+ }
549
+
550
+ /**
551
+ * 断言调用参数含 client_id
552
+ * @param {Object} params 调用参数
553
+ * @throws {Error} 缺少 client_id 时
554
+ */
555
+ _requireClientId(params) {
556
+ if (!params.client_id) {
557
+ throw new Error(`${this._label()} params.client_id required`);
558
+ }
559
+ }
560
+ }
561
+
562
+ module.exports = SocketServer;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "mm_socket_server",
3
+ "version": "1.0.0",
4
+ "description": "TCP/UDP 套接字服务器入站端口模块,继承 mm_corejs 的 Mod 基类,支持独立运行。",
5
+ "keywords": [
6
+ "超级美眉",
7
+ "入站端口",
8
+ "mm_mod",
9
+ "mm_in_port",
10
+ "mm_receiver",
11
+ "socket",
12
+ "tcp",
13
+ "udp",
14
+ "server"
15
+ ],
16
+ "license": "ISC",
17
+ "author": "qww",
18
+ "type": "commonjs",
19
+ "main": "index.js",
20
+ "files": [
21
+ "index.js",
22
+ "lib"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22.5.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://gitee.com/qiuwenwu91/mm_socket.git"
30
+ },
31
+ "homepage": "https://gitee.com/qiuwenwu91/mm_socket",
32
+ "bugs": {
33
+ "url": "https://gitee.com/qiuwenwu91/mm_socket/issues"
34
+ },
35
+ "scripts": {
36
+ "start": "node example/demo.js",
37
+ "demo": "node example/demo.js",
38
+ "test": "jest --coverage"
39
+ },
40
+ "dependencies": {
41
+ "mm_corejs": "^1.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "jest": "^30.5.2"
45
+ }
46
+ }