You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

150 lines
12 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 38 | WebSocket沙盒里的TCP
在之前讲TCP/IP协议栈的时候我说过有“TCP Socket”它实际上是一种功能接口通过这些接口就可以使用TCP/IP协议栈在传输层收发数据。
那么你知道还有一种东西叫“WebSocket”吗
单从名字上看“Web”指的是HTTP“Socket”是套接字调用那么这两个连起来又是什么意思呢
所谓“望文生义”大概你也能猜出来“WebSocket”就是运行在“Web”也就是HTTP上的Socket通信规范提供与“TCP Socket”类似的功能使用它就可以像“TCP Socket”一样调用下层协议栈任意地收发数据。
![](https://static001.geekbang.org/resource/image/ee/28/ee6685c7d3c673b95e46d582828eee28.png)
更准确地说“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字节看着好像很复杂实际非常简单。
![](https://static001.geekbang.org/resource/image/29/c4/29d33e972dda5a27aa4773eea896a8c4.png)
开头的两个字节是必须的,也是最关键的。
第一个字节的第一位“**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.1/38-0”用Wireshark抓包就可以看到WebSocket的整个通信过程。
下面的截图是其中的一个文本帧因为它是客户端发出的所以需要掩码报文头就在两个字节之外多了四个字节的“Masking-key”总共是6个字节。
![](https://static001.geekbang.org/resource/image/c9/94/c91ee4815097f5f9059ab798bb841594.png)
而报文内容经过掩码不是直接可见的明文但掩码的安全强度几乎是零用“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。
![](https://static001.geekbang.org/resource/image/8f/97/8f007bb0e403b6cc28493565f709c997.png)
服务器收到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格式的二进制帧了。
![](https://static001.geekbang.org/resource/image/84/03/84e9fa337f2b4c2c9f14760feb41c903.png)
## 小结
浏览器是一个“沙盒”环境有很多的限制不允许建立TCP连接收发数据而有了WebSocket我们就可以在浏览器里与服务器直接建立“TCP连接”获得更多的自由。
不过自由也是有代价的WebSocket虽然是在应用层但使用方式却与“TCP Socket”差不多过于“原始”用户必须自己管理连接、缓存、状态开发上比HTTP复杂的多所以是否要在项目中引入WebSocket必须慎重考虑。
1. HTTP的“请求-应答”模式不适合开发“实时通信”应用效率低难以实现动态页面所以出现了WebSocket
2. WebSocket是一个“全双工”的通信协议相当于对TCP做了一层“薄薄的包装”让它运行在浏览器环境里
3. WebSocket使用兼容HTTP的URI来发现服务但定义了新的协议名“ws”和“wss”端口号也沿用了80和443
4. WebSocket使用二进制帧结构比较简单特殊的地方是有个“掩码”操作客户端发数据必须掩码服务器则不用
5. WebSocket利用HTTP协议实现连接握手发送GET请求要求“协议升级”握手过程中有个非常简单的认证机制目的是防止误连接。
## 课下作业
1. WebSocket与HTTP/2有很多相似点比如都可以从HTTP/1升级都采用二进制帧结构你能比较一下这两个协议吗
2. 试着自己解释一下WebSocket里的”Web“和”Socket“的含义。
3. 结合自己的实际工作你觉得WebSocket适合用在哪些场景里
欢迎你把自己的学习体会写在留言区,与我和其他同学一起讨论。如果你觉得有所收获,也欢迎把文章分享给你的朋友。
![unpreview](https://static001.geekbang.org/resource/image/4b/5b/4b81de6b5c57db92ed7808344482ef5b.png)