轮询、SSE、Web Socket

在阅读组内编码规范时,遇到了一个陌生的概念:SSE 。因而去查了下,它的全称是 Server-sent events 。这是一种实现服务端向客户端(Browser)“主动推送” 的技术,相似的技术还有轮询和 Web Socket。固然 SSE 和轮询同样它们实质上并非主动推送,只是使用一种很糙的方式实现了 “等价” 的效果。本文将几种技术做简单介绍和总结,重在介绍思想,API的细节仍是参考相关文档比较好。javascript

轮询

短轮询

短轮询的实现思路很简单,即每隔一段时间就向服务器端发送 Http 请求,我看到网上的文章基本都会给短轮询加上一条特性:服务端无论数据有没有更新都会当即返回响应。对于这条特性,我我的认为是不太准确的。我觉得短轮询的诞生与 Ajax 技术是强相关的,它是远古时代人们对网站实时性开始有了愈来愈高的要求后开始出现的一种经过牺牲服务端和客户端性能来提高实时性的方式。简单来讲就是重复发请求,至于服务端要不要先判断数据有没有更新,那是服务端的事情了,固然,也有多是我对短轮询的理解有误差。php

短轮询的客户端简单实现以下:html

var xhr = new XMLHttpRequest();
    setInterval(function(){
        xhr.open('GET','/user');
        xhr.onreadystatechange = function(){

        };
        xhr.send();
    },1000)
复制代码

短轮询的实现很简单,也好理解,可缺点也是显而易见的,对于一个基于短轮询的应用,若是同时有较大数量级的人在线,每一个用户的客户端都会不断的向服务器发送 http 请求,会给服务器带来巨大并发压力。java

所以,短轮询是难以控制的。它的实时性是由发送请求的间隔时间来控制的,这致使咱们很难在实时性与良好性能间达到能够接受的平衡,通常状况下只能将其用于对实时性要求不高,同时链接数较少的应用。node

长轮询

至于长轮询,和短轮询放在一块儿就很好理解,短轮询收到请求后返回响应、关闭链接。而咱们知道 TCP 协议是支持长链接的,在此之上的 HTTP1.1 支持持久链接。所以咱们能够在收到请求后 hold 住链接,等到服务端有消息推送时再返回响应关闭链接,这就是长轮询。react

也就是说当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,而后判断服务器端数据是否有更新。若是有更新,则进行响应,若是一直没有数据,则到达必定的时间限制才返回。 客户端脚本中的响应处理函数会在处理完服务器返回的信息后,再次发出请求,从新创建链接。android

长轮询和短轮询比起来,明显减小了不少没必要要的http请求次数,相比之下节约了资源。长轮询的缺点在于,链接挂起也会致使资源的浪费。git

客户端示例:github

async function subscribe() {
  let response = await fetch("/subscribe");

  if (response.status == 502) {
    // 状态 502 是链接超时错误,
    // 链接挂起时间过长时可能会发生,
    // 远程服务器或代理会关闭它
    // 让咱们从新链接
    await subscribe();
  } else if (response.status != 200) {
    // 一个 error —— 让咱们显示它
    showMessage(response.statusText);
    // 一秒后从新链接
    await new Promise(resolve => setTimeout(resolve, 1000));
    await subscribe();
  } else {
    // 获取并显示消息
    let message = await response.text();
    showMessage(message);
    // 再次调用 subscribe() 以获取下一条消息
    await subscribe();
  }
}

subscribe();
复制代码

服务端示例:ajax

const Koa = require('koa');
const app = new Koa();

// response
app.use(async ctx => {
    let rel = await Promise.race([delay(1000 * 10), getRel(1000 * 5)]);
    ctx.body = rel;
});

function delay(ms) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('delayed');
        }, ms);
    });
}

function getRel(ms) {
    return new Promise(resolve => {
        let time = new Date();
        let it = setInterval(() => {
            if (Date.now() - time > ms) {
                clearInterval(it);
                resolve('gotRel');
            }
        }, 10);
    });
}

const port = 3000;

app.listen(port, err => {
    if (err) {
        console.error(`err: ${err}`);
    }
    console.log(`server start listening ${port}`);
});
复制代码

SSE

前面咱们已经知道 Http 协议没法作到服务端主动推送消息,可是有一种取巧的办法,就是让服务端向客户端发送的是流信息(Streaming)。即:

Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no
复制代码

使用 Cache-Control:no-transform 这一行是由于若是你用了create-react-app等工具来转发你的请求,那么你的数据流极可能被压缩,形成你怎么也收不到响应。

而加上 X-Accel-Buffering: no 这一行是由于若是网站使用Nginx 方向代理,默认会对应用的响应作缓冲(buffering),致使应用返回的消息不会立马发出去。这点在Ngnix 官网中也是有说明的:

Sets the proxy buffering for this connection. Setting this to “no” will allow unbuffered responses suitable for Comet and HTTP streaming applications. Setting this to “yes” will allow the response to be cached

一样,咱们将SSE 和 前面说的长轮询进行对比,SSE 的实现和长轮询是比较类似的,不一样之处在于 SSE 中 每一个链接不仅发送一个消息。客户端发送一个请求后,服务端会保持这个链接,即便有新消息发送回客户端,咱们仍然能够保持着链接,这样链接就能够消息的再次发送,由服务器单向发送给客户端。由于发送的不是一次性的数据包,而是一个数据流,会接二连三地发送过来,就像视频播放的数据流同样。

基本用法:

var evtSource = new EventSource(url);
复制代码

若是发送事件的脚本不一样源,应该建立一个新的包含URL和options参数的EventSource对象。例如,假设客户端脚本在example.com上:

const evtSource = new EventSource("//api.example.com/ssedemo.php", { withCredentials: true } );
复制代码

ps:在我实际测试中,浏览器对 EventSource 的跨域支持彷佛不是太好,不过也没继续深究了。

成功初始化一个事件源后,咱们就能够经过 addeventlistener 为不一样类型的事件添加监听函数或者将**事件分发(attach)**到对应属性上来监遵从服务器发出的消息。

客户端示例:

<body>
    <button id="btn">创建链接</button>
    <button id="btn2">关闭链接</button>
    <div id="result"></div>
    <script>
        var btn=document.querySelector("#btn");
        var btn2=document.querySelector("#btn2");
        var result=document.querySelector("#result");
        var source;
        btn.onclick=function () {
            source=new EventSource("http://localhost:8088/sse");
            source.addEventListener("open",function () {
                result.innerHTML+="创建链接<br/>";
            },false);
            source.addEventListener("connecttime",function (e) {
                result.innerHTML+="链接已创建:"+e.data+"<br/>";
            },false);
            source.addEventListener("message",function (e) {
                result.innerHTML+="接受更新时间:"+e.data+"<br/>";
            },false)
        };
        btn2.onclick=function () {
            if(source){
                source.close();
                result.innerHTML+="关闭链接<br/>";
            }
        }
    </script>
</body>
复制代码

服务端示例:

const onEvent = function(data) {
    res.write(`event: message\n`);
    res.write(`data: ${JSON.stringify(data)}\n\n`);
};

emitter.on('message', onEvent);
复制代码

咱们用\n来分隔每一行数据,用\n\n来分隔每个事件。每个事件中包含事件的type和事件的data,分别用两行来描述。好比上面是返回来一个message事件(若不指定事件类型,则默认message)。

而Koa 官网给出的示例是这样的:

var Transform = require('stream').Transform;
var inherits = require('util').inherits;

module.exports = SSE;

inherits(SSE, Transform);

function SSE(options) {
  if (!(this instanceof SSE)) return new SSE(options);

  options = options || {};
  Transform.call(this, options);
}

SSE.prototype._transform = function(data, enc, cb) {
  this.push('data: ' + data.toString('utf8') + '\n\n');
  cb();
};
复制代码

而后咱们将 SSE() 赋给 ctx.body 便可,要注意的一点是:

全部转换流的实现都必须提供 _transform() 方法来接收输入并生产输出。 transform._transform() 的实现会处理写入的字节,进行一些计算操做,而后使用 transform.push() 输出到可读流。

Web Socket

不一样与上面几种,WebSocket是一种在单个 TCP 链接上进行全双工通讯的协议,是 Http 协议的一个补充(借用 Http 协议来完成一部分握手)。Web Socket 通讯协议于2011年被IETF定为标准RFC 6455,并由 RFC7936 补充规范。WebSocket API也被 W3C 定为标准。

WebSocket使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只须要完成一次握手,二者之间就直接能够建立持久性的链接,并进行双向数据传输。

客户端示例:

const socket = new WebSocket('ws://localhost:8080');

// Connection opened
socket.addEventListener('open', function (event) {
    socket.send('Hello Server!');
});

// Listen for messages
socket.addEventListener('message', function (event) {
    console.log('Message from server ', event.data);
});
复制代码

Web Socket 服务端的实现就没 SSE 那么好搞了。。。不过仍是有相关框架帮咱们作了些封装:Socket.IO ,这是一个基于 Web Socket 的 Node.js 实时应用程序框架,而且对不支持 Web Socket 的浏览器降级成 comet / ajax 轮询。

另外 Web Socket 是二进制协议,通用性更好,而 SSE 是文本协议(一般使用UTF-8编码),固然了,你也能够经过转码使其能传输二进制数据。

其余

iframe 永久帧

iframe永久帧也是一种实现服务端推送的方式,很是 hack,也很是不实用。其作法就是在页面嵌入一个专用来接受数据的 iframe 页面,该页面由服务器注入相关信息,如 <script>parent.utils.exec("response")</script>

服务器不停的向iframe中写入相似的 script 标签和数据,实现另外一种形式的服务端推送。不过永久帧的技术会致使主页面的加载条始终处于加载状态,体验不好。

主动推送的困境

固然,实际的主动推送场景也许要复杂的多,好比如下两个问题:

  • 移动端如何维持稳定的长链接?
  • 多端场景如何保证推送同步?

实在知识盲区,不过看到了一些解决方案。好比这个:百度云推送

参考

相关文章
相关标签/搜索