首页 / 技术分享 / 前端开发 /
服务器主动向客户端实时推送数据

服务器主动向客户端实时推送数据

码不停提

2026-02-16
57 次浏览
0 条评论

SSE 以其简单性和对现有 HTTP 基础设施的良好兼容,成为许多轻量级实时推送场景的首选方案。

前端开发
SSE
实时推送
分享:

SSEServer-Sent Events(服务器发送事件)的缩写,是一种允许服务器通过 HTTP 连接向客户端(通常是浏览器)主动推送实时数据的轻量级技术。它是 HTML5 规范的一部分,使用简单、基于文本的协议,特别适合需要单向、持续更新的场景(如通知、行情、日志流)。


核心特点

  • 单向通信:仅服务器可以向客户端发送数据,客户端无法通过同一连接向服务器发送消息(若需双向通信,可使用 WebSocket)。
  • 基于 HTTP:复用现有的 HTTP 协议,无需额外协议或复杂握手。
  • 自动重连:连接断开时,浏览器会自动尝试重新连接,无需额外代码。
  • 文本格式:数据以 text/event-stream 格式传输,支持自定义事件类型和 ID(用于断线续传)。

工作原理

  1. 客户端发起连接 使用 JavaScript 的 EventSource 对象,向服务器指定 URL 发起请求:

    const source = new EventSource('/stream');
    source.onmessage = (event) => {
        console.log('收到消息:', event.data);
    };
  2. 服务器保持连接并推送数据 服务器设置响应头 Content-Type: text/event-stream,并保持连接打开,然后按格式发送数据块。 每条消息以 data: 开头,以两个换行符结束,例如:

    data: 这是一条消息\n\n

    也可以包含事件类型和 ID:

    event: userlogin
    data: {"name": "张三"}
    id: 1001
    
  3. 客户端接收事件 浏览器通过 onmessageaddEventListener 监听消息,并实时处理。


与 WebSocket 的对比

特性 SSE WebSocket
通信方向 单向(服务器→客户端) 双向(全双工)
协议 HTTP ws / wss(独立协议)
数据格式 纯文本(可包含 JSON) 二进制或文本
自动重连 内置支持 需手动实现
浏览器支持 广泛(IE 除外) 几乎所有现代浏览器
实现复杂度 极低(客户端几行代码) 较高(需处理连接、心跳等)
典型场景 新闻推送、股票价格、服务器日志 聊天室、在线游戏、协同编辑

使用场景

  • 实时通知:如新邮件提醒、系统告警。
  • 数据流推送:股票行情、加密货币价格更新。
  • 日志监控:服务器实时输出日志到前端。
  • 社交媒体动态:如新帖子、点赞实时计数。

注意事项

  • 连接数限制:浏览器对同一域名的并发 SSE 连接数有限制(通常为 6 个)。
  • 兼容性:IE 全系列不支持,Edge 从 79 版本开始支持(基于 Chromium)。对于旧浏览器,可使用 polyfill。
  • 关闭连接:客户端可调用 source.close() 主动断开,服务器端可正常结束响应(或使用超时机制)。

使用 PHP 实现 SSE 的步骤

以下是一个 PHP 示例,展示如何使用 SSE 将数据推送到客户端:

1. 设置服务器 HTML 页面

客户端通过 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>

2. 设置 PHP 脚本

编写 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;
    }
}

PHP 注意事项

  1. 输出控制

    • 确保关闭输出缓冲区 (ob_end_clean()),否则数据无法及时发送。
    • ob_flush()flush() 是强制刷新输出缓冲区到客户端的操作。
  2. 确保使用合规的 SSE 数据格式

    • SSE 使用 data: 开头来传递数据,之后以 \n\n 进行分隔。
    • 可以分别使用 id:event:data: 控制消息的 ID、类型以及内容。
  3. 客户端断开处理

    • connection_aborted() 函数返回一个布尔值,表示客户端是否已断开连接(如关闭浏览器)。
  4. 浏览器兼容性

    • SSE 大多数现代浏览器都支持(如 Chrome、Firefox 等),但 Internet Explorer 11 及以下版本不支持。
  5. 长时间运行的 PHP 脚本

    • 需要配置 PHP 的最大执行时间 max_execution_time,设置为 0 可以禁用超时:

      max_execution_time = 0
  6. 使用 EventSource 的重连机制

    • SSE 默认具有自动重连的能力,如果服务端断开,EventSource 会尝试重新连接。

如果应用场景需要双向通信或更复杂的实时交互,推荐使用 WebSocket 技术。

PHP 不适合做 SSE

在 PHP 中实现 SSE 时,每一个客户端连接都会开启一个独立的 PHP 进程(或线程,具体取决于服务器配置,例如 Apache 使用 mod_php 或者 Nginx + PHP-FPM)。以下是更多的详细说明以及优化方法。


1. 为什么多个客户端占用多个进程?

  • PHP 是一个短生命周期的语言,默认每次 HTTP 请求都会启动新的进程处理(Apache 或 PHP-FPM 分叉的子进程)。
  • 对于 SSE,连接是长时间保持打开的,导致一个连接对应一个 PHP 子进程,这个进程会持续运行直到连接主动关闭或超时。

因此,当有多个客户端同时连接到 SSE 时,每个连接都会占用一个 PHP 进程,而这些进程会一直保留,造成进程占用的增加。


2. 问题和潜在风险

  • 高连接占用:SSE 的长时间持久连接会使服务器承受大量并发连接,快速耗尽 PHP-FPM 或 Apache 的子进程资源。
    • 例如,默认 PHP-FPM 的子进程数量可能设置为 pm.max_children=50,这意味着最多只有 50 个用户可以同时连接,其余用户会被阻塞或拒绝连接。
  • 服务器崩溃风险:如果客户端数量过多,可能占满服务器资源(内存、CPU)。

3. 应对多个连接的优化思路

(1) 增加 PHP 进程的承受能力

  • 提升 PHP-FPM 或 Apache 的子进程配置上限,例如:

    • 增加 pm.max_children 的值。
    • 为 Apache 调整 MaxRequestWorkers

    但这种方法并不是一种长远方案,因为随着用户数量增长,资源还是会持续被占用。


(2) 使用定制化的异步服务器

  • 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'));

(3) 使用 Nginx/X-Sendfile 代理

  • 可使用 Nginx 或类似的工具处理数据推送,而 PHP 仅提供一次性处理的静态数据。
  • Nginx 支持使用 ngx_http_srcache_module 等模块缓存动态内容,在发给客户端之前处理重复性问题。

(4) 将数据存储到消息队列并使用专用服务分发

  • 如果有需要实时推送的场景,推荐使用消息队列机制,PHP 写入数据,分发服务负责广播:
    • Redis、RabbitMQ 或 Kafka:PHP 负责产生事件推送至队列中,Node.js 或其他推送服务持续监听消息队列,并通过 SSE 通知客户端。
    • 例如,使用 Node.jsRedis:
      • Redis 监听推送数据,由 Node.js 的 EventSource 机制分发到所有客户端,PHP 只负责写入 Redis。

(5) 考虑 WebSocket 替代

  • WebSocket 是双向通信技术,适合客户端和服务器之间有大量实时交互的场景。
  • 优点:
    • 能够节省服务器资源:WebSocket 建立的是长连接,但协议本身更高效,与 SSE 对比更节省带宽。
    • 许多高性能方案(如基于 Socket.io、Ratchet)支持在 PHP 或其他语言中实现。
  • 示例:可以使用 PHP 的 Ratchet 实现 WebSocket 服务,或者将 WebSocket 的服务转交给类似的 Node.js 框架如 Socket.io

4. 理论最大连接数计算

如果必须用 PHP 完成 SSE,那么可以尽量对服务器进行合理配置。以下是一些计算思路:

  • PHP 子进程的内存占用通常在几 MB,根据服务器内存(RAM)可以估算支持的并发数,例如:
    • 如果每个 PHP 子进程占 5 MB 内存,一个拥有 4 GB 可用内存的服务器大约能支持 800 并发。
  • 除了内存,还需要考虑 CPU 和网络带宽,每秒传输内容过大时,会导致瓶颈。

总结

如果 PHP 原生解决 SSE 问题:

  1. 避免高客户量场景,提升 PHP-FPM 子进程最大值。
  2. 结合高性能的代理服务器(如 Node.js、Nginx)。
  3. 对于实时性要求高的场景,建议迁移到 WebSocket 或异步语言环境中,例如 Node.js 或 Golang。

所以,一句话:虽然 PHP 可以实现 SSE,但对于高并发场景,建议选择更高效的技术栈。

PHP 可以通过 Swoole 实现高并发 SSE

使用 Swoole,PHP 可以高效地实现高并发的 SSE(Server-Sent Events),显著提升性能。Swoole 提供协程(Coroutine)和事件驱动的架构,突破了传统 PHP-FPM 模型的并发限制,非常适合处理长连接场景。

下面是详细的分析和实现方法。


1. 为什么 Swoole 可以实现高并发 SSE?

  • 异步、非阻塞 IO: Swoole 是基于事件驱动和异步 IO 的服务器,不像传统 PHP(Apache 或 PHP-FPM)的请求-响应模型。它在一个进程中可以同时处理成千上万的长连接。
  • 协程模型: Swoole 的协程使得编写阻塞逻辑(如 sleep)像普通同步代码,但实际上背后是多路复用(epollkqueue),对资源占用非常小。
  • 内置服务器: 不需要依赖外部 HTTP 服务(如 Apache、Nginx),Swoole 自带了 HTTP 服务,性能媲美 Node.js 和 Go。
  • 减少资源消耗: Swoole 可以不为每个请求分配独立的操作系统线程或进程,而是在单线程中通过协程调度同时处理多个连接,内存占用大幅减少。

2. 基本实现 - 用 Swoole 实现 SSE

以下是一个用 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();

3. 实现解析

核心逻辑:

  1. 启动 Swoole HTTP 服务器:

    • 创建 HTTP 服务监听在 0.0.0.0:9501,支持多个客户端并发连接。
    • 通过事件 on("request") 来处理每个 HTTP 请求逻辑。
  2. 设置 SSE 响应头:

    • Content-Type: text/event-stream:表明这是 SSE 数据流。
    • Cache-Control: no-cache:禁止缓存,确保客户端实时接收数据。
    • Connection: keep-alive:保持长连接。
  3. 推送数据到客户端:

    • 使用 $response->write 持续推送事件。
    • 数据格式严格遵守 SSE 标准,以 data: 开头,以 \n\n 结尾。
  4. 协程睡眠模拟间隔:

    • 使用 Swoole\Coroutine::sleep() 是非阻塞的模拟方式,不会影响其他客户端的处理。

4. 优势:使用 Swoole 提升性能

(1) 高并发能力:

  • 传统 PHP-FPM: 每个连接对应一个 PHP 子进程,内存和 CPU 开销大,随着并发用户数增加会迅速耗尽资源。
  • Swoole: 单个进程使用协程管理多连接,可以轻松实现 10 万+ 的并发用户。

(2) 资源节省:

  • 新连接不会导致 “进程/线程” 的指数级增长。Swoole 使用协程栈实现极低成本的任务调度。

(3) 更稳定可靠:

  • 客户端断开时自动清理连接资源,不会导致资源泄漏。
  • 基于 Swoole 的事件驱动,避免了线程锁或阻塞问题。

5. 进一步优化

(1) 支持大规模连接

  • 调整内核参数

    • 最大连接数受限于操作系统设置,可以调整文件描述符限制等:
      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       // 是否以守护进程方式运行
      ]);

(2) 优化数据流

  • 如果数据结构很复杂,可以考虑推送更简单的 JSON,提高客户端解析性能。
  • 结合 Redis 或 Kafka 来处理消息队列,避免 CPU 闲置等待时对系统的浪费。

(3) 客户端自动重连

  • SSE 标准自带重连机制,客户端可以通过 retry 指定重试时间:
    $response->write("retry: 5000\n");  // 5 秒后重连

6. 与其他技术的比较

技术 特点 高并发性能 实现复杂度
PHP-FPM SSE 传统 PHP,仅适合小规模应用,进程占用多 低(适合 < 1,000 并发) 容易
Swoole SSE 异步 IO 和协程模型,显著提升性能 高(适合 10,000+ 并发)
Node.js SSE 异步事件驱动,轻量高效 高(适合 10,000+ 并发)
Go SSE Goroutine 模型,内存更优 高(适合 100,000 并发)
WebSocket 双向通信,适合复杂交互,但带宽占用较大 较复杂

7. Swoole 如何支持百万级别连接?

  • 内存友好设计: Goroutine 占用 1~2KB,服务器内存限制大大降低。

  • 事件驱动模型:

    select/poll/epoll

    这些技术高效地管理了网络连接状态,而不是创建大量线程或进程。

  • 实际应用中,结合负载均衡(例如 Nginx、HAProxy)可进一步分发流量。


总结

使用 Swoole 实现高并发的 SSE 是一种高效而实用的解决方案:

  • 适合需要实时推送数据的场景(如通知、实时更新)。
  • Swoole 的异步协程模型显著降低资源消耗、提升性能,比传统 PHP 或 Apache 的实现有数量级的提升。
  • 在真正的高访问量实时交互中,如果和 WebSocket 结合,Swoole 也能轻松胜任职责。

Swoole 是现代化 PHP 大规模高并发连接的首选方案。

Go 或 Node.js 适合做 SSE

是的,使用 Go 实现 SSE 服务端推送数据会显著提升服务端的连接性能。Go 语言(Golang)原生支持高并发和高性能计算,非常适合处理长时间保持打开的网络连接,例如 SSE。

下面详细说明为什么 Go 会提升性能以及如何实现:


1. Go 的并发模型优势

Go 使用轻量级的 Goroutine 来处理并发连接,而不是像传统的语言(如 PHP)那样为每个连接分配一个操作系统线程。以下是 Go 的并发优势:

  1. 轻量级 Goroutine:

    • Goroutine 是用户态线程,与操作系统线程相比占用的内存极少(每个默认约 2 KB,动态增长),允许服务器同时打开数十万甚至百万个连接。
    • 与此对比,传统的线程模型( PHP-FPM、Java Threads)因线程的上下文切换和内存占用会限制并发连接的数量。
  2. 非阻塞 IO:

    • Go 的网络库基于多路复用机制(epollkqueue等),能够处理大规模网络连接而不会因阻塞而卡住主线程。
    • 在 SSE 实现中,即使连接数很高,Go 也能够高效管理请求和推送数据流。
  3. 更少的资源占用:

    • Go 服务端处理长连接时,内存和 CPU 的开销相较于传统语言更低,并具有出色的资源管理能力。

2. Go 的性能提升示例

假设同样需要支持 10,000 个 SSE 长连接,以下对比 PHP 与 Go:

参数比较 PHP(典型案例 - 进程/线程模型) Go(Goroutine 模型)
每连接内存占用 5 MB(进程/线程) 2 KB(Goroutine)
CPU 消耗 高(线程切换上下文)
最大连接数支持 约 1,000 ~ 5,000 100,000+
对高并发的扩展能力 极高
实现复杂度 简单

结论: Go 的 Goroutine 模型让高并发长连接更经济高效,支持的并发连接数比 PHP 等基于线程的模型高出数量级。


3. 使用 Go 实现 SSE

这是一个简单的 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秒推送
        }
    }
}

说明:

  1. 关键操作:

    • 设置 Content-Type: text/event-stream 响应头表示服务端支持 SSE。
    • 使用 w.(http.Flusher).Flush() 强制推送数据到客户端。
    • r.Context().Done() 处理客户端断开连接事件,避免资源泄漏。
  2. 高效长连接管理:

    • Go 的 HTTP 库会将连接交给 Goroutine 管理,每个连接会运行在一个独立的 Goroutine 中,性能开销非常小。
  3. 断开连接检查:

    • Go 的标准库支持上下文传播,结合 Context 在客户端断开时结束 Goroutine 以节省资源。

4. 如何进一步优化 Go SSE 服务

  1. 负载均衡:

    • SSE 是服务器到客户端的单向流,依赖于长时间保持的 HTTP 连接,可以结合负载均衡器(如 Nginx 或 HAProxy)的长连接转发功能进行优化分发。
  2. 多核并发:

    • Go 默认支持多核并行运行,在高并发场景下确保 GOMAXPROCS 的值设置为等于或接近 CPU 核心数量。
  3. 限流与速率控制:

    • 对于大量用户的请求,可以引入中间件限制同时连接数或限制数据推送的速率。
  4. 资源清理:

    • 在服务端关闭时或连接断开时确保所有已建立连接的 Goroutine 能正确退出,避免资源泄漏。

5. 与其他技术的比较

技术 适用场景 并发能力 备注
PHP SSE 小规模实时推送(< 1,000 并发) 较低 使用简单,但效率低下
Go SSE 高并发实时推送(10,000+ 并发) 轻量,对硬件要求较低
Node.js SSE 高并发实时推送(10,000+ 并发) Event Loop 模型性能优秀
WebSocket 双向实时通信 可用 Go、Node.js 等实现

6. 总结

  • 提升服务端性能: 使用 Go 替代传统的线程/进程模型能显著优化服务端 SSE 的性能表现,特别是在高并发和长连接场景下。
  • 爆发式增长支持: Go 凭借其 Goroutine 支持数十万级别的并发连接,非常适合实时推送的场景。
  • 选择技术栈: 如果需要简单的单向推流,SSE 是一个非常高效的选择;如果需要双向通信,则 WebSocket 会更适合。

推荐: 在高并发场景下,使用 Go 开发 SSE,将显著提升服务端性能并降低资源开销。

评论区 (0)

你需要先 登录 后才能发表评论。
还没有人评论,赶快成为第一个吧。