SSE技术详解:实现服务器到客户端的实时数据推送

前言

在现代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 对话场景关键技术点

  1. 流式体验优化

    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; }
    }
  2. 上下文保持

    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 });
      }
    });
  3. 中断机制

    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的限制

  1. 单向通信:只能服务器到客户端,不能反向
  2. 连接数限制:浏览器对同一域名的连接数有限制(可通过多域名解决)
  3. IE不支持:需要polyfill
  4. 纯文本:不支持二进制数据传输

结语

Server-Sent Events是一项简单而强大的技术,为服务器推送实时数据提供了优雅的解决方案。本文介绍了SSE的工作原理、实现方式以及一些经典的应用场景,下次需要处理流式响应的需求,尝试用下SSE吧~

相关推荐
棉花糖超人19 分钟前
【从0-1的HTML】第2篇:HTML标签
前端·html
exploration-earth27 分钟前
本地优先的状态管理与工具选型策略
开发语言·前端·javascript
OpenTiny社区43 分钟前
开源之夏报名倒计时3天!还有9个前端任务有余位,快来申请吧~
前端·github
ak啊1 小时前
WebGL魔法:从立方体到逼真阴影的奇妙之旅
前端·webgl
hang_bro1 小时前
使用js方法实现阻止按钮的默认点击事件&触发默认事件
前端·react.js·html
用户90738703648641 小时前
pnpm是如何解决幻影依赖的?
前端
树上有只程序猿1 小时前
Claude 4提升码农生产力的5种高级方式
前端
傻球1 小时前
没想到干前端2年了还能用上高中物理运动学知识
前端·react.js·开源
咚咚咚ddd1 小时前
前端组件:pc端通用新手引导组件最佳实践(React)
前端·react.js
Lazy_zheng1 小时前
🚀 前端开发福音:用 json-server 快速搭建本地 Mock 数据服务
前端·javascript·vue.js