说明
本文将先从websocket协议的诞生缘由,再到协议原理,最后如何使用来讲解。
本人文采不好,写的可能不是那么清晰,别介意
为什么需要WebSocket协议
webSocket协议使用场景一般是即时通信,有了它,服务器可以主动向浏览器推送数据,也就是支持双向通信,更加高效。那么在HTML5之前,服务器如何主动推送数据呢?
传统HTTP协议是基于 请求>响应,正如客户端没发送请求给服务器之前,服务器是不会主动做出响应的,客户端发送请求后,服务器才做出响应,很明显是单向的,而且如果要持续获取资源,得不断重复请求,也就是HTTP轮询,浏览器不断的询问服务器是否有新数据,从而间接的实现了服务端能及时的将数据发送给客户端。
定时轮询
定时轮询
就是每间隔一段时间发起一次HTTP请求,例如每2秒请求一次服务器讯问是否有新数据,这种方式使用场景目前一般用于扫码登录,缺点就是会有延迟,例如设置的是2秒请求一次,那么用户扫码后最长需要等待2秒后页面才会有反应,有点体验不好,那么有什么办法解决呢?很简单,使用长轮询
即可。
长轮询
长轮询
就是让HTTP请求保持一段时间,客户端需要和服务器约定请求超时时间,例如约定30秒,浏览器发起HTTP请求后将最长等待30秒,而服务端收到请求后将立刻检查是否有新数据,如果有则立刻响应请求,否则继续等待直到有新数据或者时间到了30秒后在响应请求,相当于保持持久连接,不过这里就会出问题,服务器不得不腾出资源给这个长轮循,即使没有数据的时候也得保持链接。后来就有了WebSocket
WebSocket介绍
WebSocket是一种协议
,是全双工的通讯协议,服务器可以主动向客户端推送数据。它是H5新增的功能。用于与服务器收发数据,支持双向通信。较少的开销,包头只有2-10字节,简称ws,wss 协议。
它是用来做实时传输的应用协议。websocket在建立连接的时候依旧使用HTTP,只不过后面保持TCP持久连接。webSocket和HTTP很像,请求URL用的是WS或者WSS,在HTTP协议里对应的就是HTTP和HTTPS。
连接过程
客户端如果要发送WebSocket请求,就需要在请求头里做出说明,Connection
的值写成upgrade,Upgrade
的值写成websocket。
首先客户端发起协议升级请求,用的是标准的HTTP报文格式,并且是GET请求
服务器在收到请求后,就会发现连接要求协议升级,要升级成什么协议在upgrade
里找,发现是webSocket,于是服务器就知道,客户端要建立的是webSocket连接,另外客户端还会在请求里加入Sec-WebSocket-key
,这个key提供给服务器,来验证是否收到一个有效的webSocket请求。还有Sec-WebSocket-version这个是版本号。
由于发起升级请求用的是标准的HTTP请求,所以例如Host,Origin,Cookie等协议头也会发送
客户端请求头
GET ws://localhost:3001/?name=ddd HTTP/1.1
Host: localhost:3001
Connection: Upgrade // 告诉服务器要升级协议
Upgrade: websocket // 升级成websocket协议
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: DI98YLmdRbENz/Kxqyd1Mw== //服务端响应后 也使用某公开算法计算和服务端返回结果是否一致
当服务器做出响应的时候,需要发出状态码101
切换协议,响应你的connection和upgrade
,首部值是和请求一样的,来表明验证的链接已经被升级了,接着还会在响应里加上Sec-WebSocket-accept,它的值是根据请求里key的值来生成的Sec-WebSocket-accept
,表示服务器同意建立连接。此时完成协议升级,后续数据交互将按照新的协议来
Accept计算公式
let accept = Base64( sha1( SecWebSocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))
服务端响应头
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: DBXtF85wR0DcBthMLMJlYcgGsT0= // 用公开算法使用客户端的key生成
浏览器网络请求连接示例截图
此时完成了协议升级,接下来就和HTTP协议没有半毛钱关系了,并且后续的操作都需要遵守websocket协议规范
传输数据
请求链接后就是正式传输数据了,也就是客户端和服务器可以双向传输数据了,ws数据包在websocket中称为帧
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
对于网络协议不熟悉的看到这个数据结构可能会看不懂,可以看下面这张图
先来解释一下ws协议的数据帧结构
FIN
,代表分片,如果为1就代表为最后一个分片,如果是0则代表后面还有分片。当传输数据过大时会做一个分片处理,数据小的话一般就是一次传完不分片
- 发送端: 将一条消息分割成多个帧 然后发送给服务端
- 接收端: 接收消息帧,并将关联的帧拼接成完整的消息
opcode字段
,标志这是什么类型的数据帧,1文本数据,2二进制数据,3-7暂时没有意义,8代表关闭连接的信号,9代表心跳包
payload字段
,存放要传输数据的长度,该字段的长度可变, 可能为 7 比特, 也可能为 7 + 16 比特, 也可能为 7 + 64 比特。(1byte=8bit),具体来说, 当 Payload 的实际长度在 [0, 125] 时, 则 Payload Len 字段的长度为 7 比特, 它的值直接代表了 Payload 的实际长度。
当 Payload 的实际长度为 126 时, 则 Payload Len 后跟随的 16 位将被解释为 16-bit 的无符号整数, 该整数的值指示 Payload 的实际长度; 当 Payload 的实际长度为 127 时, 其后的 64 比特将被解释为 64-bit 的无符号整数, 该整数的值指示 Payload 的实际长度
payload Data
存放的实际传输的数据
通过数据帧的结构可以发现,webSocket的数据传输也是消息头+消息体
的形式
浏览器webSocket使用
看完了webSocket的协议原理,可能会觉得有点复杂,其实在浏览器中已经将websocket的通讯层协议都已经封装好了,我们并不需要关心它建立连接的过程,数据帧组成的规则,等等都不用我们关心
WebSocket对象使用
建立连接
const socket = new WebSocket("ws://82.157.123.54:9010/ajaxchattest?token=xxxx");
发送数据到服务端
socket.send('message') // 发送消息, 文本与二进制都使用此方法
监听服务端推送消息
// 收到消息事件
socket.addEventListener('message', function (event) {
console.log('收到消息 :', event.data);
});
其它方法和事件
// 方法
socket.close() // 关闭当前连接
// 属性
socket.readyState // 当前连接状态
socket.url // 请求url
// 事件
// 连接成功事件
socket.addEventListener('open', function (event) {
socket.send('Hello 嘀嘀嘀!'); // 发送消息
});
// 连接关闭事件
socket.addEventListener('close', function (event) {
console.log('连接被关闭');
});
// 连接发生错误事件
socket.addEventListener('error', function (event) {
console.log('连接发生错误');
});
除了使用原生webSocket对象外,还可以使用socket.io这个库,算是热度比较高的一个websocket库了
补充
跨域
,websocket没有同源策略限制,而且它本身就有意被设计成可以跨域的一个手段。由于历史原因,跨域检测一直是由浏览器端来做,WebSockets是一种较新的技术,从一开始就被设计用来支持跨域场景,对于WebSocket的跨域检测工作就交给了服务端,由于升级协议时为HTTP协议,所以浏览器仍然会带上一个Origin跨域请求头,服务端则根据这个请求头判断此次跨域WebSocket请求是否合法,并执行必要的验证,而不需要像CORS那样在浏览器端采取严厉的预防措施。
协议头
,由于websocket对象未提供相关方法,所以不能携带自定义协议头
异常处理
Websocket的连接时发生的异常,是不能用try catch来捕获的,必须使用error事件,通过监听error事件来处理异常
鉴权
方式
- 通过连接地址url上携带token 进行鉴权,如token失效 服务端可以关闭客户端连接
- websocket能自动携带同源域名下的cookie,因此可以通过cookie进行鉴权
- 每次发送ws消息时,在消息中携带token进行鉴权,如token失效 服务端可以关闭客户端连接