WebSocket中Frame分帧发送是协议层语义单位,含opcode、FIN标志等结构,保障消息边界与类型;而普通send仅TCP层字节流,无帧边界控制。

Frame分帧发送和普通send的区别
WebSocket协议本身不保证消息原子性,send() 发送大块数据时,底层 TCP 可能自动拆包,客户端收到的是一连串未标记边界的字节流。而 Frame 是 WebSocket 协议层的语义单位:每个 Frame 有明确的 opcode(如 0x1 文本、0x2 二进制)、FIN 标志(是否为消息末尾)、掩码位和有效载荷。Swoole 的 $server->push($fd, $data) 实际就是封装成单个文本 Frame 发送;但若你手动用 $conn->send()(TCP 模式)或绕过 WebSocket 协议栈直发裸数据,就失去了帧边界控制能力。
什么时候必须自己控制Frame结构
需要跨帧传输超长响应、实现 token 流式分片、或与自定义协议(如 LLMP)对接时,就不能依赖 push() 封装的整包 Frame。例如 LLM 流式输出中,每个 token 或每 50ms 的 chunk 都应作为独立 Frame 推送,否则客户端无法及时渲染。此时要主动构造带 FIN=0 的中间帧和 FIN=1 的结束帧:
-
FIN=0+opcode=0x1:表示该帧是某条文本消息的中间部分,后续还有帧 -
FIN=1+opcode=0x1:表示这是该文本消息的最后一帧 - 连续多个
FIN=0帧必须使用相同opcode,且不能混用文本/二进制 - Swoole 默认不暴露原始 Frame 构造 API,需用
$server->pack()手动编码(PHP 8.1+)或改用Swoole\Coroutine\HTTP\Client接收流后自行分帧
协程环境下分帧发送的内存与调度风险
在 go(function() { ... }) 中高频调用 $server->push() 看似简单,但每调用一次都触发一次协议编码、内存拷贝和内核 write,容易打满 Worker 进程的 CPU 时间片。更危险的是:若 LLM 接口返回极快(如本地 Ollama),协程未 yield 就连续 push 数百帧,会阻塞同 Worker 内其他连接的事件处理。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 必须在循环中插入
co::sleep(0.001)或chan->push()+ 异步消费队列做节流 - 避免在单次
message回调里直接foreach ($stream as $token) { $server->push(...) } - 真实生产环境建议用
Swoole\Coroutine\Channel缓冲 token,由独立协程按固定速率 pop 并 push,解耦生成与发送节奏 - 注意
$server->connections[$fd]是进程内变量,跨 Worker 不可见;若启用了多 Worker,会话状态必须外存(如 Redis)
调试分帧问题的三个关键检查点
客户端收不到分片、乱序、或卡在中间帧,大概率不是网络问题,而是帧结构违规。用 Wireshark 抓包看 WebSocket 数据流时,重点核对:
- 每帧开头 2 字节是否符合 RFC6455:第 1 字节高 4 位是
FIN+RSV+opcode,第 2 字节高 1 位是Mask(WebSocket over HTTP 必须置 1) - 客户端是否正确处理了
opcode=0x0(continuation frame)——很多 JS WebSocket 库默认忽略它,只认0x1/0x2 - 服务端是否在 FIN=0 帧后立刻发了不同 opcode 的帧(比如中间插了个 ping 帧),这会破坏消息连续性
真正难的不是发帧,是让每一帧都落在 TCP 包边界上且被客户端严格按序重组——这要求你在协程调度、缓冲区大小、LLM 输出节奏三者间做精细平衡。

















