WebSocket 调用 close() 后为何收不到关闭帧?
结论
仅凭现有日志,无法判断浏览器是否发送了关闭帧。服务端目前只检查每次 fread() 返回数据的第一个字节,但 TCP 的读取边界不等于 WebSocket 帧边界。一次读取可能包含半个帧、多个帧,也可能从帧的中间开始。因此,ord($data[0]) & 15 无法可靠判断这次读取中是否包含 opcode 0x8。
浏览器调用 close() 会发起关闭握手,但连接不会立即关闭。关闭帧可能要等已排队的数据发送完毕后才发出。关闭事件中的 1006 是浏览器用于报告连接异常结束的保留状态码;它无法指出故障发生在哪段链路,也不是对端实际发送的关闭帧状态码。
先确认客户端状态与关闭事件
为连接添加 close 和 error 监听,并在调用 close() 前记录 bufferedAmount。这些信息可以帮助确认浏览器是否进入关闭流程、是否还有待发送数据,以及浏览器最终报告了什么:
ws.addEventListener('close', (event) => {
console.log('close event:', {
code: event.code,
reason: event.reason,
wasClean: event.wasClean,
readyState: ws.readyState
});
});
ws.addEventListener('error', (event) => {
console.log('WebSocket error:', event);
});
console.log('before close:', {
readyState: ws.readyState,
bufferedAmount: ws.bufferedAmount
});
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
readyState === WebSocket.OPEN 等价于 readyState === 1,使用数值写法不会改变关闭行为。如果 bufferedAmount 大于零,先确认应用没有继续调用 send(),并注意关闭过程可能需要等待已排队的数据发送完毕。
按 WebSocket 帧边界解析服务端读取结果
WebSocket 运行在 TCP 之上。TCP 传输有序字节流,不保留应用层的消息边界,因此 fread($sock, 8192) 返回的内容不一定正好是一帧。例如,缓冲区可能先包含一个文本帧,后面紧接着关闭帧。此时只检查 $data[0],读到的会是文本帧的 opcode,后面的字节则被忽略。也可能一次只读到帧的一部分,需要拼接后续数据再解析。
服务端应为每个客户端保留接收缓冲区,并按 WebSocket 帧格式逐帧解析。数据不足以解析完整帧时,应等待下一次读取。解析时要处理帧头、扩展长度、客户端掩码和负载长度,不能把一次 fread() 当作一帧。下面的调试方式可以避免只检查整个读取块的首字节,但仍需接入完整的帧解析器,不能代替协议解析:
$data = fread($sock, 8192);
if ($data === false) {
// 处理读取错误
} elseif ($data === '' && feof($sock)) {
// 对端已关闭 TCP 连接
} elseif ($data !== '') {
// 将数据追加到该客户端的缓冲区,再按帧格式逐帧解析。
}
不要只用 if ($data === '') continue; 处理空字符串。空读和 EOF 的含义不同,应结合 feof($sock) 和读取错误进行处理。stream_select() 告知流可读,也不代表一次读取就能得到完整的 WebSocket 帧。
区分客户端、代理和服务端问题
可以沿链路逐段检查,缩小排查范围:
- 在浏览器开发者工具中确认查看的是该连接的 WebSocket 帧列表,并检查关闭操作前后的发送帧和连接事件。
close事件会提供浏览器观察到的最终结果,但1006无法指出故障位置。 - 在 PHP 服务端保存原始读取字节,用完整的帧解析器检查所有帧,不要只打印每次读取的第一个 opcode。同时检查连接是否先收到 EOF 或读取错误。
- 比较经 Apache
proxy_wstunnel转发和直连服务端时的结果。如果直连能完成关闭握手,经代理却不行,就检查代理转发链路。如果两种方式的原始数据都到达服务端,再检查服务端的帧解析和关闭握手处理。
服务端收到客户端的关闭帧后,通常应按 WebSocket 关闭握手处理,并在适当情况下返回关闭帧,再关闭连接。具体实现取决于现有的 PHP 协议处理代码。由于题目没有提供握手、帧解析和服务端关闭逻辑,目前还不能判断是哪一处出错。纯 PHP 可以实现 WebSocket 协议处理,但需要正确处理帧边界、掩码、负载长度和关闭流程。
备注:内容仅供参考。
