我们平时打开网页,比如购物网站。他们都点击列出的产品,然后跳转到网页以获取产品详细信息。从HTTP协议的角度来说,就是在网页上点击一个按钮,前端发送一个HTTP请求,网站返回一个HTTP响应。这种客户端主动请求,服务端响应的方式也满足了大部分网页的功能场景。但是你有没有注意到,在这种情况下,服务器从来不会主动给客户端发送消息。就像你喜欢的女孩从来不会主动找你一样。但如果现在,当你浏览网页时,右下角突然弹出一个小广告,提醒你【只能一个人在家偷偷玩】。求知、好学、勤奋,这些刻在你DNA里的东西被激活了。您点击了解。长相平平的顾某提醒大家“道士有9条狗,全服横着走”。影帝牟辉先生对你说,如果你是兄弟,就来杀我吧。都来了,你选择一个角色,进入游戏界面。这时,一个小怪物从远处跑来,然后疯狂地拿着木棍抽打着你。您没有单击鼠标一次。服务器会自动不断的给你发送怪物的移动数据和攻击数据。这……太暖心了。搬家之后,问题就来了,在服务端好像主动给客户端发消息的场景下怎么办?在真正回答这个问题之前,先说说一些相关的知识背景。事实上,使用HTTP不断轮询的痛点是如何在用户不做任何事情的情况下接收消息和对网页进行更改。最常见的解决方案是网页前端代码不断定时向服务器发送HTTP请求,服务器收到请求后响应给客户端。这实际上是一种伪服务器推送的形式。其实并不是服务端主动向客户端发送消息,而是客户端自己不断暗中请求服务端,而用户却浑然不觉。使用这种方式的场景有很多,最常见的就是扫码登录。比如某信公众号平台,在登录页面出现二维码后,前端网页不知道用户有没有扫过,就一直向后台服务器询问有没有人扫过码。而且请求是每隔1~2秒左右持续发送请求,保证了用户在扫码后1~2秒内能及时得到反馈,不会等待太久。但是这样一来,就会出现两个明显的问题。打开F12页面,会发现满屏都是HTTP请求。虽然小,但这其实很消耗带宽,也增加了下游服务器的负担。最坏的情况下,用户扫描二维码后,需要等待1~2秒,才会触发下一次HTTP请求,然后页面被重定向,用户会感觉到明显的卡顿。使用体验是,出现二维码后,用手机扫描,然后在手机上点击确认。这时候你会等1~2秒再跳转到页面。那么问题又来了,有没有更好的解决办法呢?是的,实施它的成本非常低。对于长轮询,我们知道HTTP请求发出后,服务器一般会留出一定的响应时间,比如3s,如果在规定的时间内没有返回,则认为超时。如果我们的HTTP请求设置了很大的超时时间,比如30s,在30s以内,只要服务端收到扫码请求,就会立即返回客户端网页。如果超时,立即发起下一次请求。这样减少了HTTP请求的次数,而且由于大多数情况下,用户会在一定的30s间隔内扫码,响应也比较及时。比如某度的云网盘就是这样做的。所以你会发现,一旦你扫码,在手机端点击确认,电脑端的网页就会秒跳,非常好的体验。真是一石二鸟。这种发起请求并长时间等待服务器响应的机制就是所谓的长训练回合机制。在我们常用的消息队列RocketMQ中,消费者取数据的时候也会用到这个方法。这样,服务器在用户不知情的情况下向浏览器推送数据的技术就是所谓的服务器推送技术。它还有一个和它无关的英文名字,comettechnology,只要是大家都听说过的。上面提到的两种方案,本质上都是客户端主动去取数据。也可以用于扫码登录等简单场景。但是如果是页游,游戏一般会有大量的数据需要主动从服务端推送到客户端。这就不得不说到websocket了。什么是网络套接字?我们知道TCP连接的两端。同时,双方可以主动向对方发送数据。这称为全双工。现在使用最广泛的HTTP1.1也是基于TCP协议的。同时,只有client和server中的一个可以主动发送数据,这就是所谓的半双工。换句话说,一个好的全双工TCP被HTTP用作半双工。为什么?这是因为在设计HTTP协议之初,考虑的是看网页文本的场景。客户端发起请求,然后服务器响应就够了。根本不考虑网页游戏。是需要主动向对方发送大量数据的场景。所以为了更好的支持这样的场景,我们需要另外一个基于TCP的新协议。于是设计了新的应用层协议websocket。不要被名字误导了。虽然名字里有socket,但是socket和websocket其实和雷峰、雷峰塔一样,几乎没有关系。如何建立websocket连接我们平时浏览网页,一般都是在浏览器上。一段时间后,我们使用HTTP协议。当我们打开网页游戏时,我们必须切换到我们新推出的游戏。网络套接字协议。为了兼容这些使用场景。浏览器通过TCP三次握手建立连接后,首先使用HTTP协议进行通信。如果此时是普通的HTTP请求,那么双方就继续使用普通的HTTP协议,以同样的方式进行交互。这是毫无疑问的。如果此时要建立websocket连接,就会在HTTP请求中带上一些特殊的header。Connection:UpgradeUpgrade:websocketSec-WebSocket-Key:T2a6wZlAwhgQNqruZ2YUyg==\r\n这些headers表示浏览器要升级协议(Connection:Upgrade),要升级到websocket协议(Upgrade:websocket)。同时带上一个随机生成的base64编码(Sec-WebSocket-Key)发送给服务器。如果服务器刚好支持升级到websocket协议。它会经过websocket的握手过程,同时根据客户端生成的base64编码,用一个公共的算法改变另外一个字符串,放到HTTP响应的Sec-WebSocket-Accept头中,并且同时带上101状态码,发送给浏览器。HTTP/1.1101SwitchingProtocols\r\nSec-WebSocket-Accept:iBJKv/ALIW2DobfoA4dmr3JHBCY=\r\nUpgrade:websocket\r\nConnection:Upgrade\r\nhttpstatuscode=200(正常响应),你见多了。101确实不常见,其实是指协议切换。之后,浏览器也使用相同的公共算法将base64编码转换成另一个字符串。如果该字符串与服务器返回的字符串一致,则验证通过。这样经过两次HTTP握手,websocket就建立起来了,双方就可以使用websocket的数据格式进行通信了。对于websocket抓包,我们可以使用wireshark抓一个包,实际看看数据包的情况。上图中,注意第2445行红框的消息是websocket的第一次握手,表示发起了一个带有特殊Header的HTTP请求。上图中红框的4714行报文是服务器在拿到第一次握手后响应的第二次握手。可以看到这也是一个HTTP类型的消息,返回的状态码是101。同时可以看到返回的消息头中还包含了各种websocket相关的信息,比如Sec-WebSocket-Accept。上图是全貌。从截图中的注释可以看出,websocket和HTTP都是基于TCP的协议。经过三次TCP握手后,使用HTTP协议升级为websocket协议。你可能在网上看到一句话:“websocket是一种基于HTTP的新协议”。其实这是不对的,因为websocket只是在建立连接的时候用到了HTTP,升级完成之后就和HTTP没有关系了。就像你喜欢的女生通过你要了你大学室友的微信,然后她们自己聊了起来。你能说这个女孩是通过你和你的室友交流的吗?不能。你和HTTP一样,只是一个工具人。这有点“后门下蛋”的意思。上面提到的websocket的消息格式,协议升级完成后,两端都会使用websocket的数据格式进行通信。数据包在websockets中称为帧。让我们看看它的数据格式是什么样的。这里有很多字段,但是我们只需要关注下面几个。opcode字段:这个用来标记这是什么类型的数据帧。例如。等于1时表示文本类型(string)的数据包等于2,等于二进制数据类型([]byte)的数据包时等于8。关闭连接的信号有效载荷字段:它存储了我们真正要传输的数据的长度。单位是字节。比如你要发送的数据是字符串“111”,那么它的长度就是3。另外可以看到我们有几个字段用来存储payload的长度。我们可以使用前7bit,也可以使用后7+16bit或者7+64bit。那么问题来了。我们知道,在数据层面,每个人都是一个01二进制流。我怎么知道什么情况下应该读7bit,什么情况下应该读7+16bit?websocket将使用前7位作为标志。不管后面的数据有多大,先读取前7位,再根据其值决定再读取16bit还是64bit。如果前7位的值为0~125,则表示payload的全长,只读前7位即可。如果是126(0x7E)。那么就是说payload的长度范围是126到65535,然后需要读取16位。这16位将包含有效负载的实际长度。如果是127(0x7F)。那么说明payload的长度范围>=65536,接下来需要读取64位。这个64位将包含有效载荷的长度。这可以容纳2的64字节数据的次方,这绝对足以转换为数TB。Payloaddatafield:这里存放的是实际要传输的数据。知道上面payload的长度后,就可以根据这个值截取相应的数据了。有没有注意到一个小细节,websocket的数据格式也是数据头(包括payload长度)+payloaddata的形式。前面写的《既然有HTTP协议,为什么还要有RPC》中提到,TCP协议本身是全双工的,但是如果直接使用纯裸TCP传输数据,就会出现粘包的“问题”。为了解决这个问题,上层协议一般采用报文头+报文体的格式,对要发送的数据进行重新打包。消息头一般包含消息体的长度,通过它可以截获真正的消息体。HTTP协议和大部分RPC协议,还有我们今天介绍的websocket协议,都是这样设计的。Websocket使用场景Websocket完美继承了TCP协议的全双工能力,也贴心地提供了解决粘包的方法。适用于大多数需要服务端和客户端(浏览器)频繁交互的场景。比如网页/小程序游戏,网页聊天室,还有一些像飞书这样的网页协同办公软件。回到文章开头的问题,在使用websocket协议的网页游戏中,怪物移动和攻击玩家是服务端逻辑产生的,对玩家的伤害等数据需要服务端主动发送给客户端,客户端获取数据后显示相应的效果。总结一下,TCP协议本身是全双工的,但是我们最常用的HTTP1.1,虽然是基于TCP的协议,但是是半双工的。不适用于大多数需要服务端主动向客户端推送数据的场景。友好,所以我们需要使用支持全双工的websocket协议。在HTTP1.1中。只要客户端不询问,服务器就不会回答。基于此特性,对于登录页面等简单场景,可以采用定时轮询或长轮询的方式来实现服务端推送(comet)的效果。对于需要客户端和服务端频繁交互的复杂场景,比如网页游戏,可以考虑websocket协议。Websocket和socket几乎没有任何关系,只是叫法相似而已。正因为所有的浏览器都支持HTTP协议,所以websocket会先使用HTTP协议加上一些特殊的header来进行握手升级操作。升级成功后与HTTP无关,再使用websocket的数据格式收发数据。.
