ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP8.5怎么配置WebSocket支持长连接

PHP8.5怎么配置WebSocket支持长连接 前言典型症状是这样的前端new WebSocket(ws://你的域名/ws)一连就断浏览器控制台报WebSocket connection to ws://... failed: Error during WebSocket handshake: Unexpected response code: 400或者连上以后恰好 60 秒整断开一次刷新重连又能用。服务端日志里什么都没有php-fpm的 access log 只留下一条普通的 200 或 502。先把版本问题说清楚WebSocket 不是 PHP 的语言特性PHP 8.5 也没有新增 WebSocket 配置这回事。PHP 自带的是一套通用的 socket 与 stream API以及 SAPIServer API的请求模型从来没有内置的 WebSocket 服务器。网上有些文章把PHP 8.5 支持 WebSocket写成新特性这是不准确的PHP 8.5 在语言层面的新东西是管道操作符|、clone with这类语法与 WebSocket 无关。真正决定 WebSocket 能不能跑起来的是你选的运行模型和反向代理配置。本文讲清楚三件事为什么php-fpm模型天生做不了长连接、用常驻进程扩展Swoole/OpenSwoole怎么把服务跑起来、以及 Nginx 反代这一步必须改哪几个参数才能让连接真正长下去。文中示例需要 PHP 8.0 及以上扩展部分需要 Swoole 4.x/5.x 或 OpenSwoole。一、先理解为什么 php-fpm 撑不起 WebSocketphp-fpmFastCGI Process Manager的模型是一次请求一条命一个 worker 进程接到请求执行脚本把响应写回然后释放本次请求的全部内存回到进程池等下一个请求。这个模型有三个绕不过去的限制进程不能跨请求持有内存连接状态、订阅关系、房间成员全都存不住一个 worker 同一时刻只服务一个请求连接挂住不返回就等于占死一个进程几十个连接就能把pm.max_children吃光FastCGI 协议本身是一问一答的中间加 Nginx 也不会把一次 TCP 连接升级成全双工。所以长连接必须换运行模型让进程常驻、常驻内存里维护连接表。可选路线有三条选型差异很大路线代表适用场景主要代价常驻进程扩展Swoole / OpenSwoole高并发推送、IM、实时看板需要装扩展代码风格与 fpm 不同用户态框架Workerman、Ratchet不想装扩展、中小规模纯 PHP 实现靠stream_select轮询自建 socketstream_socket_server学习、极小规模、内网工具握手、分帧、心跳全自己写本文用 Swoole 系讲因为它的 WebSocket 服务器是开箱的抽象层次刚好。二、把 WebSocket 服务跑起来Swoole 提供Swoole\WebSocket\ServerOpenSwoole 是同源代码的另一分支类名带OpenSwoole\前缀用法一致。下面是一个可以直接运行的最小服务保存为ws_server.php?php // ws_server.php —— 需要 PHP 8.0 与 Swoole 4.x/5.xOpenSwoole 把命名空间换成 OpenSwoole\ 即可 declare(strict_types1); $server new Swoole\WebSocket\Server(0.0.0.0, 9502); $server-set([ worker_num 2, // 业务进程数 daemonize false, // 生产环境可交给 systemd 托管 heartbeat_check_interval 30, // 每 30 秒轮询一次所有连接 heartbeat_idle_time 90, // 超过 90 秒没有任何数据的连接被强制关闭 log_level SWOOLE_LOG_WARNING, ]); // 连接建立握手已经由扩展完成这里拿到的是已经升级过的连接 $server-on(Open, function (Swoole\WebSocket\Server $server, Swoole\Http\Request $request) { echo open: fd{$request-fd}\n; $server-push($request-fd, json_encode([ type welcome, fd $request-fd, ], JSON_UNESCAPED_UNICODE)); }); // 收到消息 $server-on(Message, function (Swoole\WebSocket\Server $server, Swoole\WebSocket\Frame $frame) { $text $frame-data; // 应用层心跳客户端发 {type:ping}服务端回 pong if (str_contains($text, ping)) { $server-push($frame-fd, {type:pong}); return; } // 群发真实项目里应换成房间/频道表别真用 connections 全量广播 foreach ($server-connections as $fd) { if ($server-isEstablished($fd)) { $server-push($fd, json_encode([ type message, from $frame-fd, data $text, ], JSON_UNESCAPED_UNICODE)); } } }); // 连接关闭 $server-on(Close, function (Swoole\WebSocket\Server $server, int $fd) { echo close: fd{$fd}\n; }); $server-start();关键点只有三个握手HTTP Upgrade由扩展自动完成业务只需要处理Open/Message/Closepush()是服务端主动下发这是长连接相对 HTTP 轮询的核心价值$frame-fd是连接标识不是用户 ID用户身份要在Open时从$request-header里取 Cookie 或 token 自己映射。需要 PHP 8.0 的str_contains()如果跑在 7.4 上要换回strpos() ! false。启动与自测php ws_server.php # 另开一个终端用一个现成的 WebSocket 客户端连上去看看 # 浏览器控制台里执行下面的 JS 也能验证// 浏览器控制台或前端页面里执行用于验证链路 const ws new WebSocket(ws://127.0.0.1:9502); ws.onopen () ws.send(hello); ws.onmessage (e) console.log(recv:, e.data); ws.onclose (e) console.log(closed:, e.code, e.reason);三、Nginx 反代长连接必须改的几个参数线上通常是wss://TLS 加 WebSocket前端连 NginxNginx 反代到本地 9502。这时默认配置一定会失败或定时断开因为 Nginx 默认走 HTTP/1.0、不带 Upgrade 头、并且proxy_read_timeout只有 60 秒。完整的配置片段# 用 map 决定升级头只有真的带 Upgrade 的请求才回 upgrade map $http_upgrade $connection_upgrade { default upgrade; close; } server { listen 443 ssl; server_name ws.example.test; # ssl_certificate / ssl_certificate_key 略 location /ws { proxy_pass http://127.0.0.1:9502; # 三件套HTTP/1.1 Upgrade Connection缺一个握手就 400 proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; # 读超时要大于心跳间隔否则 Nginx 会主动掐断空闲连接 proxy_read_timeout 3600s; proxy_send_timeout 3600s; # 关掉缓冲否则消息可能被攒着不发 proxy_buffering off; } }如果套的是 CDN 或七层负载均衡还要在那一层单独开 WebSocket 透传并确认它自己的空闲超时大于你的心跳周期。这两处超时只要有一处小于心跳间隔就会稳定地每分钟断一次。超时参数这样配的原因是Nginx 用proxy_read_timeout判断后端多久没说话空闲的长连接天然不产生数据所以这个值必须大于应用层心跳间隔。心跳本身可以交给 Swoole 的heartbeat_check_interval/heartbeat_idle_time也可以由客户端定时发{type:ping}能穿过某些对空闲连接不友好的中间设备。四、让连接真的稳定心跳、状态与扩展长连接的第二类故障不是连不上而是连上了但状态丢了。这条链路上有三个容易忽略的协作点心跳要有两条传输层保活Swoole 心跳或 WebSocket Ping 帧负责探测死连接应用层 ping/pong 负责让 Nginx 和中间设备看到流量。身份不能只挂在连接上fd在重连后会变用户断线重连后必须重新绑定fd - uid并清理旧的映射否则会出现消息发给了一个已经不存在的 fd。多进程要共享状态上面worker_num 2时两个 worker 的连接表是各自独立的A worker 里的push()推不到 B worker 的 fd。生产上要么用单 worker受单核限制要么用sendMessage()让 worker 之间转发要么把在线表放进 Redis 并用订阅/发布广播。?php // 多 worker 下安全的点对点推送worker 之间通过 sendMessage 转发 $server-on(Message, function (Swoole\WebSocket\Server $server, Swoole\WebSocket\Frame $frame) { $targetWorkerId $frame-fd % $server-setting[worker_num]; $server-sendMessage(json_encode([ target_fd $frame-fd, payload $frame-data, ]), $targetWorkerId); }); // 对应的管道消息回调 $server-on(PipeMessage, function (Swoole\WebSocket\Server $server, int $srcWorkerId, $message) { $msg json_decode($message, true); if ($server-isEstablished($msg[target_fd])) { $server-push($msg[target_fd], $msg[payload]); } });常见坑点❌ 在php-fpm里写set_time_limit(0)加死循环以为连接就能一直挂着✅fpm模型下请求结束就释放内存长连接必须换成常驻进程Swoole/Workermanset_time_limit解决不了模型问题❌ Nginx 里只写proxy_pass不加Upgrade与Connection头✅ 必须同时给proxy_http_version 1.1、proxy_set_header Upgrade $http_upgrade、proxy_set_header Connection $connection_upgrade少一个就握手失败❌ 改了业务心跳周期却没改proxy_read_timeout连接还是每小时/每分钟规律性断开✅ 反代读超时必须大于心跳间隔且 CDN/负载均衡那一层的超时也要同步放长❌ 把$frame-fd当作用户 ID 做持久化存储✅fd是连接级标识断线重连必然变化应当维护uid - fd的映射并在Close时让映射失效❌ 在onMessage里做同步阻塞操作sleep()、同步 HTTP 请求、大文件读写✅ 常驻进程里阻塞一个 worker 就是阻塞它名下所有连接改用协程客户端或把重活丢给任务队列❌ 在 Swoole 环境里用$_SERVER[HTTP_COOKIE]之类超全局变量取请求信息✅ 用$request-header[cookie]或$request-get/$request-post因为常驻进程里超全局变量语义与 fpm 不同❌ 广播时不判断连接状态直接对全量 fd 调push()✅ 先isEstablished($fd)再推否则会在已断开的 fd 上抛错误、污染日志❌ 用普通 HTTP 压测工具如只发一次 GET 的脚本来验证长连接✅ 用真正的 WebSocket 客户端浏览器、wscat之类观察open/message/close事件HTTP 工具永远测不出握手与心跳问题总结环节关键配置/做法配错的症状运行模型常驻进程Swoole/WorkerManfpm 下连接挂不住、worker 被占满Nginx 升级头UpgradeConnection HTTP/1.1握手 400浏览器直接报错Nginx 超时proxy_read_timeout大于心跳间隔空闲连接定时被掐断心跳传输层探活 应用层 ping/pong半开连接堆积消息石沉大海身份映射维护uid - fdClose时清理断线重连后收不到消息多进程状态sendMessage()转发或 Redis 共享跨 worker 推送丢失PHP 配置 WebSocket这个说法本身容易误导PHP 语言层没有可配置的开关8.5 也没有新增相关特性真正要做的是选对运行模型再把反向代理的升级头与超时对齐。把这两件事做对长连接就只是一个常驻进程加一段代理配置的问题。
返回列表