跳至正文

🔴 WebSocket vs SSE vs 长轮询

📖 定义

实时通信是使服务器和客户端之间能够立即交换数据的技术。WebSocket提供双向实时通信,SSE(Server-Sent Events)只从服务器向客户端推送数据,长轮询(Long Polling)是使用HTTP的实时通信方式。各有优缺点,需要根据使用场景进行选择。

🎯 用比喻理解

电话 vs 广播 vs 信使

普通HTTP = 邮寄
你: "你好吗?" (寄信)
↓ (几天后)
朋友: "嗨!" (回信)
↓ (几天后)
你: "最近好吗?" (再寄信)

- 慢
- 每次重新连接
- 无法实时

长轮询 = 电话(等待通话)
你: "有事就告诉我!" (打电话等待)
↓ (等待一段时间...)
朋友: "现在有话要说!" (响应)
↓ (通话结束)
你: "再有事就告诉我!" (再打电话)

- 基于HTTP
- 保持连接 → 响应 → 重连
- 效率低

SSE = 广播
朋友: "大家好!" (开始广播)
"今天天气..." (持续发送)
"下一条新闻..." (持续发送)
你: (只听)

- 服务器 → 客户端(单向)
- 保持连接
- 简单

WebSocket = 视频通话
你: "你好!" (立即发送)
朋友: "见到你很高兴!" (立即响应)
你: "在做什么?" (立即发送)
朋友: "正在编程!" (立即响应)

- 双向实时
- 保持连接
- 快速且高效

餐厅点餐

普通HTTP = 自助服务
1. 去柜台点餐
2. 等待
3. 取餐回到座位
4. 再去柜台点饮料
5. 再等待

长轮询 = 按铃等待
1. 按铃等待服务员(保持连接)
2. 服务员来了 → "请问您要点什么"
3. 点完再按铃
4. 再等待...

SSE = 厨房显示器
厨房: "1号客人,您的餐已好!"
厨房: "2号客人,正在准备!"
厨房: "3号客人,马上好!"
你: (只听)

WebSocket = 桌边服务
你: "请给我水"
服务员: "好的,马上来"
你: "泡菜也要"
服务员: "马上给您"
服务员: "您的餐来了"
你: "谢谢"

- 自由对话
- 快速响应

⚙️ 工作原理

1. 普通HTTP vs 实时通信

========== 普通HTTP(请求-响应) ==========

客户端 服务器
│ │
│ 1. 连接(Request) │
│────────────────────────>│
│ │
│ │ 处理中...
│ │
│ 2. 响应(Response) │
│<────────────────────────│
│ │
│ 连接结束 │
╳ ╳

│ 3. 重新连接 │
│────────────────────────>│
│ │

特点:
- 只有客户端请求时才响应
- 每次重新连接
- 服务器无法主动发送
- 无法实时

========== 实时通信 ==========

客户端 服务器
│ │
│ 连接 │
│<───────────────────────>│
│ │
│ 保持双向通信 │
│<───────────────────────>│
│ │
│ 数据交换 │
│<───────────────────────>│
│ │

特点:
- 保持连接
- 服务器可以主动发送
- 可以实时

2. 长轮询(Long Polling)

客户端                              服务器
│ │
│ 1. 请求(有新数据吗?) │
│──────────────────────────────────>│
│ │
│ 保持连接(等待中...) │
│ │
│ │ 没有数据...
│ │ 继续等待...
│ │
│ │ 新数据产生!
│ │
│ 2. 响应(有数据) │
│<──────────────────────────────────│
│ │
│ 连接结束 │
╳ ╳
│ │
│ 3. 立即重连 │
│──────────────────────────────────>│
│ │
│ 再次等待... │

流程:
1. 客户端 → 服务器: "有新数据吗?"
2. 服务器: 等待数据产生
3. 数据产生 → 响应
4. 连接结束
5. 立即重连(重复)

优点:
✅ 基于HTTP(利用现有基础设施)
✅ 没有防火墙问题
✅ 实现简单

缺点:
❌ 效率低(持续重连)
❌ 服务器负担大
❌ 请求头开销大

3. Server-Sent Events (SSE)

客户端                              服务器
│ │
│ 1. 连接请求 │
│──────────────────────────────────>│
│ │
│ 2. 保持连接(开始流) │
│<══════════════════════════════════│
│ │
│ 3. 数据推送 │
│<──────────────────────────────────│
│ │
│ 4. 再次数据推送 │
│<──────────────────────────────────│
│ │
│ 连接保持中... │
│<══════════════════════════════════│

特点:
- 服务器 → 客户端(单向)
- 保持连接
- 基于HTTP
- 自动重连

优点:
✅ 简单(浏览器内置)
✅ 自动重连
✅ 可通过事件ID恢复
✅ HTTP/2下高效

缺点:
❌ 单向(服务器 → 客户端)
❌ 不支持二进制数据
❌ IE不支持

4. WebSocket

客户端                              服务器
│ │
│ 1. HTTP Upgrade请求 │
│──────────────────────────────────>│
│ │
│ 2. Upgrade确认 │
│<──────────────────────────────────│
│ │
│ 切换到WebSocket协议 │
│<══════════════════════════════════>│
│ │
│ 3. 发送数据 │
│──────────────────────────────────>│
│ │
│ 4. 接收数据 │
│<──────────────────────────────────│
│ │
│ 5. 发送数据 │
│──────────────────────────────────>│
│ │
│ 双向通信持续... │
│<══════════════════════════════════>│

特点:
- 双向实时通信
- 独立协议(ws://, wss://)
- 保持连接
- 低延迟

优点:
✅ 真正的实时
✅ 双向通信
✅ 低开销
✅ 支持二进制

缺点:
❌ 复杂
❌ 可能有代理/防火墙问题
❌ 服务器负担(保持连接)

💡 实例

长轮询示例

// ========== 服务器(Express.js) ==========
const express = require('express');
const app = express();

let messages = [];
let waitingClients = [];

// 添加消息(从其他地方调用)
function addMessage(message) {
messages.push(message);

// 立即响应等待中的客户端
waitingClients.forEach(client => {
client.json({ messages });
});
waitingClients = [];
}

// 长轮询端点
app.get('/messages', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;

// 如果有新消息立即响应
if (messages.length > lastId) {
return res.json({ messages: messages.slice(lastId) });
}

// 否则添加到等待列表(最多30秒)
waitingClients.push(res);

// 30秒超时
req.setTimeout(30000, () => {
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
res.json({ messages: [] }); // 空响应
}
});
});

app.listen(3000);

// ========== 客户端 ==========
let lastMessageId = 0;

async function longPolling() {
while (true) {
try {
const response = await fetch(`/messages?lastId=${lastMessageId}`);
const data = await response.json();

// 处理新消息
if (data.messages.length > 0) {
data.messages.forEach(msg => {
console.log('新消息:', msg);
displayMessage(msg);
});
lastMessageId += data.messages.length;
}

// 立即重连
await longPolling();
} catch (error) {
console.error('错误:', error);
// 3秒后重试
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}

// 开始
longPolling();

// ========== 问题 ==========
/*
1. 持续重连(效率低)
2. 网络使用量大
3. 服务器负担大
4. 电池消耗(移动端)
*/

Server-Sent Events (SSE) 示例

// ========== 服务器(Express.js) ==========
const express = require('express');
const app = express();

app.use(express.static('public'));

// SSE端点
app.get('/events', (req, res) => {
// 设置SSE头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// CORS(如果需要)
res.setHeader('Access-Control-Allow-Origin', '*');

// 连接确认消息
res.write('data: Connected\n\n');

// 客户端ID
const clientId = Date.now();
console.log(`客户端 ${clientId} 已连接`);

// 每5秒发送时间
const intervalId = setInterval(() => {
const data = {
time: new Date().toLocaleTimeString(),
message: '你好!'
};

// 以SSE格式发送
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);

// 客户端断开连接处理
req.on('close', () => {
console.log(`客户端 ${clientId} 已断开`);
clearInterval(intervalId);
res.end();
});
});

// 事件发布API
const clients = [];

app.get('/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// 保存客户端
clients.push(res);

req.on('close', () => {
const index = clients.indexOf(res);
clients.splice(index, 1);
});
});

// 向所有客户端发送消息
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;

clients.forEach(client => {
client.write(`data: ${JSON.stringify({ message })}\n\n`);
});

res.json({ success: true });
});

app.listen(3000);

// ========== 客户端(HTML) ==========
/*
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>

<script>
// SSE连接
const eventSource = new EventSource('/events');

// 接收消息
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到数据:', data);

const div = document.getElementById('messages');
div.innerHTML += `<p>${data.time}: ${data.message}</p>`;
};

// 连接开始
eventSource.onopen = () => {
console.log('SSE已连接');
};

// 错误处理
eventSource.onerror = (error) => {
console.error('SSE错误:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('SSE连接已关闭');
}
};

// 连接关闭(离开页面时)
window.addEventListener('beforeunload', () => {
eventSource.close();
});
</script>
</body>
</html>
*/

// ========== 高级功能 ==========

// 1. 指定事件类型
app.get('/events/typed', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// 多种类型的事件
setInterval(() => {
// 普通消息
res.write(`event: message\ndata: Hello\n\n`);

// 通知
res.write(`event: notification\ndata: New notification!\n\n`);

// 更新
res.write(`event: update\ndata: {"count": 10}\n\n`);
}, 5000);
});

// 客户端按事件类型处理
/*
eventSource.addEventListener('message', (e) => {
console.log('消息:', e.data);
});

eventSource.addEventListener('notification', (e) => {
console.log('通知:', e.data);
});

eventSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log('更新:', data);
});
*/

// 2. 事件ID(重连时从中断处继续接收)
let eventId = 0;

app.get('/events/resumable', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

const lastEventId = parseInt(req.headers['last-event-id']) || 0;
console.log('最后的事件ID:', lastEventId);

// 只发送lastEventId之后的事件
setInterval(() => {
eventId++;
res.write(`id: ${eventId}\ndata: Event ${eventId}\n\n`);
}, 1000);
});

// 3. 设置重试时间
app.get('/events/retry', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// 5秒后重连
res.write('retry: 5000\n');
res.write('data: Connected\n\n');
});

WebSocket示例(Socket.io)

// ========== 服务器(Socket.io) ==========
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: '*'
}
});

app.use(express.static('public'));

// 管理已连接的用户
const users = new Map();

// WebSocket连接
io.on('connection', (socket) => {
console.log('新用户连接:', socket.id);

// 保存用户信息
socket.on('register', (username) => {
users.set(socket.id, { username, socket });
console.log(`${username} 已注册`);

// 通知所有用户
io.emit('user-joined', {
username,
totalUsers: users.size
});
});

// 接收消息
socket.on('chat-message', (message) => {
const user = users.get(socket.id);
console.log(`${user.username}: ${message}`);

// 广播给所有用户
io.emit('chat-message', {
username: user.username,
message,
timestamp: new Date().toISOString()
});
});

// 正在输入显示
socket.on('typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-typing', user.username);
});

socket.on('stop-typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-stop-typing', user.username);
});

// 私信
socket.on('private-message', ({ to, message }) => {
const targetSocket = Array.from(users.values())
.find(u => u.username === to)?.socket;

if (targetSocket) {
const sender = users.get(socket.id);
targetSocket.emit('private-message', {
from: sender.username,
message
});
}
});

// 断开连接
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user) {
console.log(`${user.username} 已断开`);
users.delete(socket.id);

// 通知所有用户
io.emit('user-left', {
username: user.username,
totalUsers: users.size
});
}
});
});

server.listen(3000, () => {
console.log('服务器运行中: http://localhost:3000');
});

// ========== 客户端(HTML + Socket.io) ==========
/*
<!DOCTYPE html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="chat">
<div id="messages"></div>
<input id="username" placeholder="名字" />
<input id="message" placeholder="消息" />
<button onclick="sendMessage()">发送</button>
</div>

<script>
// Socket.io连接
const socket = io('http://localhost:3000');

// 连接成功
socket.on('connect', () => {
console.log('已连接:', socket.id);
});

// 用户注册
function register() {
const username = document.getElementById('username').value;
socket.emit('register', username);
}

// 发送消息
function sendMessage() {
const message = document.getElementById('message').value;
socket.emit('chat-message', message);
document.getElementById('message').value = '';
}

// 接收消息
socket.on('chat-message', (data) => {
const div = document.getElementById('messages');
div.innerHTML += `
<p><strong>${data.username}:</strong> ${data.message}</p>
`;
});

// 用户加入
socket.on('user-joined', (data) => {
console.log(`${data.username} 加入 (共 ${data.totalUsers}人)`);
});

// 用户离开
socket.on('user-left', (data) => {
console.log(`${data.username} 离开 (共 ${data.totalUsers}人)`);
});

// 正在输入
let typingTimeout;
document.getElementById('message').addEventListener('input', () => {
socket.emit('typing');

clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stop-typing');
}, 1000);
});

socket.on('user-typing', (username) => {
console.log(`${username}正在输入...`);
});

// 断开连接
socket.on('disconnect', () => {
console.log('已断开连接');
});

// 重新连接
socket.on('reconnect', () => {
console.log('已重新连接');
});
</script>
</body>
</html>
*/

原生WebSocket API

// ========== 服务器(ws库) ==========
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
console.log('客户端已连接');

// 接收消息
ws.on('message', (message) => {
console.log('收到消息:', message.toString());

// 广播给所有客户端
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});

// 断开连接
ws.on('close', () => {
console.log('客户端已断开');
});

// 错误
ws.on('error', (error) => {
console.error('WebSocket错误:', error);
});

// 欢迎消息
ws.send('已连接到服务器!');
});

console.log('WebSocket服务器运行中: ws://localhost:8080');

// ========== 客户端(浏览器) ==========

// WebSocket连接
const ws = new WebSocket('ws://localhost:8080');

// 连接开始
ws.addEventListener('open', (event) => {
console.log('WebSocket已连接');
ws.send('你好!');
});

// 接收消息
ws.addEventListener('message', (event) => {
console.log('服务器:', event.data);
});

// 错误
ws.addEventListener('error', (error) => {
console.error('WebSocket错误:', error);
});

// 断开连接
ws.addEventListener('close', (event) => {
console.log('WebSocket已断开');
if (event.code === 1000) {
console.log('正常结束');
} else {
console.log('异常结束:', event.code);
}
});

// 发送消息
function sendMessage(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
console.error('WebSocket未打开');
}
}

// 关闭连接
function closeConnection() {
ws.close(1000, '正常结束');
}

// ========== 二进制数据传输 ==========

// 文件传输
async function sendFile(file) {
const arrayBuffer = await file.arrayBuffer();
ws.send(arrayBuffer);
}

// 服务器接收二进制
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
console.log('收到二进制数据:', event.data.byteLength, 'bytes');
} else {
console.log('文本数据:', event.data);
}
});

// ========== Ping/Pong(保持连接) ==========

// 服务器
wss.on('connection', (ws) => {
ws.isAlive = true;

ws.on('pong', () => {
ws.isAlive = true;
});
});

// 每30秒Ping一次
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 无响应则关闭连接
}

ws.isAlive = false;
ws.ping();
});
}, 30000);

// ========== 自动重连 ==========

let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;

function connect() {
ws = new WebSocket('ws://localhost:8080');

ws.addEventListener('open', () => {
console.log('已连接');
reconnectAttempts = 0;
});

ws.addEventListener('close', (event) => {
console.log('已断开');

// 自动重连
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`${delay}ms后尝试重连...`);
setTimeout(connect, delay);
} else {
console.error('重连失败');
}
});

ws.addEventListener('error', (error) => {
console.error('错误:', error);
ws.close();
});
}

connect();

🤔 常见问题

Q1. 应该选择哪种方式?

A:

✅ 选择长轮询的情况:
├─ 不支持WebSocket/SSE的环境
├─ 遗留系统
├─ 简单通知
└─ 例: 旧浏览器支持、简单轮询

使用案例:
- 股票价格(更新频率低)
- 邮件检查
- 简单通知

✅ 选择SSE的情况:
├─ 服务器 → 客户端单向
├─ 实时更新
├─ 希望简单实现
├─ 需要自动重连
└─ 例: 新闻推送、通知、仪表板

使用案例:
- 新闻推送(实时更新)
- 股票图表(服务器推送)
- 进度显示
- 日志流
- 服务器监控

✅ 选择WebSocket的情况:
├─ 双向实时通信
├─ 低延迟重要
├─ 频繁消息交换
├─ 二进制数据
└─ 例: 聊天、游戏、协作工具

使用案例:
- 聊天应用
- 多人游戏
- 协同文档编辑(Google Docs)
- 视频会议
- IoT实时控制

📊 比较表:

特性 | 长轮询 | SSE | WebSocket
------------|---------------|----------|----------
方向性 | 双向 | 单向 | 双向
协议 | HTTP | HTTP | WebSocket
延迟 | 高 | 低 | 非常低
开销 | 高 | 中等 | 低
浏览器 | 全部 | 大部分 | 全部
复杂度 | 低 | 低 | 高
重连 | 手动 | 自动 | 手动
二进制 | 可能 | 不可 | 可能

推荐:
- 简单实时: SSE
- 聊天/游戏: WebSocket
- 遗留支持: 长轮询

Q2. 性能差异?

A:

// ========== 基准测试示例 ==========

// 1. 长轮询
// 每次请求:
// - HTTP头: ~800 bytes
// - 重连时间: ~50ms
// - 服务器资源: 每连接1线程

// 100并发连接时:
// - 每秒请求数: 100次
// - 数据传输: 80KB/s(仅头部)
// - 服务器负担: 高

// 2. SSE
// 每个连接:
// - HTTP头: 仅首次(~800 bytes)
// - 消息开销: ~10 bytes
// - 服务器资源: Keep-Alive连接

// 100并发连接时:
// - 初始连接: 80KB
// - 每条消息: 1KB(无头部)
// - 服务器负担: 中等

// 3. WebSocket
// 每个连接:
// - Upgrade头: 仅一次(~500 bytes)
// - 帧开销: 2~6 bytes
// - 服务器资源: 保持连接

// 100并发连接时:
// - 初始连接: 50KB
// - 每条消息: ~0.002KB(仅帧)
// - 服务器负担: 低

// ========== 实际测量 ==========

// 测试: 100人每秒发送1条消息

// 长轮询
const longPollingBenchmark = {
每秒请求数: 100,
平均延迟: '200ms',
带宽: '8MB/分',
服务器CPU: '70%',
内存: '500MB'
};

// SSE
const sseBenchmark = {
每秒请求数: 0, // 保持连接
平均延迟: '10ms',
带宽: '600KB/分',
服务器CPU: '30%',
内存: '200MB'
};

// WebSocket
const webSocketBenchmark = {
每秒请求数: 0, // 保持连接
平均延迟: '2ms',
带宽: '100KB/分',
服务器CPU: '15%',
内存: '100MB'
};

// ========== 实用优化 ==========

// 1. 限制连接数
const MAX_CONNECTIONS = 1000;

io.on('connection', (socket) => {
if (io.engine.clientsCount > MAX_CONNECTIONS) {
socket.disconnect();
return;
}
});

// 2. 消息压缩
const io = socketIo(server, {
perMessageDeflate: {
threshold: 1024 // 只压缩1KB以上
}
});

// 3. 批处理
const messageQueue = [];

setInterval(() => {
if (messageQueue.length > 0) {
io.emit('batch', messageQueue);
messageQueue.length = 0;
}
}, 100); // 每100ms批量发送

// 4. 使用房间
socket.join('room1');
io.to('room1').emit('message', data); // 只发给room1

// 5. 二进制传输
// WebSocket二进制更高效
const buffer = Buffer.from('Hello');
socket.send(buffer); // 比文本快

// 6. 心跳优化
const HEARTBEAT_INTERVAL = 25000; // 25秒
const HEARTBEAT_TIMEOUT = 30000; // 30秒

setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);

Q3. 移动应用怎么办?

A:

// ========== 移动端考虑事项 ==========

// 1. 电池消耗
// 长轮询: ❌ 高(持续重连)
// SSE: ⚠️ 中等
// WebSocket: ✅ 低(保持连接)

// 2. 网络切换
// WiFi <-> 移动数据切换时

// 自动重连
let ws;

function connect() {
ws = new WebSocket('wss://api.example.com');

ws.onclose = () => {
// 指数退避
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
setTimeout(connect, delay);
};
}

// 网络状态检测
window.addEventListener('online', () => {
console.log('已在线 - 重连');
connect();
});

window.addEventListener('offline', () => {
console.log('离线');
ws.close();
});

// 3. 后台处理
// 应用进入后台怎么办?

// React Native示例
import { AppState } from 'react-native';

AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background') {
// 后台 → 保持连接或关闭
ws.close();
} else if (nextAppState === 'active') {
// 前台 → 重连
connect();
}
});

// 4. 数据节省模式
// 根据用户设置

const isDataSaverMode = await getDataSaverSetting();

if (isDataSaverMode) {
// 长轮询(频率降低)
setInterval(poll, 60000); // 每分钟
} else {
// WebSocket(实时)
connect();
}

// 5. 推送通知联动
// WebSocket + FCM/APNs组合

// 应用运行中: WebSocket
if (appIsActive) {
useWebSocket();
}

// 后台: 推送通知
if (appIsBackground) {
usePushNotification();
}

// ========== React Native示例 ==========
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';

function ChatScreen() {
const [ws, setWs] = useState(null);
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
const websocket = new WebSocket('wss://api.example.com');

websocket.onopen = () => {
console.log('已连接');
setIsConnected(true);
};

websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};

websocket.onerror = (error) => {
console.error('WebSocket错误:', error);
};

websocket.onclose = () => {
console.log('已断开');
setIsConnected(false);

// 3秒后重连
setTimeout(() => {
// 重连逻辑
}, 3000);
};

setWs(websocket);

// 清理
return () => {
websocket.close();
};
}, []);

const sendMessage = (text) => {
if (ws && isConnected) {
ws.send(JSON.stringify({ text }));
}
};

return (
<View>
<Text>连接: {isConnected ? '是' : '否'}</Text>
{messages.map((msg, i) => (
<Text key={i}>{msg.text}</Text>
))}
</View>
);
}

Q4. 如何保证安全?

A:

// ========== 1. 使用HTTPS/WSS ==========

// ❌ HTTP/WS(明文传输)
const ws = new WebSocket('ws://api.example.com');

// ✅ HTTPS/WSS(加密)
const ws = new WebSocket('wss://api.example.com');

// ========== 2. 认证 ==========

// 方法1: URL中的JWT(简单但不推荐)
const token = getJWTToken();
const ws = new WebSocket(`wss://api.example.com?token=${token}`);
// ⚠️ URL中暴露令牌的风险

// 方法2: 连接后认证(推荐)
const ws = new WebSocket('wss://api.example.com');

ws.onopen = () => {
// 发送认证消息
ws.send(JSON.stringify({
type: 'auth',
token: getJWTToken()
}));
};

// 服务器
io.use((socket, next) => {
const token = socket.handshake.auth.token;

try {
const decoded = jwt.verify(token, SECRET_KEY);
socket.userId = decoded.userId;
next();
} catch (error) {
next(new Error('认证失败'));
}
});

// 方法3: Cookie(HttpOnly)
// 客户端
const ws = new WebSocket('wss://api.example.com');
// Cookie自动发送

// 服务器
io.use((socket, next) => {
const cookies = parseCookies(socket.request.headers.cookie);
const sessionId = cookies.sessionId;

if (isValidSession(sessionId)) {
next();
} else {
next(new Error('认证失败'));
}
});

// ========== 3. CORS配置 ==========

const io = socketIo(server, {
cors: {
origin: 'https://myapp.com', // 仅特定域名
credentials: true
}
});

// 多个域名
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];

const io = socketIo(server, {
cors: {
origin: (origin, callback) => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS拒绝'));
}
},
credentials: true
}
});

// ========== 4. 速率限制 ==========

const rateLimitMap = new Map();

io.on('connection', (socket) => {
socket.on('message', (data) => {
const userId = socket.userId;
const now = Date.now();

// 跟踪每个用户的消息数
if (!rateLimitMap.has(userId)) {
rateLimitMap.set(userId, []);
}

const userMessages = rateLimitMap.get(userId);

// 过滤最近1分钟的消息
const recentMessages = userMessages.filter(
timestamp => now - timestamp < 60000
);

// 限制每分钟10条
if (recentMessages.length >= 10) {
socket.emit('error', '超过消息发送限制');
return;
}

recentMessages.push(now);
rateLimitMap.set(userId, recentMessages);

// 处理消息
handleMessage(data);
});
});

// ========== 5. 输入验证 ==========

socket.on('message', (data) => {
// 类型验证
if (typeof data !== 'object') {
return socket.emit('error', '无效的数据格式');
}

// 必填字段验证
if (!data.type || !data.content) {
return socket.emit('error', '缺少必填字段');
}

// 长度验证
if (data.content.length > 1000) {
return socket.emit('error', '消息太长');
}

// 防止XSS
const sanitizedContent = sanitizeHtml(data.content);

// 处理
broadcastMessage(sanitizedContent);
});

// ========== 6. 命名空间和房间 ==========

// 用命名空间隔离
const chatNamespace = io.of('/chat');
const adminNamespace = io.of('/admin');

chatNamespace.on('connection', (socket) => {
// 普通聊天
});

adminNamespace.use(authenticateAdmin);
adminNamespace.on('connection', (socket) => {
// 仅管理员
});

// 用房间控制权限
socket.on('join-room', (roomId) => {
if (canAccessRoom(socket.userId, roomId)) {
socket.join(roomId);
} else {
socket.emit('error', '无房间访问权限');
}
});

// ========== 7. 防止DDoS ==========

// 限制每个IP的连接数
const MAX_CONNECTIONS_PER_IP = 5;
const connectionsByIP = new Map();

io.on('connection', (socket) => {
const ip = socket.handshake.address;

const count = connectionsByIP.get(ip) || 0;

if (count >= MAX_CONNECTIONS_PER_IP) {
socket.disconnect();
return;
}

connectionsByIP.set(ip, count + 1);

socket.on('disconnect', () => {
const newCount = connectionsByIP.get(ip) - 1;
if (newCount <= 0) {
connectionsByIP.delete(ip);
} else {
connectionsByIP.set(ip, newCount);
}
});
});

Q5. 如何处理错误?

A:

// ========== 客户端错误处理 ==========

class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
}

connect() {
try {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('连接成功');
this.reconnectAttempts = 0;
this.onConnected();
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (error) {
console.error('消息解析错误:', error);
}
};

this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
this.onError(error);
};

this.ws.onclose = (event) => {
console.log('连接关闭:', event.code, event.reason);

// 正常关闭(1000)
if (event.code === 1000) {
console.log('正常结束');
return;
}

// 尝试重连
this.reconnect();
};
} catch (error) {
console.error('连接失败:', error);
this.reconnect();
}
}

reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('放弃重连');
this.onReconnectFailed();
return;
}

this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000
);

console.log(`${delay}ms后尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

setTimeout(() => {
this.connect();
}, delay);
}

send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
} catch (error) {
console.error('发送错误:', error);
}
} else {
console.error('WebSocket未打开');
// 保存到消息队列
this.queueMessage(data);
}
}

close() {
if (this.ws) {
this.ws.close(1000, '客户端关闭');
}
}

// 事件处理器(重写)
onConnected() {}
onMessage(data) {}
onError(error) {}
onReconnectFailed() {}

queueMessage(data) {
// 实现离线消息队列
}
}

// 使用
const client = new WebSocketClient('wss://api.example.com');

client.onConnected = () => {
console.log('已连接!');
};

client.onMessage = (data) => {
console.log('消息:', data);
};

client.onError = (error) => {
console.error('发生错误:', error);
// 在UI中显示错误
showErrorNotification('连接错误');
};

client.onReconnectFailed = () => {
// 处理重连失败
showErrorModal('无法连接到服务器');
};

client.connect();

// ========== 服务器错误处理 ==========

io.on('connection', (socket) => {
// 错误处理器
socket.on('error', (error) => {
console.error('套接字错误:', error);
});

// 消息处理错误
socket.on('message', async (data) => {
try {
// 输入验证
if (!isValidMessage(data)) {
throw new Error('无效消息');
}

// 处理
await processMessage(data);
} catch (error) {
console.error('消息处理错误:', error);

// 向客户端发送错误
socket.emit('error', {
code: 'MESSAGE_PROCESSING_ERROR',
message: error.message
});
}
});

// 连接断开处理
socket.on('disconnect', (reason) => {
console.log('连接断开:', reason);

// 清理用户
cleanupUser(socket.userId);

// 通知其他用户
socket.broadcast.emit('user-left', socket.userId);
});
});

// 全局错误处理器
io.engine.on('connection_error', (error) => {
console.error('连接错误:', error);
});

// ========== 超时处理 ==========

socket.on('message', (data, callback) => {
// 设置超时(5秒)
const timeout = setTimeout(() => {
callback({
error: 'TIMEOUT',
message: '响应超时'
});
}, 5000);

// 处理
processMessage(data)
.then(result => {
clearTimeout(timeout);
callback({ success: true, data: result });
})
.catch(error => {
clearTimeout(timeout);
callback({ error: error.message });
});
});

🎓 下一步

了解了实时通信后,继续学习:

  1. HTTP基础(文档待编写) - 理解HTTP协议
  2. 什么是API?(文档待编写) - API基本概念
  3. REST API vs GraphQL - API设计

动手实践

# ========== 1. Socket.io聊天应用 ==========

mkdir chat-app
cd chat-app
npm init -y
npm install express socket.io

# 编写server.js后
node server.js

# ========== 2. SSE实时通知 ==========

mkdir sse-demo
cd sse-demo
npm install express

# 编写server.js
# 编写index.html

node server.js
# 访问 http://localhost:3000

# ========== 3. WebSocket游戏 ==========

mkdir websocket-game
cd websocket-game
npm install ws express

# 实现多人游戏
node server.js

🎬 总结

实时通信是现代Web的核心技术:

  • 长轮询: 遗留支持,基于HTTP
  • SSE: 服务器推送,简单实现
  • WebSocket: 双向实时,最佳性能
  • 选择标准: 需求、环境、复杂度

为您的项目选择合适的实时通信方式,创造出色的用户体验! 🔴