WebSocket本身不解决冲突,冲突必须靠OT或CRDT算法在应用层处理;简单广播会导致操作丢失、乱序、状态错乱。因WebSocket不保证跨连接有序、无重传、无幂等机制,需在消息中携带id、baseVersion、seq等字段,并绑定连接生命周期与协同状态。

直接说结论:WebSocket本身不解决冲突,冲突必须靠OT或CRDT算法在应用层处理;消息同步机制若只做简单广播,必然丢操作、乱序、掉状态。
为什么onMessage里直接broadcast()会出问题
很多初学者把WebSocket当成“高级HTTP”,收到onMessage就立刻调用broadcast()推给所有人——这会导致三类硬伤:
- 网络抖动时,客户端发来的操作可能重复、乱序、丢失,服务端无感知
- 多个用户同时插入同一位置(比如光标都在第5个字符后),没转换规则就直接应用,内容必然错乱
- 没有操作唯一标识和序列号,无法做幂等校验或断线重连后的增量恢复
真实场景中,onMessage收到的应是带clientId、docId、timestamp和seq的操作对象,不是原始文本。
OT算法落地必须补全的4个关键字段
用Operational Transformation不是写个transform()函数就完事。服务端必须维护并校验以下字段,否则无法收敛:
-
op.id:全局唯一操作ID(建议用clientId + timestamp + seq拼接) -
op.baseVersion:该操作基于的文档版本号(不是时间戳,是服务端分配的单调递增整数) -
op.transformed:布尔值,标识该操作是否已被其他操作变换过 -
op.dependencies:数组,记录依赖哪些已确认的操作ID(用于断线后精准重放)
示例消息体:
{"type":"insert","op":{"index":12,"text":"x"},"clientId":"u-789","docId":"d-abc","baseVersion":42,"id":"u-789-1712345678901-3"}
WebSocket层必须保障有序、带序号、可追溯
WebSocket协议本身不保证跨连接的消息顺序,也不自带重传。要支撑协作,必须在应用层补足:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 每个连接需绑定
connectionId,服务端用Map管理,不能靠conn.getRemoteSocketAddress()做路由 - 所有广播消息必须携带
serverSeq(服务端全局递增序号),客户端据此检测跳号、请求重传 - 断线重连时,客户端必须发送
{"type":"reconnect","lastSeq":123},服务端只推送lastSeq+1之后的操作 - 服务端需缓存最近N条操作日志(建议至少30秒),不依赖数据库实时查询
否则,用户切后台再回来,看到的文档就是“过去式”。
CRDT方案下WebSocket消息结构怎么设计才不翻车
选CRDT不等于可以省事。它对消息结构更敏感,尤其要注意:
- 不能只传“最终值”,必须传完整
CRDT内部状态片段(如LWW-Element-Set里的(element, timestamp, clientId)三元组) - 客户端本地CRDT实例必须和服务器同构,包括时钟逻辑(推荐用
HybridLogicalClock而非系统时间) - 每次
send()前,需调用crdt.toDelta()生成差量,而不是crdt.toString()全量发送 - 服务端收到
delta后,必须先apply()再broadcast(),且广播内容仍是delta,不是合并后的快照
否则,内存爆炸、同步延迟、最终状态不一致,三个问题一个都逃不掉。
最常被忽略的一点:无论用OT还是CRDT,WebSocket连接生命周期必须和服务端协同引擎状态强绑定——连接断开时,要触发leaveDocument(docId, clientId)清理资源,否则离线用户的未确认操作会卡死整个协同流。










