Web 实时通信技术深度解析:SSE 与 WebSocket 学习笔记
一、 核心概念概述与技术选型
1.1 传统 HTTP 的局限性
传统的 HTTP 请求是“一问一答”的形式(客户端请求,服务器响应后断开)。为了实现服务器向客户端的主动数据推送,打破单向被动响应的限制,诞生了 SSE 和 WebSocket 两种实时通信技术。
1.2 核心概念对比
- SSE (Server-Sent Events) - “广播电台”模型:基于标准 HTTP 协议的单向通信。连接建立后,服务器源源不断地向客户端推送数据流,但客户端不能通过此通道向服务器发送新数据。
- WebSocket - “打电话”模型:基于 TCP 的双向全双工通信协议。连接建立后,客户端和服务器随时可以互相发送数据,地位平等。
💡 用户观点与场景决策:
在探讨“系统通知小红点”或“实时图文直播”的场景选型时,用户准确指出:“SSE 单向接受,因为用户不需要通过这个通道回复。” 这精准命中了 SSE 的核心优势——针对轻量级、被动接收的场景,使用原生自带断线重连且无惧防火墙拦截的 SSE 是最佳选择。
二、 SSE (单向数据流) 的深层原理与实战
2.1 原理阐述:如何建立持续的数据流
SSE 100% 纯正基于 HTTP 协议。其原理相当于一次“无限期延长”的 HTTP 下载。通过设置特殊的 HTTP 响应头,服务器指示浏览器不要关闭连接,并将接收到的特定格式文本解析为事件流。
PHP 服务端代码示例:
PHP
<?php
// 1. 设置头部,开启事件流
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // 禁用缓存
header('Connection: keep-alive'); // 保持连接
// 2. 发送数据 (格式必须严格遵循 "data: 内容 \n\n")
$messageId = 101;
echo "id: {$messageId}\n";
echo "data: 这是第101条消息\n\n";
// 3. 清空输出缓存,立即推送
ob_flush(); flush();
?>
JavaScript 客户端代码示例:
JavaScript
const eventSource = new EventSource('server.php');
eventSource.onmessage = function(event) {
console.log("收到服务器推送的数据:", event.data);
};
2.2 原理阐述:自动重连与断点续传机制
- 自动重连概念: 浏览器
EventSource原生支持网络闪断后的后台静默重连,无需手动编写重连逻辑。 - 断点续传原理: 服务器在下发数据时携带
id:字段(相当于书签)。当浏览器断线重连时,会在新的 HTTP 请求头中自动添加Last-Event-ID字段。PHP 后端通过读取$_SERVER['HTTP_LAST_EVENT_ID']即可得知客户端错过的消息,实现无缝补发。
2.3 深度探讨:LLM 大模型场景与消息持久化
❓ 用户提问整合:
- SSE 是通过纯 HTTP 请求发送的吗?大语言模型平台如何处理?
messageId真的需要存储到数据库吗?- 大模型输出完毕时,服务器如何通知前端关闭连接?
- 大语言模型应用原理: AI 打字机效果底层多采用 SSE。模型每生成一个 Token,后端即刻将其包装为 SSE JSON 格式推给前端追加显示。
- 持久化策略 (是否存储 messageId):
- 需要严格送达 (系统通知/交易状态): 必须持久化,通常存入 Redis Stream/List 或 Kafka 以应对高并发补发。
- 时效性至上 (AI打字机/股票大盘): 无需入库。用户断线重连只需获取“此刻最新状态”,断网期间的历史数据作废,PHP 端可忽略
Last-Event-ID。
- 结束信号机制: SSE 协议未规定强制结束语法,通常由业务层约定。
- 方法1 (主流): 发送特定文本暗号(如 OpenAI 的
data: [DONE]\n\n),前端拦截到该文本时执行eventSource.close()主动掐断连接。 - 方法2: 自定义事件名,如
event: end,前端专门监听该事件。
- 方法1 (主流): 发送特定文本暗号(如 OpenAI 的
三、 WebSocket (双向通道) 的深层原理与实战
3.1 概念与原理:协议升级与握手 (Handshake)
WebSocket 底层基于 TCP,但其首次建立连接是借用标准 HTTP 请求完成的。
原理在于 HTTP 请求头中的 Upgrade: websocket 和 Connection: Upgrade 字段,向服务器申请“升级协议”。同时附带 Sec-WebSocket-Key 验证服务器支持该协议。服务器验证后返回 101 Switching Protocols 状态码,HTTP 历史使命结束,正式切换为 TCP 双向长连接。
3.2 原理阐述:服务端架构转变与事件驱动
传统的 PHP-FPM 为“短连接”设计(运行完毕立即销毁)。而 WebSocket 必须要求进程常驻内存。因此需借助 CLI 命令行模式下的工具(如 Workerman / Swoole)。
PHP WebSocket 服务端核心骨架 (Workerman 风格):
PHP
<?php
$server = new WebSocketServer("0.0.0.0:8080");
// 建立连接事件
$server->on('Connect', function($connection) { ... });
// 收到消息事件
$server->on('Message', function($connection, $data) {
$connection->send("服务器收到: " . $data);
});
$server->start();
?>
3.3 深度探讨:群聊广播与私聊的身份映射
💡 用户观点提取:
关于如何实现消息广播,用户指出需要“保存发给谁也就是群聊中的人”。关于私聊,用户指出在连接时需要“记录 userid 方便私聊发送”。
- 群聊广播原理: 服务器在内存中维护一个全局“花名册”(关联数组
$roster = [])。新用户连接时存入该数组,发消息时foreach遍历该数组进行$connection->send()。 - 私聊身份映射原理: 默认连接仅包含无意义的网络 ID。前端在连接建立时(通过 URL 参数或首条鉴权 JSON 消息)传递真实身份标识。后端将花名册的键名替换为真实的
UserId,实现精准寻址:$roster[$userId] = $connection。
四、 生产环境部署原理与 Nginx 避坑指南
4.1 Nginx 代理机制与协议识别
💻 用户提供的核心配置记录:
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
原理剖析: 这段配置使得 Nginx 能够识别 HTTP 握手请求中的升级意图。Nginx 作为公网大门(通常监听 80/443),作为透明管道将握手请求及后续 TCP 数据流转发至内网 PHP 独立进程监听的端口(如 8080)。
4.2 核心避坑:两大隐形陷阱与原理
- SSE 陷阱:打字卡顿现象
- 原理: Nginx 默认开启代理缓冲 (Proxy Buffering),为节约网络资源会截留数据直至凑够一定体积(如 4KB)才下发,破坏了实时字符流输出。
- 解决: PHP 下发头部
header('X-Accel-Buffering: no');或 Nginx 配置proxy_buffering off;。
- WebSocket 陷阱:离奇失联 (60秒魔咒)
- 原理: Nginx 的
proxy_read_timeout默认为 60 秒,无数据流动会被判定为死连接并强制斩断。 - 解决: 心跳机制 (Heartbeat)。用户指出这是“发送活动报文”。前端通过
setInterval定期发送极小体积的 JSON (Ping),后端回复 (Pong),以保持通道活跃,重置 Nginx 倒计时。
- 原理: Nginx 的
4.3 安全加密策略 (WSS 与 混合内容拦截)
❓ 用户提问与思考:
为什么必须用 Nginx 而不直接连后端 8080 端口?用户准确回答出浏览器安全机制:“拦截 CORS (准确讲为混合内容 Mixed Content拦截)”。
- 加密原理: 现代网站均使用
https://。如果在安全环境下发起明文的ws://请求,会被浏览器以安全策略强行拦截。必须升级为wss://(WebSocket Secure)。 - 架构分工: Nginx 负责持有 SSL 证书并进行“SSL 卸载”,解密外网的
wss://数据,然后通过内网以明文ws://转发给后端的 PHP 进程,实现安全与性能的解耦。
五、 分布式架构进阶:高并发集群与 Redis 通信
随着在线人数激增,单台 PHP 服务器遭遇内存与连接数瓶颈,必须扩展为多台服务器集群(Server A, B, C)。
5.1 核心痛点:内存孤岛效应
每台服务器只维护连接到自己机器上的本地“花名册”。Server A 无法直接将消息发送给连接在 Server B 上的用户。
5.2 解决方案原理:Redis Pub/Sub (发布/订阅)
引入 Redis 作为中央通信枢纽:
- 订阅 (Subscribe): 所有 Server 启动时订阅 Redis 的同一全局频道。
- 发布 (Publish): Server A 收到用户消息后,不再直接处理,而是打包推送给 Redis 频道。
- 广播与分发: Redis 瞬间将消息同步给所有 Server,每台 Server 监听到消息后,再遍历本地“花名册”下发给用户。
5.3 深度机制:防回音 (Echo Prevention)
💡 用户架构设计思路:
“利用哈希表存储连接 id 确保不会发送回去”
防回音处理原理: 当 Server A 将消息推给 Redis 后,Redis 会广播给所有节点(包含 Server A 自身)。为了防止发送者收到自己发出的重复消息:
- 数据包封装: Server A 推送到 Redis 的 Payload 必须包含发送者的身份标识 (
sender_id)。 - 分发过滤: Server A 从 Redis 监听到广播,在遍历本地
$roster下发时,执行比对:如果遍历到的$userId === $sender_id,则continue跳过,实现精准去重。
六、 综合回顾与技术选型总结表
| 核心维度 | SSE (单向广播) | WebSocket (双向通话) | |||
|---|---|---|---|---|---|
| 数据流向 | 单向:服务器 ➔ 客户端 | 双向:服务器 ⬌ 客户端 | |||
| 底层通信 | 标准 HTTP 协议 | 独立协议 (仅首次握手借用 HTTP) | |||
| 断线重连 | 浏览器原生自动重连 (EventSource) |
需前端手动编写 JS 逻辑恢复连接 | |||
| PHP 实现环境 | 普通 Web 脚本 (fpm下 header+flush) |
需 CLI 常驻内存进程 (Workerman/Swoole) |
|||
| 典型黄金场景 | AI 对话打字机、系统通知红点、股票大盘 | 网页在线客服、多人实时游戏、协同编辑文档 |