websocket仅提供低延迟通信管道,帧同步依赖上层逻辑:客户端按递增frameid顺序执行操作,服务端只广播校验后的actionlist;断连需补帧,二进制传输+时间戳+序列号保障确定性。

WebSocket 本身不直接实现帧同步,它只是提供低延迟、全双工的通信管道。真正的帧同步(Lockstep)依赖于上层逻辑设计:客户端统一执行相同输入、服务端协调步调、所有终端在相同“逻辑帧”上推进游戏世界。JavaScript 中用 WebSocket 做帧同步,关键不是“怎么发消息”,而是“怎么让每帧都对齐、可回放、可恢复”。
WebSocket 在帧同步中的真实角色是可靠信道,不是同步引擎
它负责安全、有序、带序号地传递每帧的玩家操作(actionList),比如 {"frameId":123,"actions":[{"playerId":"p1","move":"right"},{"playerId":"p2","attack":true}]}。服务端不计算结果,只做广播与校验;客户端收到后,在本地按 frameId 顺序排队执行——这才是帧同步的核心。
客户端必须严格按帧号顺序执行,不能跳帧或乱序
- 每帧有唯一递增 frameId,从 0 或 1 开始,服务端下发时保证单调递增
- 客户端维护一个待执行队列(如
queue = []),只执行queue[0]且frameId === expectedNextFrame的帧 - 若收到 frameId=125,但本地还在等 124 → 缓存进 queue,不执行;直到收到 124 才触发连续执行
- 使用
requestAnimationFrame驱动帧推进节奏,但逻辑更新与渲染分离:updateLogic()按帧号触发,render()按屏幕刷新率绘制
服务端要做最小化协调,不替代客户端逻辑
- 不计算物理、不判定碰撞、不生成动画——只做三件事:
- 收集各客户端本帧的操作(带时间戳和签名防篡改)
- 校验合法性(如移动距离是否超限、指令频率是否异常)
- 统一分配 frameId,广播包含完整 actionList 的帧包给房间内所有客户端
- 心跳机制维持连接活性:每
heartBeatTick(如 2000ms)发一次轻量心跳帧,超时offlineTimeLength(如 100s)未响应则踢出
断连重连后必须补帧,不能靠“当前状态”热修复
- 断线期间服务端持续缓存最近 N 帧(如最后 60 帧),重连成功后立即返回
{"type":"sync_start","fromFrame":120,"toFrame":179,"frames":[...]} - 客户端收到后清空旧队列,把补帧按 frameId 插入队列头部,再逐帧执行,确保世界状态与服务端严格一致
- 补帧过程中暂停用户输入,避免新操作与旧帧冲突;补完后再恢复采集
二进制传输 + 序列号 + 精确时间戳提升确定性
- 用
ArrayBuffer封装帧数据,比 JSON 节省 30%+ 体积,避免解析歧义 - 每帧附带
serverTimestamp(毫秒级),客户端可据此估算网络偏移,用于本地插值渲染(位置平滑)与逻辑帧对齐(如判断是否该推进下一帧) - 所有 actionList 加递增 seq 字段,客户端丢弃重复或倒退的 seq,防止多线程处理导致的乱序执行
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











