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>
This commit is contained in:
@@ -13,31 +13,26 @@ export type DecodeResult =
|
||||
| { kind: 'message'; message: InboundMessage } // 正常业务内层
|
||||
| { kind: 'ignore' }; // 无法解析,安全忽略
|
||||
|
||||
function parseMaybe(v: unknown): unknown {
|
||||
return typeof v === 'string' ? JSON.parse(v) : v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解一帧原始字符串为分类结果。docs/protocol/01 §3.2 步骤 3–8。
|
||||
* 解一帧为分类结果。**服务器协议是单层 `{route, rpc, data}`**(可能带 `app`)。
|
||||
* 入参 frame 已是服务器实发帧——传输层已取 `MessageEvent.data`(旧 12_Logic.js 里被当成
|
||||
* "外层 data" 的其实是浏览器 WebSocket MessageEvent 这个壳,非协议层)。故此处不再剥 `.data`。
|
||||
* 不做 TcpID/isSendLoginState/submit_error 过滤(那是连接级,见 net-client)。
|
||||
*/
|
||||
export function decodeFrame(frame: string): DecodeResult {
|
||||
let outer: any;
|
||||
try { outer = JSON.parse(frame); } catch { return { kind: 'ignore' }; }
|
||||
let data = outer?.data;
|
||||
// 特殊串按原始帧判定(与旧 12_Logic.js 对 MessageEvent.data 的判断一致)。
|
||||
if (frame === 'webserve-服务器未工作') return { kind: 'serverDown' };
|
||||
if (frame.substr(0, 9) === '@toconcon') return { kind: 'handshake' };
|
||||
|
||||
if (data === 'webserve-服务器未工作') return { kind: 'serverDown' };
|
||||
let msg: any;
|
||||
try { msg = JSON.parse(frame); } catch { return { kind: 'ignore' }; }
|
||||
// 兼容服务器偶发"再包一层字符串"(旧源码对每层都做 typeof=='string' 再 parse)。
|
||||
if (typeof msg === 'string') {
|
||||
try { msg = JSON.parse(msg); } catch { return { kind: 'ignore' }; }
|
||||
}
|
||||
if (!msg || typeof msg !== 'object') return { kind: 'ignore' };
|
||||
|
||||
if (typeof data === 'string') {
|
||||
if (data.substr(0, 9) === '@toconcon') return { kind: 'handshake' };
|
||||
try { data = JSON.parse(data); } catch { return { kind: 'ignore' }; }
|
||||
}
|
||||
if (data && typeof data === 'object' && (data as any).com === '@serverheartbeat') {
|
||||
return { kind: 'heartbeat' };
|
||||
}
|
||||
const inner = parseMaybe(data) as InboundMessage | undefined;
|
||||
if (inner && typeof inner === 'object' && 'route' in inner && 'rpc' in inner) {
|
||||
return { kind: 'message', message: inner };
|
||||
}
|
||||
if ((msg as any).com === '@serverheartbeat') return { kind: 'heartbeat' };
|
||||
if ('route' in msg && 'rpc' in msg) return { kind: 'message', message: msg as InboundMessage };
|
||||
return { kind: 'ignore' };
|
||||
}
|
||||
|
||||
@@ -21,18 +21,18 @@ test('login 全流程:握手→登录→业务包,且 parseLoginResponse 识
|
||||
client.start();
|
||||
await t.flush();
|
||||
|
||||
// 服务器先发握手包(应被忽略)
|
||||
t.serverPush(JSON.stringify({ data: '@toconconHELLO' }));
|
||||
// 服务器先发握手包(原始串,应被忽略)
|
||||
t.serverPush('@toconconHELLO');
|
||||
await t.flush();
|
||||
|
||||
// 客户端应已发出 player_login
|
||||
assert.equal(JSON.parse(t.sent[0]).rpc, 'player_login');
|
||||
|
||||
// 服务器回 login 响应(带房间恢复 + 对局)
|
||||
t.serverPush(JSON.stringify({ data: JSON.stringify({
|
||||
// 服务器回 login 响应(单层,带房间恢复 + 对局)
|
||||
t.serverPush(JSON.stringify({
|
||||
route: 'agent', rpc: 'player_login',
|
||||
data: { state: 0, playerid: 42, bean: 500, roomcode: 'ROOM1', isbattle: 1, deskinfo: { round: 3 } },
|
||||
}) }));
|
||||
}));
|
||||
await t.flush();
|
||||
|
||||
assert.ok(loginData);
|
||||
@@ -43,10 +43,10 @@ test('login 全流程:握手→登录→业务包,且 parseLoginResponse 识
|
||||
assert.equal(parsed.hasBattle, true);
|
||||
assert.deepEqual(parsed.deskinfo, { round: 3 });
|
||||
|
||||
// 心跳包(忽略,不进 business)
|
||||
t.serverPush(JSON.stringify({ data: JSON.stringify({ com: '@serverheartbeat' }) }));
|
||||
// 正常业务推送(门控已清,放行)
|
||||
t.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'room', rpc: 'other_join_room', data: { seat: 2 } }) }));
|
||||
// 心跳包(单层,忽略,不进 business)
|
||||
t.serverPush(JSON.stringify({ com: '@serverheartbeat' }));
|
||||
// 正常业务推送(单层,门控已清,放行)
|
||||
t.serverPush(JSON.stringify({ route: 'room', rpc: 'other_join_room', data: { seat: 2 } }));
|
||||
await t.flush();
|
||||
|
||||
assert.equal(business.length, 1);
|
||||
|
||||
@@ -7,29 +7,34 @@ test('encodeOutbound 产出单层信封字符串', () => {
|
||||
assert.deepEqual(JSON.parse(s), { app: 'youle', route: 'agent', rpc: 'player_login', data: { openid: 'o' } });
|
||||
});
|
||||
|
||||
// 服务器协议为单层:frame 即服务器实发帧(传输层已取 MessageEvent.data)。
|
||||
|
||||
test('decodeFrame:握手包 @toconcon 识别为 handshake', () => {
|
||||
const frame = JSON.stringify({ data: '@toconconXYZ...' });
|
||||
assert.deepEqual(decodeFrame(frame), { kind: 'handshake' });
|
||||
assert.deepEqual(decodeFrame('@toconconXYZ...'), { kind: 'handshake' });
|
||||
});
|
||||
|
||||
test('decodeFrame:心跳包 @serverheartbeat 识别为 heartbeat', () => {
|
||||
const frame = JSON.stringify({ data: JSON.stringify({ com: '@serverheartbeat' }) });
|
||||
const frame = JSON.stringify({ com: '@serverheartbeat' });
|
||||
assert.deepEqual(decodeFrame(frame), { kind: 'heartbeat' });
|
||||
});
|
||||
|
||||
test('decodeFrame:特殊错误包 webserve-服务器未工作', () => {
|
||||
const frame = JSON.stringify({ data: 'webserve-服务器未工作' });
|
||||
assert.deepEqual(decodeFrame(frame), { kind: 'serverDown' });
|
||||
assert.deepEqual(decodeFrame('webserve-服务器未工作'), { kind: 'serverDown' });
|
||||
});
|
||||
|
||||
test('decodeFrame:正常业务包解出内层 {route,rpc,data}', () => {
|
||||
test('decodeFrame:正常业务包(单层)解出 {route,rpc,data}', () => {
|
||||
const inner = { route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } };
|
||||
const frame = JSON.stringify({ data: JSON.stringify(inner) });
|
||||
assert.deepEqual(decodeFrame(frame), { kind: 'message', message: inner });
|
||||
assert.deepEqual(decodeFrame(JSON.stringify(inner)), { kind: 'message', message: inner });
|
||||
});
|
||||
|
||||
test('decodeFrame:内层为对象(非字符串)也能解', () => {
|
||||
const inner = { route: 'room', rpc: 'other_join_room', data: { seat: 1 } };
|
||||
const frame = JSON.stringify({ data: inner });
|
||||
assert.deepEqual(decodeFrame(frame), { kind: 'message', message: inner });
|
||||
test('decodeFrame:带 app 字段的单层业务包(服务器实测格式)', () => {
|
||||
const frame = JSON.stringify({ app: 'youle', route: 'agent', rpc: 'kick_server', data: { msg: '检查到新版本' } });
|
||||
const r = decodeFrame(frame);
|
||||
assert.equal(r.kind, 'message');
|
||||
assert.equal(r.kind === 'message' && r.message.rpc, 'kick_server');
|
||||
});
|
||||
|
||||
test('decodeFrame:无法解析的帧安全忽略', () => {
|
||||
assert.deepEqual(decodeFrame('not json'), { kind: 'ignore' });
|
||||
assert.deepEqual(decodeFrame(JSON.stringify({ foo: 1 })), { kind: 'ignore' });
|
||||
});
|
||||
|
||||
@@ -53,15 +53,15 @@ test('isSendLoginState 门控:login 响应前的其它包被丢弃,login 响
|
||||
client.start();
|
||||
await t.flush();
|
||||
// 登录响应前推一个业务包 → 应被门控丢弃
|
||||
t.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }) }));
|
||||
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({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
|
||||
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({ data: JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 5 } }) }));
|
||||
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');
|
||||
@@ -76,10 +76,10 @@ test('握手包与心跳包被忽略且不进 message', async () => {
|
||||
client.start();
|
||||
await t.flush();
|
||||
// 先完成登录以清门控
|
||||
t.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
|
||||
t.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
|
||||
await t.flush();
|
||||
t.serverPush(JSON.stringify({ data: '@toconconABC' })); // 握手
|
||||
t.serverPush(JSON.stringify({ data: JSON.stringify({ com: '@serverheartbeat' }) })); // 心跳
|
||||
t.serverPush('@toconconABC'); // 握手(原始串)
|
||||
t.serverPush(JSON.stringify({ com: '@serverheartbeat' })); // 心跳(单层)
|
||||
await t.flush();
|
||||
assert.equal(messages.length, 0);
|
||||
});
|
||||
@@ -93,7 +93,7 @@ test('收包超时 emit slow 并触发重连(onClose→新连接重发 login
|
||||
client.start();
|
||||
await t1.flush();
|
||||
// 先完成登录:清 4s 守护、feed 收包看门狗(否则 advance 会先撞上 loginGuard 而非 watchdog)
|
||||
t1.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
|
||||
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }));
|
||||
await t1.flush();
|
||||
// 30s 无收包 → watchdog → slow + 关闭当前连接
|
||||
advance(30000);
|
||||
@@ -114,12 +114,12 @@ test('TcpID 去重:旧连接的残留包被丢弃', async () => {
|
||||
client.on('message', (m: any) => messages.push(m));
|
||||
client.start();
|
||||
await t1.flush();
|
||||
t1.serverPush(JSON.stringify({ data: JSON.stringify({ route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }) }));
|
||||
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({ data: JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 1 } }) }));
|
||||
t1.serverPush(JSON.stringify({ route: 'agent', rpc: 'update_bean', data: { bean: 1 } }));
|
||||
await t1.flush();
|
||||
assert.equal(messages.length, 0);
|
||||
});
|
||||
@@ -139,7 +139,7 @@ test('debug=true 记录 send 与 recv(注入 logger)', async () => {
|
||||
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 } }) }));
|
||||
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] ←')); // 收包被记录
|
||||
});
|
||||
@@ -157,7 +157,7 @@ test('debug 默认关闭:logger 不被调用', async () => {
|
||||
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 } }) }));
|
||||
t.serverPush(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