HTML5中的WebSocket协议原理

340 阅读8分钟

说明

本文将先从websocket协议的诞生缘由,再到协议原理,最后如何使用来讲解。

本人文采不好,写的可能不是那么清晰,别介意

为什么需要WebSocket协议

webSocket协议使用场景一般是即时通信,有了它,服务器可以主动向浏览器推送数据,也就是支持双向通信,更加高效。那么在HTML5之前,服务器如何主动推送数据呢?

image.png

传统HTTP协议是基于 请求>响应,正如客户端没发送请求给服务器之前,服务器是不会主动做出响应的,客户端发送请求后,服务器才做出响应,很明显是单向的,而且如果要持续获取资源,得不断重复请求,也就是HTTP轮询,浏览器不断的询问服务器是否有新数据,从而间接的实现了服务端能及时的将数据发送给客户端。

定时轮询

定时轮询就是每间隔一段时间发起一次HTTP请求,例如每2秒请求一次服务器讯问是否有新数据,这种方式使用场景目前一般用于扫码登录,缺点就是会有延迟,例如设置的是2秒请求一次,那么用户扫码后最长需要等待2秒后页面才会有反应,有点体验不好,那么有什么办法解决呢?很简单,使用长轮询即可。

长轮询

image.png

长轮询就是让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生成

浏览器网络请求连接示例截图

image.png

此时完成了协议升级,接下来就和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 ...                |
 +---------------------------------------------------------------+

对于网络协议不熟悉的看到这个数据结构可能会看不懂,可以看下面这张图

image.png

先来解释一下ws协议的数据帧结构

FIN,代表分片,如果为1就代表为最后一个分片,如果是0则代表后面还有分片。当传输数据过大时会做一个分片处理,数据小的话一般就是一次传完不分片

  1. 发送端: 将一条消息分割成多个帧 然后发送给服务端
  2. 接收端: 接收消息帧,并将关联的帧拼接成完整的消息

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事件来处理异常

鉴权方式

  1. 通过连接地址url上携带token 进行鉴权,如token失效 服务端可以关闭客户端连接
  2. websocket能自动携带同源域名下的cookie,因此可以通过cookie进行鉴权
  3. 每次发送ws消息时,在消息中携带token进行鉴权,如token失效 服务端可以关闭客户端连接