Workerman多客户端写入同一连接内容交织的根本原因是$connection->send()不保证原子性,需通过拼接完整响应、协程化I/O或连接级锁$connection->lock()解决。

Workerman 多客户端写入同一连接时为什么内容会交织?
不是 Workerman 本身设计缺陷,而是 $connection->send() 调用本身不保证原子性——它只是把数据推入连接的发送缓冲区,底层 TCP 层可能分片、合并或延迟发送。当多个协程/线程(或同一线程内多个异步回调)并发调用 $connection->send() 向同一个 $connection 写数据,且未加协调,就极易出现 A 的响应头被 B 的响应体插在中间,最终客户端收到乱序、截断甚至 JSON 解析失败的报文。
用 Connection::send() + 锁保护单次完整响应
Workerman 的 $connection 对象是线程/协程安全的,但「写入」动作不是原子的。必须确保一次逻辑响应(比如一个 JSON 包 + 换行符)被完整、不可中断地送入缓冲区。
- 不要在
onMessage中直接多次调用$connection->send()拼接响应(如先 send header,再 send body) - 所有需要发给同一客户端的数据,先拼成一个字符串(或
string/Buffer),再一次性调用$connection->send($full_payload) - 若业务逻辑中存在多个异步分支(如 await DB 查询 + await HTTP 请求),必须等全部结果就绪、组装完成后再 send,不能边等边发
- 对极少数需流式响应的场景(如大文件分块推送),应在应用层定义帧格式(如 length-prefix 或 \0 分隔),并在接收端解析,而非依赖 TCP 流边界
Workerman 5.0 协程环境下用 async + await 避免伪并发
Workerman 5.0 默认启用协程运行时,onMessage 回调本质是协程函数。此时「并发写入同一连接」往往源于误用同步风格代码触发竞态,而非真正多线程。
- 禁止在
onMessage中使用sleep()、file_get_contents()、阻塞式 PDO 查询等 —— 它们会让当前协程挂起,但其他协程仍可抢占并往同一$connection发送数据 - 必须改用协程兼容组件:用
workerman/mysql替代 PDO,用workerman/http-client替代curl_exec,所有 I/O 都要await - 协程内无需额外加
std::mutex类锁 —— 协程是协作式调度,只要不主动让出控制权(即不 await),同一协程对$connection的操作就是串行的 - 若需跨协程共享状态(如广播消息队列),才需用
Revolt\EventLoop::queue()或Channel等协程安全通信机制,而非原始锁
连接级写入序列化:用 $connection->lock() 和 $connection->unlock()
Workerman 4.1+ 提供了原生连接级锁(非系统 mutex),专为解决单连接多路写入竞争而设。它比手动维护外部锁更轻量、更精准。
- 调用
$connection->lock()后,该连接的所有后续send()会被排队,直到当前锁被unlock()或协程结束自动释放 - 适合「先查 DB → 再查缓存 → 最后组合响应」这类多步异步依赖,且必须保证响应顺序严格的场景
- 注意:锁只作用于当前
$connection,不影响其他客户端;也不阻塞事件循环,只是串行化该连接的输出队列 - 示例:
$connection->lock(); $result = await $db->query("SELECT ..."); $cacheData = await $redis->get("key"); $connection->send(json_encode(['data' => $result, 'cache' => $cacheData])); $connection->unlock();
真正容易被忽略的是:即使用了 lock(),如果锁内执行了耗时同步操作(如未 await 的 curl_exec),整个 Worker 进程的事件循环仍会被拖住 —— 此时错乱问题没解决,反而引发全局延迟。所以根本解法永远是「消除阻塞」,锁只是兜底手段。

















