去掉WebSocket味,如何基于 Spring的WebFlux实现SSE功能,也就是服务端主动消息推送,轻松加愉快

274 阅读6分钟

 本文会详细讲解一下如何基于 Spring Boot WebFlux 实现 SSE(Server-Sent Events)功能。

一、SSE 原理

SSE 是一种允许服务器在任何时间向客户端(通常是浏览器)推送数据的 Web 技术。它建立在单一的、长期的 HTTP 连接之上。

工作原理:

  1. 建立连接:客户端(如浏览器)使用 EventSource API 向服务器发起一个常规的 HTTP 请求,但请求头中包含 Accept: text/event-stream
  2. 保持连接:服务器收到请求后,将响应头 Content-Type 设置为 text/event-stream,并保持这个 HTTP 连接处于打开状态,而不是在发送一次响应后立即关闭它。
  3. 流式传输:服务器通过这个持久连接周期性地发送数据块。每个数据块遵循特定的文本格式(以 data:event:id:retry: 等关键字开头,并以两个换行符 \n\n 结尾)。
  4. 处理数据:客户端的 EventSource 对象会持续监听这些 incoming 的数据流,解析格式,并触发相应的事件(如 onmessageonopen)来处理数据。

与 WebSocket 的区别:

  • 单向 vs 双向:SSE 是服务器到客户端的单向通信。WebSocket 是双向的。
  • 协议:SSE 基于 HTTP(因此更容易融入现有 HTTP 生态,如认证、代理),WebSocket 是基于 TCP 的独立协议(ws:// 或 wss://)。
  • 重连机制:SSE 内置了断线重连和事件 ID 跟踪的功能(通过 retry 和 id 字段),WebSocket 需要手动实现。
  • 数据格式:SSE 仅支持文本(通常使用 UTF-8 编码的 JSON),WebSocket 支持二进制和文本。

二、使用场景

SSE 非常适合服务器需要主动、持续地向客户端推送数据的场景,且不需要客户端频繁向服务器发送消息。

  • 实时通知:新闻推送、站内信、价格提醒、社交网络点赞/评论通知。
  • 实时监控仪表盘:服务器性能监控(CPU、内存)、实时业务数据(在线用户数、订单成交额)、物联网设备传感器数据流。
  • 实时日志:将服务器后台任务的执行日志实时输出到浏览器控制台或管理页面。
  • 进度更新:报告长时间运行的操作(如文件处理、数据导出)的完成进度。

三、后端实现 (Spring Boot WebFlux)

Spring WebFlux 的响应式特性使其成为实现 SSE 的绝佳选择,因为它天然支持处理异步数据流。

1. 添加依赖 (Maven pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
</dependencies>

2. 创建控制器-关键示例 (Controller)

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/sse")
public class SseController {

    /**
     * 模拟一个简单的每秒推送时间信息的 SSE 流
     * produces = MediaType.TEXT_EVENT_STREAM_VALUE 是关键,它声明了响应是 SSE 流。
     */
    @GetMapping(path = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamEvents() {
        // 创建一个每 1 秒触发一次的无限流
        // 消息内容格式遵循 SSE 规范: "data: {json}\n\n"
        return Flux.interval(Duration.ofSeconds(1))
                .map(sequence -> "data: {"time": "" + LocalDateTime.now() + "", "sequence": " + sequence + "}\n\n");
    }

    /**
     * 更复杂的例子:推送自定义事件和 ID
     */
    @GetMapping(path = "/advanced-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamAdvancedEvents() {
        Flux<Long> interval = Flux.interval(Duration.ofSeconds(2));
        Flux<String> events = Flux.generate(sink -> {
            // 生成一个自定义事件消息
            String eventData = "This is a message at " + LocalDateTime.now();
            sink.next(eventData);
        });

        // 合并间隔和事件流,并格式化为 SSE 格式
        return Flux.zip(interval, events)
                .map(tuple -> {
                    Long seq = tuple.getT1();
                    String message = tuple.getT2();
                    // 构建一个完整的 SSE 消息块
                    // 可以指定事件类型(event:)和ID(id:)
                    return "id: " + seq + "\n" +
                           "event: custom-message\n" + // 前端监听的事件名
                           "data: " + message + "\n" +
                           "retry: 10000\n\n"; // 建议客户端10秒后重连
                });
    }

    /**
     * 使用 ServerSentEvent 工具类(推荐,更清晰)
     */
    @GetMapping(path = "/sse-object", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<org.springframework.http.codec.ServerSentEvent<?>> streamSseObject() {
        return Flux.interval(Duration.ofSeconds(1))
                .map(sequence -> org.springframework.http.codec.ServerSentEvent.builder()
                        .id(String.valueOf(sequence)) // ID
                        .event("periodic-event")      // 事件类型
                        .data("SSE Message - " + sequence + " at " + LocalDateTime.now()) // 数据
                        .build());
    }
}

关键点说明:

  • produces = MediaType.TEXT_EVENT_STREAM_VALUE:这是最重要的注解,它告诉 Spring 该方法产生的是 SSE 流。
  • Flux:WebFlux 的核心响应式类型,代表一个包含 0 到 N 个元素的异步序列,完美用于表示持续的事件流。
  • Flux.interval():创建一个按固定时间间隔发出递增数字的 Flux,是生成周期性事件的常用方法。
  • 消息格式:返回值必须是遵循 SSE 格式的 String 流,或者 ServerSentEvent<?> 对象流(Spring 会帮你自动转换为正确的文本格式)。格式必须以两个换行符 \n\n 结束一个消息块。
  • ServerSentEvent builder:这是 Spring 提供的工具类,构建 SSE 消息更规范、更安全,避免了手动拼接字符串可能出现的格式错误。

四、前端实现 (Vue.js)

前端使用 Vue 3 的 Composition API 和 EventSource API。

<template>
  <div>
    <h1>SSE 实时数据流</h1>
    <button @click="connectSSE" :disabled="isConnected">连接</button>
    <button @click="disconnectSSE" :disabled="!isConnected">断开</button>
    <div v-if="isConnected">状态: <span style="color: green;">已连接</span></div>
    <div v-else>状态: <span style="color: red;">未连接</span></div>

    <h2>收到的消息:</h2>
    <ul>
      <li v-for="(message, index) in messages" :key="index">
        {{ message }}
      </li>
    </ul>

    <h2>收到的 ServerSentEvent 对象:</h2>
    <ul>
      <li v-for="(sse, index) in sseEvents" :key="index">
        <p>ID: {{ sse.id }}</p>
        <p>Event: {{ sse.event || 'message' }}</p>
        <p>Data: {{ sse.data }}</p>
        <hr>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue';

const isConnected = ref(false);
const messages = ref([]);
const sseEvents = ref([]);
let eventSource = null;

const connectSSE = () => {
  // 连接到后端 SSE 端点
  // 注意 URL 与你的后端 Controller 路径匹配
  eventSource = new EventSource('http://localhost:8080/sse/sse-object');

  // 监听通用的 'message' 事件(服务器未指定 event 字段时默认触发)
  eventSource.onmessage = (event) => {
    console.log('Generic message event:', event);
    messages.value.push(event.data);
    // 如果数据是 JSON,可以解析
    // try { const data = JSON.parse(event.data); } catch (e) { ... }
  };

  // 监听特定的自定义事件(对应服务器端的 `event: custom-message`)
  eventSource.addEventListener('custom-message', (event) => {
    console.log('Custom event received:', event);
    messages.value.push(`[Custom] ${event.data}`);
  });

  // 监听特定的自定义事件(对应服务器端的 `event: periodic-event`)
  eventSource.addEventListener('periodic-event', (event) => {
    console.log('Periodic event received:', event);
    // 将整个 ServerSentEvent 对象保存下来用于展示
    sseEvents.value.push({
      id: event.lastEventId, // 获取服务器发送的 id
      event: event.type,     // 事件类型
      data: event.data       // 数据
    });
  });

  // 连接打开时
  eventSource.onopen = (event) => {
    console.log('SSE Connection opened.', event);
    isConnected.value = true;
  };

  // 发生错误时
  eventSource.onerror = (error) => {
    console.error('SSE Error:', error);
    // EventSource 在出错时会自动尝试重连,onerror 触发后 onopen 可能会再次触发
    // 如果连接彻底失败(如 404),需要手动关闭
    // eventSource.close();
    // isConnected.value = false;
  };
};

const disconnectSSE = () => {
  if (eventSource) {
    eventSource.close(); // 关闭连接
    eventSource = null;
    isConnected.value = false;
    console.log('SSE Connection closed.');
  }
};

// 组件卸载时自动断开连接,防止内存泄漏
onUnmounted(() => {
  disconnectSSE();
});
</script>

关键点说明:

  • EventSource:浏览器原生对象,用于创建到 SSE 端点的连接。
  • onmessage:监听所有未指定事件类型的消息。
  • addEventListener('event-name'):监听服务器端发出的特定类型的事件(如 event: custom-message)。
  • onopen & onerror:处理连接状态。
  • event.data:获取服务器发送的实际数据内容。
  • close()重要! 当不再需要连接时(例如组件销毁、用户手动断开),必须调用此方法以释放资源。
  • 自动重连EventSource 在连接意外断开时会自动尝试重新连接,这是其内置的强大功能之一。
  • 你应该会看到消息列表开始每秒更新一条数据。后端控制器的不同端点(/stream/advanced-stream/sse-object)会产生不同格式的消息,前端需要对应地监听不同的事件。

总结

通过结合 Spring Boot WebFlux 的 Flux 响应式流和 SSE 协议,你可以非常轻松地构建出高效的服务器推送功能。Vue 前端的 EventSource API 使用起来也十分简单直观。

这种组合非常适合构建需要低延迟、高吞吐量、单向数据流的实时应用,同时享受 Spring 生态和 Vue 生态带来的开发便利。