前言
在现代Web应用中,实时数据交互已成为提升用户体验的关键因素。当我们需要服务器向客户端推送数据时,通常有几种技术可以选择:WebSocket、长轮询和Server-Sent Events (SSE)。其中SSE技术,是一种简单而强大的服务器推送解决方案。
看文字直播的老哥们应该知道,如果需要手动刷新才能看到赛况,那这直播就没法看了,而SSE就能实现源源不断的数据流(举个栗子方便理解,并不是说直播用到了SSE),实时推送赛况。
SSE是什么?
Server-Sent Events (SSE) 是一种基于HTTP协议的服务器推送技术,允许服务器通过HTTP连接向客户端推送事件流。与WebSocket不同,SSE是单向的,只能从服务器推送数据到客户端,但其简单性和与HTTP的无缝集成使它在许多场景下成为优秀的选择。
核心概念
- 单向通信:服务器→客户端的单向数据流
- 简单协议:纯文本格式,易于实现
- 自动恢复:连接中断时自动重连
- 标准支持:现代浏览器原生支持(除IE)
SSE与WebSocket的比较
特性 | SSE | WebSocket |
---|---|---|
通信方向 | 单向(服务器→客户端) | 双向 |
协议 | HTTP(兼容现有设施) | 独立协议 |
复杂度 | 简单易用 | 需要复杂实现 |
数据格式 | 文本 | 文本/二进制 |
自动恢复 | 原生支持 | 需手动实现 |
从零实现完整 SSE 流程
前端实现
javascript
// 创建连接
const initSSE = () => {
const eventSource = new EventSource('/api/live-data', {
withCredentials: true
});
// 连接成功
eventSource.onopen = () => {
console.log('🚀 连接服务器成功');
showStatus('connected');
};
// 接收普通消息
eventSource.onmessage = ({ data }) => {
updateUI(JSON.parse(data));
};
// 接收自定义事件
eventSource.addEventListener('critical-alert', (event) => {
triggerAlert(JSON.parse(event.data));
});
// 错误处理
eventSource.onerror = (err) => {
console.error('连接异常:', err);
showStatus('reconnecting');
// 5秒后重试
setTimeout(initSSE, 5000);
eventSource.close();
};
return eventSource;
};
// 页面加载时启动
const sseConnection = initSSE();
// 页面关闭时断开
window.addEventListener('beforeunload', () => {
sseConnection.close();
});
服务端实现(Node.js)
javascript
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors());
// 存储所有活跃连接
const clients = new Set();
app.get('/api/live-data', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no' // 禁用Nginx缓冲
});
// 发送初始握手信号
res.write('retry: 5000\n\n');
// 存储新连接
const clientId = Date.now();
clients.add(res);
console.log(`客户端 ${clientId} 已连接, 当前连接数: ${clients.size}`);
// 客户端断开处理
req.on('close', () => {
clients.delete(res);
console.log(`客户端 ${clientId} 断开, 剩余连接数: ${clients.size}`);
res.end();
});
});
// 广播消息给所有客户端
export const broadcast = (data, eventType = 'message') => {
clients.forEach(client => {
try {
client.write(`event: ${eventType}\n`);
client.write(`data: ${JSON.stringify(data)}\n\n`);
} catch (err) {
console.error('发送失败:', err);
}
});
};
// 示例:定时发送系统时间
setInterval(() => {
broadcast({
timestamp: new Date().toISOString(),
type: 'heartbeat'
}, 'system-ping');
}, 30000);
app.listen(3000, () => {
console.log('SSE服务运行在: http://localhost:3000');
});
应用场景示例
1. 文件上传进度()
JavaScript
// 前端代码
const uploadFile = async (file) => {
const eventSource = new EventSource(`/api/upload/${file.id}`);
eventSource.addEventListener('progress', ({ data }) => {
const { progress } = JSON.parse(data);
updateProgressBar(progress); // 更新进度条
});
eventSource.addEventListener('complete', () => {
showSuccess('上传成功!');
eventSource.close();
});
eventSource.addEventListener('error', ({ data }) => {
showError(`上传失败: ${data}`);
eventSource.close();
});
// 实际文件上传逻辑
const formData = new FormData();
formData.append('file', file);
await fetch('/api/upload', { method: 'POST', body: formData });
};
// 服务端代码(Node.js)
app.get('/api/upload/:id', (req, res) => {
const uploadId = req.params.id;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
});
// 监听上传进度事件
uploadTracker.on(uploadId, (progress) => {
res.write(`event: progress\n`);
res.write(`data: ${JSON.stringify({ progress })}\n\n`);
if (progress === 100) {
res.write(`event: complete\n\n`);
res.end();
}
});
});
2. 实时协同编辑
JavaScript
const initCollaboration = (docId) => {
const eventSource = new EventSource(`/api/doc/${docId}/updates`);
// 接收他人编辑
eventSource.addEventListener('edit', ({ data }) => {
const { userId, position, content } = JSON.parse(data);
if (userId !== currentUser.id) {
applyRemoteEdit(position, content);
}
});
// 发送本地编辑
const sendEdit = throttle((edit) => {
fetch(`/api/doc/${docId}/edit`, {
method: 'POST',
body: JSON.stringify(edit)
});
}, 100);
return { sendEdit };
};
// 服务端代码
const docSessions = new Map();
app.get('/api/doc/:id/updates', (req, res) => {
const docId = req.params.id;
// 设置SSE头...
// 存储连接
if (!docSessions.has(docId)) {
docSessions.set(docId, new Set());
}
docSessions.get(docId).add(res);
// 清理断连
req.on('close', () => {
docSessions.get(docId).delete(res);
});
});
// 广播编辑事件
app.post('/api/doc/:id/edit', (req, res) => {
const docId = req.params.id;
const edit = req.body;
docSessions.get(docId).forEach(client => {
client.write(`event: edit\n`);
client.write(`data: ${JSON.stringify(edit)}\n\n`);
});
res.sendStatus(200);
});
3. AI 对话流式响应
JavaScript
const askAI = async (question) => {
// 创建SSE连接
const eventSource = new EventSource(`/api/ai/chat?q=${encodeURIComponent(question)}`);
let fullResponse = '';
// 接收流式响应
eventSource.onmessage = ({ data }) => {
const token = JSON.parse(data).token;
fullResponse += token;
updateChatUI(fullResponse); // 逐步显示回答
};
// 对话结束
eventSource.addEventListener('end', () => {
saveConversation(fullResponse);
eventSource.close();
});
// 错误处理
eventSource.onerror = (err) => {
showError('AI服务暂时不可用');
eventSource.close();
};
};
// 服务端代码 - 连接AI服务
app.get('/api/ai/chat', async (req, res) => {
const question = req.query.q;
// 设置SSE头...
try {
// 连接AI服务(OpenAI示例)
const aiResponse = await openAI.chat.completions.create({
model: 'gpt-4',
messages: [{ role: 'user', content: question }],
stream: true
});
// 流式返回结果
for await (const chunk of aiResponse) {
const token = chunk.choices[0]?.delta?.content || '';
res.write(`data: ${JSON.stringify({ token })}\n\n`);
}
// 结束标记
res.write(`event: end\n\n`);
} catch (error) {
res.write(`event: error\n`);
res.write(`data: ${error.message}\n\n`);
} finally {
res.end();
}
});
// 前端效果增强
const updateChatUI = (text) => {
// 打字机效果
chatElement.innerHTML = marked.parse(text); // Markdown渲染
hljs.highlightAll(); // 代码高亮
scrollToBottom(); // 自动滚动
// 添加光标动画
if (!document.getElementById('typing-cursor')) {
const cursor = document.createElement('span');
cursor.id = 'typing-cursor';
cursor.className = 'blinking-cursor';
cursor.textContent = '▋';
chatElement.appendChild(cursor);
}
};
AI 对话场景关键技术点
-
流式体验优化
css/* 打字机光标效果 */ .blinking-cursor { display: inline-block; width: 8px; margin-left: 2px; animation: blink 1s infinite; background: #4a5568; } @keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
-
上下文保持
php// 服务端维护对话上下文 const chatSessions = new Map(); app.get('/api/ai/chat', (req, res) => { const sessionId = req.cookies.sessionId || generateId(); if (!chatSessions.has(sessionId)) { chatSessions.set(sessionId, []); } const history = chatSessions.get(sessionId); history.push({ role: 'user', content: req.query.q }); // 发送给AI时包含完整上下文 const aiResponse = openAI.chat.completions.create({ messages: history, stream: true }); // 将AI回复加入上下文 for await (const chunk of aiResponse) { // ... 流式传输 history.push({ role: 'assistant', content: token }); } });
-
中断机制
JavaScript// 前端实现中断 let aiConnection = null; const stopGeneration = () => { if (aiConnection) { aiConnection.close(); showStopButton(false); } }; // 服务端处理中断 const activeRequests = new Map(); app.get('/api/ai/chat', (req, res) => { const requestId = generateId(); activeRequests.set(requestId, res); req.on('close', () => { // 中断AI生成过程 cancelAIRequest(requestId); activeRequests.delete(requestId); }); });
SSE的限制
- 单向通信:只能服务器到客户端,不能反向
- 连接数限制:浏览器对同一域名的连接数有限制(可通过多域名解决)
- IE不支持:需要polyfill
- 纯文本:不支持二进制数据传输
结语
Server-Sent Events是一项简单而强大的技术,为服务器推送实时数据提供了优雅的解决方案。本文介绍了SSE的工作原理、实现方式以及一些经典的应用场景,下次需要处理流式响应的需求,尝试用下SSE吧~