7.7 KiB
03 · 数据收发与通信协议
本篇是日常手册:数据包长什么样、收包要做哪些固定步骤、三种发包方式怎么选、为什么"主动推送"是唯一可靠的下发通道,以及本项目最重要的一条——成败标志只认 data.success。
1. 数据包结构
所有前后端数据包统一四字段:
{
app: "youle", // 应用名(游戏固定 "youle")
route: "<你的游戏>", // 路由名 = 模块 routename
rpc: "playCard", // 方法名,决定调到 mod 的哪个函数 / 前端走哪个解析分支
data: { /* 业务数据 */ }
}
app/route/rpc三字段驱动三层路由(见 01)。data里必含平台参数:agentid、playerid、gameid、roomcode、seat等;数值参数收包时要parseInt。- 一包多信息:一个响应包应携带本次状态变更的完整信息(出了什么牌、轮到谁、各家剩余、倒计时、比分……),减少往返。
2. 收包处理的固定步骤
每个 RPC handler 开头都是同一套"安检流程",不可省略、不可简化:
mod_<游戏>.playCard = function(pack) {
// 1) 提取参数(数值 parseInt)
var agentid = pack.data.agentid;
var playerid = parseInt(pack.data.playerid);
var gameid = pack.data.gameid;
var roomcode = parseInt(pack.data.roomcode);
var seat = parseInt(pack.data.seat);
// 2) 校验玩家与房间(必做)——失败静默 return
var o_room = mod_<游戏>.import.check_player(
agentid, gameid, roomcode, seat, playerid, pack.conmode, pack.fromid);
if (!o_room) return;
// 3) 取桌对象与对局状态
var o_desk = o_room.o_desk;
if (!o_desk) return;
// 4) 业务校验(游戏阶段?轮到该座?操作合法?)——任一不过即 return
// 5) 执行业务(委托权威模块)
// 6) 构建并【主动推送】结果(见下文)
};
check_player是强制安检:它校验座位、连接、身份,返回o_room或null。返回null一律直接return。- 校验失败静默:不要回 "你作弊了" 之类提示,直接
return,避免给作弊者反馈。 - 调试记录(若框架提供
o_desk.debug.save_receivepack等)在关键节点调用,便于复盘。
3. 三种发包方式
| 方式 | 接口 | 用于 |
|---|---|---|
| 点对点 | o_room.method.sendpack_toseat(msg, seat) |
只发给某个座位(个人状态、定向数据) |
| 广播其他人 | o_room.method.sendpack_toother(msg, seat) |
发给除 seat 外所有人(seat=-1 即全发) |
| 差异化广播 | 逐座位组 msg 后各自 sendpack_toseat |
每个玩家看到的内容不同(如手牌只发本人) |
平台发包内部会从 seatlist[seat] 上取 conmode/fromid 填入包,再交给底层 SendPack 按 TCP/HTTP 下发——这两个连接字段不需要你手工设置,只要座位上有在线玩家即可。
差异化广播:棋牌最常用
牌类游戏里"同一动作、各家可见不同",所以广播时为每个座位深拷贝一份基础数据再定制:
for (var seat = 0; seat < o_room.seatlist.length; seat++) {
if (!o_room.seatlist[seat]) continue; // 空座跳过
var msg = {
app: "youle", route: "<游戏>", rpc: "playCard",
data: deepCopy(baseData) // 公共信息
};
if (seat === actionSeat) {
msg.data.handCards = hands[seat]; // 仅本人可见手牌
} else {
msg.data.handCards = []; // 他人看不到
}
o_room.method.sendpack_toseat(msg, seat);
}
敏感信息(手牌、暗牌、未公开的判定)只发给该看到的人,这是服务器权威的一部分。
4. 主动推送是唯一可靠的下发通道
前端收包的唯一通道是服务端主动推送(sendpack_toseat / toother → 底层 SendPack)。
而 RPC 方法的 return 值不可靠下发前端:框架虽会对 DoPack 的返回值做一次回发,但它只回当前请求连接、不按座位定向,浏览器/友乐链路下不能当作状态推送通道(机制见 01 §4)。
推论:凡需要让前端看到的结果(包括操作的成功/失败),都必须放进主动推送的 data 里。
5. 成败标志协议(本项目最重要的一条)
这条覆盖并修正了早期文档里"用 HTTP 风格
status码判成败"的写法。以本协议为准。
规则
- 成败唯一权威字段是
data.success(boolean)。前端一律if (!data.success)判断操作成败。 - 禁止用
data.status(如status === 200)或data.code判成败。status/code只能作展示/日志用的细分信息,不参与成败裁定。 - 主动推送必须自带
success:因为return不下发前端,凡有成败语义的推送包,其data必须显式带success: true/false,不能只放status。 - 禁止双轨/兼容兜底:前端不得写
status !== 200 && !success之类的status兜底;新增/改动的推送一律补齐success,前端一律只认success。
正反例
// ❌ 错误:推送只带 status,前端读 data.success 恒 undefined → 误判失败
o_room.method.sendpack_toseat({
app:"youle", route:"<游戏>", rpc:"setHostingState",
data: { status: 200, hosting: true } // 少了 success
}, seat);
// ✅ 正确:成败语义放 success,status 仅作细分
o_room.method.sendpack_toseat({
app:"youle", route:"<游戏>", rpc:"setHostingState",
data: { success: true, status: 200, hosting: true }
}, seat);
// 前端:只认 success
if (!data.success) { /* 失败处理 */ return; }
// data.status / data.code 仅用于展示或日志细分
典型事故:托管状态推送
{ status: 200, ... }不带success,前端读data.success恒为undefined,于是"已进入托管却报失败"。根因就是违反了本协议第 3 条。
6. 前后端对接的固定接缝
下发结构变了,对应的前端解析接口必须同步检查(前端平台代码与受限接口文件不可随意改,见 04):
| 场景 | 服务端产出 | 前端接收接口 |
|---|---|---|
| 开战 | export.makewar 的返回包 |
Game_Modify.StartWar(makewar 返回值) |
| 重连/中途加入 | export.get_deskinfo 的返回 |
Game_Modify.Reconnect(get_deskinfo 返回值) |
| 对局操作 | RPC handler 的主动推送(按 rpc 区分) |
前端对应 rpc 的解析分支 |
改服务端下发 = 同步核对前端解析,否则前端按旧结构解析必然出错。
7. 服务端代替玩家操作时的透明性
服务端自动替玩家执行的操作(如 AI 托管出牌/碰杠胡),必须复用真人操作的同一套数据包与广播链路:产生的包结构、rpc、data 与真人操作完全一致,前端无需也禁止为它单开一套接收/解析分支。唯一区别只在触发源(前端请求 → 服务端决策),其后的数据组织、协议、广播路径不变。(详见 04 的对应红线。)
8. 小结
- 包结构恒为
{app, route, rpc, data};收包先check_player,失败静默return。 - 三种发包方式按"谁该看到什么"选;差异化广播逐座位定制,敏感信息只发本人。
- 主动推送是唯一可靠下发通道,
return不算。 - 成败只认
data.success,推送必自带success,禁止status/code判成败与兼容兜底。 - 改下发结构必同步核对前端
StartWar/Reconnect/对应rpc解析。
下一篇 04-开发规范与红线 汇总所有必须遵守的工程纪律。