WebSocket 协议解析

前言

该文章只为了我学习websocket协议总结知识点的作用,不具备原创性,参考链接都会放在末尾

WebSocket 的诞生

做客户端开发时,接触最多的应用层网络协议,就是 HTTP 协议,而今天介绍的 WebSocket,下层和 HTTP 一样也是基于 TCP 协议,这是一种轻量级网络通信协议,也属于应用层协议。

![img](picutre/WebSocket 协议解析/1394238-20200411111724935-2058356714.png)

WebSocket 与 HTTP/2 一样,其实都是为了解决 HTTP/1.1 的一些缺陷而诞生的,而 WebSocket 针对的就是「请求-应答」这种”半双工”的模式的通信缺陷。

「请求-应答」是”半双工”的通信模式,数据的传输必须经过一次请求应答,这个完整的通信过程,通信的同一时刻数据只能在一个方向上传递。它最大的问题在于,HTTP 是一种被动的通信模式,服务端必须等待客户端请求才可以返回数据,无法主动向客户端发送数据。

这也导致在 WebSocket 出现之前,一些对实时性有要求的服务,通常是基于轮询(Polling)这种简单的模式来实现。轮询就是由客户端定时发起请求,如果服务端有需要传递的数据,可以借助这个请求去响应数据。轮询的缺点也非常明显,大量空闲的时间,其实是在反复发送无效的请求,这显然是一种资源的损耗。

虽然在之后的 HTTP/2、HTTP/3 中,针对这种半双工的缺陷新增了 Stream、Server Push 等特性,但是「请求-应答」依然是 HTTP 协议主要的通信方式。

WebSocket 协议是由 HTML5 规范定义的,原本是为了浏览器而****设计的,可以避免同源的限制,浏览器可以与任意服务端通信,现代浏览器基本上都已经支持 WebSocket。

虽然 WebSocket 原本是被定义在 HTML5 中,但它也适用于移动端,尽管移动端也可以直接通过 Socket 与服务端通信,但借助 WebSocket,可以利用 80(HTTP) 或 443(HTTPS)端口通信,有效的避免一些防火墙的拦截。

![img](picutre/WebSocket 协议解析/1394238-20200411111556753-88807539.png)

WebSocket 是真正意义上的全双工模式,也就是我们俗称的「长连接」。当完成握手连接后,客户端和服务端均可以主动的发起请求,回复响应,并且两边的传输都是相互独立的。

一、WebSocket 的特点

WebSocket 的数据传输,是基于 TCP 协议,但是在传输之前,还有一个握手的过程,双方确认过眼神,才能够正式的传输数据。

WebSocket 的握手过程,符合其 “Web” 的特性,是利用 HTTP 本身的 “协议升级” 来实现。

在建立连接前,客户端还需要知道服务端的地址,WebSocket 并没有另辟蹊径,而是沿用了 HTTP 的 URL 格式,但协议标识符变成了 “ws” 或者 “wss”,分别表示明文和加密的 WebSocket 协议,这一点和 HTTP 与 HTTPS 的关系类似。

二、WireShark抓包分析

WireShark(前称Ethereal)是一个网络封包分析软件。网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料。WireShark抓包是根据TCP/IP五层协议来的,也就是物理层、数据链路层、网络层、传输层、应用层。我们主要关注传输层和应用层。

TCP三次握手

我们都知道,TCP建立连接时,会有三次握手过程。下图是WireShark截获到的三次握手的三个数据包(虽然叫数据包,但是三次握手包是没有数据的)。

![image-20240117101437513](picutre/WebSocket 协议解析/image-20240117101437513.png)

点击上图中的数据包就可以查看每个数据包的详情,这里我们需要明确几个概念才能看懂每个数据包代表啥意义:
SYN:同步比特,建立连接。
ACK:确认比特,置1表示这是一个确认的TCP包,0则不是。
PSH:推送比特,当发送端PSH=1时,接收端应尽快交付给应用进程。

  • 第一次握手

可以看到我们打开的Transmission Control Protocol即为传输层(Tcp)
SYN置为1,客户端向服务端发送连接请求包。

![image-20240117101617900](picutre/WebSocket 协议解析/image-20240117101617900.png)

  • 第二次握手

服务器收到客户端发过来的TCP报文,由SYN=1知道客户端要求建立联机,向客户端发送一个SYN=1,ACK=1的TCP报文,将确认序号设置为客户端的序列号加1。

![image-20240117101725171](picutre/WebSocket 协议解析/image-20240117101725171.png)

  • 第三次握手

客户端接收到服务器发过来的包后检查确认序列号是否正确,即第一次发送的序号+1,以及标志位ACK是否为1。若正确则再次发送确认包,ACK标志为1。链接建立成功,可以发送数据了。

![image-20240117101802700](picutre/WebSocket 协议解析/image-20240117101802700.png)

一次特殊的HTTP请求

紧接着是一次Http请求(第四个包),说明Http的确是使用Tcp建立连接的。

先来看传输层(Tcp): PSH(推送比特)置1,ACK置1,PSH置1说明开始发送数据,同时发送数据ACK要置1,因为需要接收到这个数据包的端给予确认。PSH为1的情况,一般只出现在 DATA内容不为0的包中,也就是说PSH为1表示的是有真正的TCP数据包内容被传递。

![image-20240117101923478](picutre/WebSocket 协议解析/image-20240117101923478.png)

再来看应用层(Http):这是一次特殊的Http请求,为什么是一次特殊的Http请求呢?Http请求头中Connection:Upgrade Upgrade:websocket,Upgrade代表升级到较新的Http协议或者切换到不同的协议。很明显WebSocket使用此机制以兼容的方式与HTTP服务器建立连接。WebSocket协议有两个部分:握手建立升级后的连接,然后进行实际的数据传输。首先,客户端通过使用Upgrade: WebSocket和Connection: Upgrade头部以及一些特定于协议的头来请求WebSocket连接,以建立正在使用的版本并设置握手。服务器,如果它支持协议,回复与相同Upgrade: WebSocket和Connection: Upgrade标题,并完成握手。握手完成后,数据传输开始。

![image-20240117102030757](picutre/WebSocket 协议解析/image-20240117102030757.png)

响应:

接下来会发送一个ACK的包给客户端,表示服务端已经收到上一个GET请求了

![image-20240117102146902](picutre/WebSocket 协议解析/image-20240117102146902.png)

然后会发送一个状态码为101的数据包,表示服务器已经理解了客户端的请求,在发送完这个响应后,服务器将会切换到在Upgrade请求头中定义的那些协议。

![image-20240117102310078](picutre/WebSocket 协议解析/image-20240117102310078.png)

由此我们可以总结出:
Websocket协议本质上是一个基于TCP的协议。建立连接需要握手,客户端(浏览器)首先向服务器(web server)发起一条特殊的http请求,web server解析后生成应答到浏览器,这样子一个websocket连接就建立了,直到某一方关闭连接。

Websocket的世界

通信协议格式是WebSocket格式,服务器端采用Tcp Socket方式接收数据,进行解析,协议格式如下:

![img](picutre/WebSocket 协议解析/1394238-20200411112924120-1082043320.png)

 首先我们需要知道数据在物理层,数据链路层是以二进制进行传递的,而在应用层是以16进制字节流进行传输的。

  • 第一个字节:

  ![img](picutre/WebSocket 协议解析/1394238-20200411113057496-1197028264.png)

  FIN:1位,用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;
  RSV1,RSV2,RSV3:各1位,用于扩展定义的,如果没有扩展约定的情况则必须为0
  OPCODE:4位,用于表示消息接收类型,如果接收到未知的opcode,接收端必须关闭连接。长连接探活包就是这里标识的。

  OPCODE定义的范围:

    0x0表示附加数据帧
    0x1表示文本数据帧
    0x2表示二进制数据帧
    0x3-7暂时无定义,为以后的非控制帧保留
    0x8表示连接关闭
    0x9表示ping
    0xA表示pong
    0xB-F暂时无定义,为以后的控制帧保留

举例:

下面的数据包就表示这是该消息的最后一个数据包,并且没有扩展,消息对的接受类型为二进制数据帧

![image-20240117103938021](picutre/WebSocket 协议解析/image-20240117103938021.png)

  • 第二个字节以及以后字节:

  **![img](picutre/WebSocket 协议解析/1394238-20200411113908424-1896475971.png)或者![img](picutre/WebSocket 协议解析/1394238-20200411114456812-1105181802.png)
**

  ![img](picutre/WebSocket 协议解析/1394238-20200411115206182-1775516430.png)

   MASK:1位,用于标识PayloadData是否经过掩码处理,客户端发出的数据帧需要进行掩码处理,所以此位是1。数据需要解码。

  Payload length === x,如果

  如果 x值在0-125,则是payload的真实长度。
  如果 x值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。
  如果 x值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。

  此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。

 举例抓到客户端返回包:

![image-20240117104547930](picutre/WebSocket 协议解析/image-20240117104547930.png)

上图是客户端发送给服务端的数据包,其中PayloadData的长度为二进制:1111110——>十进制:126;如果值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。也就是圈红的十六进制:00f4——>十进制:244byte。所以PayloadData的真实数据长度是244 bytes;

 举例抓到服务端返回包:

![image-20240117104909329](picutre/WebSocket 协议解析/image-20240117104909329.png)

可以发现服务器发送给客户端的数据包中第二个字节中MASK位为0,这说明服务器发送的数据帧未经过掩码处理,这个我们从客户端和服务端的数据包截图中也可以发现,客户端的数据被加密处理,而服务端的数据则没有。(如果服务器收到客户端发送的未经掩码处理的数据包,则会自动断开连接;反之,如果客户端收到了服务端发送的经过掩码处理的数据包,也会自动断开连接)。

TCP KeepAlive

![image-20240117111446091](picutre/WebSocket 协议解析/image-20240117111446091.png)

如上图所示,TCP保活报文总是成对出现,包括TCP保活探测报文和TCP保活探测确认报文。

TCP保活探测报文是将之前TCP报文的确认序列号减1,并设置1个字节,内容为“00”的应用层数据,如下图所示:

![image-20240117111605657](picutre/WebSocket 协议解析/image-20240117111605657.png)

TCP保活探测确认报文就是对保活探测报文的确认,其报文格式如下:

![image-20240117111645869](picutre/WebSocket 协议解析/image-20240117111645869.png)

因为Websocket通过Tcp Socket方式工作,现在考虑一个问题,在一次长连接中,服务器怎么知道消息的顺序呢?这就涉及到tcp的序列号(Sequence Number)和确认号(Acknowledgment Number)。我的理解是序列号是发送的数据长度;确认号是接收的数据长度。这样讲比较抽象,我们从TCP三次握手开始(结合下图)详细分析一下。

![image-20240117114821791](picutre/WebSocket 协议解析/image-20240117114821791.png)

包1客户端——>服务器
TCP会话的每一端的序列号都从0开始,同样的,确认号也从0开始,因为此时通话还未开始,没有通话的另一端需要确认
包2:服务器——>客户端
服务端响应客户端的请求,响应中附带序列号0(由于这是服务端在该次TCP会话中发送的第一个包,所以序列号为0)和相对确认号1(表明服务端收到了客户端发送的包1中的SYN)。需要注意的是,尽管客户端没有发送任何有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位。

包3客户端——>服务器
和包2中一样,客户端使用确认号1响应服务端的序列号0,同时响应中也包含了客户端自己的序列号(由于服务端发送的包中确认收到了客户端发送的SYN,故客户端的序列号由0变为1)此时,通信的两端的序列号都为1。

包4客户端——>服务器
这是流中第一个携带有效数据的包(确切的说,是客户端发送的HTTP请求),序列号依然为1,因为到上个包为止,还没有发送任何数据,确认号也保持1不变,因为客户端没有从服务端接收到任何数据。需要注意的是,包中有效数据的长度为226字节

包5:服务器——>客户端
当上层处理HTTP请求时,服务端发送该包来确认客户端在包4中发来的数据,需要注意的是,确认号的值增加了226(226是包4中有效数据长度),变为227,简单来说,服务端以此来告知客户端端,目前为止,我总共收到了227字节的数据,服务端的序列号保持为1不变。

包6:服务器——>客户端
这个包标志着服务端返回HTTP响应的开始,序列号依然为1,因为服务端在该包之前返回的包中都不带有有效数据,该包带有129字节的有效数据。

包7:服务器——>客户端
由于上个数据包的发送,TCP服务端的发送序列号增长至130,确认序列号还是227,这次发送的数据包长度为22

包8客户端——>服务器
由于上个数据包的发送,TCP客户端的确认序列号增长至154,从服务端接收了154字节的数据,客户端的确认号由1增长至227

理解了序列号和确认序列号是怎么工作的之后,我们也就知道“TCP保活探测报文是将之前TCP报文的确认序列号减1,并设置1个字节”为什么要这么搞了。减一再加一,是为了保证一次连接中keep alive不影响序列号和确认序列号。Keep alive 中的1byte 00的数据并不是真正要传递的数据,而是tcp keep alive约定俗称的规则。

关于掩码

=======客户端加密和服务端数据不加密,抓包区别明显==========

  看客户端有掩码的数据 服务端无掩码的数据

  ![img](picutre/WebSocket 协议解析/1394238-20200411124046936-1553559875.png) ![img](picutre/WebSocket 协议解析/1394238-20200411124137629-2114974279.png)

掩码算法了解

掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:

首先,假设:

    • original-octet-i:为原始数据的第i字节。
    • transformed-octet-i:为转换后的数据的第i字节。
    • j:为i mod 4的结果。
    • masking-key-octet-j:为mask key第j字节。

算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。

用通俗的语言来讲,就是掩码密钥逐个与数据异或

掩码的诞生

  WebSocket协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法。

  那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益

答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题。

总结

WebSocket 是一个独立的基于 TCP 的协议,它与 HTTP 之间的唯一关系就是它的握手请求可以作为一个升级请求(Upgrade request)经由 HTTP 服务器解释。再严谨一点:WebSocket是一个网络通讯协议, 只要理解上面的数据帧格式和握手流程, 都可以完成基于websokect的即时通讯。

参考链接

https://www.cnblogs.com/zhangmingda/p/12678630.html

https://zhuanlan.zhihu.com/p/631304425