PHP长连接推送实战指南:从WebSocket到SSE的架构选型与代码实现
目录导读
- 为什么PHP需要长连接推送? —— 从HTTP无状态到实时交互的痛点
- 长连接技术全景图 —— WebSocket / SSE / 轮询 / 长轮询 的优劣对比
- 原生WebSocket实现(Workerman+Swoole) —— 性能之王
- SSE(Server-Sent Events) —— 轻量级单向推送
- Redis Pub/Sub + 前端轮询 —— 传统架构的优雅转型
- 核心代码示例 —— 手写可靠的心跳检测与断线重连
- 高频问答 —— 解决并发瓶颈、Nginx配置、跨域及安全拦截
- SEO优化技巧 —— 如何让搜索引擎索引你的实时内容
为什么PHP需要长连接推送?
传统HTTP协议是“请求-响应”模式,服务端无法主动向客户端推送数据,但在实时弹幕、股票行情、在线协作等场景中,服务器需要将事件毫秒级推送给用户,PHP虽然被诟病为“短命进程”,但通过常驻内存的框架(如Workerman、Swoole),完全能构建高性能长连接服务,根据Google官方文档,将页面更新延迟从500ms降至100ms,用户流失率可降低21%,长连接推送不只是技术升级,更是产品竞争力的核心。

长连接技术全景图
| 技术方案 | 连接方向 | 实时性 | 浏览器兼容 | 服务器压力 |
|---|---|---|---|---|
| WebSocket | 全双工 | 主流 | 中 | |
| SSE | 服务端→客户端 | IE除外 | 低 | |
| 长轮询 | 半双工 | 全支持 | 高 | |
| 短轮询 | 半双工 | 全支持 | 极高 |
核心结论:
- 若需客户端双向交互(如聊天室),必须选WebSocket;
- 若仅服务端推送通知(如新闻订阅),选用SSE可省去复杂协议,且支持自动重连;
- 若现有系统基于FPM且不想引入新框架,Redis长轮询是最平滑的方案。
方案一:原生WebSocket实现(Workerman + Swoole)
为什么选Workerman?
- 纯PHP开发,无需C扩展,便于二次开发;
- 自带进程管理、定时器、异步TCP客户端。
核心代码(服务端):
use Workerman\Worker;
$ws_worker = new Worker("websocket://0.0.0.0:2346");
$ws_worker->onConnect = function($conn) {
echo "新连接: ".$conn->id;
};
$ws_worker->onMessage = function($conn, $data) {
$conn->send("已收到: ".$data);
};
Worker::runAll();
前端JS:
const ws = new WebSocket('ws://你的服务器IP:2346');
ws.onmessage = (event) => console.log(event.data);
关键点:
- 必须设置心跳检测(每30秒发送ping,避免Nginx超时断开);
- 使用
TcpConnection::$maxSendBufferSize防止慢客户端阻塞。
方案二:SSE(Server-Sent Events)
SSE通过EventSource接口实现,服务端需设置Content-Type: text/event-stream,它自动重连、自带事件ID,特别适合股市价格、进度条刷新。
PHP服务端实现(结合Swoole或FPM):
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
while (true) {
echo "data: ".json_encode(['price'=>rand(100,200)])."\n\n";
ob_flush();
flush();
sleep(1);
}
前端订阅:
const source = new EventSource('/stock/price');
source.onmessage = (event) => console.log(event.data);
注意:FPM模式下,建议在Nginx配置proxy_read_timeout 3600s并禁用压缩。
方案三:Redis Pub/Sub + 长轮询
此方案适合不想改变PHP-FPM架构的团队:
- 后端生产者通过
Redis::publish('channel', $data)发布消息; - 消费端(另一个PHP进程)通过
subscribe订阅并写入到MySQL或直接推送给前端长轮询接口。
长轮询核心逻辑:
while (true) {
$data = $redis->blPop('notify_queue', 30);
if ($data) {
echo json_encode($data);
break;
}
}
优势:不新增端口,纯HTTP即可;劣势:实时性受轮询间隔影响。
核心代码示例:心跳检测与断线重连
完整服务端(Workerman)心跳方案:
$ws_worker->onConnect = function($conn) {
$conn->lastMessageTime = time();
};
$ws_worker->onMessage = function($conn, $data) {
$conn->lastMessageTime = time();
if ($data === 'ping') {
$conn->send('pong');
}
};
// 定时检测(每10秒)
$timer = Timer::add(10, function() use ($ws_worker) {
foreach ($ws_worker->connections as $conn) {
if (time() - $conn->lastMessageTime > 60) {
$conn->close("Heartbeat timeout");
}
}
});
前端自动重连:
function connect() {
const ws = new WebSocket('ws://your-domain');
ws.onclose = () => setTimeout(connect, 3000);
ws.onerror = () => ws.close();
ws.onopen = () => setInterval(() => ws.send('ping'), 30000);
}
connect();
高频问答
Q1:Nginx如何配置WebSocket反代?
server {
location /ws/ {
proxy_pass http://127.0.0.1:2346;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
}
Q2:为什么连接数过万后CPU飙升?
答:可能未使用事件驱动库(如libevent或Swoole),在Workerman中启用Worker::$eventLoopClass = \Workerman\Events\Ev::class可提升并发能力。
Q3:跨域如何解决?
在服务端设置Access-Control-Allow-Origin: *即可,注意若开启credentials:true,则不能用通配符,需指定具体域名。
Q4:如何优雅关闭服务?
使用Worker::stopAll()并捕获SIGTERM信号,确保连接数据完整保存。
SEO优化技巧
如实时行情)对爬虫不友好,但可遵循以下策略:
- 预渲染:使用
puppeteer生成静态快照,暴露给百度爬虫; - 结构化数据:用
<meta name="ir-scan" content="...">标记动态区域; - 渐进增强:无JS时降级为传统HTTP刷新,确保内容可索引。