Server-sent events (SSE)

Server-sent events

Server-sent events 简称 SSE,是一种服务端向客户端推送数据的协议,与 websocket 类似,服务端可以随时向客户端推送数据,且可以多次推送数据,但与 websocket 不同的是,数据只能在一个方向上传输,即服务端到客户端。客户端在建立连接时可以通过 url、query string 传递少量参数,但建立连接后,客户端无法再通过 SSE 通道向服务端发送数据,SSE 通道只允许服务端向客户端发送数据。

协议详情

SSE 的 MIME 类型为 text/event-stream,也就是说 request header accept=text/event-stream,response header content-type=text/event-stream

SSE 的事件流是一个简单的文本数据流,浏览器在解析事件流时遵循以下规则:

每条消息以一行或多行文本构成,每条消息以双换行 \n\n 分隔。若最后一个块没有以双换行 \n\n 结尾,则会被丢弃。

复制代码
data: This is the first message.

event: userconnect
id: 1
data: {"username": "bobby", "time": "02:33:48"}
retry: 3000

data: some text

event: add
data: 73857293

以下事件流会被合并为 data: YHOO\n+2\n10

复制代码
data: YHOO
data: +2
data: 10

第二个data 会被丢弃,因为没有以双换行 \n\n 结尾:

复制代码
data: This is the first message.

data: This is the second message

事件流每行以第一个冒号 : 分割为 fieldNamefieldValue

如果 fieldValue 包含前导空格,只会删除第一个空格。

如果一行不包含冒号,则整行被视为 fieldNamefieldValue 为一个空字符串。

复制代码
id: 10

id

若一行以冒号 : 开头会被视为注释,不会触发事件,注释行可用于防止连接超时;服务器可以定期发送注释以保持连接活跃。

复制代码
: this is a test stream

: ping

每条消息都包含以下字段的某种组合。

  • event 是事件类型,若不包含此字段,则默认为 message。

  • idlastEventId,在连接断开后重连时会作为 request header Last-Event-ID=lastEventId 自动发送给服务端。

    设置 lastEventId 后,若想取消可以发送 id 为空的消息:

    复制代码
    data: first event
    id: 1
    
    data:second event
    id
  • data 是消息的数据字段,如果收到多个以 data: 开头的连续行时,会将多个 fieldValue 使用 \n 连接起来作为一个 fieldValue

  • retry 重新连接时间,单位为毫秒。

  • 所有其他字段名都将被忽略。

以下流触发两个事件:第一个块触发事件,data 设置为空字符串。中间块触发一个事件,data 设置为一个换行符 \n。最后一个块被丢弃,因为它后面没有双换行 \n\n

复制代码
data

data
data

data:

以下流中前两个块触发相同的事件,data 设置为 ok,第三个块触发不同的事件,data 设置为 ok

复制代码
data:ok

data: ok

data:  ok

以下流不会触发任何事件,因为其被视为注释:

复制代码
: ping

代码

前端 JS

浏览器可以使用 EventSource 接收事件:

js 复制代码
const evtSource = new EventSource("http://localhost:8080/sse");

当与事件源建立连接时, open 事件会被触发。

js 复制代码
// addEventListener version
evtSource.addEventListener("open", (e) => {
  console.log("The connection has been established.");
});

// onopen version
evtSource.onopen = (e) => {
  console.log("The connection has been established.");
};

当与事件源的连接未能打开时,error 事件会被触发。

js 复制代码
// addEventListener version
evtSource.addEventListener("error", (e) => {
  console.log("An error occurred while attempting to connect.");
});

// onerror version
evtSource.onerror = (e) => {
  console.log("An error occurred while attempting to connect.");
};

当收到 event 字段为空的事件时,message 事件会被触发:

js 复制代码
// addEventListener version
evtSource.addEventListener("message", (e) => {
  const newElement = document.createElement("li");

  newElement.textContent = `message: ${e.data}`;
  eventList.appendChild(newElement);
});

// onmessage version
evtSource.onmessage = (event) => {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");
  // event.data 为数据
  newElement.textContent = `message: ${event.data}`;
  eventList.appendChild(newElement);
};

当收到 event 字段不为空的事件时,自定义事件会被触发:

js 复制代码
// 监听 ping 事件
evtSource.addEventListener("ping", (event) => {
  const newElement = document.createElement("li");

  const eventList = document.getElementById("list");
  const time = JSON.parse(event.data).time;
  newElement.textContent = `ping at ${time}`;
  eventList.appendChild(newElement);
});

默认情况下,如果客户端和服务器之间的连接关闭,连接将在 retry 毫秒后重新启动。通过 .close() 方法可以终止连接。

js 复制代码
evtSource.close();

后端 spring

spring 提供了 SseEmitter 来发送 SSE 消息,produces 必须为 MediaType.TEXT_EVENT_STREAM_VALUE,即 text/event-stream

可以使用 SseEmitter.send() 发送 String;也可以发送对象,SseEmitter 会使用 jackson 将对象序列化为 json 后再发送。且 SseEmitter 会自动在开头添加 data:

可以使用 SseEmitter.event() 获取 SseEmitter.SseEventBuilder,使用其构造如前面所述的多字段消息。

java 复制代码
package com.example.feigntest;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Set;

@RestController
public class SseController {

    @GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter handle() {
        SseEmitter emitter = new SseEmitter();

        new Thread(() -> {
            try {
                emitter.send("some text");
                emitter.send(new User(1, "tom"));

                SseEmitter.SseEventBuilder builder = SseEmitter.event()
                        // 注释
                        .comment("comment")
                        // event 字段
                        .name("userconnect")
                        // data 字段
                        .data("some data")
                        // id 字段
                        .id("1")
                        // retry 字段
                        .reconnectTime(5000);
                Set<ResponseBodyEmitter.DataWithMediaType> event = builder.build();
                emitter.send(event);

                // 关闭连接
                emitter.complete();
            } catch (IOException e) {
                // ignore
            }
        }).start();

        return emitter;
    }
}

record User(int id, String name) {
}

代码最终发送的事件流如下:

复制代码
data:some text

data:{"id":1,"name":"tom"}

:comment
event:userconnect
data:some data
id:1
retry:5000

也支持返回 ResponseEntity<SseEmitter> 以自定义 http status、header 等。

java 复制代码
@GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> handle() {
    SseEmitter emitter = new SseEmitter();
	// ...
    return ResponseEntity
            .status(HttpStatus.OK)
            .header("X-TENANT-ID", "1")
            .body(emitter);
}

参考

  1. HTML 标准 - WHATWG 网页超文本应用技术工作小组
  2. HTML Standard
  3. 使用服务器发送事件 - Web API | MDN - MDN 文档
  4. EventSource - Web API | MDN - MDN 文档
  5. Asynchronous Requests :: Spring Framework
相关推荐
tuokuac3 小时前
Spring 最核心的注解@Bean本质
java·后端·spring
Lyyaoo.3 小时前
Spring中的拦截器
java·后端·spring
wuqingshun3141593 小时前
说说你对spring的IOC的理解
java·后端·spring
逸Y 仙X3 小时前
文章十二:索引数据的写入和删除
java·大数据·spring boot·spring·elasticsearch·搜索引擎·全文检索
代码探秘者3 小时前
【算法篇】5.链表
java·数据结构·人工智能·python·算法·spring·链表
Binary-Jeff3 小时前
Maven 依赖作用域详解:compile、provided、runtime、test
java·spring·spring cloud·servlet·java-ee·maven
yiyaozjk4 小时前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud
dddaidai1234 小时前
Spring AI Alibaba(二)Hooks 和Interceptors
java·人工智能·spring
小江的记录本4 小时前
【Logback】Logback 日志框架 与 SLF4J绑定、三层模块、MDC链路追踪、异步日志、滚动策略
java·spring boot·后端·spring·log4j·maven·logback