SSE的使用

1 阅读1分钟

SSE的使用

客户端请求头

1. 基本请求头

const eventSource = new EventSource('/api/stream');

浏览器会自动发送以下请求头:

GET /api/stream HTTP/1.1
Accept: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

2. 自定义请求头配置

注意:EventSource API 本身不支持自定义请求头,如需自定义头部,可以使用以下方案:

方案一:通过 URL 参数传递认证信息
const token = localStorage.getItem('authToken');
const eventSource = new EventSource(`/api/stream?token=${token}`);
方案二:使用 fetch API 实现类似 SSE 的功能
async function createSSEConnection(url, headers = {}) {
    const response = await fetch(url, {
        method: 'GET',
        headers: {
            'Accept': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Authorization': `Bearer ${token}`,
            ...headers
        }
    });

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    while (true) {
        const { value, done } = await reader.read();
        if (done) break;
      
        const chunk = decoder.decode(value);
        const lines = chunk.split('\n');
      
        for (const line of lines) {
            if (line.startsWith('data: ')) {
                const data = line.slice(6);
                // 处理数据
                handleSSEMessage(data);
            }
        }
    }
}

服务端响应头配置

1. 必需的响应头

// Node.js/Express 示例
app.get('/api/stream', (req, res) => {
    // 设置 SSE 必需的响应头
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*', // CORS
        'Access-Control-Allow-Headers': 'Cache-Control',
        'X-Accel-Buffering': 'no' // Nginx 禁用缓冲
    });
});

2. CORS 相关配置

app.get('/api/stream', (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      
        // CORS 配置
        'Access-Control-Allow-Origin': 'http://localhost:3000',
        'Access-Control-Allow-Credentials': 'true',
        'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
        'Access-Control-Allow-Methods': 'GET, OPTIONS'
    });
});

// 处理 OPTIONS 预检请求
app.options('/api/stream', (req, res) => {
    res.writeHead(200, {
        'Access-Control-Allow-Origin': 'http://localhost:3000',
        'Access-Control-Allow-Credentials': 'true',
        'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
        'Access-Control-Allow-Methods': 'GET, OPTIONS'
    });
    res.end();
});

3. 认证和安全相关头部

app.get('/api/stream', authenticateToken, (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
      
        // 安全相关
        'X-Content-Type-Options': 'nosniff',
        'X-Frame-Options': 'DENY',
        'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
      
        // CORS
        'Access-Control-Allow-Origin': process.env.ALLOWED_ORIGIN,
        'Access-Control-Allow-Credentials': 'true'
    });
});

// 中间件:验证 token
function authenticateToken(req, res, next) {
    const token = req.query.token || req.headers.authorization?.split(' ')[1];
  
    if (!token) {
        return res.status(401).json({ error: 'Token required' });
    }
  
    // 验证 token 逻辑
    jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
        if (err) return res.status(403).json({ error: 'Invalid token' });
        req.user = user;
        next();
    });
}

反向代理配置

Nginx 配置

location /api/stream {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  
    # SSE 特有配置
    proxy_cache_bypass $http_upgrade;
    proxy_buffering off;
    proxy_read_timeout 24h;
    proxy_send_timeout 24h;
  
    # 禁用缓冲
    proxy_set_header X-Accel-Buffering no;
}

Apache 配置

<Location "/api/stream">
    ProxyPass http://backend/api/stream
    ProxyPassReverse http://backend/api/stream
  
    # 禁用缓冲
    ProxyPreserveHost On
    SetEnv proxy-nokeepalive 1
    SetEnv proxy-sendchunked 1
</Location>

完整的前端实现示例

class SecureSSE {
    constructor(url, options = {}) {
        this.url = url;
        this.options = options;
        this.eventSource = null;
        this.token = options.token || this.getToken();
      
        this.connect();
    }
  
    getToken() {
        return localStorage.getItem('authToken') || 
               sessionStorage.getItem('authToken');
    }
  
    connect() {
        // 由于 EventSource 不支持自定义头部,通过 URL 传递 token
        const urlWithAuth = `${this.url}?token=${encodeURIComponent(this.token)}`;
      
        this.eventSource = new EventSource(urlWithAuth);
      
        this.eventSource.onopen = (event) => {
            console.log('SSE 连接建立成功');
            if (this.options.onOpen) {
                this.options.onOpen(event);
            }
        };
      
        this.eventSource.onmessage = (event) => {
            if (this.options.onMessage) {
                this.options.onMessage(event);
            }
        };
      
        this.eventSource.onerror = (event) => {
            console.error('SSE 连接错误:', event);
          
            // 如果是认证错误,可能需要重新获取 token
            if (event.target.readyState === EventSource.CLOSED) {
                this.handleAuthError();
            }
          
            if (this.options.onError) {
                this.options.onError(event);
            }
        };
    }
  
    handleAuthError() {
        // 尝试刷新 token
        this.refreshToken().then(newToken => {
            this.token = newToken;
            setTimeout(() => this.connect(), 1000);
        }).catch(err => {
            console.error('Token 刷新失败:', err);
            // 重定向到登录页面
            window.location.href = '/login';
        });
    }
  
    async refreshToken() {
        const response = await fetch('/api/refresh-token', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.token}`
            }
        });
      
        if (!response.ok) {
            throw new Error('Token refresh failed');
        }
      
        const data = await response.json();
        localStorage.setItem('authToken', data.token);
        return data.token;
    }
  
    close() {
        if (this.eventSource) {
            this.eventSource.close();
        }
    }
}

// 使用示例
const sse = new SecureSSE('/api/stream', {
    token: localStorage.getItem('authToken'),
    onMessage: (event) => {
        const data = JSON.parse(event.data);
        console.log('收到消息:', data);
    },
    onError: (event) => {
        console.error('连接错误:', event);
    }
});

关键要点总结

  1. 客户端限制:EventSource API 不支持自定义请求头
  2. 认证方案:通过 URL 参数或使用 fetch API 替代
  3. CORS 配置:服务端必须正确配置跨域头部
  4. 缓冲控制:服务端和代理都需要禁用缓冲
  5. 安全考虑:实现适当的认证和授权机制

与传统http长连接的区别

使用 HTTP 长连接的判断标准:

  1. 频繁的请求-响应交互
  2. 短时间内多个API调用
  3. 需要优化连接开销
  4. 传统的客户端主动请求模式
// 决策示例:电商购物车
class ShoppingCart {
    // ✅ 适合 HTTP 长连接
    // 用户在短时间内可能进行多次操作:
    // - 添加商品
    // - 删除商品  
    // - 更新数量
    // - 应用优惠券
    // - 计算总价
    
    async addItem(productId, quantity) {
        return fetch('/api/cart/add', {
            method: 'POST',
            headers: {
                'Connection': 'keep-alive', // 保持连接用于后续操作
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ productId, quantity })
        });
    }
    
    async updateQuantity(itemId, quantity) {
        return fetch('/api/cart/update', {
            method: 'PUT',
            headers: {
                'Connection': 'keep-alive',
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ itemId, quantity })
        });
    }
}

使用 SSE 的判断标准:

  1. 需要服务器主动推送数据
  2. 实时性要求高
  3. 单向数据流(服务器→客户端)
  4. 长时间保持连接监听
// 决策示例:股票交易系统
class StockTradingDashboard {
    // ✅ 适合 SSE
    // 股价需要实时更新,服务器主动推送
    
    constructor() {
        this.priceStream = new EventSource('/api/stock-prices/stream');
        
        this.priceStream.addEventListener('price-update', (event) => {
            const priceData = JSON.parse(event.data);
            this.updateStockPrice(priceData);
        });
        
        this.priceStream.addEventListener('market-alert', (event) => {
            const alertData = JSON.parse(event.data);
            this.showMarketAlert(alertData);
        });
    }
    
    updateStockPrice(data) {
        // 实时更新股价显示
        const element = document.getElementById(`stock-${data.symbol}`);
        element.textContent = `$${data.price}`;
    }

	showMarketAlert(data) {
        // 实时更新股价显示
        const element = document.getElementById(`market-${data.symbol}`);
        element.textContent = `$${data.price}`;
    }
}