Think-Swoole 实现 WebSocket 必须独立常驻服务,PHP-FPM 不支持 ws 协议,cURL 和 file_get_contents 无法直接连接;需用 Redis pub/sub 或内部 HTTP API 推送消息,并注意配置、端口、多 Worker 连接共享及心跳与离线消息处理。

think-swoole 是 ThinkPHP6 实现实时通信的官方路径,但直接在控制器里调用 $server->push() 或写 new WebSocket() 必然失败——PHP-FPM 进程一结束,连接就丢,协议也不支持。
为什么 curl_init("ws://") 和 file_get_contents("ws://") 一定报错
这不是配置问题,是协议层面不兼容:cURL 和 file_get_contents 都只认 HTTP/HTTPS,压根不解析 ws:// 或 wss://。浏览器能用 WebSocket 构造函数,是因为 JS 运行在渲染引擎里;PHP 没这个能力。
常见错误现象:
-
Unsupported protocol [ws](cURL 报错) -
Connection refused(误以为端口没开,其实是根本没走 WebSocket 握手) - 手动用
stream_socket_client发 raw 字符串,客户端收不到或解析失败——缺Sec-WebSocket-Key、没帧掩码、opcode 错位
onMessage 只处理上行,后端推送必须另起进程
onMessage 是客户端发消息过来时触发的回调,它不负责“从订单系统推一条通知”。ThinkPHP 的 HTTP 请求生命周期太短,$server 实例只活在 Swoole Worker 进程里,PHP-FPM 完全访问不到。
可行方案只有两种,且都要求 WebSocket 服务独立常驻:
立即学习“PHP免费学习笔记(深入)”;
- 用
Redis pub/sub:业务逻辑中$redis->publish('notify:order', json_encode(['order_id' => 123])),Swoole 进程启动时$redis->subscribe(['notify:order']),收到后查user_id → fd映射表再$server->push() - 用内部 HTTP API:Swoole 同时监听一个 HTTP 端口(如
9502),暴露/api/push接口;ThinkPHP 用file_get_contents("http://127.0.0.1:9502/api/push", [...])触发,该接口内调用$server->push()
别把 Redis 当成兜底方案——如果用 publish 后立刻 exit,而 Swoole 还没来得及 subscribe,消息就丢了。确保订阅早于任何发布。
swoole.php 配置错一个键,WebSocket 就静默失败
握手失败、连接立刻断开、前端 onerror 但日志空空如也?大概率是 config/swoole.php 里这几个地方写错了:
-
'type'必须是'websocket'(不是'http',也不是留空) -
'websocket.enabled'必须为true(布尔值,不是字符串"true") -
'handler'类路径要真实存在,且该类必须继承\think\swoole\WebSocket,不能只是普通控制器 -
'route_file'路径必须可读,文件里必须返回一个数组,格式如['chat.send' => \app\event\ChatSend::class],否则自定义消息类型全被忽略
端口冲突也常被忽略:'port' => 9502 若被其他服务占了,php think swoole:start 表面成功,实际监听失败,前端连 ws:// 就超时。
多 Worker 下 $server->connections 不共享,广播前必须校验
默认 mode => SWOOLE_PROCESS,每个 Worker 进程有自己独立的 $server->connections 数组。你在一个 Worker 里遍历 connections 广播,只能触达本进程内的连接。
正确做法:
- 用
Swoole\Table或 Redis 存全局fd → user_id映射,广播前先查表获取所有在线fd - 每次
onOpen时往 Redis 写user:1001:fd_list,onClose时删对应fd,避免僵尸连接 - 广播前对每个
fd调用$server->isEstablished($fd),跳过已断开但未清理的句柄 - 心跳必须配:
'heartbeat_idle_time' => 300,'heartbeat_check_interval' => 60,否则 NAT 网关或代理会静默断连
最容易被忽略的是离线消息兜底——用户断网重连时,onOpen 里得主动拉取 Redis 中 user:1001:offline 列表,推完再清空。这个逻辑不在 ThinkPHP 控制器里,必须塞进 WsHandler 的 onOpen 方法里。



















