基于SSE的客户端代码实现

概要

Server-Sent Events(服务器发送事件,简称 SSE) 是一种用于实现服务器向客户端推送实时更新的技术。与 WebSocket 不同,SSE 是一种单向通信技术,适用于只需要从服务器到客户端的数据流场景。
可以理解为打字机的输出方式,一般AI产品都是这种方式,优点是一次响应的时间太久而导致http超时断开。

一、SSE技术原理

单向通信机制:基于HTTP协议实现服务器到客户端的单向数据推送,客户端通过EventSource API建立持久连接
长连接特性:使用text/event-stream内容类型,服务器保持连接开放状态持续发送事件流
自动重连机制:内置心跳检测和断线重连功能,通过retry字段指定重连间隔
事件格式规范:每条消息包含data(必需)和可选字段(event/id/retry),以\n\n分隔不同事件

二、协议对比分析

特性SSEHTTPWebSocketMQ
通信方向单向(服务器→客户端)单向(客户端→服务器)双向全双工通信发布订阅
协议基础HTTP长连接HTTP独立协议(ws://)mqtt等
数据格式仅文本支持文本和二进制支持文本和二进制字节
实现复杂度简单,无需额外协议支持简单需要专门协议支持中等
优缺点复用HTTP协议但保持长连接,避免频繁建立新连接的开销短连接长连接保证消息可靠性,适用网络不好和高并发要求
应用场景类似ChatGPT的渐进式响应输出体验,如新闻推送、股票行情、实时日志监控适用于请求-响应模式的常规交互,如网页加载、API调用适用于全双工实时通信场景,如在线聊天、协同编辑、游戏适用于异步解耦、削峰填谷的场景,如订单处理、事件驱动架构
技术选型建议轻量级、兼容HTTP协议,适合浏览器端无需双向通信的场景,但需注意浏览器兼容性简单易用,但实时性较差;长轮询可减少延迟,但服务器资源消耗较高,适合低频更新场景低延迟、高效,需浏览器和服务器双向支持,适合高频交互,但需处理连接稳定性问题分布式系统首选,支持持久化和高吞吐,但需额外维护中间件,适合服务间通信而非直接前端交互

客户端代码java版本

以下是关于SSE技术原理、与其他协议的对比以及应用场景的详细说明:

package com.ai;


import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class HttpURLConnectionClient {
    public static void main(String[] args) {
        String sseUrl = "http://ip:port/chat-messages";

        try {
            // 1. 创建URL对象
            URL url = new URL(sseUrl);
            // 2. 打开连接
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            // 3. 设置请求方法为POST
            connection.setRequestMethod("POST");
            connection.setRequestProperty("Accept", "text/event-stream");
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(0); // 长连接

            // 4. 设置请求头
            connection.setRequestProperty("Authorization", "Bearer xxx");
            connection.setRequestProperty("Content-Type", "application/json");

            // 5. 启用输出流
            connection.setDoOutput(true);

            // 6. 准备请求体数据,业务数据,自行编写
            JSONObject jsonObject = new JSONObject();

            String requestBody = jsonObject.toJSONString();
            byte[] postData = requestBody.getBytes(StandardCharsets.UTF_8);

            // 7. 设置Content-Length头
            connection.setRequestProperty("Content-Length", String.valueOf(postData.length));

            // 8. 发送请求体
            try (OutputStream os = connection.getOutputStream()) {
                os.write(postData);
                os.flush();
            }

            // 9. 获取响应
            BufferedReader reader = new BufferedReader(
                    new InputStreamReader(connection.getInputStream()));

            String line;
            while ((line = reader.readLine()) != null) {
                if (line.startsWith("data:")) {
                    String responseData = line.substring(5).trim();
                    System.out.println(UnicodeConverter.unicodeToString(responseData));
//                    System.out.println("收到消息: " + line.substring(5).trim());
                }
            }
            reader.close();

            // 10. 关闭连接
            connection.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

UnicodeConverter.java用于unicode与中文切换

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UnicodeConverter {
    public static void main(String[] args) {
        String unicodeStr = "\u5173\u4e8e\u63d2\u5ea7";
        System.out.println("Unicode转中文: " + unicodeToString(unicodeStr));

        String chineseStr = "你好,世界!";
        System.out.println("中文转Unicode: " +stringToUnicode(chineseStr));
    }

    // Unicode转中文
    public static String unicodeToString(String str) {
        Pattern pattern = Pattern.compile("(\\\\u[0-9a-fA-F]{4})");
        Matcher matcher = pattern.matcher(str);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            String group = matcher.group(1);
            char ch = (char) Integer.parseInt(group.substring(2), 16);
            matcher.appendReplacement(sb, String.valueOf(ch));
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    // 中文转Unicode
    public static String stringToUnicode(String str) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i);
            if (ch > 127) {
                sb.append("\\u").append(Integer.toHexString(ch));
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();
    }
}

使用原生JavaScript实现SSE客户端

SSE(Server-Sent Events)允许服务器通过HTTP连接向客户端推送事件。以下是实现SSE客户端的核心代码:

const eventSource = new EventSource('你的SSE服务端URL');

eventSource.onopen = function() {
    console.log('SSE连接已建立');
};

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

eventSource.addEventListener('customEvent', function(event) {
    console.log('自定义事件:', event.data);
});

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

处理不同类型的服务器事件
SSE可以处理多种事件类型,包括默认消息和自定义事件:

// 处理未指定类型的消息
eventSource.onmessage = function(event) {
    const data = JSON.parse(event.data);
    updateUI(data);
};

// 处理特定类型的自定义事件
eventSource.addEventListener('priceUpdate', function(event) {
    const priceData = JSON.parse(event.data);
    updatePriceDisplay(priceData);
});

// 处理带ID的事件(可帮助断线重连)
eventSource.addEventListener('userNotification', function(event) {
    const lastEventId = event.lastEventId;
    const notification = JSON.parse(event.data);
    storeLastEventId(lastEventId);
    showNotification(notification);
});
 

连接管理和错误处理
稳健的SSE客户端需要完善的连接管理:

let reconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;

eventSource.onerror = function() {
    if (eventSource.readyState === EventSource.CLOSED) {
        if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
            setTimeout(() => {
                reconnectAttempts++;
                initSSEConnection();
            }, 1000 * Math.pow(2, reconnectAttempts));
        } else {
            showConnectionError();
        }
    }
};

function initSSEConnection() {
    if (eventSource) eventSource.close();
    eventSource = new EventSource('服务端URL?lastEventId=' + getLastEventId());
}
 

浏览器兼容性处理
虽然现代浏览器都支持SSE,但仍需考虑兼容性:

if (typeof EventSource !== 'undefined') {
    // 正常初始化SSE连接
} else {
    // 降级方案:使用轮询或WebSocket
    console.warn('浏览器不支持Server-Sent Events');
    setupPollingFallback();
}

function setupPollingFallback() {
    let lastUpdate = 0;
    setInterval(() => {
        fetch('/polling-endpoint?since=' + lastUpdate)
            .then(response => response.json())
            .then(data => {
                lastUpdate = Date.now();
                processUpdates(data);
            });
    }, 3000); // 每3秒轮询一次
}
 

安全注意事项
实现SSE时需要关注的安全问题:

// 确保使用HTTPS协议
const sseUrl = window.location.protocol === 'https:' 
    ? 'https://api.example.com/sse'
    : 'http://api.example.com/sse';

// 添加认证令牌
const token = getAuthToken();
const eventSource = new EventSource(`${sseUrl}?token=${token}`);

// 验证消息来源
eventSource.onmessage = function(event) {
    try {
        const data = JSON.parse(event.data);
        if (!data.signature || !verifySignature(data)) {
            throw new Error('消息签名验证失败');
        }
        processValidData(data);
    } catch (err) {
        console.error('消息处理错误:', err);
    }
};
 

实际应用示例
股票行情实时更新实现:

const stockEventSource = new EventSource('/stocks/stream');

stockEventSource.addEventListener('stockUpdate', function(event) {
    const update = JSON.parse(event.data);
    const stockElement = document.getElementById(`stock-${update.symbol}`);
    if (stockElement) {
        stockElement.querySelector('.price').textContent = update.price;
        stockElement.querySelector('.change').textContent = update.change;
        
        // 添加价格变化动画
        stockElement.classList.add('updated');
        setTimeout(() => stockElement.classList.remove('updated'), 500);
    }
});

// 处理市场状态变化
stockEventSource.addEventListener('marketStatus', function(event) {
    const status = JSON.parse(event.data);
    document.getElementById('market-status').textContent = status.isOpen 
        ? '市场开市' : '市场闭市';
});
 
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

瑞瑞绮绮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值