初始化仓库:友乐/gameabc 房卡游戏平台脚手架(client + server + docs)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
# 03 · 数据收发与通信协议
|
||||
|
||||
本篇是**日常手册**:数据包长什么样、收包要做哪些固定步骤、三种发包方式怎么选、为什么"主动推送"是唯一可靠的下发通道,以及本项目最重要的一条——**成败标志只认 `data.success`**。
|
||||
|
||||
> **命名说明**:本篇代码里的业务 `rpc` 名(`playCard` 等)与 handler/类/函数名(`BroadcastManager`、`deepCopy` 等)均为**示例、可自定**;只有平台接缝上的名字(包四字段、平台 API `sendpack_toseat`/`sendpack_toother`、成败字段 `data.success`)是契约。详见 [README「命名约定」](./README.md)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据包结构
|
||||
|
||||
所有前后端数据包统一四字段:
|
||||
|
||||
```js
|
||||
{
|
||||
app: "youle", // 应用名(游戏固定 "youle")
|
||||
route: "<你的游戏>", // 路由名 = 服务端 cls_mod.new 第二参 routename(见 §1.1)
|
||||
rpc: "playCard", // 方法名,决定调到 mod 的哪个函数 / 前端走哪个解析分支(见 02 §2.3)
|
||||
data: { /* 业务数据 */ }
|
||||
}
|
||||
```
|
||||
|
||||
- `app/route/rpc` 三字段驱动三层路由(见 01)。
|
||||
- `data` 里**必含平台参数**:`agentid`、`playerid`、`gameid`、`roomcode`、`seat` 等;数值参数收包时要 `parseInt`。
|
||||
- **一包多信息**:一个响应包应携带本次状态变更的完整信息(出了什么牌、轮到谁、各家剩余、倒计时、比分……),减少往返。
|
||||
|
||||
### 1.1 `route` / `routename` 从哪来、在哪定义、怎么匹配
|
||||
|
||||
`route` 决定包被投到**哪个模块**,是前后端必须对齐的字符串。它的权威定义与匹配链如下(均可在代码验证):
|
||||
|
||||
- **服务端:`routename` 的唯一定义点**是 `mod.js` 里创建模块的 `cls_mod.new(模块名, 路由名, 所属应用)` 的**第二个参数**:
|
||||
|
||||
```js
|
||||
// server/games2/<你的游戏>/mod.js
|
||||
var mod_<你的游戏> = global.mod_<你的游戏>
|
||||
|| cls_mod.new("mod_<你的游戏>", "<你的游戏>", youle_app);
|
||||
// ▲ 第一参 modname ▲ 第二参 routename(就是 route 要用的值)
|
||||
```
|
||||
|
||||
`cls_mod.new` 把第二参存进 `mod.routename`,并把模块 `push` 进 `app.modlist`(`server/class/class.mod.js`)。
|
||||
- **它是你自定义的字符串**:由你起名,只要**在应用内唯一**即可;与目录名、与模块名 `modname`(第一参,用于全局暴露 `global[modname]`、`app[modname]`)都**无强制绑定**——本项目三者恰好都叫 `jinxianmahjong` 只是约定(见 02 §2.1、01 §5)。
|
||||
- **平台按它匹配模块**:收包时 `cls_app.ReceivePack` 用 `pack.route == modlist[i].routename` 找到模块,再 `DoPack` 进第三层按 `rpc` 调方法(`server/class/class.app.js`,见 01 §4)。
|
||||
- **前端发包的 `route` 必须与它逐字一致**:前端把该值固化为常量(本项目 `codes/game/network/RpcSender.js` 里 `var ROUTE_NAME = 'jinxianmahjong'`,经 `Utl.sendData(app, route, rpc, data)` 发出)。**两端字符串不一致 → 平台匹配不到模块,包被静默丢弃**(前端也收不到任何响应)。
|
||||
- **与平台房间模块区分**:平台自带的房间模块 `routename` 是 `"room"`,处理创建/加入/开战(`createRoom`、`self_join_room`、`self_makewar` 等);**你的子游戏自定义 RPC**(`playCard` 等)走**你自己的 `routename`**。两者不要混用:平台流程发 `route:"room"`,玩法操作发 `route:"<你的游戏>"`。
|
||||
|
||||
> 一句话:**`routename` 在服务端 `cls_mod.new` 第二参定义(自定、应用内唯一),前端发包 `route` 必须与它逐字相同,平台据此把包投到你的模块**;改名要两端同步改。
|
||||
|
||||
---
|
||||
|
||||
## 2. 收包处理的固定步骤(在 handler 里)
|
||||
|
||||
`mod` 上的 RPC 方法只是**薄入口**(`return XxxHandler.handleXxx(pack)`,见 02 §2.3);下面这套"安检流程"发生在**它委托到的 handler 内部**,**不可省略、不可简化**:
|
||||
|
||||
```js
|
||||
XxxHandler.handlePlayCard = function(pack) { // 由 mod_<游戏>.playCard 委托进来
|
||||
try {
|
||||
// 1) 提取并校验参数:列出必填字段 + 数值型字段的类型要求
|
||||
// (本项目用统一工具 ValidationHelper.extractAndValidateParams 做这件事)
|
||||
// 等价于:agentid/playerid/gameid/roomcode/seat 必填,playerid/roomcode/seat 需为数值
|
||||
var params = extractAndValidateParams(
|
||||
pack,
|
||||
['agentid', 'playerid', 'gameid', 'roomcode', 'seat', 'cardUniqueId'],
|
||||
{ playerid: 'number', roomcode: 'number', seat: 'number' });
|
||||
if (!params.success) return { success: false, error: params.error };
|
||||
var p = params.params;
|
||||
|
||||
// 2) 校验玩家与房间(必做)——失败直接 return
|
||||
// ⚠️ conmode / fromid 取自 pack 顶层(不是 pack.data),是发包定向要用的连接信息
|
||||
var o_room = mod_<游戏>.import.check_player(
|
||||
p.agentid, p.gameid, p.roomcode, p.seat, p.playerid, pack.conmode, pack.fromid);
|
||||
if (!o_room) return { success: false, error: '玩家验证失败' };
|
||||
|
||||
// 3) 取桌对象与对局状态
|
||||
var o_desk = o_room.o_desk;
|
||||
if (!o_desk) return { success: false, error: '游戏桌不存在' };
|
||||
|
||||
// 4) 调试记录(若框架提供)——便于复盘
|
||||
if (o_desk.debug && o_desk.debug.save_receivepack) {
|
||||
o_desk.debug.save_receivepack(pack, p.seat, p.playerid);
|
||||
}
|
||||
|
||||
// 5) 业务校验 + 执行业务:委托权威模块(handler 不内联规则,见 04)
|
||||
// 如:OperationExecutor.executePlayCard(o_room, {...})
|
||||
// 6) 构建响应 + 【主动推送】:ResponseBuilder 组包 → BroadcastManager 逐座位 sendpack_toseat
|
||||
// 推送 data 自带 success(见 §5);return 不是下发通道(见 §4)
|
||||
} catch (e) { /* 记录日志;必要时给该座推送失败包 */ }
|
||||
};
|
||||
```
|
||||
|
||||
- **参数提取要校验类型**:数值字段(`playerid`/`roomcode`/`seat` 等)必须转成数值再用;缺字段/类型不符即判失败。用统一工具集中做(本项目 `ValidationHelper.extractAndValidateParams`)比每处手写 `parseInt` 更不易漏。
|
||||
- **`check_player` 是强制安检**:校验座位、连接、身份,返回 `o_room` 或 `null`;`null` 一律 `return`。它的第 6/7 个参数 `pack.conmode`、`pack.fromid` 来自**包顶层**(框架收包时注入),是后续定向发包的连接凭据。
|
||||
- **校验失败不回作弊提示**:对客户端不下发"你作弊了"之类反馈,直接结束(如需可只给本座推送一个通用失败包),避免给作弊者信息。
|
||||
- **`return` 值仅服务端内部用**:例中的 `return {success:false,...}` 不会下发前端(见 §4);要让前端看到的结果一律走第 6 步主动推送。
|
||||
|
||||
---
|
||||
|
||||
## 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. 端到端收发全链路(前后端对照)
|
||||
|
||||
本节把**一个包从前端出发、到服务端处理、再推回前端**的完整链路摊开,标出**每一步在谁的哪个文件/函数**,供前端与服务端开发共同对照。下面的锚点里,**平台/契约名**(包字段、`packet_face.ReceivePack`、`Utl.sendData`、`Game_Modify.*`、`sendpack_toseat`、`data.success`)是真实且固定的;**子游戏侧命名**(`RpcSender`、`SubGameHooks`、收包分发器、各 handler)是本项目示例、可自定(见 [README「命名约定」](./README.md))。
|
||||
|
||||
### 6.1 方向 A:玩家主动操作(请求 → 处理 → 推送)
|
||||
|
||||
```
|
||||
前端 client 服务端 server
|
||||
──────────────── ────────────────
|
||||
业务/控制器
|
||||
└ 发包封装 / RpcHelper(自动补 agentid/playerid/roomcode/seat…)
|
||||
└ Utl.sendData(app, route, rpc, data) ──WS/HTTP──▶ packet_face.ReceivePack (按 app 找应用)
|
||||
(平台 08_Utl_Output.js) └ cls_app.ReceivePack (按 route 找模块,01 §4)
|
||||
└ cls_mod.DoPack (按 rpc 找方法)
|
||||
└ mod[rpc](pack) RPC 薄入口(02 §2.3)
|
||||
└ XxxHandler.handleXxx(pack)
|
||||
check_player → 取 o_desk → 校验
|
||||
→ 委托权威业务模块(04)
|
||||
→ o_room.method.sendpack_toseat(msg, seat)
|
||||
(填 conmode/fromid)
|
||||
Game_Modify._ReceiveData(_msg) ◀──WS/HTTP── → app.SendPack → packet_face.SendPack
|
||||
(平台 02_SubGame_Input.js,受限文件) (只定向该座位的那条连接,01 §4)
|
||||
└ 转交子游戏收包分发器(SubGameHooks._ReceiveData)
|
||||
└ 按 _msg.rpc 查路由表 → 对应 rpc 处理器
|
||||
└ if(!data.success) 失败处理;否则改数据模型 → 表现(04 篇)
|
||||
```
|
||||
|
||||
- **前端出口只有一个**:所有请求经发包封装/`RpcHelper` 最终落到平台 `Utl.sendData(app, route, rpc, data)`,业务不手拼包(前端 04 §1)。
|
||||
- **服务端入口只有一个**:`packet_face.ReceivePack`,随后三层路由 `app→route→rpc` 精确投递(01 §4)。
|
||||
- **服务端出口只有一个**:`o_room.method.sendpack_toseat/sendpack_toother`——它从 `seatlist[seat]` 取 `conmode/fromid` 填入包,交 `app.SendPack` 定向下发(`class.room.js`)。**`return` 不算下发**(§4)。
|
||||
- **前端收口只有一个**:平台 `09_Net.js` 收到主动推送后调 `Game_Modify._ReceiveData(_msg)`(受限文件只转交,不写业务),再由子游戏**收包分发器按 `_msg.rpc`** 分到对应处理器(前端 04 §2)。
|
||||
|
||||
### 6.2 方向 B:服务端主动推送(无前端请求)
|
||||
|
||||
超时、AI 托管、他人操作波及本座、每小局/大局结算等,都是**服务端主动发起**、前端没有对应请求的推送。它**复用方向 A 的后半段**——同样 `sendpack_toseat/toother` → 前端 `_ReceiveData` → 按 `rpc` 分发:
|
||||
|
||||
```
|
||||
服务端某处业务(定时器/AI决策/结算)
|
||||
→ o_room.method.sendpack_toseat(msg, seat) (与真人操作完全相同的出口与包结构)
|
||||
→ 前端 _ReceiveData → 收包分发器按 rpc → 对应处理器
|
||||
```
|
||||
|
||||
> 因此前端**无法也无需区分**一个推送是"我请求的响应"还是"服务端主动发的"——两者走同一条收口、同一张分发表。服务端替玩家操作必须保持这种一致(§7、04 §7)。
|
||||
|
||||
### 6.3 谁在哪:收 / 发 / 路由一览
|
||||
|
||||
| 环节 | 前端(client) | 服务端(server) |
|
||||
|------|----------------|------------------|
|
||||
| **发包出口** | 发包封装/`RpcHelper` → `Utl.sendData(app,route,rpc,data)`(平台 `08_Utl_Output.js`) | `o_room.method.sendpack_toseat` / `sendpack_toother` → `app.SendPack` |
|
||||
| **传输** | WebSocket/HTTP(平台 `09_Net.js` / `00_minhttp.js`) | 平台 `packet.js`(`SendPack_Tcp` / `SendPack_Http`) |
|
||||
| **收包入口** | 平台 `09_Net.js` → `Game_Modify._ReceiveData(_msg)`(受限文件转交) | `packet_face.ReceivePack`(`packet.js`) |
|
||||
| **路由依据** | 子游戏收包分发器按 `_msg.rpc` → 处理器 | 三层 `app→route→rpc`(01 §4),`rpc` 直取同名 `mod` 方法 |
|
||||
| **处理** | 对应 `rpc` 处理器,先判 `data.success` | RPC handler:`check_player` → 取 `o_desk` → 校验 → 委托权威业务 |
|
||||
| **成败标志** | 只认推送 `data.success`(§5) | 推送 `data` 必自带 `success`(§5) |
|
||||
| **开局** | `Game_Modify.StartWar(_msg)`(取本座数据,02 篇/前端 04 §3) | `export.makewar` 返回包(`sendtype:1 + seatlist[]` 差异化) |
|
||||
| **重连/中途加入** | `Game_Modify.Reconnect(_deskinfo)`(据快照重画) | `export.get_deskinfo` 返回该座完整快照 |
|
||||
|
||||
### 6.4 对接接缝:改一端必核对另一端
|
||||
|
||||
`rpc` 是前后端的**共同契约字符串**:服务端推送用哪个 `rpc`,前端就必须在收包分发表里注册同名处理器;反之前端请求的 `rpc`,服务端 `mod` 上必须有同名方法。因此:
|
||||
|
||||
| 场景 | 服务端产出 | 前端接收 |
|
||||
|------|------------|----------|
|
||||
| 开战 | `export.makewar` 的返回包 | `Game_Modify.StartWar(_msg)` → 开局处理 |
|
||||
| 重连/中途加入 | `export.get_deskinfo` 的返回 | `Game_Modify.Reconnect(_deskinfo)` → 重画 |
|
||||
| 对局/推送 | RPC handler 或主动推送(按 `rpc`) | 收包分发表里同名 `rpc` 的处理器 |
|
||||
|
||||
**改服务端下发结构 = 同步核对前端对应 `rpc` 的解析**;**新增一种推送 = 服务端选定 `rpc` + 前端在分发表加同名处理器**。任一端单方面改,另一端必按旧结构解析出错。前端侧的收发细节以 [`client/docs/development-guide/04-网络对接与启动编排`](../../../client/docs/development-guide/04-网络对接与启动编排.md) 为权威。
|
||||
|
||||
---
|
||||
|
||||
## 7. 服务端代替玩家操作时的透明性
|
||||
|
||||
服务端自动替玩家执行的操作(如 AI 托管出牌/碰杠胡),**必须复用真人操作的同一套数据包与广播链路**:产生的包结构、`rpc`、`data` 与真人操作**完全一致**,前端无需也禁止为它单开一套接收/解析分支。唯一区别只在触发源(前端请求 → 服务端决策),其后的数据组织、协议、广播路径不变。(详见 04 的对应红线。)
|
||||
|
||||
---
|
||||
|
||||
## 8. 小结
|
||||
|
||||
- 包结构恒为 `{app, route, rpc, data}`;收包先 `check_player`,失败静默 `return`。
|
||||
- 三种发包方式按"谁该看到什么"选;差异化广播逐座位定制,敏感信息只发本人。
|
||||
- **主动推送是唯一可靠下发通道**,`return` 不算。
|
||||
- **成败只认 `data.success`**,推送必自带 `success`,禁止 `status`/`code` 判成败与兼容兜底。
|
||||
- **端到端一条链**(§6):前端 `Utl.sendData` → 服务端 `packet_face.ReceivePack`→三层路由→handler→`sendpack_toseat` → 前端 `Game_Modify._ReceiveData`→按 `rpc` 分发;`rpc` 是前后端共同契约,改一端必核对另一端。
|
||||
- 改下发结构必同步核对前端 `StartWar`/`Reconnect`/对应 `rpc` 解析。
|
||||
|
||||
下一篇 [04-开发规范与红线](./04-开发规范与红线.md) 汇总所有必须遵守的工程纪律。
|
||||
</content>
|
||||
Reference in New Issue
Block a user