workerman 实现多人协作光标同步必须用 websocket 而非 http 轮询,因后者导致延迟高、状态不同步、无上下文;需传语义化位置(字符偏移+选区长度+doc_id+client_id),服务端按文档分组广播并清理过期项,客户端节流渲染、滚动重算坐标。

Workerman 本身不处理光标位置同步,必须自己实现「客户端上报 + 服务端广播 + 客户端渲染」闭环。HTTP 请求做不到低延迟、高频率的光标更新,只能靠 WebSocket 长连接实时透传。
为什么不能用 HTTP 接口轮询光标位置
轮询会快速触发连接堆积、超时、状态不同步。比如用户 A 每 200ms 发一次 POST /cursor/update,10 人同时编辑时,每秒 50 次请求,Workerman 的 HttpWorker 进程可能因响应延迟导致光标“卡住”或“跳跃”。更严重的是:HTTP 无连接上下文,服务端无法知道“谁在编辑哪个文档”,也就没法做权限隔离或广播过滤。
真实场景下,你看到的腾讯文档光标是毫秒级刷新的(compositionend 后立刻发、选区变化即刻报),这只有 WebSocket 的 onMessage 能扛住。
光标数据格式和上报时机怎么定
客户端不能只传 {x: 120, y: 80} 这种像素坐标——它依赖渲染层缩放、滚动偏移、字体宽度,服务端无法理解,也无法跨端复现。正确做法是传语义化位置:文档内字符偏移 + 选区长度 + client_id。
-
doc_id必须带,否则服务端不知道该广播给谁 -
client_id由服务端在 WebSocket 握手时分配(如$connection->id),不能用前端生成的 UUID,防伪造 -
position和length应基于纯文本内容计算(不是 HTML 或富文本 DOM),避免因标签差异导致错位 - 中文输入法下,
compositionstart到compositionend之间禁止上报光标,否则会出现“输入框里显示‘你好’,光标却停在拼音‘nihao’中间”的错乱
示例上报 payload:
{"type":"cursor","doc_id":"doc_abc123","client_id":"ws_7f3a","position":42,"length":0}
服务端如何广播且不炸内存
别把所有连接遍历一遍再 $conn->send()。Workerman 的 ConnectionInterface 不支持按 group 广播,得自己建映射表:
- 用
static $cursors = []存每个doc_id对应的活跃光标列表(含client_id、position、updated_at) - 收到新光标时,先清理
updated_at 的过期项(防僵尸光标) - 广播前,用
foreach ($connections as $conn)筛出同doc_id的连接,再逐个 send —— 别漏掉刚连上的新成员 - 别在
onMessage里做 Redis 写入,光标更新高频但非关键,可异步丢进Timer::add(0.1, ...)批量落库
漏掉清理过期光标,就会出现“人已退出,头像和光标还挂在文档上”的问题;不筛 doc_id 直接全量广播,轻则浪费带宽,重则让客户端解析失败崩溃。
客户端怎么渲染别人光标又不卡顿
别每次收到光标就重绘整个编辑器。DOM 操作成本高,尤其当 20 人同时编辑时,一秒可能收 200+ 条 cursor 消息。
- 用
requestIdleCallback或setTimeout(..., 0)做节流,合并同一帧内的多条更新 - 光标 DOM 元素必须用
position: absolute+pointer-events: none,否则会拦截鼠标事件 - 颜色区分靠
client_id哈希取色(如hsl(hash % 360, 70%, 60%)),别硬写死 5 种颜色,否则第 6 人光标就看不见了 - 光标旁的头像/昵称要用 canvas 或 SVG 渲染,别用 img 标签——加载慢会导致“光标先到、头像后到”的撕裂感
最常被忽略的一点:光标位置要随文档滚动实时重算。客户端必须监听 scroll 事件,根据当前 scrollTop + 字符偏移反推 DOM 坐标,否则一滚动,所有他人光标就“飞走”了。











