流式响应SSE方案EventSource

51 阅读3分钟

流式响应SSE方案EventSource

目录

  1. SSE基础概念
  2. 技术原理
  3. EventSource API
  4. 服务端实现
  5. 通信流程
  6. 应用场景
  7. 最佳实践
  8. 与其他方案对比
  9. 总结

SSE基础概念

Server-Sent Events (SSE) 是HTML5规范的一部分,它允许服务器主动向客户端推送数据。SSE基于HTTP协议,通过保持一个长连接来实现服务端到客户端的单向数据流传输。

核心特性

  • 单向通信:仅支持服务端向客户端推送数据
  • 基于HTTP:使用标准HTTP协议,无需特殊协议支持
  • 自动重连:客户端断线后会自动尝试重连
  • 事件驱动:支持自定义事件类型
  • 轻量级:相比WebSocket更加轻量,适合单向推送场景

技术原理

SSE基于HTTP的text/event-stream媒体类型,服务器通过特定格式发送数据流。数据格式遵循以下规范:

// SSE数据格式
data: 消息内容\n
event: 事件类型\n
id: 消息ID\n
retry: 重连时间\n
\n

SSE消息结构

graph TD
    A[SSE消息] --> B[data: 消息数据]
    A --> C[event: 事件类型]
    A --> D[id: 消息标识]
    A --> E[retry: 重连间隔]
    A --> F[comment: 注释]
    
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#f3e5f5
    style D fill:#f3e5f5
    style E fill:#f3e5f5
    style F fill:#f3e5f5

EventSource API

EventSource是浏览器提供的JavaScript API,用于建立和管理SSE连接。

基本用法

// 创建EventSource连接
const eventSource = new EventSource('/api/events');

// 监听默认消息
eventSource.onmessage = function(event) {
    console.log('收到消息:', event.data);
    const data = JSON.parse(event.data);
    updateUI(data);
};

// 监听自定义事件
eventSource.addEventListener('notification', function(event) {
    console.log('通知事件:', event.data);
    showNotification(JSON.parse(event.data));
});

// 监听连接状态
eventSource.onopen = function() {
    console.log('SSE连接已建立');
};

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

// 手动关闭连接
// eventSource.close();

连接状态管理

class SSEManager {
    constructor(url, options = {}) {
        this.url = url;
        this.options = options;
        this.eventSource = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    }

    connect() {
        try {
            this.eventSource = new EventSource(this.url);
            this.setupEventListeners();
            this.reconnectAttempts = 0;
        } catch (error) {
            console.error('创建EventSource失败:', error);
            this.handleReconnect();
        }
    }

    setupEventListeners() {
        this.eventSource.onopen = () => {
            console.log('SSE连接成功');
            this.reconnectAttempts = 0;
        };

        this.eventSource.onmessage = (event) => {
            this.handleMessage(event);
        };

        this.eventSource.onerror = (error) => {
            console.error('SSE错误:', error);
            this.handleReconnect();
        };
    }

    handleMessage(event) {
        try {
            const data = JSON.parse(event.data);
            this.onMessage(data);
        } catch (error) {
            console.error('解析消息失败:', error);
        }
    }

    handleReconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            const delay = Math.pow(2, this.reconnectAttempts) * 1000;
            console.log(`${delay}ms后重连,第${this.reconnectAttempts}次尝试`);
            
            setTimeout(() => {
                this.connect();
            }, delay);
        } else {
            console.error('达到最大重连次数,停止重连');
        }
    }

    onMessage(data) {
        // 由子类或外部实现
        console.log('收到消息:', data);
    }

    close() {
        if (this.eventSource) {
            this.eventSource.close();
            this.eventSource = null;
        }
    }
}

服务端实现

Node.js实现

const express = require('express');
const app = express();

// SSE端点
app.get('/api/events', (req, res) => {
    // 设置SSE响应头
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Headers': 'Cache-Control'
    });

    // 发送初始连接消息
    res.write('data: {"type":"connected","timestamp":' + Date.now() + '}\n\n');

    // 模拟定时推送数据
    const intervalId = setInterval(() => {
        const data = {
            id: Date.now(),
            message: '实时数据',
            timestamp: new Date().toISOString()
        };

        res.write(`data: ${JSON.stringify(data)}\n\n`);
    }, 5000);

    // 客户端断开连接时清理资源
    req.on('close', () => {
        console.log('客户端断开连接');
        clearInterval(intervalId);
        res.end();
    });

    req.on('aborted', () => {
        console.log('请求被中止');
        clearInterval(intervalId);
        res.end();
    });
});

app.listen(3000, () => {
    console.log('SSE服务启动在端口3000');
});

高级服务端实现

class SSEController {
    constructor() {
        this.clients = new Map();
        this.messageQueue = [];
    }

    // 处理SSE连接
    handleConnection(req, res) {
        const clientId = this.generateClientId();
        
        // 设置响应头
        res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Access-Control-Allow-Origin': '*'
        });

        // 注册客户端
        this.clients.set(clientId, { res, connectedAt: Date.now() });
        
        // 发送连接确认
        this.sendToClient(clientId, {
            type: 'connected',
            clientId,
            timestamp: Date.now()
        });

        // 发送历史消息
        this.sendQueuedMessages(clientId);

        // 处理断开连接
        req.on('close', () => {
            this.clients.delete(clientId);
            console.log(`客户端 ${clientId} 断开连接`);
        });

        return clientId;
    }

    // 发送消息给指定客户端
    sendToClient(clientId, data, eventType = 'message') {
        const client = this.clients.get(clientId);
        if (client) {
            const message = this.formatMessage(data, eventType);
            client.res.write(message);
        }
    }

    // 广播消息给所有客户端
    broadcast(data, eventType = 'message') {
        const message = this.formatMessage(data, eventType);
        this.clients.forEach((client) => {
            client.res.write(message);
        });

        // 保存到消息队列
        this.messageQueue.push({ data, eventType, timestamp: Date.now() });
        if (this.messageQueue.length > 100) {
            this.messageQueue.shift();
        }
    }

    // 格式化SSE消息
    formatMessage(data, eventType, id = null) {
        let message = '';
        if (eventType !== 'message') {
            message += `event: ${eventType}\n`;
        }
        if (id) {
            message += `id: ${id}\n`;
        }
        message += `data: ${JSON.stringify(data)}\n\n`;
        return message;
    }

    // 发送队列中的消息
    sendQueuedMessages(clientId) {
        this.messageQueue.forEach(({ data, eventType }) => {
            this.sendToClient(clientId, data, eventType);
        });
    }

    generateClientId() {
        return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }

    getClientCount() {
        return this.clients.size;
    }
}

通信流程

SSE连接建立流程

sequenceDiagram
    participant C as Client
    participant S as Server
    
    C->>S: GET /events (Accept: text/event-stream)
    S->>C: 200 OK (Content-Type: text/event-stream)
    Note over S,C: 建立长连接
    
    S->>C: data: 连接成功消息
    
    loop 实时数据推送
        S->>C: data: 实时数据
        Note over C: 处理接收到的数据
    end
    
    alt 连接异常
        C->>S: 自动重连
        S->>C: 重新建立连接
    else 主动断开
        C->>S: 关闭连接
        Note over S: 清理客户端资源
    end

数据传输流程

flowchart TD
    A[服务端产生数据] --> B[格式化SSE消息]
    B --> C[发送到客户端]
    C --> D[客户端接收]
    D --> E{解析消息}
    E -->|成功| F[触发onmessage事件]
    E -->|失败| G[触发onerror事件]
    F --> H[更新UI]
    G --> I[错误处理]
    I --> J{是否重连}
    J -->|是| K[等待重连]
    J -->|否| L[连接关闭]
    K --> A
    
    style A fill:#e3f2fd
    style F fill:#e8f5e8
    style G fill:#ffebee

应用场景

1. 实时通知系统

// 实时通知管理器
class NotificationManager extends SSEManager {
    constructor() {
        super('/api/notifications');
    }

    onMessage(data) {
        switch (data.type) {
            case 'system':
                this.showSystemNotification(data);
                break;
            case 'user':
                this.showUserNotification(data);
                break;
            case 'alert':
                this.showAlertNotification(data);
                break;
        }
    }

    showSystemNotification(data) {
        // 系统通知处理逻辑
        console.log('系统通知:', data.message);
    }
}

const notificationManager = new NotificationManager();
notificationManager.connect();

2. 实时数据监控

// 监控数据展示
class MonitoringDashboard {
    constructor() {
        this.sseManager = new SSEManager('/api/monitoring');
        this.sseManager.onMessage = this.handleMonitoringData.bind(this);
        this.charts = {};
    }

    handleMonitoringData(data) {
        switch (data.metric) {
            case 'cpu':
                this.updateCPUChart(data.value);
                break;
            case 'memory':
                this.updateMemoryChart(data.value);
                break;
            case 'traffic':
                this.updateTrafficChart(data.value);
                break;
        }
    }

    updateCPUChart(value) {
        // 更新CPU图表
        if (this.charts.cpu) {
            this.charts.cpu.addData(value);
        }
    }
}

3. 实时聊天系统

// 聊天室实现
class ChatRoom {
    constructor(roomId) {
        this.roomId = roomId;
        this.sseManager = new SSEManager(`/api/chat/${roomId}/events`);
        this.sseManager.onMessage = this.handleChatMessage.bind(this);
    }

    handleChatMessage(data) {
        switch (data.type) {
            case 'message':
                this.displayMessage(data);
                break;
            case 'user_joined':
                this.displayUserJoined(data);
                break;
            case 'user_left':
                this.displayUserLeft(data);
                break;
            case 'typing':
                this.displayTypingIndicator(data);
                break;
        }
    }

    displayMessage(data) {
        const messageElement = document.createElement('div');
        messageElement.innerHTML = `
            <div class="message">
                <span class="user">${data.user}</span>
                <span class="content">${data.content}</span>
                <span class="time">${new Date(data.timestamp).toLocaleTimeString()}</span>
            </div>
        `;
        document.getElementById('chat-messages').appendChild(messageElement);
    }
}

最佳实践

1. 错误处理和重连机制

class RobustSSEManager extends SSEManager {
    constructor(url, options = {}) {
        super(url, {
            maxReconnectAttempts: 10,
            reconnectInterval: 1000,
            heartbeatInterval: 30000,
            ...options
        });
        this.setupHeartbeat();
    }

    setupHeartbeat() {
        // 定期发送心跳检测
        this.heartbeatTimer = setInterval(() => {
            if (this.eventSource && this.eventSource.readyState === EventSource.OPEN) {
                // 可以通过发送特殊请求来检测连接状态
                this.sendHeartbeat();
            }
        }, this.options.heartbeatInterval);
    }

    sendHeartbeat() {
        // 实现心跳检测逻辑
        fetch('/api/heartbeat', { method: 'POST' })
            .catch(() => {
                // 心跳失败,可能需要重连
                this.handleReconnect();
            });
    }

    close() {
        if (this.heartbeatTimer) {
            clearInterval(this.heartbeatTimer);
        }
        super.close();
    }
}

2. 内存管理和性能优化

// 服务端连接管理
class SSEConnectionManager {
    constructor() {
        this.connections = new Map();
        this.maxConnections = 1000;
        this.cleanupInterval = 300000; // 5分钟
        this.startCleanupTimer();
    }

    addConnection(clientId, connection) {
        // 检查连接数限制
        if (this.connections.size >= this.maxConnections) {
            this.removeOldestConnection();
        }

        this.connections.set(clientId, {
            ...connection,
            lastActivity: Date.now()
        });
    }

    removeOldestConnection() {
        let oldestClient = null;
        let oldestTime = Date.now();

        for (const [clientId, conn] of this.connections) {
            if (conn.connectedAt < oldestTime) {
                oldestTime = conn.connectedAt;
                oldestClient = clientId;
            }
        }

        if (oldestClient) {
            this.removeConnection(oldestClient);
        }
    }

    startCleanupTimer() {
        setInterval(() => {
            const now = Date.now();
            const timeout = 600000; // 10分钟无活动超时

            for (const [clientId, conn] of this.connections) {
                if (now - conn.lastActivity > timeout) {
                    this.removeConnection(clientId);
                }
            }
        }, this.cleanupInterval);
    }

    removeConnection(clientId) {
        const conn = this.connections.get(clientId);
        if (conn) {
            conn.res.end();
            this.connections.delete(clientId);
        }
    }
}

3. 安全考虑

// 带认证的SSE连接
app.get('/api/secure-events', authenticateToken, (req, res) => {
    // 验证用户权限
    const user = req.user;
    if (!hasSSEPermission(user)) {
        return res.status(403).json({ error: '无权限访问SSE' });
    }

    // 设置安全响应头
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache, no-store, must-revalidate',
        'Connection': 'keep-alive',
        'X-Content-Type-Options': 'nosniff',
        'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN || 'localhost:3000'
    });

    // 注册用户特定的SSE连接
    const clientId = sseController.handleConnection(req, res, user);
    
    // 记录连接日志
    logger.info(`用户 ${user.id} 建立SSE连接,客户端ID: ${clientId}`);
});

function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];

    if (!token) {
        return res.sendStatus(401);
    }

    jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
    });
}

与其他方案对比

SSE vs WebSocket vs 轮询

graph TD
    A[实时通信需求] --> B{通信方向}
    B -->|单向推送| C[SSE]
    B -->|双向通信| D[WebSocket]
    B -->|简单查询| E[轮询]
    
    C --> F[优势:简单实现<br/>自动重连<br/>基于HTTP]
    D --> G[优势:全双工<br/>低延迟<br/>支持二进制]
    E --> H[优势:实现简单<br/>兼容性好<br/>调试容易]
    
    F --> I[适用:通知推送<br/>实时监控<br/>单向数据流]
    G --> J[适用:聊天应用<br/>游戏<br/>协作编辑]
    H --> K[适用:数据更新频率低<br/>对实时性要求不高]
    
    style C fill:#e8f5e8
    style D fill:#e3f2fd
    style E fill:#fff3e0

技术对比表

特性SSEWebSocket轮询
通信方向单向(服务端→客户端)双向双向
协议基础HTTPWebSocket协议HTTP
连接开销中等
实时性最高
实现复杂度简单中等最简单
自动重连内置支持需要手动实现不适用
防火墙友好可能有问题
浏览器支持现代浏览器现代浏览器全部浏览器

总结

SSE作为一种轻量级的实时通信解决方案,在以下场景中表现出色:

核心优势

  1. 简单易用:基于标准HTTP协议,实现简单
  2. 自动重连:内置断线重连机制,提高可靠性
  3. 事件驱动:支持多种事件类型,灵活性强
  4. 性能优秀:相比轮询大幅减少不必要的请求
  5. 兼容性好:现代浏览器原生支持,无需额外依赖

适用场景

  • 实时通知推送:系统通知、用户消息等
  • 数据监控面板:实时图表、状态监控等
  • 内容更新推送:新闻推送、评论更新等
  • 进度追踪:文件上传、任务处理进度等

使用建议

  1. 单向推送场景优选:当只需要服务端向客户端推送数据时,SSE是最佳选择
  2. 注意连接管理:服务端需要合理管理连接数量和资源清理
  3. 实现错误处理:客户端应实现完善的错误处理和重连机制
  4. 考虑安全性:对敏感数据传输要实现适当的认证和授权
  5. 性能监控:监控连接数量、消息发送频率等关键指标

通过合理运用SSE技术,可以为Web应用提供高效、稳定的实时数据推送能力,显著提升用户体验。