真机联调发现:服务器实发单层 {app,route,rpc,data};旧客户端的'外层 data'是浏览器
MessageEvent(00_minhttp.js:266 ws.onmessage=config.onmessage),非协议层。框架传输层
已取 ev.data,decodeFrame 不应再剥一层。修正后 kick_server 等业务包能正确解析。
- decodeFrame 单层化 + 测试改单层帧
- 更正 docs/protocol/01 §3.2(双层→单层,附 MessageEvent 依据)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
164 lines
6.2 KiB
TypeScript
164 lines
6.2 KiB
TypeScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { NetClient } from '../../YouleNexus/assets/framework/net/net-client.ts';
|
||
import { FakeTransport } from '../helpers/fake-transport.ts';
|
||
import type { Clock } from '../../YouleNexus/assets/framework/net/heartbeat.ts';
|
||
|
||
function fakeClock() {
|
||
let seq = 0, now = 0;
|
||
const timers = new Map<number, { fn: () => void; at: number }>();
|
||
const clock: Clock = {
|
||
setTimeout: (fn, ms) => { const id = ++seq; timers.set(id, { fn, at: now + ms }); return id; },
|
||
clearTimeout: (id: any) => { timers.delete(id); },
|
||
};
|
||
const advance = (ms: number) => { now += ms; for (const [id, t] of [...timers]) if (t.at <= now) { timers.delete(id); t.fn(); } };
|
||
return { clock, advance };
|
||
}
|
||
|
||
const IDENTITY = { agentid: 1, gameid: 2, openid: 'o', nickname: 'n', avatar: 'a', sex: 0, province: '', city: '', unionid: 'u', version: '1', channelid: 0, marketid: 0 };
|
||
|
||
function makeClient(transports: FakeTransport[], clock: Clock) {
|
||
let i = 0;
|
||
const client = new NetClient({
|
||
servers: 'ws://a',
|
||
transportFactory: () => transports[i++] ?? transports[transports.length - 1],
|
||
clock,
|
||
});
|
||
client.setIdentity(IDENTITY);
|
||
return client;
|
||
}
|
||
|
||
test('onOpen 后自动发 player_login(单层信封)', async () => {
|
||
const t = new FakeTransport();
|
||
const { clock } = fakeClock();
|
||
const client = makeClient([t], clock);
|
||
client.start();
|
||
await t.flush();
|
||
assert.equal(t.sent.length, 1);
|
||
const env = JSON.parse(t.sent[0]);
|
||
assert.equal(env.app, 'youle');
|
||
assert.equal(env.route, 'agent');
|
||
assert.equal(env.rpc, 'player_login');
|
||
assert.equal(env.data.openid, 'o');
|
||
});
|
||
|
||
test('isSendLoginState 门控:login 响应前的其它包被丢弃,login 响应放行并 emit login', async () => {
|
||
const t = new FakeTransport();
|
||
const { clock } = fakeClock();
|
||
const client = makeClient([t], clock);
|
||
const messages: any[] = [];
|
||
let loginResp: any = null;
|
||
client.on('message', (m: any) => messages.push(m));
|
||
client.on('login', (d: any) => { loginResp = d; });
|
||
client.start();
|
||
await t.flush();
|
||
// 登录响应前推一个业务包 → 应被门控丢弃
|
||
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }));
|
||
await t.flush();
|
||
assert.equal(messages.length, 0);
|
||
// 推 login 响应 → 放行,emit login,清门控
|
||
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
|
||
await t.flush();
|
||
assert.equal(loginResp.playerid, 9);
|
||
// 门控已清,后续业务包放行
|
||
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }));
|
||
await t.flush();
|
||
assert.equal(messages.length, 1);
|
||
assert.equal(messages[0].rpc, 'update_bean');
|
||
});
|
||
|
||
test('握手包与心跳包被忽略且不进 message', async () => {
|
||
const t = new FakeTransport();
|
||
const { clock } = fakeClock();
|
||
const client = makeClient([t], clock);
|
||
const messages: any[] = [];
|
||
client.on('message', (m: any) => messages.push(m));
|
||
client.start();
|
||
await t.flush();
|
||
// 先完成登录以清门控
|
||
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
|
||
await t.flush();
|
||
t.serverPush('@toconconABC'); // 握手(原始串)
|
||
t.serverPush(JSON.stringify({ com: '@serverheartbeat' })); // 心跳(单层)
|
||
await t.flush();
|
||
assert.equal(messages.length, 0);
|
||
});
|
||
|
||
test('收包超时 emit slow 并触发重连(onClose→新连接重发 login)', async () => {
|
||
const t1 = new FakeTransport(), t2 = new FakeTransport();
|
||
const { clock, advance } = fakeClock();
|
||
const client = makeClient([t1, t2], clock);
|
||
let slow = 0;
|
||
client.on('slow', () => { slow++; });
|
||
client.start();
|
||
await t1.flush();
|
||
// 先完成登录:清 4s 守护、feed 收包看门狗(否则 advance 会先撞上 loginGuard 而非 watchdog)
|
||
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
|
||
await t1.flush();
|
||
// 30s 无收包 → watchdog → slow + 关闭当前连接
|
||
advance(30000);
|
||
await t1.flush();
|
||
assert.equal(slow, 1);
|
||
// 重连定时器到点 → 新连接 t2 建立并重发 login
|
||
advance(10000);
|
||
await t2.flush();
|
||
assert.equal(t2.sent.length, 1);
|
||
assert.equal(JSON.parse(t2.sent[0]).rpc, 'player_login');
|
||
});
|
||
|
||
test('TcpID 去重:旧连接的残留包被丢弃', async () => {
|
||
const t1 = new FakeTransport(), t2 = new FakeTransport();
|
||
const { clock, advance } = fakeClock();
|
||
const client = makeClient([t1, t2], clock);
|
||
const messages: any[] = [];
|
||
client.on('message', (m: any) => messages.push(m));
|
||
client.start();
|
||
await t1.flush();
|
||
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
|
||
await t1.flush();
|
||
// 触发重连切到 t2
|
||
advance(30000); await t1.flush(); advance(10000); await t2.flush();
|
||
// 旧连接 t1 仍推包 → 应被 TcpID 去重丢弃
|
||
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 1 } }));
|
||
await t1.flush();
|
||
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({ 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({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
|
||
await t.flush();
|
||
assert.equal(logs.length, 0);
|
||
});
|