WebSocket 深入浅出

背景:为什么要有 WebSocket

为了克服 HTTP“请求 - 应答”模式的缺点

“请求 - 应答”有什么不好的地方呢?

请求 - 应答:”是一种“半双工”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。

虽然后来的 HTTP/2、HTTP/3 新增了 Stream、Server Push 等特性,但“请求 - 应答”依然是主要的工作方式。这就导致 HTTP 难以应用在动态页面、即时消息、网络游戏等要求“实时通信”的领域

在 WebSocket 出现之前,在浏览器环境里用 JavaScript 开发实时 Web 应用很麻烦。因为浏览器是一个“受限的沙盒”,不能用 TCP,只有 HTTP 协议可用,所以就出现了很多“变通”的技术,“轮询”(polling)就是比较常用的的一种。

简单地说,轮询就是不停地向服务器发送 HTTP 请求,问有没有数据,有数据的话服务器就用响应报文回应。如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果。

1.ajax轮询 :客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
2.长轮询(long poll ):客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。

但轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU 资源,非常不经济。

所以,为了克服 HTTP“请求 - 应答”模式的缺点,WebSocket 就“应运而生”了。

WebSocket 的特点

  • WebSocket 是一个真正“全双工”的通信协议

一旦后台有新的数据,就可以立即“推送”给客户端,不需要客户端轮询,“实时通信”的效率也就提高了。
而不再是请求-响应的模式:服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。

  • WebSocket 采用了二进制帧结构,语法、语义与 HTTP 完全不兼容.

但因为它的主要运行环境是浏览器,为了便于推广和应用,就不得不“搭便车” (蹭热点),在使用习惯上尽量向 HTTP 靠拢,这就是它名字里“Web”的含义。

  • “ws”和“wss”,分别表示明文和加密的 WebSocket 协议。

服务发现方面,WebSocket 没有使用 TCP 的“IP 地址 + 端口号”,而是延用了 HTTP 的 URI 格式,但开头的协议名不是“http”,引入的是两个新的名字:“ws”和“wss”,分别表示明文和加密的 WebSocket 协议。

  • WebSocket 的默认端口也选择了 80 和 443

因为现在互联网上的防火墙屏蔽了绝大多数的端口,只对 HTTP 的 80、443 端口“放行”,所以 WebSocket 就可以“伪装”成 HTTP 协议,比较容易地“穿透”防火墙,与服务器建立连接。

WebSocket 的帧结构

WebSocket 的帧和http 2.0 帧对比

  • WebSocket 和 HTTP/2 的关注点不同,WebSocket 更侧重于“实时通信”,而 HTTP/2 更侧重于提高传输效率,所以两者的帧结构也有很大的区别。

  • WebSocket 虽然有“帧”,但却没有像 HTTP/2 那样定义“流”,也就不存在“多路复用”“优先级”等复杂的特性,而它自身就是“全双工”的,也就不需要“服务器推送”。

WebSocket 的帧结构定义,长度不固定,最少 2 个字节,最多 14 字节
.看着好像很复杂,实际非常简单。在这里插入图片描述
开头的两个字节是必须的,也是最关键的。

  • 第一个字节的第一位“FIN”是消息结束的标志位

相当于 HTTP/2 里的“END_STREAM”,表示数据发送完毕。一个消息可以拆成多个帧,接收方看到“FIN”后,就可以把前面的帧拼起来,组成完整的消息。“FIN”后面的三个位是保留位,目前没有任何意义,但必须是 0。

  • 第一个字节的后 4 位很重要,叫“Opcode”,操作码,其实就是帧类型.

比如 1 表示帧内容是纯文本,2 表示帧内容是二进制数据,8 是关闭连接,9 和 10 分别是连接保活的 PING 和 PONG。

  • 第二个字节第一位是掩码标志位“MASK”,表示帧内容是否使用异或操作(xor)做简单的加密。

目前的 WebSocket 标准规定,客户端发送数据必须使用掩码,而服务器发送则必须不使用掩码。

  • 第二个字节后 7 位是“Payload len”,表示帧内容的长度。

它是另一种变长编码,最少 7 位,最多是 7+64 位,也就是额外增加 8 个字节,所以一个 WebSocket 帧最大是 2^64。

  • 长度字段后面是“Masking-key”,掩码密钥,它是由上面的标志位“MASK”决定的,如果使用掩码就是 4 个字节的随机数,否则就不存在。

这么分析下来,其实 WebSocket 的帧头就四个部分:“结束标志位 + 操作码 + 帧长度 + 掩码

WebSocket 的握手

在这里插入图片描述

和 TCP、TLS 一样,WebSocket 也要有一个握手过程,然后才能正式收发数据。

这里它还是搭上了 HTTP 的“便车”,利用了 HTTP 本身的“协议升级”特性,“伪装”成 HTTP,这样就能绕过浏览器沙盒、网络防火墙等等限制,这也是 WebSocket 与 HTTP 的另一个重要关联点。

WebSocket 的握手是一个标准的 HTTP GET 请求,但要带上两个协议升级的专用头字段

“Connection: Upgrade”,表示要求协议“升级”;
“Upgrade: websocket”,表示要“升级”成 WebSocket 协议。

另外,为了防止普通的 HTTP 消息被“意外”识别成 WebSocket,握手消息还增加了两个额外的认证用头字段(所谓的“挑战”,Challenge):

Sec-WebSocket-Key:一个 Base64 编码的 16 字节随机数,作为简单的认证密钥;
Sec-WebSocket-Version:协议的版本号,当前必须是 13。

在这里插入图片描述

服务器收到 HTTP 请求报文,看到上面的四个字段,就知道这不是一个普通的 GET 请求,而是 WebSocket 的升级请求,于是就不走普通的 HTTP 处理流程,而是构造一个特殊的“101 Switching Protocols”响应报文,通知客户端,接下来就不用 HTTP 了,全改用 WebSocket 协议通信。(有点像 TLS 的“Change Cipher Spec”)

WebSocket 的握手响应报文也是有特殊格式的,要用字段“Sec-WebSocket-Accept”验证客户端请求报文,同样也是为了防止误连接。

具体的做法是把请求头里“Sec-WebSocket-Key”的值,加上一个专用的 UUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,再计算 SHA-1 摘要。

encode_base64(
  sha1( 
    Sec-WebSocket-Key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' ))

客户端收到响应报文,就可以用同样的算法,比对值是否相等,如果相等,就说明返回的报文确实是刚才握手时连接的服务器,认证成功。

握手完成,后续传输的数据就不再是 HTTP 报文,而是 WebSocket 格式的二进制帧了。

小结

WebSocket 虽然是在应用层,但使用方式却与“TCP Socket”差不多,过于“原始”,用户必须自己管理连接、缓存、状态,开发上比 HTTP 复杂的多,所以是否要在项目中引入 WebSocket 必须慎重考虑。

  • HTTP 的“请求 - 应答”模式不适合开发“实时通信”应用,效率低,难以实现动态页面,所以出现了 WebSocket;
  • WebSocket 是一个“全双工”的通信协议,相当于对 TCP 做了一层“薄薄的包装”,让它运行在浏览器环境里;
  • WebSocket 利用 HTTP 协议实现连接握手,发送 GET 请求要求“协议升级”,握手过程中有个非常简单的认证机制,目的是防止误连接。
  • WebSocket 使用兼容 HTTP 的 URI 来发现服务,但定义了新的协议名“ws”和“wss”,端口号也沿用了 80 和 443;
  • WebSocket 使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;

在这里插入图片描述
webSocket如何兼容低浏览器?

Adobe Flash Socket 、 ActiveX HTMLFile (IE) 、 基于 multipart 编码发送 XHR 、 基于长轮询的 XHR

是否跨域限制

原来WebSocket根本不附属于同源策略,而且它本身就有意被设计成可以跨域的一个手段。由于历史原因,跨域检测一直是由浏览器端来做,但是WebSocket出现以后,对于WebSocket的跨域检测工作就交给了服务端,浏览器仍然会带上一个Origin跨域请求头,服务端则根据这个请求头判断此次跨域WebSocket请求是否合法

参考:透视http协议

猜你喜欢

转载自blog.csdn.net/HZ___ZH/article/details/114645865