响应式编程 之 SSE接口调用

以下是 Web 前端(浏览器)Java 客户端 调用现有 SSE(Server-Sent Events)接口的完整示例详解,涵盖连接、事件监听、错误处理、认证、重连等关键场景。


一、SSE 接口前提假设

假设后端已提供一个标准的 SSE 接口:

  • URLhttp://localhost:8080/events
  • 协议:HTTP GET
  • 响应类型Content-Type: text/event-stream
  • 事件格式示例
    event: message
    data: {"id":1,"content":"Hello"}
    id: 1001
    retry: 3000
    
    event: update
    data: {"status":"done"}
    

二、Web 前端调用 SSE(浏览器环境)

✅ 核心 API:EventSource

所有现代浏览器(Chrome, Firefox, Edge, Safari)原生支持,无需额外库

1. 基础连接与监听

<!DOCTYPE html>
<html>
<head>
  <title>SSE Client</title>
</head>
<body>
  <div id="log"></div>

  <script>
    // 1. 创建 EventSource 实例(自动发起 GET 请求)
    const eventSource = new EventSource('http://localhost:8080/events');

    // 2. 监听默认事件(无 event 字段 或 event="message")
    eventSource.onmessage = function(event) {
      console.log('收到消息:', event.data);
      document.getElementById('log').innerHTML += `<p>消息: ${event.data}</p>`;
    };

    // 3. 监听自定义事件(如 event="update")
    eventSource.addEventListener('update', function(event) {
      console.log('状态更新:', event.data);
      // 处理业务逻辑
    });

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

    // 5. 错误处理(包括断线)
    eventSource.onerror = function(err) {
      console.error('SSE 连接出错:', err);
      // 注意:浏览器会自动重连(除非调用 close())
    };

    // 6. 手动关闭连接(如用户退出页面)
    window.addEventListener('beforeunload', () => {
      eventSource.close();
    });
  </script>
</body>
</html>

2. 带认证的 SSE 请求

// 方式1:通过 URL 参数(不安全,仅用于演示)
const eventSource = new EventSource('/events?token=abc123');

// 方式2:通过 withCredentials(携带 Cookie)
const eventSource = new EventSource('/events', { withCredentials: true });

// 方式3:无法直接设置 Authorization 头!
// ❌ EventSource 不支持自定义请求头(安全限制)

⚠️ 重要限制
EventSource 不能设置自定义 HTTP 头(如 Authorization: Bearer xxx),这是浏览器安全策略决定的。
解决方案

  • 使用 Cookie 认证(配合 withCredentials: true
  • 将 token 放在 URL 查询参数中(注意:可能被日志记录,不推荐敏感场景)
  • 改用 WebSocket(支持自定义头)

3. 手动控制重连

let retryCount = 0;
const maxRetries = 5;

eventSource.onerror = function() {
  retryCount++;
  if (retryCount > maxRetries) {
    console.error('重试次数超限,停止连接');
    eventSource.close();
  } else {
    console.log(`第 ${retryCount} 次重试...`);
    // 浏览器会按服务器返回的 retry 值或默认 3 秒重连
  }
};

三、Java 客户端调用 SSE(非浏览器环境)

由于 Java 没有内置 SSE 客户端,需手动解析 text/event-stream 响应流。

方案选择:

  • Java 11+:使用 HttpClient + 自定义 BodyHandler
  • 旧版本:使用 OkHttp、Apache HttpClient 等第三方库

示例 1:Java 11+ 原生 HttpClient

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.Flow;

public class SseJavaClient {

    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(10))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:8080/events"))
                .header("Accept", "text/event-stream")
                // 如需 Cookie 认证
                // .header("Cookie", "JSESSIONID=abc123")
                .build();

        // 自定义 BodyHandler 解析流
        HttpResponse.BodyHandler<Void> bodyHandler = responseInfo ->
            HttpResponse.BodySubscribers.fromLineSubscriber(
                new Flow.Subscriber<String>() {
                    private Flow.Subscription subscription;

                    @Override
                    public void onSubscribe(Flow.Subscription subscription) {
                        this.subscription = subscription;
                        subscription.request(1); // 请求第一行
                    }

                    @Override
                    public void onNext(String line) {
                        System.out.println("收到行: " + line);

                        // 简单解析 SSE 行(实际需更健壮的解析器)
                        if (line.startsWith("data:")) {
                            String data = line.substring(5).trim();
                            System.out.println("数据内容: " + data);
                            // 可反序列化 JSON
                        }

                        subscription.request(1); // 请求下一行
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        System.err.println("SSE 流错误: " + throwable.getMessage());
                        throwable.printStackTrace();
                    }

                    @Override
                    public void onComplete() {
                        System.out.println("SSE 流结束");
                    }
                },
                "\n" // 按行分割
            );

        // 发起请求(异步)
        client.sendAsync(request, bodyHandler)
              .thenRun(() -> System.out.println("请求已发送"));

        // 保持主线程运行
        Thread.sleep(60_000);
    }
}

示例 2:使用 OkHttp(更简洁)

// 添加依赖: com.squareup.okhttp3:okhttp:4.12.0

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSource;

import java.io.IOException;

public class SseOkHttpClient {
    public static void main(String[] args) throws IOException {
        OkHttpClient client = new OkHttpClient();

        Request request = new Request.Builder()
                .url("http://localhost:8080/events")
                .addHeader("Accept", "text/event-stream")
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

            ResponseBody body = response.body();
            if (body == null) return;

            BufferedSource source = body.source();
            StringBuilder eventBuffer = new StringBuilder();

            while (!Thread.interrupted()) {
                String line = source.readUtf8Line();
                if (line == null) break; // 连接关闭

                if (line.isEmpty()) {
                    // 空行表示事件结束
                    parseAndHandleEvent(eventBuffer.toString());
                    eventBuffer.setLength(0); // 清空
                } else {
                    eventBuffer.append(line).append("\n");
                }
            }
        }
    }

    private static void parseAndHandleEvent(String eventText) {
        System.out.println("完整事件:\n" + eventText);
        // 这里可解析 event/data/id 字段
        if (eventText.contains("data:")) {
            String data = eventText.split("data:")[1].trim();
            System.out.println("数据: " + data);
        }
    }
}

四、关键注意事项

1. SSE 是单向通信

  • 服务器 → 客户端,客户端无法通过 SSE 向服务器发消息
  • 如需双向通信,改用 WebSocket。

2. 连接管理

  • 前端:页面卸载时务必调用 eventSource.close() 避免内存泄漏。
  • Java 客户端:需在异常或完成时关闭流(try-with-resources)。

3. 错误与重连

  • 浏览器会自动重连(间隔由服务器 retry: 字段或默认 3 秒控制)。
  • Java 客户端需自行实现重连逻辑(如指数退避)。

4. 跨域问题(CORS)

  • 后端需设置 CORS 头:
    @CrossOrigin(origins = "http://localhost:3000")
    @GetMapping("/events")
    public SseEmitter events() { ... }
    
  • 或全局配置:
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/events").allowedOrigins("http://localhost:3000");
        }
    }
    

5. 代理与超时

  • Nginx 等反向代理需配置:
    location /events {
        proxy_pass http://backend;
        proxy_http_version 1.1;
        proxy_set_header Connection '';
        proxy_cache off;
        proxy_buffering off; # 关键!禁用缓冲
        proxy_read_timeout 86400s; # 长超时
    }
    

五、总结对比

客户端类型 实现方式 认证支持 重连机制 适用场景
Web 前端 new EventSource(url, options) Cookie / URL 参数 浏览器自动重连 浏览器实时通知
Java 客户端 HttpClient / OkHttp 手动解析流 支持任意 Header 需手动实现 服务间调用、测试

💡 最佳实践建议

  • 浏览器场景:优先用 EventSource,简单可靠。
  • 服务端调用:若只是消费 SSE,可用 Java 手动解析;但长期建议后端改用 消息队列(如 Kafka)gRPC 流 替代 SSE(SSE 设计初衷是面向浏览器的)。
posted @ 2026-02-03 18:51  蓝迷梦  阅读(1)  评论(0)    收藏  举报