关键在于服务端过滤:为每个连接分配唯一clientid并存入属性,广播前比对消息中的clientid与连接属性,跳过匹配者;禁用ip判断,因nat/代理下ip可能重复。

WebSocket广播时如何避免“自己收到自己发的操作”
直接用broadcast()把所有消息推给全部连接,会导致当前编辑者看到自己刚输的字闪一下又重绘——这不是 UI 重绘问题,是逻辑上不该接收自己发出的操作。关键在服务端做连接标识和过滤。
- 每个
WebSocket连接初始化时,生成唯一clientId(如 UUID),存入conn.getUserProperties().put("clientId", id) - 接收
onMessage后解析 JSON,提取其中的clientId字段(前端必须随操作带上) - 广播前遍历所有连接,跳过
conn.getUserProperties().get("clientId").equals(receivedClientId)的连接 - 别依赖
conn.getRemoteSocketAddress()做判断——NAT 或代理下多个用户可能共享 IP
OT算法里Insert和Delete操作怎么写transform函数
OT不是黑盒,核心就两条规则:插入偏移要补偿后续操作位置,删除需判断是否落在已插入内容里。不写全量引擎,只聚焦最常出错的Insert → Delete转换场景。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 假设原始操作
opA = {type: "insert", index: 0, text: "Java "},opB = {type: "delete", index: 6, len: 5} -
transform(opB, opA)逻辑:若opB.index >= opA.index,则opB.index += opA.text.length;否则不变 - 但要注意边界:如果
opB.index == opA.index且opA.text非空,说明删除起点刚好压在插入点上,也要+偏移 - Java 中建议把
index和len封装进Operation类,并提供transform(Operation other)实例方法,避免静态工具类传参混乱
为什么用timestamp排序操作会失效
靠客户端时间戳timestamp排序操作,在跨设备、NTP未同步、或用户改系统时间时必然崩。真实系统中,服务器必须成为唯一权威时序源。
- 不要信任
message.timestamp字段做执行顺序判断 - 服务端收到操作后,立即打上
serverTimestamp = System.currentTimeMillis(),并分配单调递增的seqId(可用AtomicLong) - 同一文档的所有操作按
seqId排序,而不是时间戳——哪怕两个操作间隔1ms,也严格分先后 - 前端可保留
timestamp仅用于 UI 显示“X秒前”,不参与协同逻辑
CRDT比OT更适合小团队快速上线吗
不一定。CRDT省去了变换函数,但调试成本更高:你没法像OT那样单步验证“这个Delete为什么变成删了前面”。对小团队,更推荐先跑通带版本号的简单OT。
- CRDT要求所有操作携带
siteId + counter,且每个字符/段落都要有独立ID,前端序列化负担明显增大 - Java 生态缺乏开箱即用的文本CRDT库(
Yjs是 JS 的,automerge无稳定 Java 绑定) - 若只是支持纯文本+光标同步,用
ot-json0或手写两三种操作的OT,3天内能跑通;CRDT光理解LWW-Element-Set或RGA结构就得两天 - 真正容易被忽略的是:CRDT合并结果不可逆,而OT失败时还能 fallback 到取最新快照重放操作日志










