分不清轮询、长轮询?不知道何时该用websocket仍是SSE,看这篇就够了。javascript
所谓的“实时推送”,从表面意思上来看是,客户端订阅的内容在发生改变时,服务器可以实时地通知客户端,进而客户端进行相应地反应。客户端不须要主观地发送请求去获取本身关心的内容,而是由服务器端进行“推送”。html
注意上面的推送二字打了引号,这就意味着在现有的几种实现方式中,并非服务器端主动地推送,而是经过必定的手段营造了一种实时的假象。就目前现有的几种技术而言,主要有如下几类:java
文中会以一个简易聊天室的例子来分别经过上述的四种方式实现,代码地址mini-chatroom(存在些许bug,主要是为了作演示用)git
轮询的实现原理:客户端向服务器端发送一个请求,服务器返回数据,而后客户端根据服务器端返回的数据进行处理;而后客户端继续向服务器端发送请求,继续重复以上的步骤,若是不想给服务器端太大的压力,通常状况下会设置一个请求的时间间隔。github
使用轮询明显的优势是基础不须要额外的开发成本,请求数据,解析数据,做出响应,仅此而已,而后不断重复。缺点也显而易见:web
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);
}
}
复制代码
下面是对应的请求,注意左下角的请求数量一直在变化express
在上图中,每隔1s就会发送一个请求,看起来效果还不错,可是若是将timeout的值设置成5s,效果将大打折扣,如图:npm
长轮询的基本原理:客户端发送一个请求,服务器会hold住这个请求,直到监听的内容有改变,才会返回数据,断开链接,客户端继续发送请求,重复以上步骤。或者在必定的时间内,请求还得不到返回,就会由于超时自动断开链接。json
长轮询是基于轮询上的改进版本,主要是减小了客户端发起Http链接的开销,改为了在服务器端主动地去判断所关心的内容是否变化,因此其实轮询的本质并无多大变化,变化的点在于:api
// 客户端
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值,判断是否返回新数据
// 服务器端
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();
longpoll.publish(
'/v2/datas',
{
code: 200,
data: {connectors: connectors, messages: messages, key: contentKey},
}
);
}
}
longpoll.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);
复制代码
为了方便演示,我将客户端发起请求的timeout改为了4s,注意观察下面的截图:
能够看到,断开链接的两种方式,要么是超时,要么是请求有数据返回。
这种模式的具体的原理为:
execute(data)
<script>parent.execute(JSON.stringify(data))</script>
具体能够参看这里
The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code.
The protocol consists of an opening handshake followed by basic message framing, layered over TCP.
The goal of this technology is to provide a mechanism for browser-based applications that need two-way communication with servers that does not rely on opening multiple HTTP connections (e.g., using XMLHttpRequest or iframe and long polling).
The WebSocket Protocol attempts to address the goals of existing bidirectional HTTP technologies in the context of the existing HTTP infrastructure; as such, it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries, even if this implies some complexity specific to the current environment.
ws
(若是加密,则为wss
),服务器网址就是 URL关于Websocket API方面的知识,这里再也不做讲解,能够本身查阅Websocket API MDN
websocket兼容性良好,基本支持全部现代浏览器
笔者这里采用的是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 });
});
});
复制代码
响应格式以下:
传统意义上服务器端不会主动推送给客户端消息,通常都是客户端主动去请求服务器端获取最新的数据。SSE就是一种能够主动从服务端推送消息的技术。
SSE的本质其实就是一个HTTP的长链接,只不过它给客户端发送的不是一次性的数据包,而是一个stream流,格式为text/event-stream,因此客户端不会关闭链接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。
- SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
- SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
- SSE 默认支持断线重连,WebSocket 须要本身实现。
- SSE 通常只用来传送文本,二进制数据须要编码后传送,WebSocket 默认支持传送二进制数据。
- SSE 支持自定义发送的消息类型。
基本的使用方法,参看SSE API
目前除了IE以及低版本的浏览器不支持,基本支持绝大多数的现代浏览器。
// 客户端
var SSENotification = {
source: null,
subscribe: function() {
if ('EventSource' in window) {
this.source = new EventSource('/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消息类型