本文来自“糊糊糊糊糊了”的分享,原题《实时消息推送整理》,有优化和改动。javascript
对Web端即时通信技术熟悉的开发者来讲,咱们回顾网页端IM的底层通讯技术,从短轮询、长轮询,到后来的SSE以及WebSocket,使用门槛愈来愈低(早期的长轮询Comet这类技术实际属于hack手段,使用门槛并不低),技术手段愈来愈先进,网页端即时通信技术的体验也所以愈来愈好。html
但上周在编辑《IM扫码登陆技术专题》系列文章第3篇的时候突然想到,以前的这些所谓的网页端即时通信“老技术”相对于当红的WebSocket,并不是毫无用武之地。就拿IM里的扫码登陆功能来讲,用短轮询技术就很是合适,彻底不必大炮打蚊子上WebSocket。java
因此,不少时候不必盲目追求新技术,相对应用场景来讲适合的才是最好的。对于即时通信网的im和消息推送这类即时通信技术开发者来讲,掌握WebSocket当然很重要,但了解短轮询、长轮询等这些所谓的Web端即时通信“老技术”仍然大有裨益,这也正是整理分享本文的重要缘由。git
学习交流:github
即时通信/推送技术开发交流5群:215477170 [推荐]
移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
开源IM框架源码: https://github.com/JackJiang2...
(本文同步发布于:http://www.52im.net/thread-35...web
[1] 新手入门贴:史上最全Web端即时通信技术原理详解
[2] 详解Web端通讯方式的演进:从Ajax、JSONP 到 SSE、Websocket
[3] Web端即时通信技术盘点:短轮询、Comet、Websocket、SSEexpress
对于IM/消息推送这类即时通信系统而言,系统的关键就是“实时通讯”能力。json
从表面意思上来看,“实时通讯”指的是:小程序
1)客户端能随时主动发送数据给服务端;
2)当客户端关注的内容在发生改变时,服务器可以实时地通知客户端。
类比于传统的C/S请求模型,“实时通讯”时客户端不须要主观地发送请求去获取本身关心的内容,而是由服务器端进行“推送”。微信小程序
注意:上面的“推送”二字打了引号,实际上现有的几种技术实现方式中,并非服务器端真正主动地推送,而是经过必定的手段营造了一种“实时通讯”的假象。
就目前现有的几种技术而言,主要有如下几类:
1)客户端轮询:传统意义上的短轮询(Short Polling);
2)服务器端轮询:长轮询(Long Polling);
3)单向服务器推送:Server-Sent Events(SSE);
4)全双工通讯:WebSocket。
如下正文将针对这几种技术方案,为你一一解惑。
为了帮助读者更好的理解本文内容,笔者专门写了一个较完整的Demo,Demo会以一个简易聊天室的例子来分别经过上述的四种技术方式实现(代码存在些许bug,主要是为了作演示用,别介意)。
完整Demo源码打包下载:
(请从同步连接附件中下载:http://www.52im.net/thread-35...
Demo的运行效果(动图):
有兴趣能够自行下载研究学习。
短轮询的实现原理:
1)客户端向服务器端发送一个请求,服务器返回数据,而后客户端根据服务器端返回的数据进行处理;
2)客户端继续向服务器端发送请求,继续重复以上的步骤,若是不想给服务器端太大的压力,通常状况下会设置一个请求的时间间隔。
逻辑以下图所示:
使用短轮询的优势:基础不须要额外的开发成本,请求数据,解析数据,做出响应,仅此而已,而后不断重复。
缺点也显而易见:
1)不断的发送和关闭请求,对服务器的压力会比较大,由于自己开启Http链接就是一件比较耗资源的事情;
2)轮询的时间间隔很差控制。若是要求的实时性比较高,显然使用短轮询会有明显的短板,若是设置interval的间隔过长,会致使消息延迟,而若是过短,会对服务器产生压力。
短轮询客户的代码实现(片断节选):
var ShortPollingNotification = {
datasInterval: null,
subscribe: function() {
this.datasInterval = setInterval(function() { Request.getDatas().then(function(res) { window.ChatroomDOM.renderData(res); }); }, TIMEOUT); return this.unsubscribe;
},
unsubscribe: function() {
this.datasInterval && clearInterval(this.datasInterval);
}
}
PS:完整代码,请见本文“四、本文配套Demo和代码”一节。
对应本文配套Demo的运行效果以下(动图):
下面是对应的请求,注意左下角的请求数量一直在变化:
在上图中,每隔1s就会发送一个请求,看起来效果还不错,可是若是将timeout的值设置成5s,效果将大打折扣。以下图所示。
将timeout值设置成5s时的Demo运行效果(动图):
6.1 基本原理
长轮询的基本原理:
1)客户端发送一个请求,服务器会hold住这个请求;
2)直到监听的内容有改变,才会返回数据,断开链接(或者在必定的时间内,请求还得不到返回,就会由于超时自动断开链接);
3)客户端继续发送请求,重复以上步骤。
逻辑以下图所示:
长轮询是基于短轮询上的改进版本:主要是减小了客户端发起Http链接的开销,改为了在服务器端主动地去判断所关心的内容是否变化。
因此其实轮询的本质并无多大变化,变化的点在于:
1)对于内容变化的轮询由客户端改为了服务器端(客户端会在链接中断以后,会再次发送请求,对比短轮询来讲,大大减小了发起链接的次数);
2)客户端只会在数据改变时去做相应的改变,对比短轮询来讲,并非全盘接收。
6.2 代码实现
长轮询客户的代码实现(片断节选):
// 客户端
var LongPollingNotification = {
// .... subscribe: function() { var that = this; // 设置超时时间 Request.getV2Datas(this.getKey(),{ timeout: 10000 }).then(function(res) { var data = res.data; window.ChatroomDOM.renderData(res); // 成功获取数据后会再次发送请求 that.subscribe(); }).catch(function(error) { // timeout 以后也会再次发送请求 that.subscribe(); }); return this.unsubscribe; } // ....
}
笔者采用的是express,默认不支持hold住请求,所以用了一个express-longpoll的库来实现。
下面是一个原生不用库的实现(这里只是介绍原理),总体的思路是:若是服务器端支持hold住请求的话,那么在必定的时间内会自轮询,而后期间经过比较key值,判断是否返回新数据。
如下是具体思路:
1)客户端第一次会带一个空的key值,此次会当即返回,获取新内容,服务器端将计算出的contentKey返回给客户端;
2)而后客户端发送第二次请求,带上第一次返回的contentKey做为key值,而后进行下一轮的比较;
3)若是两次的key值相同,就会hold请求,进行内部轮询,若是期间有新内容或者客户端timeout,就会断开链接;
4)重复以上步骤。
代码以下:
// 服务器端
router.get('/v2/datas', function(req, res) {
const key = _.get(req.query, 'key', '');
let contentKey = chatRoom.getContentKey();
while(key === contentKey) {
sleep.sleep(5); contentKey = chatRoom.getContentKey();
}
const connectors = chatRoom.getConnectors();
const messages = chatRoom.getMessages();
res.json({
code: 200, data: { connectors: connectors, messages: messages, key: contentKey },
});
});
如下是用 express-longpoll的实现片断:
// mini-chatroom/public/javascripts/server/longPolling.js
function pushDataToClient(key, longpoll) {
var contentKey = chatRoom.getContentKey();
if(key !== contentKey) {
var connectors = chatRoom.getConnectors(); var messages = chatRoom.getMessages(); long poll.publish( '/v2/datas', { code: 200, data: {connectors: connectors, messages: messages, key: contentKey}, } );
}
}
long poll.create("/v2/datas", function(req, res, next) {
key = _.get(req.query, 'key', '');
pushDataToClient(key, longpoll);
next();
});
intervalId = setInterval(function() {
pushDataToClient(key, longpoll);
}, LONG_POLLING_TIMEOUT);
PS:完整代码,请见本文“四、本文配套Demo和代码”一节。
为了方便演示,我将客户端发起请求的timeout改为了4s,注意观察下面的截图:
能够看到,断开链接的两种方式,要么是超时,要么是请求有数据返回。
6.3 基于iframe的长轮询模式
这是长轮询技术的另外一个种实现方案。
该方案的具体的原理为:
1)在页面中嵌入一个iframe,地址指向轮询的服务器地址,而后在父页面中放置一个执行函数,好比execute(data);
2)当服务器有内容改变时,会向iframe发送一个脚本<script>parent.execute(JSON.stringify(data))</script>;
3)经过发送的脚本,主动执行父页面中的方法,达到推送的效果。
因不篇幅缘由,在此不做深刻介绍,有兴趣的同窗能够详读《新手入门贴:史上最全Web端即时通信技术原理详解》一文中的“3.3.2 基于iframe的数据流”一节。
7.1 基本介绍
从纯技术的角度讲:上两节介绍的短轮询和长轮询技术,服务器端是没法主动给客户端推送消息的,都是客户端主动去请求服务器端获取最新的数据。
本节要介绍的SSE是一种能够主动从服务端推送消息的技术。
SSE的本质其实就是一个HTTP的长链接,只不过它给客户端发送的不是一次性的数据包,而是一个stream流,格式为text/event-stream。因此客户端不会关闭链接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。
简单来讲,SSE就是:
1)SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
2)SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
3)SSE 默认支持断线重连,WebSocket 须要本身实现。
4)SSE 通常只用来传送文本,二进制数据须要编码后传送,WebSocket 默认支持传送二进制数据。
5)SSE 支持自定义发送的消息类型。
SSE的技术原理以下图所示:
SSE基本的使用方法,能够参看 SSE 的API文档,地址是:https://developer.mozilla.org/en ... _server-sent_events。
目前除了IE以及低版本的浏览器不支持,绝大多数的现代浏览器都支持SSE:
(上图来自:https://caniuse.com/?search=S...
7.2 代码实现
// 客户端
var SSENotification = {
source: null,
subscribe: function() {
if('EventSource'inwindow) { this.source = newEventSource('/sse'); this.source.addEventListener('message', function(res) { const d = res.data; window.ChatroomDOM.renderData(JSON.parse(d)); }); } return this.unsubscribe;
},
unsubscribe: function() {
this.source && this.source.close();
}
}
// 服务器端
router.get('/sse', function(req, res) {
const connectors = chatRoom.getConnectors();
const messages = chatRoom.getMessages();
const response = { code: 200, data: { connectors: connectors, messages: messages } };
res.writeHead(200, {
"Content-Type":"text/event-stream", "Cache-Control":"no-cache", "Connection":"keep-alive", "Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n");
res.write("data: "+ JSON.stringify(response) + "\n\n");
var unsubscribe = Event.subscribe(function() {
const connectors = chatRoom.getConnectors(); const messages = chatRoom.getMessages(); const response = { code: 200, data: { connectors: connectors, messages: messages } }; res.write("data: "+ JSON.stringify(response) + "\n\n");
});
req.connection.addListener("close", function() {
unsubscribe();
}, false);
});
下面是控制台的状况,注意观察响应类型:
详情中注意查看请求类型,以及EventStream消息类型:
PS:有关SSE更详尽的资料就不在这里展开了,有兴趣的同窗能够详读《SSE技术详解:一种全新的HTML5服务器推送事件技术》、《使用WebSocket和SSE技术实现Web端消息推送》。
8.1 基本介绍
PS:本小节内容引用自《Web端即时通信实践干货:如何让WebSocket断网重连更快速?》一文的“三、快速了解WebSocket”。
WebSocket诞生于2008年,在2011年成为国际标准,如今全部的浏览器都已支持(详见《新手快速入门:WebSocket简明教程》)。它是一种全新的应用层协议,是专门为web客户端和服务端设计的真正的全双工通讯协议,能够类比HTTP协议来了解websocket协议。
(图片引用自《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》)
它们的不一样点:
1)HTTP的协议标识符是http,WebSocket的是ws;
2)HTTP请求只能由客户端发起,服务器没法主动向客户端推送消息,而WebSocket能够;
3)HTTP请求有同源限制,不一样源之间通讯须要跨域,而WebSocket没有同源限制。
它们的相同点:
1)都是应用层的通讯协议;
2)默认端口同样,都是80或443;
3)均可以用于浏览器和服务器间的通讯;
4)都基于TCP协议。
二者和TCP的关系图:
(图片引用自《新手快速入门:WebSocket简明教程》)
有关Http和WebSocket的关系,能够详读:
《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》
《WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)》
有关WebSocket和Socket的关系,能够详读:《WebSocket详解(六):刨根问底WebSocket与Socket的关系》.
8.2 技术特征
WebSocket技术特征总结下就是:
1)可双向通讯,设计的目的主要是为了减小传统轮询时http链接数量的开销;
2)创建在TCP协议之上,握手阶段采用 HTTP 协议,所以握手时不容易屏蔽,能经过各类 HTTP 代理服务器;
3)与HTTP兼容性良好,一样可使用80和443端口;
4)没有同源限制,客户端能够与任意服务器通讯;
5)能够发送文本,也能够发送二进制数据;
6)协议标识符是ws(若是加密,则为wss),服务器网址就是 URL.
WebSocket的技术原理以下图所示:
关于WebSocket API方面的知识,这里再也不做讲解,能够本身查阅:https://developer.mozilla.org...
8.3 浏览器兼容性
WebSocket兼容性良好,基本支持全部现代浏览器。
(上图来自:https://caniuse.com/mdn-api_w...
8.4 代码实现
笔者这里采用的是socket.io,是基于WebSocket的封装,提供了客户端以及服务器端的支持。
// 客户端
var WebsocketNotification = {
// ...
subscribe: function(args) {
var connector = args[1]; this.socket = io(); this.socket.emit('register', connector); this.socket.on('register done', function() { window.ChatroomDOM.renderAfterRegister(); }); this.socket.on('data', function(res) { window.ChatroomDOM.renderData(res); }); this.socket.on('disconnect', function() { window.ChatroomDOM.renderAfterLogout(); });
}
// ...
}
// 服务器端
var io = socketIo(httpServer);
io.on('connection', (socket) => {
socket.on('register', function(connector) {
chatRoom.onConnect(connector); io.emit('register done'); var data = chatRoom.getDatas(); io.emit('data', { data });
});
socket.on('chat', function(message) {
chatRoom.receive(message); var data = chatRoom.getDatas(); io.emit('data', { data });
});
});
PS:完整代码,请见本文“四、本文配套Demo和代码”一节。
响应格式以下:
8.5 深刻学习
随着HTML5的普及率愈来愈高,WebSocket的应用也愈来愈普及,关于WebSocket的学习资料网上很容易找到,限于篇幅本文就不深刻展开这个话题。
若是想进一步深刻学习WebSocket的方方面面,如下文章值得一读:
《新手快速入门:WebSocket简明教程》
《WebSocket详解(一):初步认识WebSocket技术》
《WebSocket详解(二):技术原理、代码演示和应用案例》
《WebSocket详解(三):深刻WebSocket通讯协议细节》
《WebSocket详解(四):刨根问底HTTP与WebSocket的关系(上篇)》
《WebSocket详解(五):刨根问底HTTP与WebSocket的关系(下篇)》
《WebSocket详解(六):刨根问底WebSocket与Socket的关系》
《理论联系实际:从零理解WebSocket的通讯原理、协议格式、安全性》
《微信小程序中如何使用WebSocket实现长链接(含完整源码)》
《八问WebSocket协议:为你快速解答WebSocket热门疑问》
《Web端即时通信实践干货:如何让你的WebSocket断网重连更快速?》
《WebSocket从入门到精通,半小时就够!》
《WebSocket硬核入门:200行代码,教你徒手撸一个WebSocket服务器》
《长链接网关技术专题(四):爱奇艺WebSocket实时推送网关技术实践》
短轮询、长轮询实现成本相对比较简单,适用于一些实时性要求不高的消息推送,在实时性要求高的场景下,会存在延迟以及会给服务器带来更大的压力。
SSE只能是服务器端推送消息,所以对于不须要双向通讯的项目比较适用。
WebSocket目前而言实现成本相对较低,适合于双工通讯,对于多人在线,要求实时性较高的项目比较实用。
本文已同步发布于“即时通信技术圈”公众号。
▲ 本文在公众号上的连接是:点此进入。同步发布连接是:http://www.52im.net/thread-35...