在当今的 Web 应用中,实时通信已成为许多场景的必备功能。无论是聊天应用、实时数据监控还是在线协作工具,都需要服务器能够主动向客户端推送数据。在实现实时 Web 应用时,Server-Sent Events (SSE) 和 WebSocket 是两种最常用的技术方案。本文将深入探讨这两种技术的工作原理、适用场景及优缺点对比。

什么是 Server-Sent Events (SSE)

基本概念

Server-Sent Events (SSE) 是一种允许服务器向客户端单向推送数据的 HTML5 技术。它基于标准的 HTTP 协议,使用简单的文本格式传输数据。

工作原理

SSE 通过在客户端和服务器之间建立持久的 HTTP 连接来实现数据推送:

  1. 客户端 通过 EventSource API 向服务器发起请求

  2. 服务器 保持连接打开,并以 text/event-stream 格式发送数据

  3. 数据 以特定格式流式传输,支持事件类型和数据标识

客户端实现

// 创建 EventSource 实例连接至 SSE 端点
const eventSource = new EventSource('/api/events');

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

// 监听自定义事件
eventSource.addEventListener('update', function(event) {
    const data = JSON.parse(event.data);
    updateUI(data);
});

// 处理错误
eventSource.onerror = function(event) {
    console.error('SSE 连接错误:', event);
};

服务器端实现 (Node.js/Express)

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': '*'
    });
    
    // 发送初始连接确认
    res.write('data: ' + JSON.stringify({ status: '连接成功' }) + '\n\n');
    
    // 定期发送数据
    const intervalId = setInterval(() => {
        const data = {
            timestamp: new Date().toISOString(),
            value: Math.random() * 100
        };
        
        // SSE 数据格式
        res.write('event: update\n');
        res.write('data: ' + JSON.stringify(data) + '\n\n');
    }, 2000);
    
    // 客户端断开连接时清理
    req.on('close', () => {
        clearInterval(intervalId);
    });
});

SSE 数据格式

SSE 使用简单的文本格式,每个消息由字段组成,以两个换行符结尾:

text

event: notification
data: 这是第一条消息
data: 这是第二条消息
id: 12345
retry: 3000

event: update
data: {"value": 42, "timestamp": "2023-01-01T12:00:00Z"}
id: 12346

什么是 WebSocket

基本概念

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许客户端和服务器之间进行双向实时数据交换。

工作原理

WebSocket 通过 HTTP 升级机制建立连接:

  1. 客户端 发送带有 Upgrade: websocket 头的 HTTP 请求

  2. 服务器 返回 101 Switching Protocols 响应确认协议升级

  3. 双方 通过 WebSocket 协议进行双向通信

客户端实现

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

// 连接打开时
socket.onopen = function(event) {
    console.log('WebSocket 连接已建立');
    // 发送消息到服务器
    socket.send(JSON.stringify({ type: 'greeting', message: 'Hello Server!' }));
};

// 接收服务器消息
socket.onmessage = function(event) {
    const data = JSON.parse(event.data);
    console.log('收到消息:', data);
};

// 处理错误
socket.onerror = function(error) {
    console.error('WebSocket 错误:', error);
};

// 连接关闭时
socket.onclose = function(event) {
    console.log('WebSocket 连接已关闭:', event.code, event.reason);
};

// 发送消息
function sendMessage(message) {
    if (socket.readyState === WebSocket.OPEN) {
        socket.send(JSON.stringify(message));
    }
}

服务器端实现 (Node.js/ws 库)

const WebSocket = require('ws');

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

wss.on('connection', function connection(ws) {
    console.log('新的 WebSocket 连接');
    
    // 向客户端发送欢迎消息
    ws.send(JSON.stringify({ 
        type: 'welcome', 
        message: '连接成功',
        timestamp: new Date().toISOString()
    }));
    
    // 接收客户端消息
    ws.on('message', function incoming(data) {
        console.log('收到客户端消息:', data.toString());
        
        const message = JSON.parse(data);
        
        // 处理不同类型的消息
        switch (message.type) {
            case 'chat':
                // 广播消息给所有连接的客户端
                wss.clients.forEach(function each(client) {
                    if (client !== ws && client.readyState === WebSocket.OPEN) {
                        client.send(JSON.stringify({
                            type: 'chat',
                            user: message.user,
                            text: message.text,
                            timestamp: new Date().toISOString()
                        }));
                    }
                });
                break;
                
            case 'ping':
                ws.send(JSON.stringify({ type: 'pong', timestamp: new Date().toISOString() }));
                break;
        }
    });
    
    // 定期发送更新
    const intervalId = setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({
                type: 'update',
                data: Math.random() * 100,
                timestamp: new Date().toISOString()
            }));
        }
    }, 5000);
    
    // 连接关闭时清理
    ws.on('close', function close() {
        console.log('WebSocket 连接已关闭');
        clearInterval(intervalId);
    });
});

SSE 与 WebSocket 的详细对比

协议与通信模式

特性SSEWebSocket
协议基础HTTP独立的 WebSocket 协议
通信模式服务器到客户端的单向通信客户端和服务器之间的双向通信
连接建立标准的 HTTP GET 请求HTTP 升级请求
数据格式文本 (UTF-8)文本和二进制数据

浏览器兼容性

SSE 浏览器支持:

  • Chrome 6+

  • Firefox 6+

  • Safari 5+

  • Edge 79+

  • 不支持 IE

WebSocket 浏览器支持:

  • Chrome 4+

  • Firefox 4+

  • Safari 5+

  • Edge 12+

  • IE 10+

性能与资源消耗

方面SSEWebSocket
连接开销每个连接一个 HTTP 请求初始握手后持续连接
数据传输效率文本格式,有一定开销帧格式,开销较小
内存使用相对较低相对较高
服务器资源每个连接保持一个 HTTP 连接每个连接保持一个 TCP 连接

功能特性对比

功能SSEWebSocket
自动重连✅ 内置支持❌ 需要手动实现
事件类型✅ 支持自定义事件❌ 需要自行实现事件系统
消息 ID✅ 内置支持,可追踪消息❌ 需要自行实现
二进制数据❌ 仅支持文本✅ 完整支持
双向通信❌ 仅服务器到客户端✅ 完整的双向通信

安全性考虑

SSE 安全性:

  • 遵循同源策略

  • 支持 CORS

  • 可使用标准 HTTP 安全措施 (HTTPS、认证等)

WebSocket 安全性:

  • 遵循同源策略,但可配置

  • 容易受到跨站 WebSocket 劫持攻击

  • 需要额外验证 Origin 头

实际应用场景

适合使用 SSE 的场景

  1. 实时通知系统

    • 社交媒体更新

    • 新闻推送

    • 系统警报

  2. 实时数据监控

    • 股票价格更新

    • 服务器状态监控

    • IoT 设备数据流

  3. 实时仪表板

    • 业务指标展示

    • 实时分析数据

示例:股票价格监控

// 客户端
const stockSource = new EventSource('/api/stocks');

stockSource.addEventListener('price_update', function(event) {
    const update = JSON.parse(event.data);
    updateStockPrice(update.symbol, update.price, update.change);
});

stockSource.addEventListener('market_news', function(event) {
    const news = JSON.parse(event.data);
    displayMarketNews(news.headline, news.timestamp);
});

适合使用 WebSocket 的场景

  1. 实时聊天应用

    • 一对一聊天

    • 群组聊天

    • 在线客服

  2. 多人协作工具

    • 协同文档编辑

    • 白板应用

    • 项目管理工具

  3. 在线游戏

    • 多玩家游戏

    • 实时竞技游戏

  4. 实时位置追踪

    • 配送跟踪

    • 车队管理

示例:实时聊天应用

javascript

// 客户端
const chatSocket = new WebSocket('wss://example.com/chat');

chatSocket.onmessage = function(event) {
    const message = JSON.parse(event.data);
    
    switch(message.type) {
        case 'user_joined':
            displaySystemMessage(`${message.username} 加入了聊天`);
            break;
        case 'user_left':
            displaySystemMessage(`${message.username} 离开了聊天`);
            break;
        case 'chat_message':
            displayChatMessage(message.username, message.text, message.timestamp);
            break;
    }
};

function sendChatMessage(text) {
    chatSocket.send(JSON.stringify({
        type: 'chat_message',
        text: text,
        timestamp: new Date().toISOString()
    }));
}

高级技巧与最佳实践

SSE 高级用法

连接管理与重试

javascript

function createSSEConnection(url, options = {}) {
    const eventSource = new EventSource(url);
    
    eventSource.onopen = function() {
        console.log('SSE 连接已建立');
        options.onOpen && options.onOpen();
    };
    
    eventSource.onerror = function() {
        console.error('SSE 连接错误');
        options.onError && options.onError();
        
        // 实现自定义重连逻辑
        if (eventSource.readyState === EventSource.CLOSED) {
            setTimeout(() => {
                console.log('尝试重新连接...');
                createSSEConnection(url, options);
            }, options.retryTimeout || 5000);
        }
    };
    
    // 注册事件处理器
    if (options.events) {
        Object.keys(options.events).forEach(eventName => {
            eventSource.addEventListener(eventName, options.events[eventName]);
        });
    }
    
    return eventSource;
}

// 使用示例
const sse = createSSEConnection('/api/events', {
    onOpen: () => console.log('连接成功'),
    onError: () => console.log('连接失败'),
    retryTimeout: 3000,
    events: {
        update: (event) => handleUpdate(JSON.parse(event.data)),
        notification: (event) => showNotification(JSON.parse(event.data))
    }
});

WebSocket 高级用法

连接状态管理与重连

javascript

class WebSocketManager {
    constructor(url, options = {}) {
        this.url = url;
        this.options = options;
        this.socket = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
        this.reconnectInterval = options.reconnectInterval || 3000;
        this.eventHandlers = new Map();
        
        this.connect();
    }
    
    connect() {
        try {
            this.socket = new WebSocket(this.url);
            
            this.socket.onopen = () => {
                console.log('WebSocket 连接已建立');
                this.reconnectAttempts = 0;
                this.options.onOpen && this.options.onOpen();
            };
            
            this.socket.onmessage = (event) => {
                const data = JSON.parse(event.data);
                this.emit('message', data);
            };
            
            this.socket.onclose = (event) => {
                console.log('WebSocket 连接已关闭:', event.code, event.reason);
                this.options.onClose && this.options.onClose(event);
                
                if (event.code !== 1000) { // 非正常关闭
                    this.handleReconnect();
                }
            };
            
            this.socket.onerror = (error) => {
                console.error('WebSocket 错误:', error);
                this.options.onError && this.options.onError(error);
            };
            
        } catch (error) {
            console.error('创建 WebSocket 连接失败:', error);
            this.handleReconnect();
        }
    }
    
    handleReconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            this.reconnectAttempts++;
            console.log(`尝试重新连接 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
            
            setTimeout(() => {
                this.connect();
            }, this.reconnectInterval * this.reconnectAttempts); // 指数退避
        } else {
            console.error('达到最大重连次数,停止重连');
            this.emit('reconnect_failed');
        }
    }
    
    send(data) {
        if (this.socket && this.socket.readyState === WebSocket.OPEN) {
            this.socket.send(JSON.stringify(data));
            return true;
        } else {
            console.warn('WebSocket 未连接,消息发送失败');
            return false;
        }
    }
    
    on(event, handler) {
        if (!this.eventHandlers.has(event)) {
            this.eventHandlers.set(event, []);
        }
        this.eventHandlers.get(event).push(handler);
    }
    
    emit(event, data) {
        const handlers = this.eventHandlers.get(event);
        if (handlers) {
            handlers.forEach(handler => handler(data));
        }
    }
    
    close() {
        if (this.socket) {
            this.socket.close(1000, '正常关闭');
        }
    }
}

// 使用示例
const wsManager = new WebSocketManager('wss://example.com/ws', {
    onOpen: () => console.log('连接成功'),
    onClose: () => console.log('连接关闭'),
    maxReconnectAttempts: 10,
    reconnectInterval: 2000
});

wsManager.on('message', (data) => {
    console.log('收到消息:', data);
});

wsManager.send({ type: 'ping', timestamp: Date.now() });

混合使用方案

在某些复杂应用中,可以同时使用 SSE 和 WebSocket 来发挥各自的优势:

javascript

class RealTimeCommunication {
    constructor(options) {
        this.options = options;
        this.sse = null;
        this.websocket = null;
    }
    
    // 使用 SSE 接收服务器推送的通知和数据更新
    setupSSE() {
        this.sse = new EventSource('/api/notifications');
        
        this.sse.addEventListener('notification', (event) => {
            this.handleNotification(JSON.parse(event.data));
        });
        
        this.sse.addEventListener('data_update', (event) => {
            this.handleDataUpdate(JSON.parse(event.data));
        });
    }
    
    // 使用 WebSocket 进行双向通信
    setupWebSocket() {
        this.websocket = new WebSocketManager('wss://example.com/chat');
        
        this.websocket.on('message', (data) => {
            switch(data.type) {
                case 'chat_message':
                    this.handleChatMessage(data);
                    break;
                case 'user_activity':
                    this.handleUserActivity(data);
                    break;
            }
        });
    }
    
    // 发送聊天消息
    sendChatMessage(message) {
        this.websocket.send({
            type: 'chat_message',
            content: message,
            timestamp: new Date().toISOString()
        });
    }
    
    // 处理通知
    handleNotification(notification) {
        console.log('收到通知:', notification);
        // 显示通知 UI
    }
    
    // 处理数据更新
    handleDataUpdate(update) {
        console.log('数据更新:', update);
        // 更新应用状态
    }
    
    // 处理聊天消息
    handleChatMessage(message) {
        console.log('聊天消息:', message);
        // 显示聊天消息
    }
    
    // 处理用户活动
    handleUserActivity(activity) {
        console.log('用户活动:', activity);
        // 更新用户状态
    }
}

总结与选择建议

选择 SSE 的情况:

  • 只需要服务器向客户端推送数据

  • 需要简单的实现和自动重连机制

  • 项目对浏览器兼容性要求较高(相比 WebSocket)

  • 数据传输以文本为主

  • 可以利用现有的 HTTP 基础设施和安全机制

选择 WebSocket 的情况:

  • 需要客户端和服务器之间的双向通信

  • 需要传输二进制数据(如图片、音频、视频)

  • 对延迟和性能有极高要求(如游戏、实时交易)

  • 需要实现复杂的交互场景

现代 Web 开发的趋势

随着现代 Web 应用复杂度的增加,许多开发者开始使用专门的实时通信库,如 Socket.IOSignalR 等,这些库在底层自动选择最佳传输方式(包括 SSE、WebSocket 甚至轮询),并提供更高级的抽象和功能。

无论选择哪种技术,理解其底层原理和特性都是构建高效、可靠实时应用的关键。希望本文能帮助你在实际项目中做出更合适的技术选择。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐