服务器推送以及sse实战

最近在做crm系统,有一个需求是外呼系统收到来电通知时要给客户端(浏览器)实时推送一条信息,告诉客户端有来电了,也就是弹框显示来电电话号码。由于不是客户端主动请求服务端数据,所以就需要一个方案来解决服务端主动推送数据给客户端的问题。

服务器推送数据也算是一类问题了,目前的解决方法也有不少,主要可以分成两类。这两类方法的区别在于是否基于HTTP协议实现,不使用HTTP协议的做法是实用HTML5新增的WebSocket规范,而使用HTTP协议规范的做法则包括简易轮询、COMET技术和本文要介绍的HTML5服务器推送事件(Server-sent Events)。

服务端推送方案简介

  • WebSocket:WebSocket规范是HTML5中的一个重要组成部分,已经被很多主流浏览器所支持,也有不少基于WebSocket开发的应用。正如名称所表示的一样,WebSocket使用的是套接字连接,基于TCP协议。使用WebSocket之后,实际上在服务端和客户端之间建立了一个套接字连接,可以进行双向的数据传输。WebSocket的功能很强大,使用起来也灵活,可以适用不同的场景。不过WebSocket技术也比较复杂,包括服务器和浏览器端的实现都不同于一般的Web应用。

  • 简易轮询:浏览器定时向服务器端发出请求,来查询数据是否有更新。这种做法简单,在一定程度上可以解决问题。不过对于轮询的时间间隔需要进行仔细考虑。轮询的间隔过长会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务端的负担。

  • COMET:改进了简易轮询的缺点,使用的是长轮询。长轮询的基本思想是在每次客户端发出请求后,服务器检查上次返回的数据与此次请求时的数据之间是否有更新,如果有更新则返回新数据并结束此次连接,否则服务器“hold”住此次连接,直到有新数据时再返回相应。而这种长时间的保持连接可以通过设置一个较大的HTTP timeout实现。这样做的好处是在连接处于打开状态的时间段内,服务端产生的数据更新可以被及时的返回给浏览器,当上一个长连接关闭之后,浏览器会打开一个新的长连接来继续请求。不过COMET技术的实现需要在服务端和浏览器端都需要第三方库的支持。

  • SSE:当使用服务器推送事件(SSE)进行通信的时候,服务器在应用需要数据的时候,并不需要初始化请求就能把数据推送给应用。换句话说,当更新需要进行的时候,它就从服务器端自动流向客户端。SSE在服务端和客户端打开了一个单方向的通道。

综合比较上面提到的4种不同的技术,简易轮询由于其本身的缺陷,并不推荐使用。COMET技术并不是HTML5标准的一部分,从兼容标准的角度出发,也不推荐使用。WebSocket规范和服务器推送技术都是HTML5标准的组成部分,在主流浏览器上都提供了原生的支持,是推荐使用的。不过WebSocket规范更加复杂一些,适用于需要进行复杂双向数据通讯的场景。对于简单的服务器数据推送的场景,使用服务器推送事件就足够了。

分享一个通(ji)俗(qi)易(dou)懂(bi)的介绍轮询和WebSocket的文章:知乎:WebSocket 是什么原理?为什么可以实现持久连接?

为什么选择SSE而不是WebSocket

SSE(服务器发送事件)总是处在不引人注目的阴暗地方的一个原因是因为像WebSocket这样的后来的应用提供了执行双向,双工通信的更丰富的协议。具有两个方向通道对哪些诸如游戏、消息应用以及需要几乎是双向实时更新的应用来说更具吸引力。然而在某些场景下,不需要从客户端发送数据。你只需要更新某些来自服务器的动作。有几个例子:朋友状态更新、股票行情更新、新闻推送或者其他自动数据推送机制。在我们的需求中引入WebSocket太重,并且只需要服务器向浏览器推数据就够了,所以选择使用服务器推送事件Server-sent Events而不是WebSocket。
业务决定需求,需求决定采用哪种技术,并不是哪种方案好或不好,在适合的场景选择最适合的技术就对了。

SSE客户端实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html>
<body>
<div>
server-sent events
</div>
<script type="text/javascript">
var source = new EventSource('http://www.example.com/api/message');
source.onmessage = function(e){
console.log(e.data);
};
</script>

</body>
</html>

SSE服务端实现(Node.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var http = require("http");

http.createServer(function (req, res) {

var fileName = "." + req.url;

if (fileName === "./stream") {
res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive"
});
res.write("retry: 10000\n");
res.write("event: connecttime\n");
res.write("data: " + (new Date()) + "\n\n");
res.write("data: " + (new Date()) + "\n\n");

interval = setInterval(function() {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);

req.connection.addListener("close", function() {
clearInterval(interval);
}, false);
}
}).listen(80, "127.0.0.1");

参考链接及更多细节