Skip to content

概念

SSE(Server - Sent - Events):服务器发送事件

这种模式跟WebSocket相像,只不过跟它不相同的地方在于

  • ws是全双工协议,可以双方通信。SSE是单向通道,只能服务器向游览器发送
  • ws是新协议,需要服务端的支持。SSE是建立在HTTP协议之上的
  • ws是一种较重的协议,很复杂。SSE是一种轻量级协议
  • SSE支持短线重连,ws则需要额外部署
  • SSE支持自定义发送的数据类型

image-20240702104444677

返回消息的格式:data: 消息 \n\n\n 是个转义字符代表换行)

两种应用模式

客户端:

javascript
let eventSource = new EventSource('替换为SSE服务端点');

eventSource.addEventListener('message', (event) => {
  const messageData = event.data;
  // 使用返回数据
});

以下均为服务器端

服务器流式推送

java
@RestController
@RequestMapping("system")
public class SystemController {
    
    @GetMapping("resourceInfo")
    public void resourceInfo(HttpServletResponse response) throws IOException, InterruptedException {
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("UTF-8");
        while (true) {
            PrintWriter pw = response.getWriter();
            pw.write("data: " + new Date() + "\n\n");
            pw.flush();
        }
    }
}

服务器重连推送

java
@RestController
@RequestMapping("system")
public class SystemController {
    
    @GetMapping("resourceInfo")
    public void resourceInfo(HttpServletResponse response) throws IOException, InterruptedException {
        response.setContentType("text/event-stream");
        response.setCharacterEncoding("UTF-8");
        // 这次没有循环,但是客户端在接收到数据后未来的两秒内没有在收到新的数据会重新尝试请求这个接口
        PrintWriter pw = response.getWriter();
        pw.write("retry: 2000\ndata: " + new Date() + "\n\n");
        pw.flush();
    }
}

工具脚本

需要设置

java
response.setContentType("text/event-stream");
response.setCharacterEncoding("utf-8");

工具类

java
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional;

/**
 * SSE 消息构建工具类
 */
public class SseUtils {

    /**
     * 禁止外部实例化
     */
    private SseUtils() {
    }

    /**
     * 快捷构建 SSE 事件
     */
    public static String buildSseEvent(String id, String event, Object data) {
        return builder().id(id).event(event).data(data).build();
    }

    /**
     * 获取一个新的 SSE 事件构建器
     */
    public static SseEventBuilder builder() {
        return new SseEventBuilder();
    }

    /**
     * SSE 事件构建器(链式调用)
     */
    public static class SseEventBuilder {
        private String id;
        private String event;
        private String data;      // data 可以是 String 或任意对象(自动转 JSON)
        private Integer retry;    // 重连间隔(毫秒)

        public SseEventBuilder id(String id) {
            this.id = id;
            return this;
        }

        public SseEventBuilder event(String event) {
            this.event = event;
            return this;
        }

        /**
         * 设置 data,Object 会转为 JSON 字符串
         */
        public SseEventBuilder data(Object data) {
            this.data = JsonUtils.toJson(data);
            return this;
        }

        /**
         * 设置 data,直接使用原始字符串(不做 JSON 转换)
         */
        public SseEventBuilder dataString(String dataStr) {
            this.data = dataStr;
            return this;
        }

        public SseEventBuilder retry(int retryMillis) {
            this.retry = retryMillis;
            return this;
        }

        public String build() {
            StringBuilder sb = new StringBuilder();
            Optional.ofNullable(id).ifPresent(id -> sb.append("id:").append(id).append("\n"));
            Optional.ofNullable(event).ifPresent(event -> sb.append("event:").append(event).append("\n"));
            Optional.ofNullable(retry).ifPresent(retry -> sb.append("retry:").append(retry).append("\n"));
            Optional.ofNullable(data).ifPresent(data -> {
                // data 支持多行,按换行符拆分为多个 data: 行
                for (String line : data.split("\n")) {
                    sb.append("data:").append(line).append("\n");
                }
            });
            sb.append("\n");
            return sb.toString();
        }

        /**
         * 发送 SSE 事件
         *
         * @param response 当前响应对象
         */
        public void send(HttpServletResponse response) {
            try {
                PrintWriter pw = response.getWriter();
                String data = build();
                pw.write(data);
                pw.flush();
            } catch (IOException e) {
                throw new RuntimeException("获取响应流失败");
            }
        }
    }
}