如何用 WebSocket 实现实时协作编辑?

域名注册
域名注册 正式会员超兽战士 👑年卡会员
发布于 2026-10-07 16:43 ·1 浏览 ·0 回复

结论:WebSocket 只解决「消息能双向、低延迟地送达」这一件事,真正决定协作编辑是否可用的,是送达之后的冲突合并方案(OT 或 CRDT)与断线后的状态对齐机制。三者缺一,要么功能跑不起来,要么用户会看到文字互相覆盖。

WebSocket 在协作编辑里到底负责什么?

结论:WebSocket 负责传输层,不负责一致性。它提供的是基于 TCP 的全双工长连接,握手时用 HTTP Upgrade,之后同一连接双向收发帧,服务端可以主动推送,不需要客户端轮询。

落到协作编辑上,它要承担三件事:把本地操作广播给同文档的其他用户、接收远端操作并交给合并层、在连接断开时让客户端感知并触发重连。至于「A 在第 5 行插字、B 同时删第 5 行」该怎么收敛,跟 WebSocket 无关。

所以选型顺序是:先定合并算法,再定传输。反过来做,会在上线后返工。

OT 和 CRDT 有什么区别,该选哪个?

结论:需要服务端强一致校验、团队有后端改造能力,选 OT;想少写服务端逻辑、支持离线编辑与 P2P,选 CRDT。

OT(Operational Transformation,操作变换)的做法是:每个操作带文档版本号提交到服务端,服务端把它与并发操作做变换后再广播。代表实现是 ShareDB。它的代价是服务端必须为每个文档维护权威版本和操作历史,逻辑集中、好审计,但服务端代码量更大,历史操作要定期快照清理。

CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)的做法是:每个字符/节点带唯一 ID 和因果信息,任意顺序合并结果一致。代表实现是 Yjs、Automerge。Yjs 的常见组合是客户端用 Y.Doc,服务端用 y-websocket 只做二进制 update 的转发与落盘,服务端几乎不参与冲突计算。

经验判断:文档以富文本为主、需要严格权限与操作审计,走 ShareDB;文档型产品要支持离线、要快速上线,走 Yjs。两者都可以跑在裸 WebSocket 上,不必引入额外协议。

服务端最小可用实现怎么写?

结论:一个房间一个广播集合,服务端只做转发和鉴权,合并逻辑放客户端或独立模块。下面是 Node.js + ws 的最小骨架:

const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
const rooms = new Map(); // roomId -> Set<ws>

wss.on('connection', (ws, req) => {
  const roomId = new URL(req.url, 'http://localhost').searchParams.get('room');
  if (!roomId) return ws.close(4000, 'missing room');
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId).add(ws);

  ws.on('message', (buf) => {
    for (const c of rooms.get(roomId)) {
      if (c !== ws && c.readyState === 1) c.send(buf); // 1 = OPEN
    }
  });
  ws.on('close', () => rooms.get(roomId)?.delete(ws));
});

三个必须加的细节:一是二进制优先,Yjs 的 update 是 Uint8Array,直接转发 Buffer 比转 JSON 省 30% 以上带宽;二是帧大小限制,ws 默认 maxPayload 是 100MB,协作场景设 1MB 足够,防止单条超大消息打爆内存;三是鉴权放在 Upgrade 阶段校验 Cookie 或 token,不要在连接建立后再问。

光标和在线状态怎么做才不卡?

结论:光标位置这类高频状态必须节流,不要跟文档操作走同一条消息通道。

做法是分开消息类型:type: "op" 走文档更新,type: "awareness" 走光标、选区、用户名。awareness 消息在客户端按 50ms 节流(即每秒最多 20 条),超过 200ms 没有新位置就直接丢弃,用户感知不到差别。

在线人数、谁正在输入这类状态不需要持久化,连接断开即清理,不要写数据库。

断线重连和状态对齐怎么做?

结论:靠心跳检测断开、靠版本号或状态向量补齐缺口,二者都要有。

心跳:服务端每 30 秒发一次 WebSocket ping,60 秒内没收到 pong 就 terminate 连接,不要依赖 TCP 自己发现断线——移动网络下 TCP 可能十几分钟不报错。

重连:客户端用指数退避,1s、2s、4s、8s、16s,上限 30s,并加 ±20% 随机抖动,避免全房间同时重连造成惊群。

补齐:这是最容易出错的一步。OT 方案里客户端重连后带上本地版本号,服务端返回版本号之后的操作序列;CRDT 方案里重连后交换状态向量(state vector),服务端只回传缺失的那部分 update。注意 CRDT 也要做持久化,Yjs 官方提供 y-leveldb / y-redis,进程重启后文档才不会丢。

上线前必须验证的四个点

结论:并发写入、网络抖动、鉴权绕过、内存增长,这四项各做一轮压测再放量。

具体清单:两台客户端同时在同一位置输入,确认最终文本一致且无字符丢失;用 Chrome DevTools 的 Network 面板切 Offline 30 秒再恢复,确认内容自动补齐;用未授权的 token 直连 ws:// 地址,确认在 Upgrade 阶段被拒;连续跑 24 小时,观察单进程内存曲线是否平稳,Node.js 单进程维持 1 万条空闲 WebSocket 连接的常驻内存在数百 MB 量级,超出说明有句柄或房间未释放。

把合并算法选对、把重连补齐做扎实、把高频消息隔离开,WebSocket 协作编辑就基本可用;剩下的都是权限、存储和运维工程。

版权声明:本文来自 GJ站长论坛《如何用 WebSocket 实现实时协作编辑?》
原文链接:https://www.gj0.com/thread-419.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~