learn-tech/专栏/透视HTTP协议/38WebSocket:沙盒里的TCP.md
2024-10-16 11:00:45 +08:00

161 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
38 WebSocket沙盒里的TCP
在之前讲 TCP/IP 协议栈的时候我说过有“TCP Socket”它实际上是一种功能接口通过这些接口就可以使用 TCP/IP 协议栈在传输层收发数据。
那么你知道还有一种东西叫“WebSocket”吗
单从名字上看“Web”指的是 HTTP“Socket”是套接字调用那么这两个连起来又是什么意思呢
所谓“望文生义”大概你也能猜出来“WebSocket”就是运行在“Web”也就是 HTTP 上的 Socket 通信规范提供与“TCP Socket”类似的功能使用它就可以像“TCP Socket”一样调用下层协议栈任意地收发数据。
更准确地说“WebSocket”是一种基于 TCP 的轻量级网络通信协议,在地位上是与 HTTP“平级”的。
为什么要有 WebSocket
不过,已经有了被广泛应用的 HTTP 协议,为什么要再出一个 WebSocket 呢?它有哪些好处呢?
其实 WebSocket 与 HTTP/2 一样,都是为了解决 HTTP 某方面的缺陷而诞生的。HTTP/2 针对的是“队头阻塞”,而 WebSocket 针对的是“请求 - 应答”通信模式。
那么,“请求 - 应答”有什么不好的地方呢?
“请求 - 应答”是一种“半双工”的通信模式,虽然可以双向收发数据,但同一时刻只能一个方向上有动作,传输效率低。更关键的一点,它是一种“被动”通信模式,服务器只能“被动”响应客户端的请求,无法主动向客户端发送数据。
虽然后来的 HTTP/2、HTTP/3 新增了 Stream、Server Push 等特性,但“请求 - 应答”依然是主要的工作方式。这就导致 HTTP 难以应用在动态页面、即时消息、网络游戏等要求“实时通信”的领域。
在 WebSocket 出现之前,在浏览器环境里用 JavaScript 开发实时 Web 应用很麻烦。因为浏览器是一个“受限的沙盒”,不能用 TCP只有 HTTP 协议可用所以就出现了很多“变通”的技术“轮询”polling就是比较常用的的一种。
简单地说,轮询就是不停地向服务器发送 HTTP 请求,问有没有数据,有数据的话服务器就用响应报文回应。如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果。
但轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 CPU 资源,非常不经济。
所以,为了克服 HTTP“请求 - 应答”模式的缺点WebSocket 就“应运而生”了。它原来是 HTML5 的一部分后来“自立门户”形成了一个单独的标准RFC 文档编号是 6455。
WebSocket 的特点
WebSocket 是一个真正“全双工”的通信协议,与 TCP 一样,客户端和服务器都可以随时向对方发送数据,而不用像 HTTP“你拍一我拍一”那么“客套”。于是服务器就可以变得更加“主动”了。一旦后台有新的数据就可以立即“推送”给客户端不需要客户端轮询“实时通信”的效率也就提高了。
WebSocket 采用了二进制帧结构,语法、语义与 HTTP 完全不兼容,但因为它的主要运行环境是浏览器,为了便于推广和应用,就不得不“搭便车”,在使用习惯上尽量向 HTTP 靠拢这就是它名字里“Web”的含义。
服务发现方面WebSocket 没有使用 TCP 的“IP 地址 + 端口号”,而是延用了 HTTP 的 URI 格式但开头的协议名不是“http”引入的是两个新的名字“ws”和“wss”分别表示明文和加密的 WebSocket 协议。
WebSocket 的默认端口也选择了 80 和 443因为现在互联网上的防火墙屏蔽了绝大多数的端口只对 HTTP 的 80、443 端口“放行”,所以 WebSocket 就可以“伪装”成 HTTP 协议,比较容易地“穿透”防火墙,与服务器建立连接。具体是怎么“伪装”的,我稍后再讲。
下面我举几个 WebSocket 服务的例子,你看看,是不是和 HTTP 几乎一模一样:
ws://www.chrono.com
ws://www.chrono.com:8080/srv
wss://www.chrono.com:445/im?user_id=xxx
要注意的一点是WebSocket 的名字容易让人产生误解,虽然大多数情况下我们会在浏览器里调用 API 来使用 WebSocket但它不是一个“调用接口的集合”而是一个通信协议所以我觉得把它理解成“TCP over Web”会更恰当一些。
WebSocket 的帧结构
刚才说了WebSocket 用的也是二进制帧,有之前 HTTP/2、HTTP/3 的经验,相信你这次也能很快掌握 WebSocket 的报文结构。
不过 WebSocket 和 HTTP/2 的关注点不同WebSocket 更侧重于“实时通信”,而 HTTP/2 更侧重于提高传输效率,所以两者的帧结构也有很大的区别。
WebSocket 虽然有“帧”,但却没有像 HTTP/2 那样定义“流”也就不存在“多路复用”“优先级”等复杂的特性而它自身就是“全双工”的也就不需要“服务器推送”。所以综合起来WebSocket 的帧学习起来会简单一些。
下图就是 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 的帧头就四个部分:“结束标志位 + 操作码 + 帧长度 + 掩码”,只是使用了变长编码的“小花招”,不像 HTTP/2 定长报文头那么简单明了。
我们的实验环境利用 OpenResty 的“lua-resty-websocket”库实现了一个简单的 WebSocket 通信,你可以访问 URI“/38-1”它会连接后端的 WebSocket 服务“ws://127.0.0.138-0”用 Wireshark 抓包就可以看到 WebSocket 的整个通信过程。
下面的截图是其中的一个文本帧因为它是客户端发出的所以需要掩码报文头就在两个字节之外多了四个字节的“Masking-key”总共是 6 个字节。
而报文内容经过掩码不是直接可见的明文但掩码的安全强度几乎是零用“Masking-key”简单地异或一下就可以转换出明文。
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 格式的二进制帧了。
小结
浏览器是一个“沙盒”环境,有很多的限制,不允许建立 TCP 连接收发数据,而有了 WebSocket我们就可以在浏览器里与服务器直接建立“TCP 连接”,获得更多的自由。
不过自由也是有代价的WebSocket 虽然是在应用层但使用方式却与“TCP Socket”差不多过于“原始”用户必须自己管理连接、缓存、状态开发上比 HTTP 复杂的多,所以是否要在项目中引入 WebSocket 必须慎重考虑。
HTTP 的“请求 - 应答”模式不适合开发“实时通信”应用,效率低,难以实现动态页面,所以出现了 WebSocket
WebSocket 是一个“全双工”的通信协议,相当于对 TCP 做了一层“薄薄的包装”,让它运行在浏览器环境里;
WebSocket 使用兼容 HTTP 的 URI 来发现服务但定义了新的协议名“ws”和“wss”端口号也沿用了 80 和 443
WebSocket 使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;
WebSocket 利用 HTTP 协议实现连接握手,发送 GET 请求要求“协议升级”,握手过程中有个非常简单的认证机制,目的是防止误连接。
课下作业
WebSocket 与 HTTP/2 有很多相似点,比如都可以从 HTTP/1 升级,都采用二进制帧结构,你能比较一下这两个协议吗?
试着自己解释一下 WebSocket 里的”Web“和”Socket“的含义。
结合自己的实际工作,你觉得 WebSocket 适合用在哪些场景里?
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。