使用 HTTP 不断轮询
3.9 既然有 HTTP 协议,为什么 还要有 WebSocket ?
使用 HTTP 不断轮询
其实问题的痛点在于,怎么样才能在用户不做任何 操作的情况下,网页能收到消息并发生变更。
最常见的解决方案是,网页的前端代码里不断定时发 HTTP 请求到服务器, 服务器收到请求后给
客户端响应消息。
这其实时一种「伪」服务器推的形式 。
它其实并不是服务器主动发消息到客户端,而是客户端自己不断偷偷 请求服务器, 只是用户无感
知而已。
用这种方式的场景也有很多,最常见的就是扫码登录。
比如,某信公众号平台,登录页面二维码出现之后,前端网页根本不知道用户扫没扫,于是不断
去向后端服务器询问,看有没有人扫过这 个码。而且是以大概 1 到 2 秒的间隔 去不断发出请求,
这样可以保 证用户在扫码后能在 1 到 2 秒内得到及时的反馈,不至于等太久。
使用HTTP 定时轮询
但这样,会有两个 比较明显 的问题:
当你打开 F12 页面时,你会 发现满屏的 HTTP 请求。虽然很小,但这其实也消耗带宽,同时也
会增加下游服务器的负担。
最坏情况下,用户在扫码后,需要等个 1~2 秒,正好才触发下一次 HTTP 请求,然后才跳转页
面,用户会感到明显 的卡顿。
使用起来的体验就是,二维码出现后,手机扫一扫,然后在手机上点个确认,这时候卡顿等个 1~2
秒,页面才跳转。

有,而且实现起来成本还非常低。
长轮询
我们知道,HTTP 请求发出后,一般会给服务器留一定的时间做响应,比如 3 秒,规定时间内没返
回,就认为是超时。
如果我们的 HTTP 请求将超时设置的很大,比如 30 秒,在这 30 秒内只要服务器收到了扫码请
求,就立⻢返回给客户端网页。如果超时,那就立⻢发起下一次请求。
这样就减少了 HTTP 请求的个数,并且由于大部分情况下,用户都会在某个 30 秒的区间内做扫码
操作,所以响应也是及时的。

比如,某度云网盘就是这么干的。所以你会 发现一扫码,手机上点个确认,电脑端网页就秒跳
转,体验很好。

像这种发起一个请求,在较长时间内等待服务器响应的机制,就是所谓的长训轮机制。我们常用
的消息队列 RocketMQ 中,消费者去取 数据时,也用到了这种方式。

像这种,在用户不感知的情况下,服务器将数据推 送给浏览器的技术,就是所谓的服务器推送技
术,它还有个毫不沾边的英文名,comet 技术,大家听过就好。
上面提到的两种解决方案(不断轮询和长轮询),本质上,其实还是客户端主动去取 数据。
对于像扫码登录这样的简单场景还能用用。但如果是网页游戏呢,游戏一般会有大量的数据需要
从服务器主动推送到客户端。
这就得说下 WebSocket 了。
WebSocket 是什么
我们知道 TCP 连接的两端,同一时间里,双方都可以主动向对方发送数据。这就是所谓的全双
工。
而现在使用最广泛的 HTTP/1.1 ,也是基于TCP 协议的,同一时间里,客户端和服务器只能有一方
主动发数据,这就是所谓的半双 工。
也就是说,好好 的全双工 TCP ,被 HTTP/1.1 用成了半双 工。
为什么 ?
这是由于 HTTP 协议设计 之初,考虑的是看看 网页文本的场景,能做到客户端发起请求再由服务器
响应,就够了,根本就没考虑网页游戏这种,客户端和服务器之间都要互相主动发大量数据的场
景。
所以,为了 更好的支持这样的场景,我们需要另外一个基于TCP 的新协议。
于是新的应用层协议WebSocket 就被设计 出来了。
但其实 socket 和 WebSocket 之间,就跟雷峰和雷峰塔一样,二者接近毫无关系。

怎么建立WebSocket 连接
我们平时刷网页,一般都是在浏览器上刷的,一会刷刷 图文,这时候用的是 HTTP 协议,一会打
开网页游戏,这时候就得切换成我 们新介绍的 WebSocket 协议。
为了 兼容这些使用场景。浏览器在 TCP 三次握手建立连接之后,都统一使用 HTTP 协议先进行一
次通信。
如果此时是 普通的 HTTP 请求,那后续双方就还是老样子继续用普通 HTTP 协议进行交互 ,这
点没啥疑问。
如果这时候是想建立 WebSocket 连接,就会在 HTTP 请求里带上一些特殊的header 头,如
下:
Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg==\r\n这些 header 头的意思是,浏览器想升级协议(Connection: Upgrade ),并且想升级成
WebSocket 协议(Upgrade: WebSocket )。同时带上一段随机生成的 base64 码(Sec-WebSocket-Key ),发给服务器。
如果服 务器正好支持升级成 WebSocket 协议。就会走 WebSocket 握手流程,同时根据客户端生
成的 base64 码,用某个公开的算法变成另一段字符串,放在 HTTP 响应的 Sec-WebSocket-
Accept 头里,同时带上 101 状态码 ,发回给浏览器。HTTP 的响应如下:
HTTP/1.1 101 Switching Protocols\r\n
Sec-WebSocket-Accept: iBJKv/ALIW2DobfoA4dmr3JHBCY=\r\n
Upgrade: WebSocket\r\n
Connection: Upgrade\r\nHTTP 状态码=200 (正常响应)的情况,大家见得多了。101 确实不常见,它其实是指协议切换。

之后,浏览器也用同样的公开算法将 base64 码 转成另一段字符串,如果这段字符串跟服务器传回
来的字符串一致,那验证通过。

就这样经历了一来一回两次 HTTP 握手,WebSocket 就建立完成了,后续双方就可以使 用
webscoket 的数据格式进行通信了。

WebSocket 抓包
我们可以用wireshark 抓个包,实际看下数据包的情况。

上面这张图,注意画了红框的第 2445 行报文,是WebSocket 的第一次握手,意思是发起了一次带
有 特殊 Header 的HTTP 请求。

上面这个图里画了红框的 4714 行报文,就是服务器在得到第一次握手后,响应的第二次握手,可
以看到这也是个 HTTP 类型的报文,返回的状态码是 101 。同时可以看到返回的报文 header 中也
带有各种 WebSocket 相关的信息,比如 Sec-WebSocket-Accept 。

上面这张图就是全貌了,从截图上的注释可以看出,WebSocket 和HTTP 一样都是基于TCP 的协议。
经历了三 次TCP 握手之后,利用 HTTP 协议升级为 WebSocket 协议。
你在网上可能会看到一种说法:"WebSocket 是基于HTTP 的新协议",其实这并不对,因为
WebSocket 只有在建立连接时才用到了HTTP ,升级完成之后就跟HTTP 没有任何 关系了。
这就好像你喜欢的女生通过你要到了你大学室 友的微信,然后他们自己就聊起来了。你能说这个
女生是通过你去跟你室友沟通的吗?不能。你跟HTTP 一样,都只是个工具人。
这就有点"借壳生蛋"的那意思。
HTTP 和WebSocket 的关系
WebSocket 的消息格式
上面提到在完成协议升级之后,两端就会用webscoket 的数据格式进行通信。
数据包在WebSocket 中被叫做帧,我们来看下它的数据格式长什么 样子。

这里面字段很多,但我们只需要关注下面这几个。
opcode 字段:这个是用来标志这是个什么 类型的数据帧。比如。
等于 1 ,是指text 类型( string )的数据包
等于 2 ,是二进制数据类型( []byte )的数据包等于 8 ,是关闭连接的信号
payload 字段:存放的是我们真正想要传输的数据的长度,单位是字节。比如你要发送的数据是
字符串 "111" ,那它的长度就是 3 。

另外,可以看到,我们存放** payload 长度的字段有好几个** ,我们既可以用最前面的 7bit , 也
可以用后面的 7+16bit 或 7+64bit 。
那么问题就来了。
我们知道,在数据层面,大家都是 01 二进制流。我怎么知道什么 情况下应该读 7 bit ,什么 情况下
应该读7+16bit 呢?
WebSocket 会用最开始的7bit 做标志位。不管接下来的数据有多大,都先读最先的7个bit ,根据它
的取值决定还要不要再读个 16bit 或 64bit 。
如果 最开始的 7bit 的值是 0~125 ,那么它就表示了 payload 全部长度,只读最开始的 7个bit
就完事了 。

如果是 126 (0x7E ) 。那它表示payload 的长度范围在 126~65535 之间,接下来还需要再读
16bit 。这16bit 会包含payload 的真实长度。

如果是 127 (0x7F ) 。那它表示payload 的长度范围 >=65536 ,接下来还需要再读64bit 。这64bit 会包含payload 的长度。这能放2的64 次方byte 的数据,换算一下好多 个TB ,肯定够用了。

payload data 字段:这里存放的就是真正要传输的数据,在知道了上 面的payload 长度后,就可以
根据这个值去截取对应的数据。
大家有没有发现一个小细节,WebSocket 的数据格式也是数据头(内含payload 长度) + payload data 的形式 。
这是因为 TCP 协议本身就是全双工,但直接使用纯裸TCP 去传输数据,会有粘包的"问题"。为了 解
决这个问题,上层协议一般会用消息头+消息体的格式去重新包装要发的数据。
而消息头里一般含有消息体的长度,通过这 个长度可以去截取真正的消息体。
HTTP 协议和大部分 RPC 协议,以及我们今天介绍的WebSocket 协议,都是这样设计 的。

WebSocket 的使用场景
WebSocket 完美继承了 TCP 协议的全双工能力,并且还贴心的提供了解决粘包的方案。
它适用于需要服务器和客户端(浏览器) 频繁交互 的大部分场景,比如网页/小程序游戏,网页聊
天室,以及一些类似⻜书这样的网页协同办公软件。
回到文章开头的问题,在使用 WebSocket 协议的网页游戏里,怪物移动以及攻击玩家的行为是服
务器逻辑产生的,对玩家产生的伤害等数据,都需要由服务器主动发送给客户端,客户端获得数
据后展示对应的效果。

总结
TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1 ,虽然是基于 TCP 的协议,但它是半双 工
的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支
持全双工的 WebSocket 协议。
在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单
场景,可以使 用定时轮询或者长轮询的方式实现服务器推送(comet) 的效果。对于客户端和服务端之间需要频繁交互 的复杂场景,比如网页游戏,都可以考虑使用
WebSocket 协议。
WebSocket 和 socket 几乎没有任何 关系,只是叫法相似。
正因为各个浏览器都支持 HTTP 协 议,所以 WebSocket 会先利用HTTP 协议加上一些特殊的
header 头进行握手升级操作,升级成功后就跟 HTTP 没有任何 关系了,之后就用 WebSocket 的
数据格式进行收发数据。
