必须采用带语义的结构化操作(如{"type":"insert","index":7,"text":"ab","seq":42})配合OT变换与全序广播,而非直接广播全文;否则必然导致内容覆盖、错位与回滚失效。

直接用 WebSocket 广播原始文本或光标位置,不处理操作语义,必然在多人高频编辑时出现内容覆盖、错位、回滚失效——这不是网络问题,是同步模型选错了。
为什么 sendText() 直接广播整个文档会出事
很多初版实现把前端 editor.getValue() 拿到的全文发给服务端,再由服务端调用 session.getAsyncRemote().sendText() 推给其他人。这看似简单,实则埋下三类硬伤:
- 网络延迟导致 A 发送的“全文”可能比 B 的晚到,但内容却是 B 编辑前的状态,直接覆盖就丢数据
- 用户只改了一个字,却要传几 KB 文本,带宽和解析开销陡增
- 无法区分“插入”和“删除”,更无法做位置对齐——比如两人同时在索引 10 处操作,一个插字符,一个删字符,谁先应用谁赢,结果不可控
本质是把协作问题降级成了“最终谁提交得快”,不是协同,是竞速。
op 对象必须带语义、有序号、可变换
真正可用的编辑操作不能是字符串,得是结构化对象,且至少包含这四个字段:
-
type: 值为"insert"或"delete",禁止用"update"这种模糊类型 -
index: 操作作用的绝对位置(基于当前文档状态),单位是 UTF-16 code unit(不是字节数,也不是 grapheme cluster) -
text: 插入的文本(delete时为空字符串) -
seq: 客户端本地递增序列号,和服务端分配的serverSeq共同构成全序依据
例如:{"type":"insert","index":7,"text":"ab","seq":42}。服务端收到后不立即广播,而是先按 docId + serverSeq 排序,再分发;客户端收到他人操作后,必须用 OT 函数对本地待发操作做 transform(),再发出去。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
Java 后端必须做操作排队与变换协调,不能只做消息中转
Spring Boot + @ServerEndpoint 默认是“收到即转发”,这不够。你需要在内存里为每个 docId 维护一个有序队列,并引入轻量 OT 引擎(如 ShareDB 的 ot-json0 或自研简化版)。关键逻辑如下:
- 所有进来的
op先塞进ConcurrentLinkedQueue,由单线程消费者按serverSeq排序后逐条处理 - 每条操作进入队列前,服务端需校验其
index是否仍在当前文档合法范围内(防止因延迟导致的位置越界) - 广播前,操作必须带上
serverSeq和clientId,客户端靠这个做本地操作重放(replay)与变换(transform) - 断线重连时,不能只发最新快照,而要查该客户端最后收到的
serverSeq,从下一条开始补推增量操作
漏掉任意一点,都会导致“看起来同步了,但某人删掉的字又莫名其妙回来了”。
前端必须做本地暂存 + 变换 + 确认机制,不能发完就忘
用户敲一个键,前端不能立刻调用 editor.setValue() 更新 UI,也不能立刻把操作发出去。正确流程是:
- 本地执行操作,更新 UI,同时将操作对象存入
pendingOps队列 - 收到服务端广播的他人操作后,遍历
pendingOps,对每条调用transform(localOp, remoteOp),修正其index - 只有当服务端返回
{"type":"ack","clientSeq":42},才从pendingOps移除对应操作 - 若超时未收到 ack,需重发并合并相邻 insert/delete(避免产生大量碎操作)
没有 pending + transform + ack 三件套,所谓“实时协作”只是假象——你看到的是别人几秒前的状态,还自信地在上面继续编辑。
最易被忽略的一点:CRDT 虽然免去了服务端排序压力,但 Java 生态缺乏成熟、轻量、可调试的 CRDT 文本库;OT 实现复杂,但 ot-json0 或 rich-text 类型已有稳定封装。别为了“听起来先进”选 CRDT,先让 transform() 在边界 case(如连续删空行、粘贴含换行的富文本)下不崩,比算法名字重要得多。

















