
还在纠结项目到底用WebSocket还是SSE?这不是一个"选哪个都行"的问题------选错了,会直接影响你的应用性能、服务器成本,甚至用户体验。
最近我看到一个现象:很多开发者(特别是在有些大厂)在做实时通信时,往往选择WebSocket作为"万能方案",结果反而用重了。而另一些团队完全不了解SSE,错过了大量简化架构的机会。
这篇文章的目的很简单:帮你真正理解这两种技术的本质差异,不是功能对比,而是思维模型的转变。
核心概念:你的问题本质是什么?
在我们深入技术细节前,需要问自己一个问题:你的实时通信需求,方向是什么?
问题方向一:双向即时互动
-
用户A的操作需要立即让用户B看到
-
延迟必须极低(毫秒级)
-
例子:在线协作编文档、实时对战游戏、直播弹幕互动
问题方向二:服务器主动推送
-
只有服务器向客户端发数据
-
客户端被动接收,很少反馈
-
例子:库存变化通知、服务器监控告警、行情推送
这两个问题,需要的方案完全不同。
第一部分:WebSocket------双向高速通道
本质理解:它是什么
想象你和朋友在一条公路上:
-
传统HTTP像是"你大喊一声,他才能回应"(请求-响应)
-
WebSocket像是"你们之间装了个对讲机,可以随时说话"(双向常连接)
go
初始握手(HTTP Upgrade):
客户端 → 「我想升级到WebSocket」 → 服务器
客户端 ← 「好的,升级成功」 ← 服务器
升级后:
客户端 ⟷ 「随时双向通信」 ⟷ 服务器
为什么它对双向通信那么重要
-
一次握手,永久连接 --- 初始化成本只有一次,之后是纯数据流
-
双向流畅 --- 不用等对方先说话
-
低延迟 --- 没有HTTP请求-响应的开销
-
二进制支持 --- 不只能传文字
代码层面看:从HTTP升级的细节
客户端建立WebSocket:
go
// JavaScript版本
const socket = new WebSocket('ws://localhost:8080');
socket.addEventListener('open', () => {
console.log('连接建立');
socket.send('Hello from client');
});
socket.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
});
socket.addEventListener('close', () => {
console.log('连接断开');
});
go
// TypeScript版本
class ChatClient {
private socket: WebSocket | null = null;
connect(url: string): Promise<void> {
returnnewPromise((resolve, reject) => {
this.socket = new WebSocket(url);
this.socket.addEventListener('open', () => {
console.log('WebSocket已连接');
resolve();
});
this.socket.addEventListener('error', (error) => {
reject(error);
});
this.socket.addEventListener('message', (event: Event) => {
const messageEvent = event as MessageEvent<string>;
this.handleMessage(messageEvent.data);
});
});
}
private handleMessage(data: string): void {
try {
const message = JSON.parse(data);
console.log('收到消息:', message);
} catch (e) {
console.error('消息解析失败', e);
}
}
send(message: Record<string, unknown>): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(message));
}
}
disconnect(): void {
this.socket?.close();
}
}
服务器端使用ws库:
go
// Node.js + ws库
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
console.log('客户端已连接');
ws.on('message', (data) => {
console.log('收到消息:', data);
// 广播给所有客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({
type: 'message',
data: data,
timestamp: Date.now()
}));
}
});
});
ws.on('error', (error) => {
console.error('WebSocket错误:', error);
});
ws.on('close', () => {
console.log('客户端已断开');
});
});
server.listen(8080, () => {
console.log('WebSocket服务运行在8080端口');
});
go
// TypeScript版本
import WebSocket, { Server as WebSocketServer } from'ws';
import * as http from'http';
interface ClientMessage {
type: string;
content: string;
userId?: string;
}
interface BroadcastMessage {
type: string;
data: string;
timestamp: number;
from?: string;
}
class ChatServer {
private wss: WebSocketServer;
private clientCount: number = 0;
constructor(port: number) {
const server = http.createServer();
this.wss = new WebSocketServer({ server });
this.wss.on('connection', (ws) =>this.handleConnection(ws));
server.listen(port, () => {
console.log(`WebSocket服务运行在${port}端口`);
});
}
private handleConnection(ws: WebSocket): void {
this.clientCount++;
console.log(`客户端已连接,当前在线数:${this.clientCount}`);
ws.on('message', (data: WebSocket.Data) => {
try {
const message = JSON.parse(data.toString()) as ClientMessage;
this.broadcastMessage({
type: message.type,
data: message.content,
timestamp: Date.now(),
from: message.userId || 'anonymous'
});
} catch (error) {
console.error('消息解析失败:', error);
}
});
ws.on('error', (error: Error) => {
console.error('WebSocket错误:', error.message);
});
ws.on('close', () => {
this.clientCount--;
console.log(`客户端已断开,当前在线数:${this.clientCount}`);
});
}
private broadcastMessage(message: BroadcastMessage): void {
this.wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
}
}
new ChatServer(8080);
真实场景:你在用WebSocket
场景1:Alibaba的钉钉实时协作
-
10个人同时编辑一个文档
-
每个字符输入都要立即同步给其他9个人
-
WebSocket是唯一合理的选择
场景2:ByteDance的实时推荐互动
-
用户点赞、评论、分享,需要实时更新数据
-
主播和粉丝的实时互动(延迟要求非常严格)
-
WebSocket支撑高频双向消息
WebSocket的"税":你要付出什么代价
| 成本项 | 具体表现 |
|---|---|
| 内存占用 | 每个连接占用内存,1万个用户就是1万条常连接 |
| 复杂度 | 需要处理断线重连、消息去重、顺序保证等问题 |
| 水平扩展 | 多服务器时需要消息队列中转(Redis、RabbitMQ) |
| 防火墙 | 某些企业网络可能限制WebSocket |
| 状态管理 | 服务器要维护每个连接的状态 |
第二部分:Server-Sent Events(SSE)------单向推流管道
本质理解:它是什么
再用公路比喻:
- SSE就是"你们之间装了个单向的水管,水只能从服务器流向客户端"
go
建立连接:
客户端 → 「我要订阅你的消息」 → 服务器
客户端 ← 「好的,开始推送数据」 ← 服务器
推送过程:
客户端 ← 「数据1」 ← 服务器
客户端 ← 「数据2」 ← 服务器
客户端 ← 「数据3」 ← 服务器
(客户端想回复的话,得用独立的HTTP请求)
为什么它那么"轻"
SSE本质上就是一条HTTP连接,服务器把它当作一条永不关闭的数据流:
-
基于HTTP --- 不需要特殊的ws协议
-
自动重连 --- 浏览器原生支持自动断线重连
-
简化架构 --- 不用维护双向连接的复杂性
-
文本数据 --- 专为文本设计
代码层面看:看起来更简洁
客户端订阅:
go
// JavaScript版本
const eventSource = new EventSource('http://localhost:3000/subscribe');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('库存变化:', data);
updateUI(data);
};
eventSource.onerror = (error) => {
console.log('连接错误,浏览器会自动重连');
};
go
// TypeScript版本
interface StockUpdate {
symbol: string;
price: number;
change: number;
timestamp: number;
}
class StockSubscriber {
private eventSource: EventSource | null = null;
subscribe(url: string, onUpdate: (data: StockUpdate) =>void): void {
this.eventSource = new EventSource(url);
this.eventSource.onmessage = (event: MessageEvent<string>) => {
try {
const data = JSON.parse(event.data) as StockUpdate;
onUpdate(data);
} catch (error) {
console.error('数据解析失败', error);
}
};
this.eventSource.onerror = () => {
console.log('连接错误,浏览器自动重连中...');
// 浏览器会自动处理重连逻辑
};
}
unsubscribe(): void {
if (this.eventSource) {
this.eventSource.close();
}
}
}
// 使用
const subscriber = new StockSubscriber();
subscriber.subscribe(
'http://localhost:3000/subscribe/stock',
(data) =>console.log('股票更新:', data)
);
服务器端推送:
go
// Node.js + Express
const express = require('express');
const app = express();
app.get('/subscribe', (req, res) => {
// 设置SSE相关头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 模拟推送库存数据
let count = 0;
const interval = setInterval(() => {
count++;
const stockData = {
symbol: 'TSLA',
price: (100 + Math.random() * 50).toFixed(2),
change: (Math.random() - 0.5) * 10,
timestamp: Date.now()
};
// SSE格式:data: 内容\n\n
res.write(`data: ${JSON.stringify(stockData)}\n\n`);
if (count > 100) {
clearInterval(interval);
res.end();
}
}, 1000);
// 客户端断开连接时清理
req.on('close', () => {
clearInterval(interval);
});
});
app.listen(3000, () => {
console.log('SSE服务运行在3000端口');
});
go
// TypeScript + Express
import express, { Request, Response } from'express';
interface StockData {
symbol: string;
price: string;
change: number;
timestamp: number;
}
class StockFeedServer {
private app: express.Application;
constructor(port: number) {
this.app = express();
this.setupRoutes();
this.app.listen(port, () => {
console.log(`SSE服务运行在${port}端口`);
});
}
private setupRoutes(): void {
this.app.get('/subscribe/stock', (req: Request, res: Response) => {
this.handleSSEConnection(res);
});
}
private handleSSEConnection(res: Response): void {
// 设置SSE响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
let messageCount = 0;
const interval = setInterval(() => {
const stockData: StockData = {
symbol: 'TSLA',
price: (100 + Math.random() * 50).toFixed(2),
change: (Math.random() - 0.5) * 10,
timestamp: Date.now()
};
// SSE消息格式
res.write(`id: ${messageCount}\n`);
res.write(`data: ${JSON.stringify(stockData)}\n\n`);
messageCount++;
if (messageCount > 100) {
clearInterval(interval);
res.end();
}
}, 1000);
// 客户端断开连接时清理资源
req.on('close', () => {
clearInterval(interval);
res.end();
});
req.on('error', (error: Error) => {
console.error('SSE连接错误:', error.message);
clearInterval(interval);
res.end();
});
}
}
new StockFeedServer(3000);
真实场景:你可以用SSE
场景1:Alibaba商城的库存实时推送
-
库存变化只需要从服务器推给用户
-
用户的操作(下单)用独立HTTP请求
-
SSE就够了,不需要WebSocket
场景2:监控系统的告警推送
-
服务器发现问题,需要实时通知用户
-
用户不需要实时反馈
-
SSE + 独立的HTTP API完全够用
场景3:Tencent云的实时日志流
-
云平台推送日志行
-
用户只是被动查看
-
SSE是标准方案
SSE的优势:你能省什么
| 优势项 | 具体表现 |
|---|---|
| 服务器成本 | 基于HTTP,可用HTTP服务器直接支持 |
| 内存占用 | 比WebSocket少 |
| 跨域友好 | 基于HTTP的CORS,WebSocket有自己的跨域机制 |
| 自动重连 | 浏览器原生支持,代码省一堆 |
| 负载均衡 | 标准HTTP,任何负载均衡器都支持 |
| 调试 | 在浏览器DevTools里和HTTP一样 |
第三部分:对比与选择决策树
逐条对比:技术细节层面
go
┌─────────────────────────────────────────────────────────┐
│ 对比维度 │ WebSocket │ SSE │
├─────────────────────────────────────────────────────────┤
│ 通信方向 │ 双向 │ 单向(服务器→客户端)
│ 连接类型 │ 持久化双向 │ 持久化单向 │
│ 建立协议 │ 需要Upgrade │ 标准HTTP │
│ 消息格式 │ 二进制或文本 │ 纯文本 │
│ 自动重连 │ 需手动实现 │ 浏览器原生 │
│ 防火墙兼容 │ 某些企业网络限制 │ 无问题 │
│ 服务器成本 │ 高 │ 低 │
│ 实现复杂度 │ 高 │ 低 │
│ 延迟 │ 极低(毫秒) │ 低(毫秒) │
│ 浏览器支持 │ IE 10+ │ IE不支持 │
└─────────────────────────────────────────────────────────┘
决策流程图
go
你的实时通信需求
│
├─ 需要「客户端」主动发送数据给「其他客户端」或「服务器」吗?
│ │
│ ├─ 是 → 需要「立即」看到反馈吗?
│ │ │
│ │ ├─ 是(毫秒级延迟)→ WebSocket
│ │ │
│ │ └─ 否(秒级以上)→ SSE + 独立POST请求
│ │
│ └─ 否 → 只需要「被动」接收服务器推送 → SSE ✓
│
└─ 总结:双向 + 低延迟 = WebSocket
单向或低频反馈 = SSE
实际选择场景
| 应用类型 | 推荐方案 | 理由 |
|---|---|---|
| 在线协作编文档 | WebSocket | 需要实时同步每个字符 |
| IM聊天应用 | WebSocket | 双向即时消息 |
| 直播弹幕 | WebSocket | 高频双向互动 |
| 股票行情推送 | SSE | 单向推送,客户端不反馈 |
| 系统通知 | SSE | 单向推送,用户被动接收 |
| 库存变化 | SSE | 单向推送,订单用独立HTTP |
| 监控告警 | SSE | 单向推送 |
| 邮件推送 | SSE | 单向推送 |
| 实时对战游戏 | WebSocket | 极低延迟双向互动 |
第四部分:大厂是怎么用的
ByteDance的选择
-
抖音直播 → WebSocket(弹幕、点赞、送礼需要实时双向)
-
推荐流更新 → SSE(内容推送是单向的)
-
实时通知 → SSE(通知推送)
Alibaba的选择
-
钉钉实时协作 → WebSocket(文档编辑需要双向同步)
-
淘宝消息中心 → SSE + 异步任务(消息推送)
-
商城库存 → SSE(库存变化推送,下单用HTTP)
Tencent的选择
-
QQ即时通讯 → 自有协议(比WebSocket更优化的私有协议)
-
微信支付通知 → WebHook(不是实时推送,是回调)
-
腾讯云告警 → SSE(告警推送)
核心启发
大厂都在用"混合"方案:
-
需要低延迟互动 → WebSocket
-
需要单向推送 → SSE
-
需要异步通知 → 消息队列 + Webhook
第五部分:生产环境的隐藏坑
WebSocket的隐藏成本
坑1:多服务器部署
go
问题:用户A连到服务器1,用户B连到服务器2
服务器1上的消息怎样推给服务器2的用户B?
解决方案:用Redis/RabbitMQ中转
go
// 使用Redis作为消息中转
import Redis from'redis';
import WebSocket from'ws';
const redis = Redis.createClient();
const subscriber = Redis.createClient();
class DistributedChat {
private wss: WebSocket.Server;
private userId: Map<WebSocket, string> = new Map();
constructor(port: number) {
this.wss = new WebSocket.Server({ port });
this.setupRedisSubscriber();
this.wss.on('connection', (ws) => {
const id = Date.now().toString();
this.userId.set(ws, id);
ws.on('message', async (data) => {
const message = JSON.parse(data);
// 发布到Redis,其他服务器可以订阅
await redis.publish('chat-channel', JSON.stringify({
from: id,
content: message.content,
timestamp: Date.now()
}));
});
ws.on('close', () => {
this.userId.delete(ws);
});
});
}
private setupRedisSubscriber(): void {
subscriber.subscribe('chat-channel');
subscriber.on('message', (channel: string, message: string) => {
// 广播给所有本服务器的连接
this.wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
}
}
new DistributedChat(8080);
坑2:断线重连逻辑
go
// 简单的重连策略
class RobustWebSocket {
private socket: WebSocket | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
connect(url: string): void {
this.socket = new WebSocket(url);
this.socket.onopen = () => {
this.reconnectAttempts = 0;
console.log('连接成功');
};
this.socket.onclose = () => {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
setTimeout(
() => {
this.reconnectAttempts++;
console.log(`第${this.reconnectAttempts}次重连...`);
this.connect(url);
},
this.reconnectDelay * Math.pow(2, this.reconnectAttempts) // 指数退避
);
}
};
this.socket.onerror = (error) => {
console.error('WebSocket错误:', error);
};
}
send(data: Record<string, unknown>): void {
if (this.socket?.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
console.warn('连接未打开,消息缓存待重连后发送');
// 实际项目中应该缓存消息
}
}
}
SSE的注意事项
坑1:浏览器同时连接限制
go
// 问题:浏览器对同域EventSource有连接数限制(通常是6个)
// 解决:用子域名或JSONP替代(较少见)
// 正确的做法:一个EventSource订阅多个事件类型
const eventSource = new EventSource('/events');
eventSource.addEventListener('stock-update', (event) => {
// 处理股票更新
});
eventSource.addEventListener('notification', (event) => {
// 处理通知
});
eventSource.addEventListener('alert', (event) => {
// 处理告警
});
go
// 服务器端对应的实现
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const interval = setInterval(() => {
// 使用event字段指定事件类型
res.write('event: stock-update\n');
res.write(`data: ${JSON.stringify(stockData)}\n\n`);
res.write('event: notification\n');
res.write(`data: ${JSON.stringify(notificationData)}\n\n`);
}, 1000);
req.on('close', () => clearInterval(interval));
});
坑2:SSE不支持二进制数据
go
// 错误的尝试:SSE只支持文本
res.write(binaryData); // ❌ 不行
// 正确的做法:Base64编码
const base64Data = Buffer.from(binaryData).toString('base64');
res.write(`data: ${base64Data}\n\n`);
// 客户端解码
eventSource.onmessage = (event) => {
const binaryData = Buffer.from(event.data, 'base64');
// 处理二进制数据
};
第六部分:性能数据对比
延迟对比(在相同网络条件下)
go
建立连接延迟:
WebSocket: ~50-100ms(需要HTTP升级握手)
SSE: ~30-50ms(标准HTTP连接)
消息往返延迟(1KB消息):
WebSocket: ~5-10ms(极低)
SSE: ~8-15ms(稍高)
断线重连时间:
WebSocket: 取决于实现(通常秒级)
SSE: 浏览器自动,通常1-3秒内重连
服务器成本对比(以ByteDance规模估算)
go
假设:100万同时在线用户
WebSocket方案:
- 内存占用:~500MB(单连接约500字节)
- CPU占用:中等(需要处理消息路由)
- 需要中间件:Redis/消息队列
- 估计成本:$$$$
SSE方案:
- 内存占用:~200MB
- CPU占用:低(服务器主推,客户端被动)
- 中间件需求:无(或仅用于数据源)
- 估计成本:$$
差异:SSE成本约为WebSocket的40-50%
常见问题(FAQ)
Q1:我应该同时用WebSocket和SSE吗?
A:完全可以。 最佳实践是混合方案:
-
WebSocket处理需要双向即时的部分(如编辑、弹幕)
-
SSE处理单向推送的部分(如通知、行情)
这样既降低成本,又满足需求。
Q2:如果我的SSE连接断了,未读的消息怎么办?
A:用id和retry字段解决:
go
// 服务器端
res.write(`id: ${messageId}\n`);
res.write(`retry: 5000\n`); // 断线5秒后自动重连
res.write(`data: ${JSON.stringify(data)}\n\n`);
go
// 客户端
eventSource.onopen = () => {
lastReceivedId = localStorage.getItem('lastMessageId');
// 服务器可以根据lastReceivedId重发未读消息
};
eventSource.onmessage = (event) => {
const messageId = event.lastEventId;
localStorage.setItem('lastMessageId', messageId);
};
Q3:WebSocket vs 其他方案(Long Polling、WebRTC)怎么选?
A:不推荐其他方案了:
| 方案 | 为什么不推荐 |
|---|---|
| Long Polling | 浪费带宽,已被WebSocket/SSE淘汰 |
| WebRTC | 太复杂,除非需要P2P |
| GraphQL Subscriptions | 这是应用层,下层还是WebSocket |
现在的选择就是:WebSocket vs SSE
Q4:能在生产环境用SSE吗?大厂都用吗?
A:完全可以。 腾讯云、Alibaba Cloud都用SSE做通知系统。
关键是:
-
✓ 选对场景(单向推送)
-
✓ 处理好重连
-
✓ 考虑好浏览器兼容性(IE不支持)
Q5:如何监控和调试WebSocket/SSE连接?
A:使用Chrome DevTools:
go
// WebSocket调试
ws.addEventListener('open', () => console.log('WS open'));
ws.addEventListener('message', (e) => console.log('WS message:', e.data));
ws.addEventListener('close', () => console.log('WS close'));
ws.addEventListener('error', (e) => console.error('WS error:', e));
// SSE调试
eventSource.addEventListener('open', () => console.log('SSE open'));
eventSource.addEventListener('message', (e) => console.log('SSE message:', e.data));
eventSource.addEventListener('error', () => console.error('SSE error'));
// DevTools看法:
// - 打开Network标签
// - WebSocket会单独显示WS协议
// - SSE在XHR中显示为永不完成的请求
总结:做出正确的选择
这篇文章的核心观点
- 不存在"最好的方案",只有"最合适的方案"
-
双向即时互动 → WebSocket
-
单向推送 → SSE
-
混合需求 → 两者结合
-
大多数开发者用重了
-
-
很多只需要SSE的场景,被架设成了WebSocket
-
结果是多花了钱、多写了复杂代码
-
-
生产环境用SSE很成熟
-
-
不用自己实现重连逻辑
-
完全可以应对百万级用户
-
成本更低
-
-
选择技术的标准是:
-
-
理解本质(通信方向、延迟要求)
-
评估成本(服务器、复杂度)
-
考虑长期(扩展性、维护性)
三个行动项
-
审视你现有的项目 --- 有没有用WebSocket做SSE的活儿的?可以优化。
-
新项目从问题出发 --- 先问"需要什么方向的通信",再选技术。
-
建立监控和告警 --- 无论用什么,都要监控连接数、消息延迟、错误率。
深入学习资源
-
MDN: WebSocket API
-
MDN: Server-Sent Events
-
Node.js ws库
-
WebSocket vs SSE性能测试报告
如果这篇文章对你有帮助,请点赞、分享、评论!
你的支持是我持续输出高质量技术内容的动力。
-