看起来服务器主动发消息给客户端的场景,是怎么做到的?
使用 HTTP 不断轮询
怎么样才能在用户不做任何操作的情况下,网页能收到消息并发生变更。
最常见的解决方案是,网页的前端代码里不断定时发 HTTP 请求到服务器,服务器收到请求后给客户端响应消息。
这其实时一种「伪」服务器推的形式。
它其实并不是服务器主动发消息到客户端,而是客户端自己不断偷偷请求服务器,只是用户无感知而已。
用这种方式的场景也有很多,最常见的就是扫码登录。
登录页面二维码出现之后,前端网页根本不知道用户扫没扫,于是不断去向后端服务器询问,看有没有人扫过这个码。而且是以大概 1 到 2 秒的间隔去不断发出请求,这样可以保证用户在扫码后能在 1 到 2 秒内得到及时的反馈,不至于等太久。
但这样,会有两个比较明显的问题:
- 当你打开 F12 页面时,你会发现满屏的 HTTP 请求。虽然很小,但这其实也消耗带宽,同时也会增加下游服务器的负担。
- 最坏情况下,用户在扫码后,需要等个 1~2 秒,正好才触发下一次 HTTP 请求,然后才跳转页面,用户会感到明显的卡顿。
使用起来的体验就是,二维码出现后,手机扫一扫,然后在手机上点个确认,这时候卡顿等个 1~2 秒,页面才跳转。
那么问题又来了,有没有更好的解决方案?
有,而且实现起来成本还非常低。
长轮询
HTTP 请求发出后,一般会给服务器留一定的时间做响应,比如 3 秒,规定时间内没返回,就认为是超时。
如果我们的 HTTP 请求将超时设置的很大,比如 30 秒,在这 30 秒内只要服务器收到了扫码请求,就立马返回给客户端网页。如果超时,那就立马发起下一次请求。
这样就减少了 HTTP 请求的个数,并且由于大部分情况下,用户都会在某个 30 秒的区间内做扫码操作,所以响应也是及时的。
像这种,在用户不感知的情况下,服务器将数据推送给浏览器的技术,就是所谓的服务器推送技术,它还有个毫不沾边的英文名,comet 技术,大家听过就好。
上面提到的两种解决方案(不断轮询和长轮询),本质上,其实还是客户端主动去取数据。
对于像扫码登录这样的简单场景还能用用。但如果是网页游戏呢,游戏一般会有大量的数据需要从服务器主动推送到客户端。这就需要WebSocket 了。
WebSocket是什么
我们知道 TCP 连接的两端,同一时间里,双方都可以主动向对方发送数据。这就是所谓的全双工。
而现在使用最广泛的HTTP/1.1,也是基于TCP协议的,同一时间里,客户端和服务器只能有一方主动发数据,这就是所谓的半双工。
也就是说,好好的全双工 TCP,被 HTTP/1.1 用成了半双工。
这是由于 HTTP 协议设计之初,考虑的是看看网页文本的场景,能做到客户端发起请求再由服务器响应,就够了,根本就没考虑网页游戏这种,客户端和服务器之间都要互相主动发大量数据的场景。
所以,为了更好的支持这样的场景,我们需要另外一个基于TCP的新协议。于是新的应用层协议WebSocket就被设计出来了。
虽然名字带了个socket,但其实 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\n
101 确实不常见,它是指协议切换。
之后,浏览器也用同样的公开算法将base64码转成另一段字符串,如果这段字符串跟服务器传回来的字符串一致,那验证通过。
就这样经历了一来一回两次 HTTP 握手,WebSocket就建立完成了,后续双方就可以使用 webscoket 的数据格式进行通信了。
先进行三次TCP握手建立连接,再在HTTP请求时根据需要看是否带上特殊的header头。
WebSocket的消息格式
数据包在WebSocket中被叫做帧,我们来看下它的数据格式长什么样子。
我们只需要关注下面这几个字段。
opcode字段:这个是用来标志这是个什么类型的数据帧。比如。
- 等于 1 ,是指text类型(string)的数据包
- 等于 2 ,是二进制数据类型([]byte)的数据包
- 等于 8 ,是关闭连接的信号
payload字段:存放的是我们真正想要传输的数据的长度,单位是字节。比如你要发送的数据是字符串"111",那它的长度就是3。
另外,可以看到,存放payload 长度的字段有好几个,我们既可以用最前面的7bit, 也可以用后面的7+16bit 或 7+64bit。
WebSocket会用最开始的7bit做标志位。不管接下来的数据有多大,都先读最先的7个bit,根据它的取值决定还要不要再读个 16bit 或 64bit。
- 如果最开始的7bit的值是 0~125,那么它就表示了 payload 全部长度,只读最开始的7个bit就完事了。
- 如果是126(0x7E)。那它表示payload的长度范围在 126~65535 之间,接下来还需要再读16bit。这16bit会包含payload的真实长度。
- 以此类推
为防止粘包,WebSocket的数据格式也是数据头(内含payload长度) + payload data 的形式。
- TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,需要使用支持全双工的 WebSocket 协议。
- 在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送(comet)的效果。
- 对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。
- WebSocket 和 socket 几乎没有任何关系,只是叫法相似。
- 正因为各个浏览器都支持 HTTP协议,所以 WebSocket 会先利用HTTP协议加上一些特殊的 header 头进行握手升级操作,升级成功后就跟 HTTP 没有任何关系了,之后就用 WebSocket 的数据格式进行收发数据。