房间内消息同步
概述
对局中的核心流量是房间内消息同步:谁的牌变了、谁出了什么、局面到了哪一步,都要实时同步给房间内所有人。本文讲清三件事:广播/单播/组播的实现方式、帧同步 vs 状态同步的选型、以及消息序号与去重、可靠投递的保证机制。
一、房间内消息的类型
| 类型 | 方向 | 例子 |
|---|---|---|
| 操作上报 | 客户端 → 服务器 | 出牌、落子、移动 |
| 状态广播 | 服务器 → 全体 | 出牌结果、比分变化 |
| 定向通知 | 服务器 → 单玩家 | 轮到你了、私信 |
| 系统事件 | 服务器 → 全体 | 开局、结算、倒计时 |
消息流:
玩家操作 → 服务器校验 → 更新状态 → 广播给全房间
服务器是唯一权威,所有同步以服务器广播为准二、广播 / 单播 / 组播
2.1 三种发送方式
单播(单玩家):
只发给一个玩家:你的手牌、你的回合提示
广播(全体):
发给房间内所有玩家:出牌结果、比分
组播(子集):
发给部分玩家:观战者、某方队伍
实现:按成员分组过滤后广播2.2 广播实现
java
public class RoomBroadcaster {
public void broadcast(Room room, GameMessage msg) {
// 快照遍历,避免遍历中集合被修改
for (Member m : room.snapshotMembers()) {
GameSession session = sessionManager.getByPlayer(m.getPlayerId());
if (session != null && session.isOnline()) {
session.send(msg);
}
}
}
// 组播:只发给符合条件的人
public void broadcastTo(Room room, GameMessage msg, Predicate<Member> filter) {
for (Member m : room.snapshotMembers()) {
if (filter.test(m)) {
GameSession session = sessionManager.getByPlayer(m.getPlayerId());
if (session != null && session.isOnline()) {
session.send(msg);
}
}
}
}
}广播要点:
成员快照遍历(防并发修改)
离线玩家跳过(重连后补快照)
同一消息对象可复用(编码器各自编码)2.3 广播性能
房间内广播瓶颈:
每成员一次 writeAndFlush
房间人数 × 消息频率 = 广播吞吐
优化:
复用 ByteBuf(引用计数 + retain/release)
批量合并小消息
限制单房间人数(10 人内无压力)
高频消息(位置)用帧同步/增量同步三、帧同步 vs 状态同步
3.1 两种同步模型
状态同步(回合制/休闲类适用):
服务器维护权威状态
状态变化 → 广播"变化结果"
客户端根据状态渲染
例:出牌 → 广播"场上有牌A"
帧同步(实时竞技适用):
服务器只转发玩家输入(操作指令)
所有客户端跑同一逻辑,逐帧演算
例:移动 → 广播"按下前进键"
客户端本地确定性计算3.2 对比
| 维度 | 状态同步 | 帧同步 |
|---|---|---|
| 服务器职责 | 权威状态 + 广播变化 | 输入转发 |
| 客户端职责 | 渲染状态 | 本地逻辑演算 |
| 带宽 | 状态数据(中等) | 输入数据(很小) |
| 服务器压力 | 高(每步都算) | 低(只转发) |
| 作弊防护 | 强(服务端权威) | 弱(需校验逻辑) |
| 适用 | 回合制/策略/休闲 | 动作/格斗/实时竞技 |
| 断线恢复 | 快照即可 | 需重放输入序列 |
选型结论:
休闲小游戏绝大多数用状态同步
实时动作类(跑酷、弹射)才考虑帧同步
帧同步详见第 5 周(实时交互场景)专题3.3 混合使用
混合示例:
回合制对战:状态同步(出牌结果广播)
房间内聊天:直接转发(类帧同步,无逻辑)
倒计时/比分:状态同步(服务器广播)
各消息按其性质选模型,不强制统一四、消息序号与去重
4.1 为什么需要序号
TCP 保证传输有序,但:
重连后消息可能重复(客户端重发)
客户端需要知道"有没有漏消息"
服务器广播顺序即对局顺序,客户端要按序渲染
方案:每条对局消息带递增序号4.2 序号设计
房间内全局递增序号:
serverSeq:服务器对局消息统一编号
每条广播消息携带 serverSeq
客户端记录"最后收到的序号"
用途:
去重:重复收到同一序号 → 丢弃
补漏:序号跳变 → 需要全量快照
排序:保证渲染顺序java
public class MessageSequencer {
private long serverSeq; // 房间内对局消息序号
public GameMessage next(GameMessage msg) {
long seq = ++serverSeq;
return new SyncedMessage(seq, msg); // 包装序号
}
public boolean isDuplicate(long clientSeq, long lastSeq) {
return clientSeq <= lastSeq;
}
}4.3 序号与快照配合
断线重连恢复:
客户端上报"最后收到的序号 lastSeq"
服务器判断:
lastSeq 与当前序号连续 → 增量补发(可选)
不连续/未知 → 下发全量快照
快照即"序号对齐点",之后继续增量
简化实践(休闲游戏):
重连一律全量快照 + 最新序号
快照内状态完整,客户端从新序号续接五、可靠投递保证
5.1 可靠性的分层
传输层:
TCP 提供有序可靠字节流(默认)
WebSocket 同样基于 TCP
弱网丢包由 TCP 重传兜底
应用层:
服务器广播的可靠性 = 客户端最终能重建状态
依赖:
序号(去重/排序)
快照(断线补全)
幂等(重复操作不重复生效)5.2 应用层保证清单
| 机制 | 作用 |
|---|---|
| 序号 | 去重、排序、对齐 |
| 全量快照 | 重连恢复 |
| 幂等校验 | 重复指令不重复执行 |
| 服务端权威 | 状态以服务器为准 |
| 客户端确认 | 关键消息回执(如结算) |
5.3 关键消息的确认
结算、奖励等关键消息:
客户端收到后回执 ACK
服务器未收到 ACK → 重发 / 补发
超时未确认 → 邮件/离线补偿(见邮件系统篇)
保证关键结果不丢失六、常见问题
| 问题 | 处理 |
|---|---|
| 广播丢消息 | TCP 可靠性 + 快照兜底 |
| 消息重复 | 序号去重 |
| 顺序错乱 | 序号排序 + 服务器单线程发 |
| 断线重连状态旧 | 快照对齐序号 |
| 广播卡顿 | 检查慢玩家/批量发送 |
七、小结
房间内消息同步围绕"服务器权威广播"展开:广播/单播/组播三种发送方式覆盖对局消息的三种范围,成员快照遍历保证并发安全;状态同步(广播结果)与帧同步(转发输入)是两种模型,休闲回合制选状态同步即可;消息序号承担去重、排序与对齐三大职责,重连时用全量快照 + 最新序号完成状态续接;可靠投递最终靠"TCP 可靠传输 + 应用层序号 + 快照 + 幂等"四层兜底。同步层做稳了,玩家在任何网络环境下都能看到一致、不重、不乱的对局。