WebSocket连接必须携带身份凭证,服务端需在onOpen时校验token并存储用户信息;消息协议须区分原子操作与状态快照,操作需带type、pos、version等字段;广播应按文档ID分组路由;离线重连后须通过HTTP接口拉取增量事件,version由服务端生成。
WebSocket连接建立时必须携带身份凭证
不带认证的 websocket 连接在协同编辑中等于裸奔——你无法区分谁发了什么,更没法做权限控制或冲突归因。服务端一收到 onopen,就得立刻检查 url 参数或初始帧里的 token 字段。
常见错误是前端用 new WebSocket("ws://localhost:8080/ws") 硬编码连接,没传任何用户标识;服务端也默认接受,后续广播时只能靠 socket.id 做临时标记,断连重连后就丢失上下文。
- 正确做法:前端构造连接时拼上
userId和短期有效的JWT,如ws://example.com/ws?userId=U123&token=eyJhbG... - 服务端在
onOpen中解析并校验 token,失败则调用conn.close(4001, "invalid token")主动拒绝 - 校验通过后,把
userId、docId(若已知)、role存进内存 Map 或 Redis,键为conn.id
消息协议必须区分“操作”与“状态快照”
协同编辑里最常踩的坑,就是把“用户输入”直接当“最终内容”来广播。比如 A 输入 “hello”,B 同时删除开头 “h”,如果服务端只转发两段字符串,客户端各自覆盖,结果必然错乱。
真正可落地的协议要强制语义化:所有变更必须是带位置和类型的原子操作,而不是整段文本。
- 推荐结构:
{"type":"insert","pos":0,"text":"Java","version":5}或{"type":"delete","pos":6,"length":5,"version":5} - 禁止发送
{"type":"update","content":"Java Hello "}这类无上下文的状态快照(除非是新用户首次同步) - 每个操作必须附带
clientId(非 userId),用于客户端识别是否是自己发出的操作,避免本地回显重复触发 - 服务端收到操作后,先写 DB 持久化,再广播;失败则记日志,不丢不跳
广播不能无差别推给所有人
一个 200 人的大团队共编一份文档,但只有 3 人在编辑——如果每次按键都推给全部在线连接,99% 的消息对收件人毫无意义,还拖慢渲染、浪费带宽。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
真实系统里,广播是带路由逻辑的:按文档 ID 分组、按角色过滤、按操作类型分流。
- 服务端维护一个
Map<String, Set<WebSocket>>,key 是"doc:123",value 是当前订阅该文档的所有活跃连接 - 收到
task:assign类消息,只推给teamId匹配的成员;收到doc:edit,只推给同docId的连接 - 前端监听到消息后,先比对
data.docId是否等于当前打开的文档 ID,再决定是否处理 - 撤回、锁定、评论等控制类消息,可额外加
scope: "admin"字段,普通用户直接忽略
离线重连后必须主动拉取增量事件
WebSocket 断开几秒再连上,期间别人可能已改了 17 处。如果只是静默重连,用户看到的还是断连前的旧状态,且后续操作会基于错误版本产生冲突。
可靠的做法是:连接建立后,前端立刻发一条同步请求,服务端返回断连期间该用户有权限读取的变更流。
- 前端在
onOpen后立即发送{"type":"sync","since":1716448740000,"docId":"D456"}(时间戳单位毫秒) - 服务端查数据库中
doc_id = 'D456'且created_at > since的操作记录,按version排序返回数组 - 前端收到后,逐条 apply,跳过自己发出的操作(靠
clientId判断),避免重复渲染 - 注意:这个 sync 请求不能走 WebSocket 数据帧,而应走独立 HTTP 接口(如
GET /api/v1/docs/D456/sync?since=...),防止阻塞主通道
最易被忽略的一点:操作序列号(version 或 seq)必须由服务端统一生成并返回,不能由前端自增。否则多个设备同时编辑时,序列会冲突,OT 算法彻底失效。

















