1. 推送技术常见的几种方案
1.1 短连接轮询
前端使用定时器,每间隔一段时间发送请求来获取数据是否更新,前端可以通过比较数据是否更新来判断是否需要更新页面,通常采取setInterval实现。这种方式最简单也是兼容性最高的。
缺点:
- 页面会出现‘假死’
setInterval在等到每次EventLoop时,都要判断是否到指定时间,直到时间到再执行函数,一旦遇到页面有大量任务或者任务处理时间特别耗时的情况下,页面就会出现‘假死’现象,无法及时更新到最新的数据。
- 多余的网络传输
当客户端按固定频率向服务器发起请求,数据可能并没有更新,非常浪费服务器资源,并且在客户端浏览器由于请求接口不断,同样导致调试困难。
1.2 长轮询
客户端像传统轮询一样从服务端请求数据,服务端会阻塞请求不会立刻返回,直到有数据或超时才返回给客户端,然后关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
长轮询解决了频繁的网络请求浪费服务器资源可以及时返回给浏览器。
缺点:
- 保持连接会消耗网络资源。
- 服务器没有返回有效数据,程序超时,又会触发重新连接。
1.3 SSE
SSE( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。
sse与长轮询机制类似,区别是每个连接不只发送一个消息。客户端发送一个请求,服务端保持这个连接直到有新消息发送回客户端,仍然保持着连接,这样连接就可以消息的再次发送,由服务器单向发送给客户端。
1.4 WebSocket
HTML5提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。
WebSocket是HTML5出的东西(协议),也就是说HTTP协议没有变化,或者说没关系,但HTTP是不支持持久连接的(长连接,循环连接的不算)首先HTTP有 1.1 和 1.0 之说,也就是所谓的 keep-alive ,把多个HTTP请求合并为一个,但是 Websocket 其实是一个新协议,跟HTTP协议基本没有关系,只是为了兼容现有浏览器的握手规范而已,也就是说它和HTTP协议有交集,但是并不是全部。
2. 为什么要使用WebScoket?
* | 短轮询 | 长轮询 | WebSocket | SSE |
---|
通讯方式 | http | http | 基于 TCP 长连接的通讯 | http | 触发方式 | 轮询 | 轮询 | 事件 | 事件 | 优点 | 兼容性好,容错率高,实现简单 | 同短连接 | 全双工通讯协议,性能开销小,安全性高,可扩展性高 | 实现简便,开发成本低 | 缺点 | 性能差,占用较大的内存与请求数 | 性能差,占用较大的内存与请求数 | 传输数据需要二次解析,增加开发成本和难度 | 只适用于高级浏览器,不支持ie浏览器 | 适用范围 | b/s 服务 | b/s 服务 | 需要频繁的服务器与客服端交互的场景,如游戏,支付等 | 服务端到客户端的单向推送,如实时报警系统等 |
WebSocket 浏览器支持情况
https://caniuse.com/?search=websocket
更多的特点
- 数据格式比较轻量,性能开销小,通信高效
协议控制的数据包头部较小,而HTTP协议每次通信都需要携带完整的头部 - 更好的二进制支持
- 没有同源限制,客户端可以与任意服务器通信
- 与 HTTP 协议有着良好的兼容性。握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器,例如nginx
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL,同HTTP -> HTTPS 一样,WSS 也是 WS + TLS 之后得到的
3.怎么初步使用?
<script>
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('成功打开 websocket 连接');
ws.send('客户端发送数据: hello');
};
ws.onmessage = (e) => {
console.log('接受到数据: ' + e.data);
};
ws.onclose = (e) => {
// 关闭 websocket
console.log('webSocket连接已关闭...', e);
};
</script>
4. 请求头和响应头
请求头
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: Upgrade
Host: xiaobang-business-application.dasouche.net
Origin: http://localhost:3000
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: iKbSTV8zdY0ujmJwTTQL2g==
Sec-WebSocket-Version: 13
Upgrade: websocket
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36
重点请求头部意义如下:
- Connection: Upgrade:表示要升级协议
- Upgrade: websocket:表示要升级到websocket协议。
- Sec-WebSocket-Version: 13:表示websocket的版本。如果服务端不支持该版本,需要返回一个Sec-WebSocket-Version 的头部,里面包含服务端支持的版本号。
- Sec-WebSocket-Key:与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
响应头
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Max-Age: 3600
Connection: upgrade
Date: Mon, 09 Aug 2021 03:21:38 GMT
SC_tid: XfN16n
Sec-WebSocket-Accept: Z3qdMmIpICebXE6GJGFZxtv68fE=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15
Server: openresty
Upgrade: websocket
注意:Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。
计算公式为:
- 将Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接。
- 通过SHA1计算出摘要,并转成base64字符串。
5. 保持连接,建立心跳
首先明确为什么要建立心跳呢?很明显,是因为 websocket 可能会断开。
那么带着以下几个问题去探究:
5.1. WebSocket 为什么会断开?
首先明确WebSocket是基于TCP的应用层技术,那么疑问来了,TCP本身是有keepalive机制的为什么还会断开连接呢?
原因有以下几个:
- client异常挂死,此时keepalive机制无法反馈真实的client状态;
- client 异常断电断网出现TCP假死keepalive并不能根本性解决问题,实际上互联网环境很不稳定
- WebSocket在应用层,基于传输层,在WebSocket中操作TCP也很不方便。封装就意味着易用性提高灵活性降低。
5.2 WebSocket 是否有对应的状态值?
对于不确定不稳定的东西,一般都有其状态值,对于 WebSocket 同样如此。
下面是WebSocket.readyState的四个值(四种状态):
- 0: 表示正在连接
- 1: 表示连接成功,可以通信了
- 2: 表示连接正在关闭
- 3: 表示连接已经关闭,或者打开连接失败
5.3 如何优雅的建立心跳,保持连接
建立心跳
ws.onopen = () => {
// Web Socket 已连接上,使用 send() 方法发送数据
console.log('成功打开webSocket连接...');
// 建立十秒的心跳连接
const creatHeartBeat = () => {
heartInterval = setInterval(() => {
ws.send(
JSON.stringify({
topic,
heartBeat: 'heartBeat',
}),
);
}, 10000);
};
if (!heartInterval) {
creatHeartBeat();
}
};
断开重连
ws.onclose = (e) => {
// 关闭 websocket
console.log('webSocket连接已关闭...', e);
// 10秒之后自动重连
setTimeout(() => {
createWebSocket(dataProps);
}, 10000);
};
ws.onerror = (e) => {
// websocket 错误
console.log('webSocket发生错误:', e);
if(ws.readyState === 3){
// 10秒之后自动重连
setTimeout(() => {
createWebSocket(dataProps);
}, 10000);
}
};
6. 结合使用nginx
6.1 nginx 的特点
- 它能够承受、高并发的大量的请求,然后将这些请求均匀的转发给内部的服务器,分摊压力。
- 反向代理能够解决跨域引起的问题,因为Nginx,Node,应用服务器,数据库都处于内网段中。
- Nginx非常擅长处理静态资源(img,css,js,video),所以也经常作为静态资源服务器,也就是我们平时所说的CDN,比如:前一个用户访问index.html, 经过Nginx-Node-应用服务器-数据库链路之后,Nginx会把index.html返回给用户,并且会把index.html缓存在Nginx上,下一个用户再想请求index.html的时候,请求Nginx服务器,Nginx发现有index.html的缓存,于是就不用去请求服务端了,会直接将缓存的页面(如果没过期的话)返回给用户。
6.2 怎么配置 nginx 来支持 websocket 呢?
为了将客户端和服务器之间的连接从 HTTP/1.1 转换为 WebSocket,使用了 HTTP/1.1 中可用的协议切换机制。
由于 “upgrade” 是一个 “hop-by-hop” (TCP/IP协议中在由路由器连接的两种物理网中网络层中使用的协议) 的标头,它不会从客户端传递到代理服务器。通过正向代理,客户端可以使用该 CONNECT 方法来规避此问题。然而,这不适用于反向代理,因为客户端不知道任何代理服务器,并且需要在代理服务器上进行特殊处理。
所以,nginx 实现了特殊的操作模式,如果代理服务器返回代码为 101(切换协议)的响应,并且客户端通过请求中的 “upgrade” 标头。包括 “Upgrade” 和 “Connection” 在内的 “hop-by-hop” 标头不会从客户端传递到代理服务器,因此为了让代理服务器了解客户端将协议切换到 WebSocket 的意图,这些标头必须明确传递:
location /wss/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
6.3 关于超时
默认情况下,如果代理服务器在 60 秒内没有传输任何数据,连接将被关闭。可以使用proxy_read_timeout指令增加此超时 。
或者,可以将代理服务器配置为定期发送 WebSocket 消息以重置超时并检查连接是否仍然有效。这也是为什么要使用 WebSocket 心跳的原因。
7. 关于集群方案的解决?
在实际生产环境中,服务器一般是采用集群模式,接收第三方响应的服务器可能是有多台,然后具体是根据nginx随机路由转发。
假设异步响应的服务器有2台,A和B,而且连接websocket的代码是写在响应工程里面的,这时p1客户端去连接websocket的时候,通过域名实现websocket协议,具体可在nginx里面配置,是随机转发到某一台,假设连接到A,这时session信息保存在A服务器,假设用户处理事件完成后,第三方异步通知也是随机转发A和B,如果运气好,转发到A,此时异步响应能拿到session值,则能成功通知到客户端。如果不巧,转发到了B,则异步通知逻辑是执行了,但发现客户端一直没有收到消息,因为B服务器根本拿不到这个session值。
7.1 集群条件下 session 共享的问题?
- 使用redis共享
- 使用MQ消息通知,具体根据实际需求选择合适的消息队列方案
8. 结尾
WebSocket可写的东西很多,比如WebSocket扩展。 客户端、服务端之间是如何协商、使用扩展的。 WebSocket扩展可以给协议本身增加很多能力和想象空间,比如数据的压缩、加密,以及多路复用等。
推荐文章
WebSocket从入门到精通:https://juejin.cn/post/6844903544978407431#heading-17
WebSocket协议以及ws源码分析:https://juejin.cn/post/6844903850667671560
|