171 lines
7.7 KiB
Markdown
171 lines
7.7 KiB
Markdown
# 03 · 数据收发与通信协议
|
||
|
||
本篇是**日常手册**:数据包长什么样、收包要做哪些固定步骤、三种发包方式怎么选、为什么"主动推送"是唯一可靠的下发通道,以及本项目最重要的一条——**成败标志只认 `data.success`**。
|
||
|
||
---
|
||
|
||
## 1. 数据包结构
|
||
|
||
所有前后端数据包统一四字段:
|
||
|
||
```js
|
||
{
|
||
app: "youle", // 应用名(游戏固定 "youle")
|
||
route: "<你的游戏>", // 路由名 = 模块 routename
|
||
rpc: "playCard", // 方法名,决定调到 mod 的哪个函数 / 前端走哪个解析分支
|
||
data: { /* 业务数据 */ }
|
||
}
|
||
```
|
||
|
||
- `app/route/rpc` 三字段驱动三层路由(见 01)。
|
||
- `data` 里**必含平台参数**:`agentid`、`playerid`、`gameid`、`roomcode`、`seat` 等;数值参数收包时要 `parseInt`。
|
||
- **一包多信息**:一个响应包应携带本次状态变更的完整信息(出了什么牌、轮到谁、各家剩余、倒计时、比分……),减少往返。
|
||
|
||
---
|
||
|
||
## 2. 收包处理的固定步骤
|
||
|
||
每个 RPC handler 开头都是同一套"安检流程",**不可省略、不可简化**:
|
||
|
||
```js
|
||
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 下发——**这两个连接字段不需要你手工设置**,只要座位上有在线玩家即可。
|
||
|
||
### 差异化广播:棋牌最常用
|
||
|
||
牌类游戏里"同一动作、各家可见不同",所以广播时为每个座位**深拷贝一份基础数据再定制**:
|
||
|
||
```js
|
||
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` 码判成败"的写法。**以本协议为准。**
|
||
|
||
### 规则
|
||
|
||
1. **成败唯一权威字段是 `data.success`(boolean)**。前端一律 `if (!data.success)` 判断操作成败。
|
||
2. **禁止用 `data.status`(如 `status === 200`)或 `data.code` 判成败**。`status`/`code` 只能作展示/日志用的细分信息,不参与成败裁定。
|
||
3. **主动推送必须自带 `success`**:因为 `return` 不下发前端,凡有成败语义的**推送包**,其 `data` 必须显式带 `success: true/false`,不能只放 `status`。
|
||
4. **禁止双轨/兼容兜底**:前端不得写 `status !== 200 && !success` 之类的 `status` 兜底;新增/改动的推送一律补齐 `success`,前端一律只认 `success`。
|
||
|
||
### 正反例
|
||
|
||
```js
|
||
// ❌ 错误:推送只带 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);
|
||
```
|
||
|
||
```js
|
||
// 前端:只认 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-开发规范与红线](./04-开发规范与红线.md) 汇总所有必须遵守的工程纪律。
|
||
</content>
|