PHP WebSocket客户端断线检测唯一可靠方式是监听onClose回调,需结合手动心跳、退避重连及状态清理。textalk/websocket需手动实现重连逻辑,Workerman需注意reconnect参数仅作用于连接前,Swoole客户端存在SSL和协程并发等限制。

PHP 客户端连接 WebSocket 后如何检测断线
PHP 本身不原生支持 WebSocket 客户端长连接(fsockopen 或 stream_socket_client 只能做一次握手,无法维持 WebSocket 帧通信),所以「PHP 连接 WebSocket」通常指用 ReactPHP、Workerman、Swoole 等扩展实现的异步客户端,或通过 cURL + 自定义协议解析(极少见且不推荐)。断线检测依赖底层 socket 状态和心跳响应:
-
onClose回调是唯一可靠信号——所有主流库(如textalk/websocket、workerman/websocket-client)都提供该事件,必须注册处理 - 手动
ping不等于保活:WebSocket 协议要求服务端响应pong,但 PHP 客户端若未启用自动心跳(如textalk/websocket默认不发 ping),就收不到pong,也就无法触发超时断线判断 - 不要依赖
feof($stream)或stream_get_meta_data的timed_out:它们对非阻塞 socket 或已关闭但未通知的连接反应滞后,容易误判
使用 textalk/websocket 实现带退避的重连
textalk/websocket 是纯 PHP 实现、无扩展依赖的轻量客户端,适合 CLI 场景。它不内置重连,需手动封装。关键点在连接失败后延迟重试,避免雪崩式重连:
- 在
onError和onClose中统一触发重连逻辑,不要只监听其中一个 - 用
usleep()或sleep()控制间隔,首次失败建议 1–2 秒,每次失败后指数退避(如 ×1.5),上限设为 30 秒,防止压垮服务端 - 必须限制最大重试次数(例如 10 次),否则网络持续异常时会无限 fork 进程或占用内存
- 示例片段:
$reconnectCount = 0; $maxRetries = 10; $delay = 1;
$client->on('close', function ($code = null, $reason = null) use ($client, &$reconnectCount, &$delay, $maxRetries) { if ($reconnectCount < $maxRetries) { $reconnectCount++; echo "Connection closed ({$code}): {$reason}. Reconnecting in {$delay}s...\n"; sleep($delay); $delay = min($delay * 1.5, 30); // 退避上限 $client->connect(); } });
Workerman websocket-client 的 reconnect 配置陷阱
workerman/websocket-client 基于 Workerman,自带 reconnect 参数,但默认值易被忽略:
-
reconnect默认是false,即使写了'reconnect' => true,也仅在连接建立前失败时重试,**不覆盖已连接后断开的情况** - 真正生效的是
onClose内手动调用$client->connect(),且必须确保$client实例未被销毁(常见错误:在回调里unset($client)或作用域丢失) - 如果启用了
heartbeat(心跳),要确认服务端是否响应pong;否则onClose可能因心跳超时触发,此时重连逻辑需与普通断线一致 - 注意进程模型:CLI 下单进程运行没问题,但若用
Worker::reload()或守护模式,重连定时器可能被中断,需用Timer::add()显式管理
为什么不用 Swoole 的 WebSocket\Client 做重连?
Swoole\WebSocket\Client 支持 onConnect/onMessage/onClose,看起来很合适,但实际有硬伤:
立即学习“PHP免费学习笔记(深入)”;
- 它不支持
SSL/TLS握手后的 WebSocket 升级(wss://在旧版本会直接失败,v4.8+ 虽支持但需显式设置['ssl_host_name' => 'xxx'],否则证书校验报错SSL routines:ssl3_get_record:wrong version number) -
onClose触发后,$client->connect()必须等当前事件循环空闲才能执行,否则报Operation not permitted;需用Swoole\Coroutine::defer()或go(function() use ($client) { $client->connect(); }) - 没有内置连接状态机,多次快速断连+重连容易堆积协程,需自行加锁(如
static $isConnecting = false)防止并发 connect - 相比 ReactPHP 或 Workerman,Swoole 客户端生态弱,出问题时调试日志少,
strace看不到底层 socket 行为
重连不是加个循环就能稳,核心在于区分「连接建立失败」和「连接中意外断开」,前者靠重试参数,后者靠事件驱动 + 状态清理。最容易被忽略的是:重连前没清空上一次的定时器、没重置心跳计数器、没检查服务端是否真在返回 pong——这些都会让重连逻辑形同虚设。











