本文目录导读:

- 方式一:最简单直接的
while + flush()(适合新手理解) - 方式二:使用框架特性(以 Laravel 为例)
- 方式三:使用
Swoole或Workerman(生产环境推荐) - 方式四:使用
SSE库(如php-sse) - 关键细节注意事项(踩坑必看)
- 总结对比表
在 PHP 中实现 SSE(Server-Sent Events,服务器发送事件),其实质是持续输出 HTTP 响应,并设置特定的 Content-Type 和 缓存控制 头。
由于 PHP 是脚本语言,默认执行完就结束,要实现 SSE,必须打破脚本执行上限并及时刷新缓冲区。
以下是 4 种主流实现方式,按复杂度和使用场景排序:
最简单直接的 while + flush()(适合新手理解)
这是最纯粹的 SSE 写法,不依赖任何框架。核心逻辑: 用 set_time_limit(0) 防止超时,用 ob_end_flush() 关闭输出缓冲,用 flush() 强制推送。
<?php
// 1. 必须设置响应头
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no'); // 禁用 Nginx 缓冲(如果用了 Nginx)
// 2. 关闭执行超时(PHP 默认 30 秒超时)
set_time_limit(0);
// 3. 核心:关闭 PHP 输出缓冲,让数据立刻发送
// 如果之前有开启 ob_start(),需要先清理掉
while (ob_get_level() > 0) {
ob_end_clean();
}
// 4. 主循环:无限推送数据
$counter = 0;
while (true) {
// 构建 SSE 格式的数据(必须是 "data: ...\n\n" 格式)
echo "id: " . $counter . "\n";
echo "data: " . json_encode(['time' => date('H:i:s'), 'count' => $counter]) . "\n\n";
// 强制刷新输出缓冲区到浏览器
flush();
// 防止 CPU 跑满,休眠 1 秒
sleep(1);
$counter++;
// 可选:设置退出条件(比如客户端断开检测)
if (connection_aborted()) {
break;
}
}
?>
使用框架特性(以 Laravel 为例)
在 Laravel 中,你可以用 response()->stream() 实现,语法更优雅,且会自动处理响应头。
// routes/web.php 或 routes/api.php
Route::get('/sse', function () {
return response()->stream(function () {
// 关闭缓冲区
while (ob_get_level() > 0) {
ob_end_clean();
}
$i = 0;
while (true) {
echo "event: ping\n";
echo "data: " . json_encode(['message' => 'Hello ' . $i]) . "\n\n";
ob_flush();
flush();
if (connection_aborted()) break;
sleep(1);
$i++;
}
}, 200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'X-Accel-Buffering' => 'no',
]);
});
使用 Swoole 或 Workerman(生产环境推荐)
如果追求高性能和长连接数以万计,原生 PHP 的 apache + mod_php 或 php-fpm 会因为每个连接占用一个进程而内存耗尽。
推荐使用常驻内存框架:
// 使用 Swoole 示例
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
$server = new Server("0.0.0.0", 9501);
$server->on('Request', function (Request $request, Response $response) {
// 设置 SSE 头
$response->header('Content-Type', 'text/event-stream');
$response->header('Cache-Control', 'no-cache');
// 发送初始数据
$response->write("retry: 1000\n\n");
// 定时器每秒推送数据(Swoole 的 tick 是异步非阻塞的)
$timerId = swoole_timer_tick(1000, function () use ($response) {
$data = json_encode(['time' => date('H:i:s')]);
$response->write("data: {$data}\n\n");
// 如果客户端断开,停止定时器
if (!$response->isWritable()) {
swoole_timer_clear($timerId);
}
});
});
$server->start();
使用 SSE 库(如 php-sse)
对于复杂的推送场景(如多频道、断线重连、事件广播),可以直接用现成库:
composer require sse/sse
require 'vendor/autoload.php';
use Sse\Event;
use Sse\SSE;
class MyEvent extends Event {
public function update() {
// 返回要推送的数据
return json_encode(['data' => date('H:i:s')]);
}
public function check() {
// 检查是否需要推送
return true;
}
}
$sse = new SSE();
$sse->addEventListener('my_event', new MyEvent());
$sse->start();
关键细节注意事项(踩坑必看)
Nginx 缓冲问题(最常见坑)
如果不加 X-Accel-Buffering: no,Nginx 会缓冲你的输出,直到脚本结束才发给客户端,导致“实时性”失效。
header('X-Accel-Buffering: no');
// 或者 Nginx 配置 fastcgi_buffering off;
Apache 压缩问题
如果开启了 gzip,SSE 数据流会被压缩,浏览器可能无法实时解析,需要关闭压缩:
SetEnv no-gzip 1
PHP-FPM 超时问题
不仅要在脚本里 set_time_limit(0),还要在 php.ini 里设置:
max_execution_time = 0
连接断开的处理
客户端刷新或关闭页面时,PHP 不会立刻停止,要定期检查:
if (connection_aborted()) {
// 结束循环,释放资源
break;
}
如果使用 Swoole,则监听 close 事件。
数据格式标准
必须严格遵守 SSE 协议:
- 每条数据前必须有
data:前缀 - 必须以空行
\n\n- 可选字段:
id:(断线重连依据)、event:(自定义事件名)、retry:(重连间隔) - 可选字段:
总结对比表
| 方式 | 并发能力 | 稳定性 | 适用场景 |
|---|---|---|---|
| 原生态 while | 低(~几百) | 中等 | 学习、内网小工具 |
| Laravel Stream | 中(~几千) | 高 | 基于框架的业务推送 |
| Swoole/Workerman | 高(~几万) | 很高 | 生产环境、金融行情、实时聊天 |
| 第三方库 | 中 | 高 | 快速集成,不想写底层 |
建议:
- 如果只是搞个 Demo 或内部系统,方式一就够了。
- 如果是线上正式项目且用
PHP-FPM,一定要用方式二(框架封装)并处理好 Nginx 缓冲。 - 如果是高并发实时推送(如股票、在线人数),建议直接用 Node.js 或 Go,PHP 做 SSE 开销较大,如果必须用 PHP,强烈建议 Swoole 常驻内存方案。