前段时间,完成了项目组分配的一个任务——数据实时大屏,即把商场销售、车流、客流和人流等数据展示到页面上。本次项目采用的是定时轮询的方式去查询数据,总结一下,存在的问题:十分钟执行的定时任务,有时候定时任务尚未执行完成,这时若有前端查询请求,实际上这是无效的查询请求。当然大屏的用户量较少,定时轮询获取数据并无太大问题,但是如果是百万甚至千万的用户,采用定时轮询问题就来了。所以,我根据已有的经验,总结了一下前端如果及时获取后端数据的方法,记录一下,以便将来针对不同的业务场景可以快速的进行技术选型。下面主要对四种方法进行分析和总结:
1、短轮询。2、长轮询,3、长连接SSE。4、websocket。并对websocket进行重点分析,如有不当之处,欢迎各位大神进行纠正。
短轮询原理比较简单,客户端按照一定的频率定时向后台服务器发送请求,服务器接收到请求后,进行响应返回数据给客户端,通常采取setInterval实现。
什么是短轮询?用通俗易懂的话举例解释,小张在看直播的时候,看到一个自己十分心仪的女主播小红,于是小张每十分钟就向女主播发一句问候语:“小姐姐,我要刷个游艇吗”,女主播一接到消息就回复他:“小哥哥,你真帅”,下一次回复他:“小哥哥,可以多发几个游艇吗”。
//短轮询伪代码
var xhr = new XMLHttpRequest();
setInterval(function () {
xhr.open('GET', '/sendGifToZB');
xhr.onreadystatechange = function () {
};
xhr.send();
}, 60000)
短轮询的优点:
轮询适用于那些同时在线用户数量比较少,对数据及时性要求不高,并且不特别注重性能或低版本浏览器的B/S应用。
客户端向服务端发起请求,如果服务器没有可以返回的数据,不会立刻返回一个空结果,而是保持这个连接,一直等待数据,一旦有数据,便将数据作为结果返回给客户端。
什么是长轮询?用通俗易懂的话举例解释,经过了几轮的直播发礼物之后,小红成了小张的女神。小张便给小红发消息:“你现在在干嘛”,小张心里满怀着期待,等啊等,一晚上之后,女神回复他:“昨晚我睡着了”,此刻,小张已经兴奋得无法用言语来形容,马上就说:“你现在在干嘛呀”。又过了一个钟,女神回复他:“我刚吃完早饭”,…
//长轮询伪代码,小张(客户端)向后小红(服务端)轮询消息
function getMessagesFromNS() {
$.ajax({
async: true,//异步
url: '/getMessagesFromNS',
type: 'post',
dataType: 'json',
data: {
question: "nv shen,what are you doing now?"
},
timeout: 30000,//超时时间设定30秒
error: function (xhr, textStatus, thrownError) {
getMessagesFromNS();//发生异常错误后再次发起请求
},
success: function (response) {
if (message != "timeout") {
//收到消息后置处理
}
//继续问女神在干嘛(请求客服端数据)
getMessagesFromNS();
}
});
}
轮询适用于那些同时在线用户数量比较小,对数据及时性要求不高,并且不特别注重性能或低版本浏览器的B/S应用。
SSE是Server-sent Event的简写,是一种服务器端到客户端的单向消息推送。对应的浏览器端实现 Event Source 的接口被制定为HTML5 的一部分。SSE与长轮询机制类似,客户端向服务器发送一个请求,服务端会一直保持着连接,通过这个连接就可以让消息再次发送,由服务器单向发送给客户端。与长轮询的区别是,长轮询服务端发消息给客户端后,双方的连接就断了,需要客户端重新发起一个连接请求,一次连接接收一次消息。而SSE一次连接可以接收多次消息。
什么是SSE?用通俗易懂的话举例解释,小张经过不懈的努力,小张终于熬成了恋爱候选对象,小张便对女神小红说:“女神,你那边有什么需求吗,可以尽管提,我一定办到”。女神这时候心里有了一丝丝触动,于是回复小张说:“我要买一部手机”,两天后又跟小张说:“我要去马尔代夫旅游”,…
//客户端
//创建EventSource 实例
var source = new EventSource(url)
// 建立连接后,触发`open` 事件
source.onopen = (event) => {
// ...
}
// 收到消息,触发`message` 事件
source.onmessage = (event) => {
// ...
}
// 发生错误,触发`error` 事件
source.onerror = (event) => {
// ...
}
// 自定义事件
source.addEventListener('eventName', event => {
// ...
}, false)
source.close()
//服务端
//SSE的相应,需要设置如下的Http头信息
Content - Type: text / event - stream
Cache - Control: no - cache
Connection: keep - alive
适用于对数据及时性有要求,服务端向客户端单向推送数据的场景。
websocket是html5规范中的一个部分,它借鉴了socket这种思想,为web应用程序客户端和服务端之间提供了一种全双工通信机制。同时,它又是一种新的应用层协议,websocket协议是为了提供web应用程序和服务端全双工通信而专门制定的一种应用层协议,通常它表示为:ws://echo.websocket.org/?encoding=text HTTP/1.1,可以看到除了前面的协议名和http不同之外,它的表示地址就是传统的url地址。Websocket其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它是HTTP协议上的一种补充,或者说借用了http的握手功能实现初始连接。
什么是websocket?用通俗易懂的话举例解释,小张经过不懈的努力,终于和小红成为了情侣,从此跟小红过上了幸福的生活,小红对小张也不再不冷不热了,双方都会主动发起聊天对话,小红经常问小张:“我们去吃什么”,“去哪里玩”…小张也会跟小红说:“你在干嘛”,“多喝热水”…
//websocket 客户端伪代码
var ws = new WebSocket("ws://nvshen:520");
ws.onopen = function () {
ws.send("dring more hot water");
};
ws.onmessage = function (e) {
console.log(e.data);
};
ws.onclose = function () {
console.log("closed...");
};
ws.onerror = function () {
console.log(this.readyState);
}
适用于对数据有及时性要求,服务端和客户端双工通信的场景,当然,也可以使用websocket进行单向的数据推送。
websocket底层原理可以简要的概括为三个阶段:1、握手阶段。2、数据交换阶段。3、关闭阶段。
所以,握手阶段WebSocket 首先发起一个 HTTP 请求,在请求头加上 Upgrade 字段,该字段用于改变 HTTP 协议版本或者是换用其他协议,把 Upgrade 的值设为 websocket ,即将它升级为 WebSocket 协议。
Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Version、Sec-WebSocket-Extension 几个属性是 WebSocket 的核心。
Upgrade、Connection: websocket属性通知 Apache 、 Nginx 等服务器,此次发起的请求要用 WebSocket 协议,而不是http或其他协议。
Sec-WebSocket-Key : 用于验证服务器端是否采用WebSocket 协议。由客户端生成并发给服务端,用于证明服务端接收到的是一个可受信的连接握手,可以帮助服务端排除自身接收到的由非 WebSocket 客户端发起的连接,该值是一串随机经过 base64 编码的字符串。
Sec-WebSocket-Version: 表示客户端所使用的协议版本。
Sec-WebSocket-Extensions: 表示客户端想要表达的协议级的扩展。
客户端浏览器会生成一个随机字符串(sec-websocket-key),自己留一份,然后基于http协议将随机字符串放在请求头中发送给服务端。
1.
服务端收到随机字符串后会和服务端的魔法字符串(magic string)(魔法字符串是全球公认的)做一个拼接生成一个大的字符串,然后再用全球公认的算法(sha1+base64)进行加密,生成一个密文,接着将这个密文返回给客户端浏览器。
服务端通过从客户端请求头中读取 Sec-WebSocket-Key 与一串全局唯一的标识字符串(俗称魔串)“258EAFA5-E914-47DA- 95CA-C5AB0DC85B11”做拼接,生成长度为160位的 SHA-1 字符串,然后进行 base64 编码,作为 Sec-WebSocket-Accept 的值回传给客户端。
Connection和Upgrade: 与请求头中的作用相同
Sec-WebSocket-Accept: 表明服务器接受了客户端的请求。
数据交换阶段
Websocket 的数据传输是以frame 形式传输的,我们先看一下frame的数据结构:
按照RFC中的描述:
FIN: 1 bit
表示这是一个消息的最后的一帧。第一个帧也可能是最后一个。
%x0 : 还有后续帧
%x1 : 最后一帧
RSV1、2、3: 1 bit each
除非一个扩展经过协商赋予了非零值以某种含义,否则必须为0。
如果没有定义非零值,并且收到了非零的RSV,则websocket链接会失败。
Opcode: 4 bit
解释说明 “Payload data” 的用途/功能
如果收到了未知的opcode,最后会断开链接
定义了以下几个opcode值:
%x0 : 代表连续的帧
%x1 : text帧
%x2 : binary帧
%x3-7 : 为非控制帧而预留的
%x8 : 关闭握手帧
%x9 : ping帧
%xA : pong帧
%xB-F : 为非控制帧而预留的
Mask: 1 bit
定义“payload data”是否被添加掩码,
如果置1, “Masking-key”就会被赋值,
所有从客户端发往服务器的帧都会被置1。
Payload length: 7 bit | 7+16 bit | 7+64 bit
“payload data” 的长度如果在0~125 bytes范围内,它就是“payload length”,
如果是126 bytes, 紧随其后的被表示为16 bits的2 bytes无符号整型就是“payload length”,
如果是127 bytes, 紧随其后的被表示为64 bits的8 bytes无符号整型就是“payload length”。
Masking-key: 0 or 4 bytes
所有从客户端发送到服务器的帧都包含一个32 bits的掩码(如果“mask bit”被设置成1),否则为0 bit。一旦掩码被设置,所有接收到的payload data都必须与该值以一种算法做异或运算来获取真实值。
Payload data: (x+y) bytes
它是"Extension data"和"Application data"的总和,一般扩展数据为空。
Extension data: x bytes
除非扩展被定义,否则就是0。
任何扩展必须指定其Extension data的长度。
Application data: y bytes
占据"Extension data"之后的剩余帧的空间。
客户端浏览器向服务端发送消息,但是发过来的数据是在浏览器内部进行加密过的密文,服务端收到密文后,会进行解密。主要步骤如下:
Websocket 的数据以frame数据格式,按照先后顺序传输出去。这样做的好处:
大数据的传输可以分片传输,避免长度标志位不足够的情况。
1.
生成数据边传递消息,传输效率得到了极大的提高。
关闭阶段
如果TCP连接在Close handshake完成之后关闭,就表示WebSocket连接已经彻底关闭了。如果WebSocket连接并未成功建立,状态也为连接已关闭,但并不是彻底关闭。
正常关闭过程属于clean close,应当包含close handshake。
通常来讲,应该由服务器关闭底层TCP连接,而客户端应该等待服务器关闭连接,除非等待超时的话,那么自己关闭底层TCP连接。
服务器可以随时关闭WebSocket连接,而客户端不可以主动断开连接。
由于某种算法或规范要求指定连接失败。这时,客户端和服务器必须关闭WebSocket连接。当一端得知连接失败时,不准再处理数据,包括响应close frame。
websocket 在使用过程中,最担心的问题就是:如果遭遇网络问题等,这个时候服务端没有触发onclose事件,这样会产生多余的连接,并且服务端会继续发送消息给客户端,造成数据丢失。需要一种机制来检测客户端和服务端是否处于正常连接的状态,因此,心跳检测和重连机制就产生了。
实现思路:
1、定时器,每隔一段指定的时间,向服务器发送一个数据,服务器收到数据后再发送给客户端,如果客户端通过onmessage事件能监听到服务器返回的数据,那么请求正常。
2、如果指定时间内,客户端没有收到服务器端返回的响应消息,判定连接断开,使用websocket.close关闭连接。
3、关闭连接的动作可以通过onclose事件监听到,因此在 onclose 事件内,我们可以调用reconnect事件进行重连。
以下对心跳检测和重连进行了封装:
/** * websocket心跳检测和重连封装 * * @Author:Fannie * @Date:2021年7月25日 */
class Heart {
//心跳计时器
heartTimeout = 0;
//心跳计时器
serverHeartTimeout = 0;
//间隔时间
timeout: number;
/** *构造方法 */
constructor() {
//设置间隔时间5s
this.timeout = 5000;
}
//重置
reset() {
clearTimeout(this.heartTimeout);
clearTimeout(this.serverHeartTimeout);
return this;
}
//启动心跳
start(cb: CallableFunction): void {
this.heartTimeout = setTimeout(() => {
cb();
this.serverHeartTimeout = setTimeout(() => {
cb();
//重新开始检测
this.reset().start(cb());
}, this.timeout);
}, this.timeout);
}
}
//配置参数
interface options {
url: string;
hearTime?: number;//心跳时间间隔
heartMsg?: string;//心跳信息
isReconnect?: boolean;//是否自动重连
isRestory?: boolean;//是否销毁
reconnectTime?: number;//重连时间间隔
reconnectCount?: number;//重连次数 -1 则不限制
openCb?: CallableFunction;//连接成功的回调
closeCb?: CallableFunction;//关闭的回调
messageCb?: CallableFunction;//消息的回调
errorCb?: CallableFunction;//错误的回调
}
/** * 编写Socket,继承Heart * **/
class Socket extends Heart {
ws!: WebSocket;
reConnecTimer!: number;//重连计时器
// reConnecCount = 10;//变量保存,防止丢失
options: options = {
url: "",
hearTime: 0,
heartMsg: "ping",//心跳信息
isReconnect: true,//是否自动重连
isRestory: false,//是否销毁
reconnectTime: 5000,//重连时间间隔
reconnectCount: -1,//重连次数 -1 则不限制
openCb: (event: Event) => { console.log("连接成功" + event) },
closeCb: (event: Event) => { console.log("连接关闭" + event) },
messageCb: (data: string) => { console.log("接收消息为:" + data) },
errorCb: (event: Event) => { console.log("错误信息" + event) }
};
constructor(ops: options) {
super();
this.create();
}
//建立连接
create() {
//浏览器兼容性判读
if (!("WebSocket" in window)) {
new Error("当前浏览器不支持,无法使用");
return;
}
//服务端地址判断
if (!this.options.url) {
new Error("地址不存在,无法建立通道");
return;
}
//websocket四部曲
this.ws = new WebSocket(this.options.url);
this.onopen();
this.onclose()
this.onmessage()
}
/** * 自定义连接成功事件 * 如果callback存在,调用callback,不存在调用options中的回调 * @param callback 回调函数 */
onopen(callback?: CallableFunction) {
this.ws.onopen = (event) => {
clearTimeout(this.reConnecTimer);//清除重连定时器
// this.options.reconnectCount = this.reConnecCount;//计数器重置
//建立心跳机制
super.reset().start(() => {
this.send(this.options.heartMsg as string);
});
if (typeof callback === "function") {
callback(event)
} else {
(typeof this.options.openCb === "function") && this.options.openCb(event)
}
}
}
/** * 自定义关闭事件 * 如果callback存在,调用callback,不存在调用options中的回调 * @param callback 回调函数 */
onclose(callback?: CallableFunction) {
this.ws.onclose = (event) => {
super.reset();
!this.options.isRestory && this.onreconnect()
if (typeof callback === "function") {
callback(event);
} else {
(typeof this.options.closeCb === "function") && this.options.closeCb(event)
}
}
}
/** * 自定义错误事件 * 如果callback存在,调用callback,不存在调用options中的回调 * @param callback 回调函数 */
onerror(callback?: CallableFunction) {
this.ws.onerror = (event) => {
if (typeof callback === "function") {
callback(event)
} else {
(typeof this.options.errorCb === "function") && this.options.errorCb(event)
}
}
}
/** * 自定义消息监听事件 * 如果callback存在,调用callback,不存在调用options中的回调 * @param callback 回调函数 */
onmessage(callback?: CallableFunction) {
this.ws.onmessage = (event) => {
//收到任何消息,重新开始倒计时心跳检测
super.reset().start(() => {
this.send(this.options.heartMsg as string);
})
if (typeof callback === "function") {
callback(event.data)
} else {
(typeof this.options.messageCb === "function") && this.options.messageCb(event.data)
}
}
}
/** * 自定义发送消息 * @param data 发送的信息 */
send(data: string) {
if (this.ws.readyState !== this.ws.OPEN) {
new Error("没有连接到服务器,无法推送信息");
return;
}
this.ws.send(data);
}
/** * 连接事件 */
onreconnect() {
if (this.options.reconnectCount as number > 0 || this.options.reconnectCount === -1) {
this.options.reconnectTime = setTimeout(() => {
this.create();
if (this.options.reconnectCount !== -1) {
(this.options.reconnectCount as number)--;
}
}, this.options.reconnectTime);
} else {
clearTimeout(this.options.reconnectTime);
// this.options.reconnectCount = this.reConnecCount;
}
}
/** * 销毁 */
destroy() {
super.reset();
clearTimeout(this.reConnecTimer);//清除重连定时器
this.options.isRestory = true;
this.ws.close();
}
}
//-----------------------------------示例-------------------------------
let options: options = {
url: "ws://127.0.0.1:520",//需要服务端启动了该服务
hearTime: 0,
heartMsg: "ping",//心跳信息
isReconnect: true,//是否自动重连
isRestory: false,//是否销毁
reconnectTime: 1000,//重连时间间隔
reconnectCount: -1,//重连次数 -1 则不限制
openCb: (event: Event) => { console.log("连接成功" + event) },
closeCb: (event: Event) => { console.log("连接关闭" + event) },
messageCb: (data: string) => { console.log("接收消息为:" + data) },
errorCb: (event: Event) => { console.log("错误信息" + event) }
};
let ws = new Socket(options)
通过此次的微博总结,加深了自己对前端数据推送各个技术的理解与记忆。时代是在进步的,技术也是如此,不断的呈现问题,解决问题,不断进化。作为一名程序员,也应该保持着与时俱进的能力,应对新时代的技术潮流,不断完善自己的能力,弥补自己的不足。让自己更强大,在工作中更有用武之地。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/vipshop_fin_dev/article/details/119100613
内容来源于网络,如有侵权,请联系作者删除!