「Spring Reactive Stack」服务端事件推送Server-Sent Events

SSEServer-Sent Events服务器推送事件,是一种仅发送文本消息的技术。SSE基于HTTP协议中的持久连接。SSEHTML5标准协议中的一部分。

客户端接收服务端异步更新的消息可以分为两类:客户端拉取和服务端推送。

客户端拉取:通过短轮询或者长轮询定期请求服务器进行更新。

服务端推送:SSEWebSocketSSE是单向,WebSocket是双向;SSE基于HTTP协议,WebSocket基于WebSocket协议(HTTP以外的协议);

SSE网络协议

  • 基于纯文本的简单协议。服务器端的响应内容类型必须是text/event-stream。响应文本的内容是一个事件流,事件流是一个简单的文本流,仅支持UTF-8格式的编码。
  • 事件流由不同的事件组成。不同事件间通过仅包含回车符和换行符的空行(\r\n)来分隔。
  • 每个事件可以由多行构成,每行由类型和数据两部分组成。类型与数据通过冒号(:)进行分隔,冒号前的为类型,冒号后的为其对应的值。每个事件可以包含如下类型的行:
    • 类型为空白,表示该行是注释,会在处理时被忽略。
    • 类型为data,表示该行是事件所包含的数据。以data开头的行可以出现多次。所有这些行都是该事件的数据。
    • 类型为event,表示该行用来声明事件的类型,即事件名称。浏览器在收到数据时,会产生对应名称的事件。
    • 类型为id,表示该行用来声明事件的标识符。
    • 类型为retry,表示该行用来声明浏览器在连接断开之后进行重连的等待时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
data: china // 该事件仅包含数据

data: Beijing // 该事件包含数据与事件标识
id: 100

event: myevent // 该事件指定了名称
data:shanghai
id: 101

: this is a comment // 该事件具有注释、名称,且包含两行数据
event:city
data: guangzhou
data: shenzhen
  • 事件标识id作用: 如果服务端发送的事件中包含事件标识id,那么浏览器会将最近一次接收到的事件标识id记录到HTTP头的Last-Event-ID属性中。如果浏览器与服务端的连接中断,当浏览器再次连接时,会将Last-Event-ID记录的事件标识id发送给服务端。服务器端通过浏览器端发送的事件标识id来确定将继续连接哪个事件。

订阅一个服务端推送事件(GET请求),需要设置 包含如下请求头的Request

1
2
3
Accept: text/event-stream # 指明MediaType是事件流
Cache-Control: no-cache # 不要对事件进行缓存
Connection: keep-alive # 长连接

服务端需要提供 包含以下响应头的Response:

1
2
Content-Type: text/event-stream;charset=UTF-8  # 告诉客户端响应是一个事件流
Transfer-Encoding: chunked # 告诉客户端内容大小未知,为流传输

在WebFlux中实现发送事件

首先,在pom.xml文件中,引入webflux

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

创建一个controller类并用@RestController注解标记;
创建一个接受Http GET请求的方法,该方法返回一个Flux对象,并配置produces=text/event-stream

1
2
3
4
@GetMapping(value = "/test/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
private Flux<String> flux() {
return Flux.interval(Duration.ofMillis(1000)).map(i -> " -> " +i);
}

浏览器访问/test/sse就会看到每一秒推送一个数据:

1
2
3
4
5
data: -> 0
data: -> 1
data: -> 2
data: -> 3
...
  • 一次性事件:短暂的去发送事件是比较简单的,只需要使用Flux.just()将消息列表里的消息一条条发送出去即可。
  • 周期性事件:长期的发送事件在发送本身上是没有区别,主要是需要一个周期性线程定期处理发送事务,这里直接使用Flux.inteval()来轮询。
  • 非周期性事件:可以通过Spring的事件监听接口来实现,关键点在于要把监听消息的处理器和Flux的构造结合起来。

SSE的注意事项

  1. SSE只适合发送文本消息;尽管可以使用Base64编码和gzip压缩来发送二进制消息,但效率可能很低。
  2. 早期的一些浏览器,如Internet Explorer不支持。
  3. Internet Explorer/Edge和许多移动浏览器不支持SSE;尽管可以使用polyfills,但它们可能效率低下
  4. 在系统设计时,同一个页面最好只维持1个SSE连接,通过事件来区分。因为浏览器对同时并发的连接数有限制,一般最大是6个。

前端接收Server-Sent事件通知

JavaScript里用EventSource对象来接收服务器发送事件通知:

1
2
3
4
5
6
7
8
if (typeof(EventSource)!=="undefined") {
var source=new EventSource("http://localhost:8080/test/sse");
source.onmessage=function(event) {
alert(event.data);
};
} else {
alert("抱歉,你的浏览器不支持 server-sent 事件...");
}

EventSource是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream格式发送事件, 会一直保持开启直到被要求关闭。

  • EventSource属性
    • EventSource.onerror:EventHandler,当发生错误时被调用,并且在此对象上派发error事件。
    • EventSource.onmessage:EventHandler,当收到一个message事件,当接收到消息时被调用。
    • EventSource.onopen:EventHandler,当收到一个open事件,当连接刚打开时被调用。
    • EventSource.readyState(只读):unsigned short值,代表连接状态。可能值是CONNECTING(0), OPEN(1), 或者CLOSED(2)。
    • EventSource.url(只读):一个DOMString,代表事件源的URL。