服务器主动向客户端实时推送数据
码不停提
SSE 以其简单性和对现有 HTTP 基础设施的良好兼容,成为许多轻量级实时推送场景的首选方案。
码不停提
SSE 以其简单性和对现有 HTTP 基础设施的良好兼容,成为许多轻量级实时推送场景的首选方案。
SSE 是 Server-Sent Events(服务器发送事件)的缩写,是一种允许服务器通过 HTTP 连接向客户端(通常是浏览器)主动推送实时数据的轻量级技术。它是 HTML5 规范的一部分,使用简单、基于文本的协议,特别适合需要单向、持续更新的场景(如通知、行情、日志流)。
text/event-stream 格式传输,支持自定义事件类型和 ID(用于断线续传)。客户端发起连接
使用 JavaScript 的 EventSource 对象,向服务器指定 URL 发起请求:
const source = new EventSource('/stream');
source.onmessage = (event) => {
console.log('收到消息:', event.data);
};
服务器保持连接并推送数据
服务器设置响应头 Content-Type: text/event-stream,并保持连接打开,然后按格式发送数据块。
每条消息以 data: 开头,以两个换行符结束,例如:
data: 这是一条消息\n\n
也可以包含事件类型和 ID:
event: userlogin
data: {"name": "张三"}
id: 1001
客户端接收事件
浏览器通过 onmessage 或 addEventListener 监听消息,并实时处理。
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向(全双工) |
| 协议 | HTTP | ws / wss(独立协议) |
| 数据格式 | 纯文本(可包含 JSON) | 二进制或文本 |
| 自动重连 | 内置支持 | 需手动实现 |
| 浏览器支持 | 广泛(IE 除外) | 几乎所有现代浏览器 |
| 实现复杂度 | 极低(客户端几行代码) | 较高(需处理连接、心跳等) |
| 典型场景 | 新闻推送、股票价格、服务器日志 | 聊天室、在线游戏、协同编辑 |
source.close() 主动断开,服务器端可正常结束响应(或使用超时机制)。以下是一个 PHP 示例,展示如何使用 SSE 将数据推送到客户端:
客户端通过 JavaScript 监听消息事件,这些消息被服务器发送。首先准备好前端界面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SSE Example</title>
</head>
<body>
<h1>Server-Sent Events (SSE) Example</h1>
<div id="events"></div>
<script>
const eventSource = new EventSource('sse.php'); // 与服务器的 SSE 连接
// 监听消息
eventSource.onmessage = function(event) {
const eventContainer = document.getElementById('events');
const newEvent = document.createElement('div');
newEvent.textContent = `Received data: ${event.data}`;
eventContainer.appendChild(newEvent);
};
// 当发生连接错误时
eventSource.onerror = function() {
console.log('Connection error or server closed.');
};
</script>
</body>
</html>
编写 PHP 脚本 sse.php 来读取数据并通过 SSE 推送到客户端。
<?php
// 关掉输出缓冲区控制
if (ob_get_level()) ob_end_clean();
// 设置正确的 SSE 响应头
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
while (true) {
// 输出数据并使用 SSE 格式
echo "data: " . json_encode(['time' => date('Y-m-d H:i:s')]) . "\n\n";
// 刷新缓冲区数据
ob_flush();
flush();
// 每 2 秒推送一次数据
sleep(2);
// 检查客户端是否断开连接
if (connection_aborted()) {
break;
}
}
输出控制:
ob_end_clean()),否则数据无法及时发送。ob_flush() 和 flush() 是强制刷新输出缓冲区到客户端的操作。确保使用合规的 SSE 数据格式:
data: 开头来传递数据,之后以 \n\n 进行分隔。id:、event: 和 data: 控制消息的 ID、类型以及内容。客户端断开处理:
connection_aborted() 函数返回一个布尔值,表示客户端是否已断开连接(如关闭浏览器)。浏览器兼容性:
长时间运行的 PHP 脚本:
需要配置 PHP 的最大执行时间 max_execution_time,设置为 0 可以禁用超时:
max_execution_time = 0
使用 EventSource 的重连机制:
如果应用场景需要双向通信或更复杂的实时交互,推荐使用 WebSocket 技术。
在 PHP 中实现 SSE 时,每一个客户端连接都会开启一个独立的 PHP 进程(或线程,具体取决于服务器配置,例如 Apache 使用 mod_php 或者 Nginx + PHP-FPM)。以下是更多的详细说明以及优化方法。
因此,当有多个客户端同时连接到 SSE 时,每个连接都会占用一个 PHP 进程,而这些进程会一直保留,造成进程占用的增加。
pm.max_children=50,这意味着最多只有 50 个用户可以同时连接,其余用户会被阻塞或拒绝连接。提升 PHP-FPM 或 Apache 的子进程配置上限,例如:
pm.max_children 的值。MaxRequestWorkers。但这种方法并不是一种长远方案,因为随着用户数量增长,资源还是会持续被占用。
PHP 并不是处理高并发长连接的最佳选择,推荐将 PHP 作为业务逻辑层,结合其他语言实现 SSE 的长连接,比如:
Node.js
Node.js 是单线程事件驱动的,非常适合长时间保持连接。
可以将 Node.js 用作 SSE 通信的代理层,不占用 PHP 子进程。
示例代码(Node.js):
const http = require('http');
http.createServer((req, res) => {
// 设置 SSE 头信息
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
// 定时发送数据
setInterval(() => {
res.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
}, 2000);
// 当客户端断开连接时执行
req.on('close', () => {
console.log('Connection closed');
res.end();
});
}).listen(8080, () => console.log('SSE server running on port 8080'));
ngx_http_srcache_module 等模块缓存动态内容,在发给客户端之前处理重复性问题。Node.js 和 Redis:Socket.io。如果必须用 PHP 完成 SSE,那么可以尽量对服务器进行合理配置。以下是一些计算思路:
如果 PHP 原生解决 SSE 问题:
所以,一句话:虽然 PHP 可以实现 SSE,但对于高并发场景,建议选择更高效的技术栈。
使用 Swoole,PHP 可以高效地实现高并发的 SSE(Server-Sent Events),显著提升性能。Swoole 提供协程(Coroutine)和事件驱动的架构,突破了传统 PHP-FPM 模型的并发限制,非常适合处理长连接场景。
下面是详细的分析和实现方法。
sleep)像普通同步代码,但实际上背后是多路复用(epoll、kqueue),对资源占用非常小。以下是一个用 Swoole 实现 SSE 的示例:
<?php
use Swoole\Http\Server;
$server = new Server("0.0.0.0", 9501); // 创建 Swoole HTTP 服务器实例
$server->on("start", function () {
echo "Swoole HTTP Server started at http://0.0.0.0:9501\n";
});
// 处理 HTTP 请求
$server->on("request", function ($request, $response) {
// 设置 SSE 的响应头
$response->header('Content-Type', 'text/event-stream');
$response->header('Cache-Control', 'no-cache');
$response->header('Connection', 'keep-alive');
// 持续发送数据
for ($i = 1; $i <= 10; $i++) {
$data = json_encode([
'id' => $i,
'time' => date('Y-m-d H:i:s'),
'message' => "Hello, this is message {$i}",
]);
$response->write("data: {$data}\n\n");
// 模拟 2 秒间隔推送
Swoole\Coroutine::sleep(2);
}
// 结束 SSE
$response->end();
});
// 启动服务器
$server->start();
启动 Swoole HTTP 服务器:
0.0.0.0:9501,支持多个客户端并发连接。on("request") 来处理每个 HTTP 请求逻辑。设置 SSE 响应头:
Content-Type: text/event-stream:表明这是 SSE 数据流。Cache-Control: no-cache:禁止缓存,确保客户端实时接收数据。Connection: keep-alive:保持长连接。推送数据到客户端:
$response->write 持续推送事件。data: 开头,以 \n\n 结尾。协程睡眠模拟间隔:
Swoole\Coroutine::sleep() 是非阻塞的模拟方式,不会影响其他客户端的处理。调整内核参数
ulimit -n 100000 # 增大文件描述符数量
设置 Swoole 的最大连接数
$server = new Swoole\Http\Server("0.0.0.0", 9501, SWOOLE_BASE);
$server->set([
'worker_num' => 4, // 设置工作进程数量
'max_conn' => 100000, // 设置最大连接数
'daemonize' => false // 是否以守护进程方式运行
]);
retry 指定重试时间:$response->write("retry: 5000\n"); // 5 秒后重连
| 技术 | 特点 | 高并发性能 | 实现复杂度 |
|---|---|---|---|
| PHP-FPM SSE | 传统 PHP,仅适合小规模应用,进程占用多 | 低(适合 < 1,000 并发) | 容易 |
| Swoole SSE | 异步 IO 和协程模型,显著提升性能 | 高(适合 10,000+ 并发) | 中 |
| Node.js SSE | 异步事件驱动,轻量高效 | 高(适合 10,000+ 并发) | 中 |
| Go SSE | Goroutine 模型,内存更优 | 高(适合 100,000 并发) | 中 |
| WebSocket | 双向通信,适合复杂交互,但带宽占用较大 | 高 | 较复杂 |
内存友好设计: Goroutine 占用 1~2KB,服务器内存限制大大降低。
事件驱动模型:
select/poll/epoll
这些技术高效地管理了网络连接状态,而不是创建大量线程或进程。
实际应用中,结合负载均衡(例如 Nginx、HAProxy)可进一步分发流量。
使用 Swoole 实现高并发的 SSE 是一种高效而实用的解决方案:
Swoole 是现代化 PHP 大规模高并发连接的首选方案。
是的,使用 Go 实现 SSE 服务端推送数据会显著提升服务端的连接性能。Go 语言(Golang)原生支持高并发和高性能计算,非常适合处理长时间保持打开的网络连接,例如 SSE。
下面详细说明为什么 Go 会提升性能以及如何实现:
Go 使用轻量级的 Goroutine 来处理并发连接,而不是像传统的语言(如 PHP)那样为每个连接分配一个操作系统线程。以下是 Go 的并发优势:
轻量级 Goroutine:
非阻塞 IO:
epoll、kqueue等),能够处理大规模网络连接而不会因阻塞而卡住主线程。更少的资源占用:
假设同样需要支持 10,000 个 SSE 长连接,以下对比 PHP 与 Go:
| 参数比较 | PHP(典型案例 - 进程/线程模型) | Go(Goroutine 模型) |
|---|---|---|
| 每连接内存占用 | 5 MB(进程/线程) | 2 KB(Goroutine) |
| CPU 消耗 | 高(线程切换上下文) | 低 |
| 最大连接数支持 | 约 1,000 ~ 5,000 | 100,000+ |
| 对高并发的扩展能力 | 弱 | 极高 |
| 实现复杂度 | 中 | 简单 |
结论: Go 的 Goroutine 模型让高并发长连接更经济高效,支持的并发连接数比 PHP 等基于线程的模型高出数量级。
这是一个简单的 Go 实现示例:
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/sse", handleSSE)
fmt.Println("Server running on http://localhost:8080/sse")
http.ListenAndServe(":8080", nil)
}
func handleSSE(w http.ResponseWriter, r *http.Request) {
// 设置 SSE 相关的 HTTP 头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// 自动刷新数据流
for {
select {
case <-r.Context().Done(): // 客户端断开时自动退出
fmt.Println("Connection closed by client")
return
default:
// 推送数据
fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.RFC3339))
// 刷新缓冲区数据到客户端
flusher, ok := w.(http.Flusher)
if ok {
flusher.Flush()
}
time.Sleep(2 * time.Second) // 每隔2秒推送
}
}
}
关键操作:
Content-Type: text/event-stream 响应头表示服务端支持 SSE。w.(http.Flusher).Flush() 强制推送数据到客户端。r.Context().Done() 处理客户端断开连接事件,避免资源泄漏。高效长连接管理:
断开连接检查:
Context 在客户端断开时结束 Goroutine 以节省资源。负载均衡:
多核并发:
GOMAXPROCS 的值设置为等于或接近 CPU 核心数量。限流与速率控制:
资源清理:
| 技术 | 适用场景 | 并发能力 | 备注 |
|---|---|---|---|
| PHP SSE | 小规模实时推送(< 1,000 并发) | 较低 | 使用简单,但效率低下 |
| Go SSE | 高并发实时推送(10,000+ 并发) | 高 | 轻量,对硬件要求较低 |
| Node.js SSE | 高并发实时推送(10,000+ 并发) | 高 | Event Loop 模型性能优秀 |
| WebSocket | 双向实时通信 | 高 | 可用 Go、Node.js 等实现 |
推荐: 在高并发场景下,使用 Go 开发 SSE,将显著提升服务端性能并降低资源开销。
暂无目录