本文目录导读:

我来详细介绍PHP实现SSE(Server-Sent Events)服务端推送的技术方案。
SSE基础概念
什么是SSE?
- 服务器向客户端单向推送数据的web技术
- 基于HTTP长连接
- 自动重连机制
- 比WebSocket更轻量,实现更简单
基础实现
1 简单的SSE服务端
<?php
// sse.php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // 禁用Nginx缓冲
// 禁用PHP执行时间限制
set_time_limit(0);
// 关闭输出缓冲
while (ob_get_level()) {
ob_end_flush();
}
ob_implicit_flush(true);
function sendEvent($data, $event = 'message', $id = null) {
if ($id !== null) {
echo "id: {$id}\n";
}
if ($event !== 'message') {
echo "event: {$event}\n";
}
echo "data: " . json_encode($data) . "\n\n";
flush();
}
// 循环推送数据
$counter = 0;
while (true) {
$counter++;
// 发送消息
sendEvent([
'time' => date('H:i:s'),
'counter' => $counter,
'message' => '这是第' . $counter . '条消息'
], 'message', $counter);
// 每2秒发送一次
sleep(2);
// 可以设置结束条件
if ($counter >= 10) break;
}
?>
2 完整的前端客户端
<!DOCTYPE html>
<html>
<head>SSE Demo</title>
</head>
<body>
<h1>SSE 实时数据</h1>
<div id="output"></div>
<script>
// 创建EventSource连接
const eventSource = new EventSource('sse.php');
// 处理连接打开
eventSource.onopen = function(e) {
console.log('连接已建立');
addMessage('系统', '连接成功');
};
// 处理默认消息
eventSource.onmessage = function(e) {
const data = JSON.parse(e.data);
addMessage('消息', data.message, e.lastEventId);
};
// 处理自定义事件
eventSource.addEventListener('custom', function(e) {
const data = JSON.parse(e.data);
addMessage('自定义', data.message);
});
// 处理错误
eventSource.onerror = function(e) {
console.log('连接错误,准备重连...', e);
addMessage('错误', '连接中断,正在重连...');
};
function addMessage(type, message, id) {
const div = document.getElementById('output');
const p = document.createElement('p');
p.innerHTML = `<strong>${type}:</strong> ${message} ${id ? '(ID: ' + id + ')' : ''}`;
div.appendChild(p);
}
// 关闭连接
function closeConnection() {
eventSource.close();
addMessage('系统', '连接已关闭');
}
</script>
</body>
</html>
高级应用模式
1 事件驱动异步推送
<?php
// async_sse.php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
// 使用Redis订阅模式实现事件驱动
class SSEEventManager {
private $redis;
public function __construct() {
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function run() {
// 记录客户端连接
$clientId = $_GET['client'] ?? uniqid();
// 订阅频道
$this->redis->subscribe(['channel_' . $clientId, 'global'], function($redis, $channel, $message) {
if ($message === 'PING') return;
echo "data: {$message}\n\n";
flush();
});
}
// 推送消息到特定客户端
public function pushToClient($clientId, $data) {
$this->redis->publish('channel_' . $clientId, json_encode($data));
}
}
?>
2 支持多用户分区推送
<?php
// multi_user_sse.php
class MultiUserSSE {
private $sessionId;
private $lastEventId;
public function __construct() {
$this->sessionId = $_GET['session'] ?? 'default';
$this->lastEventId = $_GET['lastEventId'] ?? null;
}
public function stream() {
$this->setupHeaders();
while (true) {
// 从数据库或缓存获取新数据
$events = $this->fetchNewEvents();
if (!empty($events)) {
foreach ($events as $event) {
$this->sendEvent($event);
}
}
// 发送心跳包
echo ": heartbeat\n\n";
flush();
// 检查客户端是否断开
if (connection_aborted()) {
break;
}
sleep(5);
}
}
private function fetchNewEvents() {
// 从Redis或数据库获取增量事件
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$events = [];
$key = "events:{$this->sessionId}";
// 获取新事件
$lastId = $this->lastEventId ?? 0;
$newEvents = $redis->zRangeByScore($key, $lastId + 1, '+inf');
foreach ($newEvents as $event) {
$events[] = json_decode($event, true);
}
return $events;
}
private function sendEvent($event) {
$id = $event['id'];
$eventType = $event['type'] ?? 'message';
echo "id: {$id}\n";
echo "event: {$eventType}\n";
echo "data: " . json_encode($event['data']) . "\n\n";
flush();
}
private function setupHeaders() {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
set_time_limit(0);
while (ob_get_level()) {
ob_end_flush();
}
ob_implicit_flush(true);
}
}
?>
3 与数据库事件集成
<?php
// db_sse.php
class DatabaseSSE {
private $db;
public function __construct() {
$this->db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
}
public function streamNewRecords() {
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$lastId = 0;
while (true) {
try {
// 查询新增记录
$stmt = $this->db->prepare("
SELECT *, id FROM messages
WHERE id > ? AND status = 'new'
ORDER BY id ASC
LIMIT 10
");
$stmt->execute([$lastId]);
$newRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($newRecords as $record) {
$lastId = $record['id'];
echo "id: {$record['id']}\n";
echo "event: new-record\n";
echo "data: " . json_encode($record) . "\n\n";
flush();
// 标记已处理
$updateStmt = $this->db->prepare("
UPDATE messages SET status = 'sent' WHERE id = ?
");
$updateStmt->execute([$record['id']]);
}
echo ": keepalive\n\n";
flush();
} catch (Exception $e) {
echo "event: error\n";
echo "data: " . json_encode(['message' => $e->getMessage()]) . "\n\n";
flush();
}
if (connection_aborted()) {
break;
}
sleep(2);
}
}
}
// 后台推送脚本示例
// push_message.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = $_POST['message'] ?? '';
// 确保所有已连接的客户端收到消息
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->publish('global', json_encode([
'time' => time(),
'message' => $message
]));
echo 'Message pushed';
}
?>
性能优化与最佳实践
1 使用长连接池
<?php
// optimized_sse.php
class OptimizedSSE {
private $keepAliveTime = 30;
public function start() {
$this->setupEnvironment();
// 发送初始消息确认连接
$this->send('connected', ['status' => 'ok', 'time' => time()]);
$lastKeepAlive = time();
while (true) {
// 获取新数据(这里使用Redis)
$data = $this->getIncrementalData();
if ($data) {
$this->send('update', $data);
}
// 心跳包维护连接
$now = time();
if ($now - $lastKeepAlive >= $this->keepAliveTime) {
echo ": heartbeat\n\n";
flush();
$lastKeepAlive = $now;
}
// 检测客户端断开
if (connection_aborted()) {
$this->cleanup();
break;
}
usleep(100000); // 100ms
}
}
private function getIncrementalData() {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 从Redis列表获取新数据
$data = $redis->lPop('events_queue');
return $data ? json_decode($data, true) : null;
}
}
?>
2 Nginx配置优化
# nginx.conf 配置
location /sse {
proxy_pass http://php-backend;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
# 禁用压缩
gzip off;
# 允许CORS
add_header Access-Control-Allow-Origin *;
add_header X-Accel-Buffering no;
}
完整示例:通知系统
<?php
// notification_system.php
class NotificationSystem {
private $userId;
private $redis;
private $db;
public function __construct($userId) {
$this->userId = $userId;
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379);
}
public function stream() {
// 设置SSE头
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
// 订阅用户通知频道
$channel = "user:{$this->userId}:notif";
$this->redis->subscribe([$channel], function($redis, $channel, $message) {
$notification = json_decode($message, true);
echo "id: {$notification['id']}\n";
echo "event: notification\n";
echo "data: " . json_encode([
'type' => $notification['type'],
'content' => $notification['content'],
'time' => $notification['created_at']
]) . "\n\n";
flush();
});
}
public function sendNotification($content, $type = 'info') {
$notification = [
'id' => uniqid(),
'user_id' => $this->userId,
'content' => $content,
'type' => $type,
'created_at' => date('Y-m-d H:i:s')
];
// 存储通知到数据库
$this->saveToDatabase($notification);
// 发布到Redis频道
$json = json_encode($notification);
$this->redis->publish("user:{$this->userId}:notif", $json);
}
private function saveToDatabase($notification) {
// 数据库操作...
}
}
?>
注意事项
1 内存和资源管理
- 设置合理的执行时间限制
- 定期清理资源和关闭数据库连接
- 处理客户端断线情况
2 错误处理
try {
// SSE逻辑
} catch (Exception $e) {
echo "event: error\n";
echo "data: " . json_encode(['message' => '服务器错误']) . "\n\n";
flush();
} finally {
// 清理资源
if (isset($redis)) {
$redis->close();
}
}
3 测试注意事项
- 浏览器限制:一个域名的并发连接数
- 移动设备电池消耗
- 服务器资源占用
选择SSE还是WebSocket取决于具体需求,SSE更适合单向推送、自动重连的场景,实现简单、支持HTTP/2,适合实时通知、实时数据更新等应用。