3.9既然有HTTP协议,为什么还要有WebSocket?
前端发一次HTTP请求,网站返回一次HTTP响应,这种客户端主动请求,服务器响应的方式也满足大部分网页的功能场景。
另外一种,看起来服务器主动发消息给客户端的场景,是怎么做到的?
使用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/1.1协议设计之初,考虑的是看看网页文本的场景,能做到客户端发起请求再由服务器响应就够了,根本没考虑网页游戏这种,客户端和服务器之间都要主动发大量数据的场景。为了更好的支持这样的场景,我们需要另外一种基于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
Upgrade:WebSocket\r\n
Connection:Upgrade\r\n
HTTP状态码=200(正常响应),101表示协议切换。
之后,浏览器也用同样的公开算法将base64码转成另一段字符串,如果这段字符串跟服务器传回来的字符串一致,那验证通过。经历一来一回两次HTTP握手,WebSocket就建立完成了,后续双方就可以使用WebSocket的数据格式进行通信了。
WebSocket抓包
使用Wireshark抓个包,实际看下数据包的情况:
WebSocket第一次握手,发起一次带有特殊Header的HTTP请求。
上图是服务器在得到第一次握手后,响应的第二次握手,可以看出这也是个HTTP类型的报文,返回的状态码是101,同时返回的报文header中也带有各种WebSocket相关的信息,比如Sec-WebSocket-Accept.
WebSocket和HTTP一样都是基于TCP的协议,经历了三次握手之后,利用HTTP协议升级为WebSocket。
“WebSocket是基于HTTP的新协议”这一说法并不正确,因为WebSocket只有在建立连接时才用到HTTP,升级完成之后跟HTTP没有任何关系了。
WebSocket消息格式
数据包在WebSocket中被叫做帧。
opcode字段:标志数据帧的类型
- 1,是指text类型(string)的数据包
- 2,是二进制数据类型([]byte)的数据包
- 8,是关闭连接的数据包
payload字段:真正想要传输的数据的长度,单位是字节。比如你要发送的数据是字符串“111”,那它的长度就是3.
存放payload长度的字段有好几个,我们既可以用最前面的7bit,也可以用后面的7+16bit或7+64bit.
WebSocket会用最开始的7bit作为标志位,不管接下来的数据有多大,都先读最先的7bit,根据它的取值决定还要不要再读个16bit或64bit.
- 如果最开始的7bit的值是0-125,则它就表示payload全部长度,只读最开始的7bit即可
- 如果是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全双工,最常用的基于TCP的HTTP/1.1半双工,对于大部分需要服务器主动推送数据到客户端的场景不太友好,因此需要使用支持全双工的WebSocket协议。
- HTTP/1.1,只要客户端不问,服务端就不答。对于登录页面只要简单的场景,可以使用定时轮询或长轮询的方式实现服务器推送(comet)效果。
- 对于客户端和服务器之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用WebSocket协议
- WebSocket和socket几乎没有任何关系,只是叫法相似
- 正因为各个浏览器都支持HTTP协议,所以WebSocket会先利用HTTP协议加上一些特殊的header头进行握手升级操作,升级成功后就跟HTTP没有任何关系了,之后就用WebSocket的数据格式进行收发数据