WebSocket 协议的原理和实践

WebSocket 是一种基于 TCP 的双向通信协议,它可以在客户端和服务器之间建立一个持久连接,并且可以在连接生命周期内进行数据的传输。WebSocket 协议的目的是为了解决 HTTP 协议的一些局限性,比如:

  • HTTP 协议是单向的:HTTP 协议是基于请求-响应的模式,客户端只能主动发起请求,服务器只能被动响应,不能主动推送数据给客户端,这导致了数据的不及时和资源的浪费。
  • HTTP 协议是无状态的:HTTP 协议是无状态的,每次请求都是独立的,服务器无法识别客户端的身份和状态,需要借助于 Cookie、Session 等机制来维持连接的状态,这增加了复杂度和开销。
  • HTTP 协议是文本的:HTTP 协议是基于文本的,请求和响应的格式都是由 ASCII 字符组成的,这导致了数据的冗余和低效,尤其是对于二进制数据的传输,需要进行编码和解码,增加了计算量和传输量。

WebSocket 协议通过以下几个方面来克服 HTTP 协议的局限性:

  • WebSocket 协议是双向的:WebSocket 协议是基于帧的模式,客户端和服务器都可以主动发送和接收数据帧,不需要等待对方的请求或响应,实现了真正的双向通信,提高了数据的实时性和效率。
  • WebSocket 协议是有状态的:WebSocket 协议是有状态的,客户端和服务器之间只需要建立一次握手,就可以建立一个持久的连接,服务器可以识别客户端的身份和状态,不需要使用 Cookie、Session 等机制来维持连接的状态,简化了逻辑和开销。
  • WebSocket 协议是二进制的:WebSocket 协议是基于二进制的,请求和响应的格式都是由字节组成的,这减少了数据的冗余和低效,尤其是对于二进制数据的传输,不需要进行编码和解码,减少了计算量和传输量。

WebSocket 协议的握手过程

WebSocket 协议的握手过程是基于 HTTP 协议的,客户端和服务器之间需要先通过 HTTP 协议进行一次握手,然后升级为 WebSocket 协议,建立一个持久的连接。握手过程的具体步骤如下:

  • 客户端发起握手请求:客户端通过 HTTP 协议向服务器发送一个 GET 请求,请求头中包含以下几个字段:
    • Upgrade:表示要升级的协议,值为 websocket。
    • Connection:表示要保持的连接类型,值为 Upgrade。
    • Sec-WebSocket-Key:表示一个随机的字符串,用于服务器生成响应的校验码。
    • Sec-WebSocket-Version:表示 WebSocket 协议的版本,值为 13。
    • Origin:表示请求的来源,用于服务器验证和过滤。
    • Sec-WebSocket-Protocol:表示客户端支持的子协议,用于服务器选择和协商。
    • Sec-WebSocket-Extensions:表示客户端支持的扩展,用于服务器选择和协商。
  • 服务器响应握手请求:服务器收到客户端的握手请求后,如果同意升级为 WebSocket 协议,就会发送一个 HTTP 响应,响应头中包含以下几个字段:
    • Upgrade:表示要升级的协议,值为 websocket。
    • Connection:表示要保持的连接类型,值为 Upgrade。
    • Sec-WebSocket-Accept:表示一个校验码,由服务器根据客户端的 Sec-WebSocket-Key 生成,用于客户端验证服务器的身份。
    • Sec-WebSocket-Protocol:表示服务器选择的子协议,必须是客户端请求中包含的子协议之一。
    • Sec-WebSocket-Extensions:表示服务器选择的扩展,必须是客户端请求中包含的扩展之一。
  • 客户端验证服务器响应:客户端收到服务器的响应后,需要验证服务器的身份,方法是根据自己的 Sec-WebSocket-Key 和一个固定的字符串,按照一定的算法,生成一个期望的校验码,然后和服务器的 Sec-WebSocket-Accept 进行比较,如果相同,表示服务器的身份有效,握手成功;如果不同,表示服务器的身份无效,握手失败,关闭连接。

WebSocket 协议的数据帧格式

WebSocket 协议的数据是通过一系列的帧(frame)来进行传输的,每个帧由头部(header)和负载(payload)两部分构成。帧的头部包含了一些控制位和长度字段,用于指示帧的类型、大小和边界。帧的负载是实际传输的数据,长度由头部指示,内容由类型指示,如果有掩码,需要进行异或运算。具体来说,帧的头部有以下几个字段:

  • FIN:1 位,表示是否是最后一个帧,如果是 1,表示这是消息的最后一个帧;如果是 0,表示后面还有更多的帧。
  • RSV1, RSV2, RSV3:各占 1 位,保留位,用于扩展,一般为 0,除非协商另外的含义:
    • 压缩扩展:这种扩展可以用于对数据帧进行压缩,以减少传输的数据量,提高效率。比如 permessage-deflate 扩展,它使用了 RSV1 位来标识数据帧是否经过了 deflate 压缩算法,如果 RSV1 为 1,表示数据帧已经压缩,需要解压缩;如果 RSV1 为 0,表示数据帧没有压缩,不需要解压缩。
    • 多路复用扩展:这种扩展可以用于在一个 TCP 连接上建立多个 WebSocket 逻辑通道,以支持多个并发的数据流,提高并发性。比如 multiplexing 扩展,它使用了 RSV1 位来标识数据帧是否属于一个新的通道,如果 RSV1 为 1,表示数据帧是一个新通道的第一个帧,需要创建一个新的通道;如果 RSV1 为 0,表示数据帧是一个已有通道的后续帧,不需要创建新的通道。
    • 分片扩展:这种扩展可以用于对数据帧进行分片,以支持大数据量的传输,提高可靠性。比如 fragmentation 扩展,它使用了 RSV1 位来标识数据帧是否是一个分片的开始,如果 RSV1 为 1,表示数据帧是一个分片的开始,需要记录分片的信息;如果 RSV1 为 0,表示数据帧是一个分片的中间或结束,不需要记录分片的信息。
  • Opcode:4 位,表示帧的类型,可以是以下几种:
    • 0x0:表示一个延续帧,用于分片传输的消息的中间帧。
    • 0x1:表示一个文本帧,用于传输文本数据。
    • 0x2:表示一个二进制帧,用于传输二进制数据。
    • 0x8:表示一个关闭帧,用于关闭连接。
    • 0x9:表示一个心跳帧(ping),用于保持连接。
    • 0xA:表示一个心跳帧(pong),用于响应 ping。
  • Mask:1 位,表示是否有掩码,如果是 1,表示负载数据需要用一个 32 位的掩码进行异或运算,以增加安全性;如果是 0,表示负载数据不经过掩码处理。客户端发出的帧必须设置掩码位,而服务器发出的帧必须清除掩码位。
  • Payload length:7 位或 7+16 位或 7+64 位,表示负载数据的长度,如果值在 0-125 之间,那么就是负载数据的长度;如果值是 126,那么后面 2 个字节表示负载数据的长度;如果值是 127,那么后面 8 个字节表示负载数据的长度。
  • Masking-key:0 或 4 字节,表示掩码,如果 Mask 位是 1,那么这里是 4 个字节的掩码;如果 Mask位是 0,那么这里没有掩码。
  • Payload data:表示负载数据,长度由 Payload length 字段指示,内容由 Opcode 字段指示,如果 Mask 位是 1,那么需要用 Masking-key 字段的掩码进行异或运算。

业务层心跳机制

WebSocket 协议虽然自带了心跳ping和pong帧,但是这些帧只能用于检测TCP连接的存活,不能保证应用层的可用性。因此,有些业务场景下,还需要自行实现心跳机制,以实现以下目的

  • 保证连接的稳定性:有时候,即使 TCP 连接是正常的,但是中间的网络设备可能会对空闲的连接进行关闭,导致连接的断开。比如某些防火墙或者代理服务器可能会对长时间没有数据交换的连接进行主动关闭,如果使用 WebSocket 自身的机制无法防止这种情况,可能会导致客户端和服务器之间的连接不稳定,影响用户的体验。
  • 保证应用层的可用性:有时候,即使 TCP 连接是正常的,但是应用层可能出现故障,导致无法处理业务请求。比如某台服务器因为某些原因导致负载超高,CPU飙高,或者线程池打满等等,无法响应任何业务请求,如果使用 WebSocket 自身的机制无法发现任何问题,然而对客户端而言,这时的最好选择就是断连后重新连接其他服务器,而不是一直认为当前服务器是可用状态,向当前服务器发送一些必然会失败的请求。
  • 保证数据的实时性:有时候,即使 TCP 连接是正常的,但是网络层可能出现延迟,导致数据的传输不及时。比如某个路由器或者交换机出现故障,导致数据包的丢失或者重传,如果使用 WebSocket 自身的机制无法感知到这种情况,可能会导致客户端收到的数据是过期的或者不完整的,影响业务的正确性。

业务代码中自行实现心跳机制,可以根据不同的业务需求,定制不同的心跳策略,比如心跳的频率、内容、超时时间等,以达到更好的效果。

你可能感兴趣的:(网络协议,websocket,网络协议,网络)