Files
erqiwang_youle/server/docs/development-guide/03-数据收发与通信协议.md
T

171 lines
7.7 KiB
Markdown
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.
# 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>