Web单向后台服务端推送技术SSE
概述
服务端向浏览器推送信息,除了WebSocket
之外,我们还可以使用另外一种技术SSE
,即Server-Sent Events
(以下简称SSE)。在我们处理有些浏览器的耗时任务时,比如导出Excel,密集型复杂计算,任务进度控制,支付回调等待处理等,我们都需要异步在适合的时候将消息推送给浏览器(单向)。
严格来说,HTTP协议无法做到服务器主动推送消息给浏览器,因为它是短连接的,但有一种变通方法,就是服务器先浏览器申明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。在IE浏览器里,你可以使用polyfill
,那么你就可以在IE10+(含IE10)中SSE
技术。它比WebSocket
更轻量,更简单。
与WebSocket相比
SSE
与 WebSocket
作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。WebSocket
更强大和灵活。因为它是全双工通道,可以双向通信;SSE
是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。那么,相比WebSocket
,两者都支持跨域,那么,SSE
有哪些优点呢?
-
SSE
使用HTTP
协议,现有的服务器软件都支持。WebSocket
是一个独立协议。 -
SSE
属于轻量级,使用简单;WebSocket
协议相对复杂。 -
SSE
默认支持断线重连,WebSocket
需要自己实现。 -
SSE
一般只用来传送文本,二进制数据需要编码后传送,WebSocket
默认支持传送二进制数据。 -
SSE
支持自定义发送的消息类型,实现起来更为简洁。
所以,两者的使用具体还是根据业务场景,比如SSE
不能用于全双工通信使用场景,比如实时聊天系统等。
客户端API
SSE
客户端的代码使用EventSource
对象,比起websocket,客户端代码连接SSE服务只需几行代码:
//检测EventSource对象浏览器是否支持
if (!!window.EventSource) {
//连接SSE服务,也支持跨域
var source = new EventSource('/sse');
//侦听服务端推送消息
source.addEventListener('message', function(e) {
console.log("onmessage",e.data);
});
//侦听SSE服务连接信息
source.addEventListener('open', function(e) {
}, false);
//侦听错误信息
source.addEventListener('error', function(e) {
if (e.readyState == EventSource.CLOSED) {
console.log("连接关闭");
} else {
console.log("error:",e);
source.close();
}
}, false);
} else {
console.log("你的浏览器不支持SSE");
}
但SSE跨域时,跨域指定EventSource的第二个参数,发送Cookies信息如下:
var source = new EventSource(url, { withCredentials: true });
EventSource实例的readyState属性,表明连接的当前状态。该属性只读,可以取以下值。
- 0:相当于常量
EventSource.CONNECTING
,表示连接还未建立,或者断线正在重连。 - 1:相当于常量
EventSource.OPEN
,表示连接已经建立,可以接受数据。 - 2:相当于常量
EventSource.CLOSED
,表示连接已断,且不会重连。
客户端也支持服务端发过来的自定义事件,比如侦听SSE的foo事件代码如下:
source.addEventListener('foo', function (event) {
var data = event.data;
// handle message
}, false);
至于服务端如何发送自定义事件,继续往下看。
服务端实现
服务端向客户端发送的数据是UTF-8编码的文本,HTTP头信息如下:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
我们可以看到服务端发送到客户端的响应头必须是text/event-stream
,在您以前开发RestfulAPI时,它可能是application/json
或text/plain
等。
每次发送的消息,由若干个message
组成,每个message
之间用\n\n
分隔。每个message
内部由若干行组成,每一行都是如下格式。
[field]: value\n
上面的field可以取四个值:data,event,id,retry,还可以用冒号打头,表示注释,一般来说,服务端每隔一段时间就会向浏览器发送一个注释,保持连接不中断,你无需执行处理断线重连。比如我们向客户端推送进度例子如下,每隔1s推送一条,服务端代码:
private ExecutorService nonBlockingService = Executors.newCachedThreadPool();
@GetMapping("/sse")
public SseEmitter handleSse() {
SseEmitter emitter = new SseEmitter();
nonBlockingService.execute(() -> {
try {
for (int i = 0; i < 5; i++) {
emitter.send("当前进度 @ " + (i+1)*20 + "%");
//模拟耗时任务
Thread.sleep(1000);
}
emitter.complete();
} catch (Exception ex) {
ex.printStackTrace();
emitter.completeWithError(ex);
}
});
return emitter;
}
客户端收到的消息:
image看一下客户端的请求头:
image那么如何发送自定义事件给客户端端呢,非常简单:
SseEventBuilder event = SseEmitter.event().data((i+1)*20)
.id(String.valueOf(i)).name("foo");
emitter.send(event);
客户端收到:
image注:以上代码同时标注了消息ID,浏览器用lastEventId属性读取这个值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。
也可以通过设置reconnectTime(100000)
来设置重启发起连接的时间间隔,两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。
好了,说了那么多,相信您对SSE技术有了全面的了解,同时,也请了解该技术使用的业务场景,可不能乱用哦~