java

Web单向后台服务端推送技术SSE

2019-11-07  本文已影响0人  空山雪林

概述

服务端向浏览器推送信息,除了WebSocket之外,我们还可以使用另外一种技术SSE,即Server-Sent Events(以下简称SSE)。在我们处理有些浏览器的耗时任务时,比如导出Excel,密集型复杂计算,任务进度控制,支付回调等待处理等,我们都需要异步在适合的时候将消息推送给浏览器(单向)。

严格来说,HTTP协议无法做到服务器主动推送消息给浏览器,因为它是短连接的,但有一种变通方法,就是服务器先浏览器申明,接下来要发送的是流信息。也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向浏览器推送信息。它基于 HTTP 协议,目前除了 IE/Edge,其他浏览器都支持。在IE浏览器里,你可以使用polyfill,那么你就可以在IE10+(含IE10)中SSE技术。它比WebSocket更轻量,更简单。

与WebSocket相比

SSEWebSocket 作用相似,都是建立浏览器与服务器之间的通信渠道,然后服务器向浏览器推送信息。WebSocket 更强大和灵活。因为它是全双工通道,可以双向通信;SSE 是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次 HTTP 请求。那么,相比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属性,表明连接的当前状态。该属性只读,可以取以下值。

客户端也支持服务端发过来的自定义事件,比如侦听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/jsontext/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技术有了全面的了解,同时,也请了解该技术使用的业务场景,可不能乱用哦~

上一篇下一篇

猜你喜欢

热点阅读