漫谈WebSocket

写在前面

想到重新再去了解下WebSocket这个概念,其实是出于对某脚手架进行Network分析时偶然留意到的。

image-20210403173025232

于是乎就突然想到了去年新柚杯的考试系统在生产使用中遇到的一个问题,就想再回过头来吧WebSocket重新深入理解一下。

Server&client

众所周知,在校科协每年的招新题中,几乎每年都会出一个计网方向的题目,去年(大概是前年?)我记得就是解释TCP/IP的三次握手和四次挥手的具体过程。而去年的话,似乎硬硬的Web基础部分洛天依还是我也出了道问WebSocket协议的题目。

在说WebSocket之前,简单说一下HTTP通信的基本原理吧~(学过计网的人都懂吧)

目前的web应用的通讯过程,其实还是基于HTTP1.0的基础协议,一个Request 一个Response。也就是客户端通过浏览器向服务器发送一个http请求,其中会包括Http Header和具体的传递内容,具体的传递方式这里就不展开了,大家自己去了解。

这种一次请求一次回复的机制,在面对实时性要求高、高并发、以及需要用户实时响应的Web应用时候,会占用大量的带宽,同时伴随巨量的无效请求,甚至导致服务器瘫痪!(这是去年新柚杯考试系统在实际使用中出现的问题,尽管是因为CPU降频的原因,但是这确实不是一个高效的做法)

而WebSocket呢,它是HTML5下一种新的协议。

也有人说WWebSocket 根本不是 HTML5 的东西。

WebSocket 是一个协议,归属于 IETF。

WebSocket API 是一个 Web API,归属于 W3C。

两个规范是独立发布的。

广义上的 HTML5 里面包含的是 WebSocket API,并不是 WebSocket。简单的说,可以把 WebSocket 当成 HTTP,WebSocket API 当成 Ajax。

不过这里就简单都直接称WebSocket了.

WebSocket实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。

它最大的作用就是解决了HTTP长连接的问题。

WebSocket的特点

首先HTTP有1.1和1.0之说,也就是所谓的keep-alive,把多个HTTP请求合并为一个,但是归根到底本质上还是一个Request一个Response

Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的(注意啦!这是考点!圈起来QWQ),不能主动发起。这是HTTP的一个生命周期。

这和WebSocket还是有本质的区别。

Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。

过程实例

我们拿一个最典型的Websocket握手的建立过程为例(借用Ovear的例子):

1
2
3
4
5
6
7
8
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。
我会顺便讲解下作用。

1
2
Upgrade: websocket
Connection: Upgrade

这个就是Websocket的核心了,告诉Apache、Nginx等服务器:注意啦,窝发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。

1
2
3
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。不过现在还好,已经定下来啦:服务员,我要的是13 sui的噢→_→

然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~

1
2
Upgrade: websocket
Connection: Upgrade

这一块依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket。

然后,Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:好啦好啦,知道啦,给你看我的ID CARD来证明行了吧。。
后面的,Sec-WebSocket-Protocol 则是表示最终使用的协议。

至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。
具体的协议就不在这阐述了。

整体来看,建立一次WebSocket协议的过程最后的Headers内容如下:

拿**[写在前面]**中那个脚手架中的例子来看

image-20210403175608564

好吧……这上面一大堆bb其实就是解释了一下建立WebSocket链接时,它的Http Header的内容。

下面就谈下重点,WebSocket到底有个什么鬼用?说它是长连接,到底是个怎么长连接法?

为什么不用ajax轮询机制来实现?或者http long pull?

作用解析

曾几何时(这个词或许不能这么用),我们实现频繁的服务器-客户端通讯有ajax轮询机制,或者是http long pull机制。

首先是 ajax轮询 ,ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

场景复现:

客户端:啦啦啦,小服,有没有新的信息啊?(Request)

服务器:(高冷╭(╯^╰)╮)无。(Response)

客户端:啦啦啦,小服,有没有新的信息啊?(Request)

服务器:(傲娇( ̄_, ̄ ))无。(Response)

客户端:啦啦啦,小服,有没有新的信息啊?(Request)

服务器:(不屑(  ̄ー ̄))无。(Response)

客户端:啦啦啦,小服,有没有新的信息啊?(Request)

服务器:(无奈ㄟ( ▔, ▔ )ㄏ)没有。(Response)

客户端:啦啦啦,小服,有没有新的信息啊?(Request)

服务器:(人设崩塌-_-|||)你好烦啊,说了没有!!!(Response)

客户端:啦啦啦,小服,有没有新的信息啊?(Request)

服务器:( <(-︿-)>)。。。没有。。。(Response)

……Loop……

而http long pull,原理和ajax轮询类似,不过采取的是阻塞模型,也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

大概的场景是:

客户端:(默默拨打小F的电话),滴,滴,滴……对不起,您所拨打的电话暂时无法接通,请稍后再拨……(Request)

客户端:(默默拨打小F的电话),滴,滴,滴……对不起,您所拨打的电话暂时无法接通,请稍后再拨……(Request)

客户端:(默默拨打小F的电话),滴,滴,滴……对不起,您所拨打的电话暂时无法接通,请稍后再拨……(Request)

客户端:(默默拨打小F的电话),滴,滴,滴……对不起,您所拨打的电话暂时无法接通,请稍后再拨……(Request)

……经历了N次疯狂的电话轰炸后,终于打通了……

服务端:( εっ´`)=ε=′ ̄(<︶)↗[GO)ノ))☆.。ε ̄) ̄▽/(o(≦)O;)(๑°°‼(o°;)(Response)

通过以上两个机制,其实都体现了,http协议最大的一个特点:被动性这是考点!

何为被动性呢,其实就是,服务端不能主动联系客户端,只能有客户端发起。

说完这个,我们再来说一说上面的缺陷。

从上面很容易看出来,不管怎么样,上面这两种都是非常消耗资源的。

ajax轮询 需要服务器有很快的处理速度和资源。(速度)

long poll 需要有很高的并发,也就是说同时接待客户的能力。(场地大小)

所以ajax轮询 和long poll 都有可能发生这种情况:

客户端:啦啦啦啦,有人接电话吗?(Request)
服务端:对不起,您所拨打的电话暂时无法接通,请稍后再试。(503 Server Unavailable)
客户端:。。。。啦啦啦,有人接电话吗么?(Request)
服务端:对不起,您所拨打的电话暂时无法接通,请稍后再试。(503 Server Unavailable)


言归正传,我们说一下WebSocket的机制吧。

通过上面的例子,我们发现那两种长连接的方式都很消耗资源,归根到底是因为http协议的核心就是每一个Request对应一个Response,也就是说,服务器是一个渣👦,ta每天需要接待太多的客户,你和ta一次(那啥)连接之后,ta就把你忘了,下一次又要重新连接。

哦,尤其是重新建立连接的时候又要重新发送一遍HTTP Header头,每次都要互换一下个人信息重新认识(你说渣不渣?

所以在这种情况下,Websocket出现了,作为一个良心的好人他出现了。

首先,被动性考点,这里是第三遍了吧……),当服务器完成协议升级后(HTTP->Websocket),服务端就可以主动推送信息给客户端啦。

所以上面的场景可以变成这样:

客户端:啦啦啦,我要建立Websocket协议,需要的服务:chat,Websocket协议版本:13**(HTTP Request)**
服务端:ok,确认,已升级为Websocket协议**(HTTP Protocols Switched)**
客户端:麻烦你有信息的时候推送给我噢。。
服务端:ok,有的时候会告诉你的。
服务端:balabalabalabala
服务端:balabalabalabala
服务端:哈哈哈哈哈啊哈哈哈哈
服务端:笑死我了哈哈哈哈哈哈哈
客户端:……

恭喜你,在WebSocket协议下,只需要一次HTTP请求,就可以做到源源不断的信息传送了。

这和JavaScript里面的回调函数的机制类似,所谓回调机制,举个例子:

小Z和他新认识👧说:“回到家后记得打个电话给我。”

这里“打电话给我”就是一个回调函数,“回家后”是主函数。当主函数的达到了执行回调函数的时候才会去调用。

这样的协议解决了上面同步有延迟,而且还非常消耗资源的这种情况。
那么为什么他会解决服务器上消耗资源的问题呢?
其实我们所用的程序是要经过两层代理的,即HTTP协议在Nginx、Apache等服务器的解析后,然后再传送给相应的Handler(PHP、Java、Nodejs等)来处理。
简单地说,我们有一个非常快速的
接线员
(Nginx、Apache),他负责把问题转交给相应的客服(Handler)。

其实接线员往往性能和速度是足够的,但是由于HTTP是非状态性的,每次都要重新传输identity info(鉴别信息),来告诉接线员(服务端)你是谁,接线员还要一一发给后面的客服(服务端应用),一时间就无法有足够的客服来处理请求,这就导致诸如503错误之类的情况。

而WebSocket就解决了传统HTTP1.0和HTTP1.1中每次都要发送请求的弊端,直接跟接线员建立一个持久的链接,有信息的时候客服(服务器)想办法通知接线员,然后接线员在统一转交给客户。Websocket只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直知道你的信息,直到你关闭请求。

缺点?兼容性?

当然,虽然前文将HTTP协议称作渣男,但是,WebSocket也只是基于Http的一种优化罢了,这种优化同时也带来一些缺陷。

例如最重要的兼容性问题。

很多传统的老的非现代浏览器,IE7,IE8,IE9,或者很久的Android系统,对于WebSocket并不兼容。(当然咯,我相信你不会还在用XP系统吧,不会吧不会吧 -_-! )

另外,维系这种长连接,需要服务端进行一些兼容性配置,比如在Nginx下,如果你用Socket.io的话,在服务端推送这块有一个redis的adaptor,一行代码即可搭建服务端到客户端的推送集群。
如果是client连接服务端的负载的话可以在Nginx通过sticky session来实现。

具体的实现

至于WebSocket在Web开发中具体的实现,目前主流的脚手架都已经封装好了,除非你去手搓原生三剑客。如果是这样的话,那也许得好好吧计网的知识巩固巩固,什么数据帧的解析啊,连接的控制啊等等,这里就不展开了。

这里有一篇Blog细说了手搓WebSocket的具体流程,可以参考:细说WebSocket


好叭,今天就写到这里了,写完睡觉。。。(这是一个回调函数 O(∩_∩)O~~)