lz-nframe 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/lib/NApp.d.ts +13 -0
- package/lib/NApp.js +23 -0
- package/lib/NConfigsMgr.d.ts +50 -0
- package/lib/NConfigsMgr.js +162 -0
- package/lib/NHttpClient.d.ts +71 -0
- package/lib/NHttpClient.js +196 -0
- package/lib/NSimpleHttpServer.d.ts +75 -0
- package/lib/NSimpleHttpServer.js +222 -0
- package/lib/NType.d.ts +64 -0
- package/lib/NType.js +8 -0
- package/lib/NWSClient.d.ts +44 -0
- package/lib/NWSClient.js +134 -0
- package/lib/NWSServer.d.ts +46 -0
- package/lib/NWSServer.js +133 -0
- package/lib/NWSServiceHub.d.ts +301 -0
- package/lib/NWSServiceHub.js +807 -0
- package/lib/NWSSocket.d.ts +23 -0
- package/lib/NWSSocket.js +30 -0
- package/lib/NWSUserClientJ.d.ts +76 -0
- package/lib/NWSUserClientJ.js +232 -0
- package/lib/NWSUserMgrJ.d.ts +82 -0
- package/lib/NWSUserMgrJ.js +208 -0
- package/package.json +28 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/*******************************************************************************
|
|
2
|
+
文件: NWSServiceHub.ts
|
|
3
|
+
创建: 2026年05月07日
|
|
4
|
+
作者: 老张
|
|
5
|
+
描述:
|
|
6
|
+
NWS 后台服务通讯模块(单文件实现)
|
|
7
|
+
- 同时支持 Client + Server 双角色
|
|
8
|
+
- A/B 节点互连时单连接复用(自动仲裁)
|
|
9
|
+
- 支持 request/response、push、心跳、自动重连
|
|
10
|
+
|
|
11
|
+
使用方法(示例):
|
|
12
|
+
const hub = new NWSServiceHub({ nodeId: 'nodeA', log: true });
|
|
13
|
+
// 1) 开启服务端能力,供其他节点连入
|
|
14
|
+
hub.startServer(9001);
|
|
15
|
+
// 2) 注册对端并发起连接
|
|
16
|
+
hub.registerService({ nodeId: 'nodeB', url: 'http://127.0.0.1:9002', token: 'demo-token' });
|
|
17
|
+
hub.connectAll();
|
|
18
|
+
// 3) 注册请求处理器
|
|
19
|
+
hub.onRequest('user', 'ping', () => ({ pong: true }));
|
|
20
|
+
// 4) 发起请求
|
|
21
|
+
const res = await hub.request('nodeB', 'user', 'ping', { t: Date.now() });
|
|
22
|
+
// 5) 订阅推送
|
|
23
|
+
const onTip = (payload) => { console.log(payload); };
|
|
24
|
+
hub.onPush('notice', 'broadcast', onTip);
|
|
25
|
+
// 6) 发送推送
|
|
26
|
+
hub.send('nodeB', 'notice', 'broadcast', { msg: 'hello' });
|
|
27
|
+
// 7) 注销/下架(可选, 长期运行场景常用):
|
|
28
|
+
hub.offRequest('user', 'ping'); // 移除单个 request 路由
|
|
29
|
+
hub.offPush('notice', 'broadcast', onTip); // 移除某个 push 订阅(不传 handler 则清空该路由)
|
|
30
|
+
hub.unregisterService('nodeB'); // 完整释放对端的所有资源(连接/重连/挂起请求)
|
|
31
|
+
// 8) 关闭整个 hub
|
|
32
|
+
hub.disconnect();
|
|
33
|
+
|
|
34
|
+
命名约定 nodeId vs peerId(两者经常指向同一字符串,但语义不同):
|
|
35
|
+
- nodeId: 协议层"节点身份", 表示某个节点自己的唯一 id。
|
|
36
|
+
使用场景:
|
|
37
|
+
* this.nodeId 当前 Hub 自身节点 id
|
|
38
|
+
* packet.nodeId 协议包里发包方声明的节点 id
|
|
39
|
+
* NWSServicePeerConfig.nodeId 对端配置中的节点 id
|
|
40
|
+
- peerId: 本地视角下的"对端 key", 表示"我正在和哪个对端通信", 通常等于对端的 nodeId。
|
|
41
|
+
使用场景:
|
|
42
|
+
* peerChannels / clients / reconnectTimers 等 Map 的 key
|
|
43
|
+
* pendingRequests 中记录的归属 peerId
|
|
44
|
+
* request(peerId, ...) / send(peerId, ...) 等 public API 的参数
|
|
45
|
+
举例(A 收到 B 的包):
|
|
46
|
+
this.nodeId = 'nodeA' // A 自己
|
|
47
|
+
packet.nodeId = 'nodeB' // 包里 B 自称
|
|
48
|
+
const peerId = packet.nodeId; // peerId='nodeB', 作为本地索引使用
|
|
49
|
+
简记: 协议层/配置层/自身用 nodeId, 本地索引/函数参数/Map key 用 peerId。
|
|
50
|
+
|
|
51
|
+
两端互联(A/B 双方都 registerService 了对方)行为说明:
|
|
52
|
+
1) 初始建连——会出现"瞬时双连接":
|
|
53
|
+
两端各自的 connectAll() 没有先后约束, 会同时各发起一条 outbound,
|
|
54
|
+
因此每端短时间都会持有 1 条 outbound + 1 条 inbound, 物理上有 2 条 TCP。
|
|
55
|
+
2) 仲裁收敛——installOrArbitrateChannel + shouldKeepOutbound:
|
|
56
|
+
判定规则: nodeId 字典序更大的节点保留 outbound, 更小的节点保留 inbound。
|
|
57
|
+
两端独立做出的决定一定一致(规则与输入都对称), 不会出现互相打架的情况。
|
|
58
|
+
多余的那条连接会被 close()掉, 最终只剩一条物理连接 = "B.outbound ↔ A.inbound" (设 A < B)。
|
|
59
|
+
3) 稳态后的断线重连:
|
|
60
|
+
- outbound 侧(字典序大的一方): onOutboundDisconnected → scheduleReconnect, 进入指数退避重连。
|
|
61
|
+
- inbound 侧(字典序小的一方): 仅清理资源, 不主动重连(服务端不追客户端)。
|
|
62
|
+
所以重连只由仲裁后保留 outbound 的那一方承担, 不会形成重连风暴。
|
|
63
|
+
4) 后续并发再次互连仍然安全:
|
|
64
|
+
无论谁再随时多发起一次连接, 都会再次进入 installOrArbitrateChannel 仲裁,
|
|
65
|
+
多余的物理连接会被关掉, 状态收敛回唯一连接。
|
|
66
|
+
5) 心跳判死路径:
|
|
67
|
+
心跳判死后由 handleChannelDead 走与 onOutboundDisconnected 等价的清理 + 重连流程,
|
|
68
|
+
即 outbound 侧重连, inbound 侧只清理。
|
|
69
|
+
边角注意:
|
|
70
|
+
- outbound 侧进程死了不重启时, 链路会一直断开, 直到 outbound 侧再次启动主动连过来。
|
|
71
|
+
- 由于仲裁直接以 nodeId 字符串比较, 改名可能让"活跃的连接方向"换边, 一般无感。
|
|
72
|
+
|
|
73
|
+
TODO:
|
|
74
|
+
1.路由
|
|
75
|
+
*******************************************************************************/
|
|
76
|
+
import { EventEmitter } from 'events';
|
|
77
|
+
/**
|
|
78
|
+
* 协议名说明(PacketType):
|
|
79
|
+
*
|
|
80
|
+
* - 'hello':握手请求。用于建立连接时,主动方向发起,包含节点身份与鉴权信息。
|
|
81
|
+
* - 'hello_ack':握手响应。收到 'hello' 后回复,表明握手/鉴权通过。
|
|
82
|
+
* - 'request':请求。发起业务请求,需服务端处理并返回响应。
|
|
83
|
+
* - 'response':响应。对应 'request',携带结果返回发起方。
|
|
84
|
+
* - 'push':推送消息。用于无应答的单向推送,不需要业务响应。
|
|
85
|
+
* - 'ping':心跳检测。定期发送以检测链路活跃状态并防止断线。
|
|
86
|
+
* - 'pong':心跳响应。收到 'ping' 后回复,以确认链接正常。
|
|
87
|
+
*/
|
|
88
|
+
type PacketType = 'hello' | 'hello_ack' | 'request' | 'response' | 'push' | 'ping' | 'pong';
|
|
89
|
+
export interface NWSServiceHubOptions {
|
|
90
|
+
/** 当前节点唯一标识(用于仲裁单连接) */
|
|
91
|
+
nodeId: string;
|
|
92
|
+
/** 记录日志 */
|
|
93
|
+
log?: boolean;
|
|
94
|
+
/** 默认请求超时时间 */
|
|
95
|
+
requestTimeoutMs?: number;
|
|
96
|
+
/** 心跳间隔(毫秒) */
|
|
97
|
+
heartbeatIntervalMs?: number;
|
|
98
|
+
/** 重连初始间隔(毫秒),首次断线后等待的时间 */
|
|
99
|
+
reconnectIntervalMs?: number;
|
|
100
|
+
/** 重连最大间隔(毫秒),指数退避封顶值,避免对端长时间宕机时高频重试 */
|
|
101
|
+
reconnectMaxIntervalMs?: number;
|
|
102
|
+
/** 心跳判死倍数:连续 N 个心跳周期未收到任何数据则视为死链,默认 3 */
|
|
103
|
+
heartbeatDeadFactor?: number;
|
|
104
|
+
}
|
|
105
|
+
/** 对端配置 */
|
|
106
|
+
export interface NWSServicePeerConfig {
|
|
107
|
+
/** 对端节点唯一标识 */
|
|
108
|
+
nodeId: string;
|
|
109
|
+
/** 对端服务地址(例如 http://127.0.0.1:9001) */
|
|
110
|
+
url?: string;
|
|
111
|
+
/** 对端鉴权 token(握手时发送与校验) */
|
|
112
|
+
token?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* NWS 内部协议包。
|
|
116
|
+
* 泛型 T 表示 payload 的类型:
|
|
117
|
+
* - request/push 时通常是业务请求体或推送数据体
|
|
118
|
+
* - response 时通常是业务返回值
|
|
119
|
+
* - hello_ack 等控制包也可放少量控制信息
|
|
120
|
+
*/
|
|
121
|
+
interface NWSProtocolPacket<T = any> {
|
|
122
|
+
/** 包类型, 决定 onPacket 后续分发到哪个处理流程 */
|
|
123
|
+
type: PacketType;
|
|
124
|
+
/** 发包方节点 id, 用于识别对端身份以及建立 peerId 映射 */
|
|
125
|
+
nodeId: string;
|
|
126
|
+
/** 发包时间戳(毫秒), 主要用于排查日志和链路观测 */
|
|
127
|
+
ts: number;
|
|
128
|
+
/** 请求追踪 id, request/response 必填; push/心跳/握手通常不需要 */
|
|
129
|
+
traceId?: string;
|
|
130
|
+
/** 业务服务名, request/response/push 路由的一部分 */
|
|
131
|
+
service?: string;
|
|
132
|
+
/** 业务动作名, request/response/push 路由的一部分 */
|
|
133
|
+
action?: string;
|
|
134
|
+
/** 协议携带的数据体, 由不同 PacketType 决定具体含义 */
|
|
135
|
+
payload?: T;
|
|
136
|
+
/** 响应状态码, response 使用; 0 表示成功, 非 0 表示失败 */
|
|
137
|
+
code?: number;
|
|
138
|
+
/** 响应或错误描述, response 使用; 对端异常时只传精简 message */
|
|
139
|
+
message?: string;
|
|
140
|
+
/** 握手鉴权 token, hello 使用 */
|
|
141
|
+
token?: string;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 请求处理器类型(对应 'request' 包)。
|
|
145
|
+
* - payload 对端发来的请求体
|
|
146
|
+
* - ctx.peerId 发起方节点 id
|
|
147
|
+
* - ctx.service 路由 service 名
|
|
148
|
+
* - ctx.action 路由 action 名
|
|
149
|
+
* - ctx.traceId 本次请求的追踪 id, 与对端 pendingRequest 一一对应
|
|
150
|
+
* - 返回值 可同步返回, 也可返回 Promise; 抛异常会被 catch 后以 code=500 回 response
|
|
151
|
+
*/
|
|
152
|
+
type RequestHandler = (payload: any, ctx: {
|
|
153
|
+
peerId: string;
|
|
154
|
+
service: string;
|
|
155
|
+
action: string;
|
|
156
|
+
traceId: string;
|
|
157
|
+
}) => Promise<any> | any;
|
|
158
|
+
/**
|
|
159
|
+
* 推送处理器类型(对应 'push' 包, 单向, 无响应)。
|
|
160
|
+
* - payload 对端推送的数据体
|
|
161
|
+
* - ctx.peerId 发起方节点 id
|
|
162
|
+
* - ctx.service 路由 service 名
|
|
163
|
+
* - ctx.action 路由 action 名
|
|
164
|
+
* - 没有 traceId, 因为 push 不需要回包匹配
|
|
165
|
+
* - 同一路由可注册多个 handler, 按注册顺序依次回调; 单个 handler 抛错会被框架捕获并通过 EventError 抛出, 不影响其它 handler
|
|
166
|
+
*/
|
|
167
|
+
type PushHandler = (payload: any, ctx: {
|
|
168
|
+
peerId: string;
|
|
169
|
+
service: string;
|
|
170
|
+
action: string;
|
|
171
|
+
}) => void;
|
|
172
|
+
export default class NWSServiceHub extends EventEmitter {
|
|
173
|
+
static EventPeerConnected: string;
|
|
174
|
+
static EventPeerDisconnected: string;
|
|
175
|
+
static EventError: string;
|
|
176
|
+
/** 节点唯一标识 */
|
|
177
|
+
private readonly nodeId;
|
|
178
|
+
/** 是否记录日志 */
|
|
179
|
+
private readonly log;
|
|
180
|
+
/** 请求超时时间 */
|
|
181
|
+
private readonly requestTimeoutMs;
|
|
182
|
+
/** 心跳间隔 */
|
|
183
|
+
private readonly heartbeatIntervalMs;
|
|
184
|
+
/** 重连初始间隔 */
|
|
185
|
+
private readonly reconnectIntervalMs;
|
|
186
|
+
/** 重连最大间隔(指数退避封顶) */
|
|
187
|
+
private readonly reconnectMaxIntervalMs;
|
|
188
|
+
/** 心跳判死倍数 */
|
|
189
|
+
private readonly heartbeatDeadFactor;
|
|
190
|
+
private server;
|
|
191
|
+
private readonly peerConfigs;
|
|
192
|
+
private readonly clients;
|
|
193
|
+
private readonly peerChannels;
|
|
194
|
+
private readonly inboundLinkPeerMap;
|
|
195
|
+
private readonly pendingRequests;
|
|
196
|
+
private readonly requestHandlers;
|
|
197
|
+
private readonly pushHandlers;
|
|
198
|
+
private readonly reconnectTimers;
|
|
199
|
+
/** 当前每个 peer 的下一次重连等待时长(毫秒),用于指数退避 */
|
|
200
|
+
private readonly reconnectDelays;
|
|
201
|
+
private heartbeatTimer;
|
|
202
|
+
/** 请求追踪计数器 */
|
|
203
|
+
private traceCounter;
|
|
204
|
+
constructor(options: NWSServiceHubOptions);
|
|
205
|
+
registerService(config: NWSServicePeerConfig): void;
|
|
206
|
+
/**
|
|
207
|
+
* 注销已注册的对端服务, 完成完整资源释放:
|
|
208
|
+
* - 删除静态配置 peerConfigs(避免后续被 connectAll 再次拉起)
|
|
209
|
+
* - 取消该对端的重连定时器并清空退避计数
|
|
210
|
+
* - 关闭并丢弃 NWSClient 实例(包括其 EventEmitter 监听)
|
|
211
|
+
* - 关闭当前 channel(无论 inbound/outbound), 并清理 inboundLinkPeerMap 反查项
|
|
212
|
+
* - 拒绝该 peer 所有挂起请求, 防止永远等不到响应
|
|
213
|
+
* - 触发一次 EventPeerDisconnected, 便于上层做联动清理
|
|
214
|
+
* @returns 是否真的存在并被清理过(全部不存在时返回 false)
|
|
215
|
+
*/
|
|
216
|
+
unregisterService(peerId: string): boolean;
|
|
217
|
+
/** 启动当前节点的 WS 服务端能力,用于承接其他节点主动连入 */
|
|
218
|
+
startServer(port: number, host?: string): void;
|
|
219
|
+
/** 停止当前节点的 WS 服务端能力(不影响已注册配置) */
|
|
220
|
+
stopServer(): void;
|
|
221
|
+
/** 按静态配置连接所有对端节点,并启动心跳 */
|
|
222
|
+
connectAll(): void;
|
|
223
|
+
/** 关闭 hub 的所有能力(客户端连接、服务端监听、定时器、挂起请求) */
|
|
224
|
+
disconnect(): void;
|
|
225
|
+
/**
|
|
226
|
+
* 向指定 peer 发起请求并等待响应。
|
|
227
|
+
* 泛型:
|
|
228
|
+
* - TRes 返回值类型, 默认 any。建议显式指定以获得调用处的类型推导。
|
|
229
|
+
* - TReq 请求体类型, 默认 any。一般可由 payload 自动推断, 必要时显式指定。
|
|
230
|
+
* 用法: const user = await hub.request<UserInfo>('nodeB', 'user', 'get', { id: 1 });
|
|
231
|
+
*/
|
|
232
|
+
request<TRes = any, TReq = any>(peerId: string, service: string, action: string, payload: TReq, timeoutMs?: number): Promise<TRes>;
|
|
233
|
+
/** 向指定 peer 发送 push 消息(单向,不等待响应) */
|
|
234
|
+
send(peerId: string, service: string, action: string, payload: any): boolean;
|
|
235
|
+
/** 发送底层协议包(高级接口) */
|
|
236
|
+
sendToPeer(peerId: string, packet: NWSProtocolPacket): boolean;
|
|
237
|
+
/** 注册请求处理器,匹配键为 service:action */
|
|
238
|
+
onRequest(service: string, action: string, handler: RequestHandler): void;
|
|
239
|
+
/**
|
|
240
|
+
* 注销请求处理器(同 onRequest 一对)。
|
|
241
|
+
* @returns 之前是否注册过该路由(false 表示不存在, 不会报错)
|
|
242
|
+
*/
|
|
243
|
+
offRequest(service: string, action: string): boolean;
|
|
244
|
+
/** 注册 push 处理器,支持同一路由多个订阅者 */
|
|
245
|
+
onPush(service: string, action: string, handler: PushHandler): void;
|
|
246
|
+
/**
|
|
247
|
+
* 注销 push 处理器:
|
|
248
|
+
* - 传入 handler: 仅移除该具体 handler(若注册过多次会全部移除, 与 EventEmitter.off 习惯一致)
|
|
249
|
+
* - 不传 handler: 移除该路由下所有 handler
|
|
250
|
+
* @returns 实际被移除的 handler 数量(0 表示无对应注册)
|
|
251
|
+
*/
|
|
252
|
+
offPush(service: string, action: string, handler?: PushHandler): number;
|
|
253
|
+
private ensurePeerConnected;
|
|
254
|
+
/** 处理客户端连接成功 */
|
|
255
|
+
private onOutboundConnected;
|
|
256
|
+
private onOutboundDisconnected;
|
|
257
|
+
private onOutboundMessage;
|
|
258
|
+
/** 处理客户端消息 */
|
|
259
|
+
private onInboundMessage;
|
|
260
|
+
/** 处理客户端断开连接 */
|
|
261
|
+
private onInboundDisconnected;
|
|
262
|
+
/** 安装或仲裁客户端连接 */
|
|
263
|
+
private installOrArbitrateChannel;
|
|
264
|
+
/** 是否保持 outbound 连接 */
|
|
265
|
+
private shouldKeepOutbound;
|
|
266
|
+
private onPacket;
|
|
267
|
+
private handleHello;
|
|
268
|
+
private handleRequest;
|
|
269
|
+
private handleResponse;
|
|
270
|
+
private handlePush;
|
|
271
|
+
private sendResponse;
|
|
272
|
+
private replyPong;
|
|
273
|
+
private startHeartbeat;
|
|
274
|
+
/**
|
|
275
|
+
* 断线后调度重连。
|
|
276
|
+
* 规则:
|
|
277
|
+
* 1) 只有"持有对端 URL"的一侧才会重连(即作为客户端发起方),
|
|
278
|
+
* 服务端只是被动承接,不会去追客户端,避免对端不可达时一直占用资源。
|
|
279
|
+
* 2) 采用指数退避:reconnectIntervalMs * 2 * 2 * ...,封顶 reconnectMaxIntervalMs,
|
|
280
|
+
* 连接成功后由 onOutboundConnected 清空 reconnectDelays 计数恢复初始间隔。
|
|
281
|
+
*/
|
|
282
|
+
private scheduleReconnect;
|
|
283
|
+
/**
|
|
284
|
+
* 心跳判死后清理链路。
|
|
285
|
+
* 注意:channel.close() 内部走的是 client.disconnect()/server.disconnectClient(),
|
|
286
|
+
* 为防止重复进入正常断开回调流程,这里需要手动完成 peerChannels 清理与重连调度。
|
|
287
|
+
* - outbound 死链:client.disconnect() 已抹掉监听不会回调,需要主动 scheduleReconnect。
|
|
288
|
+
* - inbound 死链:server 的 close 事件可能仍会触发 onInboundDisconnected,
|
|
289
|
+
* 但由于已先一步删除 inboundLinkPeerMap 项,回调会因找不到 peerId 直接返回。
|
|
290
|
+
*/
|
|
291
|
+
private handleChannelDead;
|
|
292
|
+
private parsePacket;
|
|
293
|
+
private sendBySocket;
|
|
294
|
+
private sendByClient;
|
|
295
|
+
private makeRouteKey;
|
|
296
|
+
private buildUrlWithToken;
|
|
297
|
+
private genTraceId;
|
|
298
|
+
private logInfo;
|
|
299
|
+
private emitError;
|
|
300
|
+
}
|
|
301
|
+
export {};
|