Files
youle_cocos/cocoscreator_projects/framework-tests/net/net-client.test.ts
T
joywayerandClaude Opus 4.8 96c6c7c914 fix(framework): decodeFrame 改单层(服务器协议非双层)
真机联调发现:服务器实发单层 {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>
2026-06-28 20:25:36 +08:00

164 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});