feat(framework): net NetClient 可选 debug 收发包日志
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,8 @@ export interface NetClientOptions {
|
|||||||
transportFactory: () => Transport;
|
transportFactory: () => Transport;
|
||||||
clock?: Clock;
|
clock?: Clock;
|
||||||
bus?: EventBus<NetClientEvents>;
|
bus?: EventBus<NetClientEvents>;
|
||||||
|
debug?: boolean; // 为真时打印每帧 send/recv
|
||||||
|
logger?: (...args: unknown[]) => void; // 日志函数,默认 console.log(便于测试注入)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 网络客户端:编排 transport/codec/heartbeat/reconnect,事件驱动,忠实 docs/protocol/01。 */
|
/** 网络客户端:编排 transport/codec/heartbeat/reconnect,事件驱动,忠实 docs/protocol/01。 */
|
||||||
@@ -41,6 +43,8 @@ export class NetClient {
|
|||||||
private loginGuard: unknown = null;
|
private loginGuard: unknown = null;
|
||||||
private reconnectTimer: unknown = null;
|
private reconnectTimer: unknown = null;
|
||||||
private stopped = false;
|
private stopped = false;
|
||||||
|
private debug: boolean;
|
||||||
|
private logger: (...args: unknown[]) => void;
|
||||||
|
|
||||||
constructor(opts: NetClientOptions) {
|
constructor(opts: NetClientOptions) {
|
||||||
this.bus = opts.bus ?? new EventBus<NetClientEvents>();
|
this.bus = opts.bus ?? new EventBus<NetClientEvents>();
|
||||||
@@ -48,6 +52,8 @@ export class NetClient {
|
|||||||
this.clock = opts.clock ?? realClock;
|
this.clock = opts.clock ?? realClock;
|
||||||
this.policy = new ReconnectPolicy(opts.servers, SERVER_ROTATE_EVERY);
|
this.policy = new ReconnectPolicy(opts.servers, SERVER_ROTATE_EVERY);
|
||||||
this.watchdog = new HeartbeatWatchdog(RECV_TIMEOUT_MS, () => this.onRecvTimeout(), this.clock);
|
this.watchdog = new HeartbeatWatchdog(RECV_TIMEOUT_MS, () => this.onRecvTimeout(), this.clock);
|
||||||
|
this.debug = opts.debug ?? false;
|
||||||
|
this.logger = opts.logger ?? console.log;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIdentity(identity: LoginRequestData): void { this.identity = identity; }
|
setIdentity(identity: LoginRequestData): void { this.identity = identity; }
|
||||||
@@ -66,7 +72,13 @@ export class NetClient {
|
|||||||
|
|
||||||
/** 发业务包(单层信封)。 */
|
/** 发业务包(单层信封)。 */
|
||||||
send(route: string, rpc: string, data: unknown): void {
|
send(route: string, rpc: string, data: unknown): void {
|
||||||
this.transport?.send(encodeOutbound(route, rpc, data));
|
this.sendFrame(encodeOutbound(route, rpc, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统一发送切面:debug 时打印出站帧。 */
|
||||||
|
private sendFrame(frame: string): void {
|
||||||
|
if (this.debug) this.logger('[net] →', frame);
|
||||||
|
this.transport?.send(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
private connect(url: string): void {
|
private connect(url: string): void {
|
||||||
@@ -87,12 +99,13 @@ export class NetClient {
|
|||||||
private sendLogin(): void {
|
private sendLogin(): void {
|
||||||
if (!this.identity) return; // 无身份不发(docs §6.1:首登需身份)
|
if (!this.identity) return; // 无身份不发(docs §6.1:首登需身份)
|
||||||
this.isSendLoginState = true;
|
this.isSendLoginState = true;
|
||||||
this.transport?.send(encodeOutbound(Route.agent, 'player_login', this.identity));
|
this.sendFrame(encodeOutbound(Route.agent, 'player_login', this.identity));
|
||||||
if (this.loginGuard != null) this.clock.clearTimeout(this.loginGuard);
|
if (this.loginGuard != null) this.clock.clearTimeout(this.loginGuard);
|
||||||
this.loginGuard = this.clock.setTimeout(() => { this.loginGuard = null; this.onLoginGuardTimeout(); }, LOGIN_GUARD_MS); // docs §6.2
|
this.loginGuard = this.clock.setTimeout(() => { this.loginGuard = null; this.onLoginGuardTimeout(); }, LOGIN_GUARD_MS); // docs §6.2
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMessage(frame: string): void {
|
private onMessage(frame: string): void {
|
||||||
|
if (this.debug) this.logger('[net] ←', frame);
|
||||||
const r = decodeFrame(frame);
|
const r = decodeFrame(frame);
|
||||||
if (r.kind === 'ignore') return;
|
if (r.kind === 'ignore') return;
|
||||||
this.watchdog.feed(); // 收任意有效帧重置看门狗 docs §4.1
|
this.watchdog.feed(); // 收任意有效帧重置看门狗 docs §4.1
|
||||||
|
|||||||
@@ -123,3 +123,41 @@ test('TcpID 去重:旧连接的残留包被丢弃', async () => {
|
|||||||
await t1.flush();
|
await t1.flush();
|
||||||
assert.equal(messages.length, 0);
|
assert.equal(messages.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('debug=true 记录 send 与 recv(注入 logger)', async () => {
|
||||||
|
const t = new FakeTransport();
|
||||||
|
const { clock } = fakeClock();
|
||||||
|
const logs: unknown[][] = [];
|
||||||
|
const client = new NetClient({
|
||||||
|
servers: 'ws://a',
|
||||||
|
transportFactory: () => t,
|
||||||
|
clock,
|
||||||
|
debug: true,
|
||||||
|
logger: (...a: unknown[]) => logs.push(a),
|
||||||
|
});
|
||||||
|
client.setIdentity(IDENTITY);
|
||||||
|
client.start();
|
||||||
|
await t.flush();
|
||||||
|
assert.ok(logs.some((l) => l[0] === '[net] →')); // 发出 player_login 被记录
|
||||||
|
t.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
|
||||||
|
await t.flush();
|
||||||
|
assert.ok(logs.some((l) => l[0] === '[net] ←')); // 收包被记录
|
||||||
|
});
|
||||||
|
|
||||||
|
test('debug 默认关闭:logger 不被调用', async () => {
|
||||||
|
const t = new FakeTransport();
|
||||||
|
const { clock } = fakeClock();
|
||||||
|
const logs: unknown[][] = [];
|
||||||
|
const client = new NetClient({
|
||||||
|
servers: 'ws://a',
|
||||||
|
transportFactory: () => t,
|
||||||
|
clock,
|
||||||
|
logger: (...a: unknown[]) => logs.push(a),
|
||||||
|
});
|
||||||
|
client.setIdentity(IDENTITY);
|
||||||
|
client.start();
|
||||||
|
await t.flush();
|
||||||
|
t.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
|
||||||
|
await t.flush();
|
||||||
|
assert.equal(logs.length, 0);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user