Skip to content

WebSocket技术

更新: 2025/2/24 字数: 0 字 时长: 0 分钟

平时我们打开网页,例如购物网站,点击商品就跳转到了商品的详情页,从HTTP协议的角度来看,就是前端发送一次HTTP请求,网站返回一次HTTP响应,这种由客户端主动请求服务器响应的方式也满足大部分网页的功能场景。

20230816103521

服务器推送

**有人可能会发现,在这种情况下,服务器从来就不会主动给客户端发一次消息。假设有个场景需求,在我们没有任何操作的情况下,服务器需要主动将数据传输过来,也就是服务器主动推送。**实现服务器主动推送有两种技术方案:AJAX轮询、WebSocket连接(WS连接)。

AJAX轮询

AJAX轮询:在用户无感知的情况下,网页的前端代码不断定时发送HTTP请求到服务器,服务器收到请求后响应消息。本质上,它其实不是服务器主动发消息到客户端,而是客户端不断请求服务器。

20230816105306

短轮询

**在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器,这就是短轮询机制。**使用短轮询的场景有很多,最常见的就是扫码登录,例如微信公众平台,二维码出现之后,前端网页不知道用户扫没扫,于是不断向后端服务器询问,大概1到2秒的间隔去不断发出请求,这样可以保证用户在扫码后能得到及时的反馈。

20230816105957

?> 提示:使用短轮询会有两个比较明显的问题,第一就是不断的发送请求会消耗带宽,同时增加下游服务器的负担,第二就是用户在扫码后需要等待1到2秒触发下一次HTTP请求,然后才跳转页面,用户会感到明显的卡顿。

长轮询

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

20230816110945

**像这种发起一个请求,在较长时间内等待服务器响应的机制,就是长轮询机制。**百度云网盘就是这么干的,所以你会发现手机扫码确认后,电脑端网页就秒跳转,体验很好。

20230816111735

?> 提示:常用的消息队列rocketMQ中,消费者取数据也用到了长轮询。

WS连接

Ajax轮询本质上还是客户端主动取数据,对扫码这种简单场景还能用用,但如果是网页游戏,这种有大量数据从服务器主动推送到客户端的,就得依靠WebSocket连接(WS连接)。

了解协议

首先我们知道TCP连接的两端,同一时间里双方都可以主动向对方发送数据,这就是所谓的“全双工”。

20230816113557

现在使用最广泛的HTTP1.1也是基于TCP协议的,同一时间里客户端和服务器只能有一方发送数据,这就是所谓的“半双工”。

20230816113907

也就是说,好好的TCP全双工被HTTP弄成了半双工形式,为什么不直接使用TCP传输数据呢?原因是直接使用TCP传输数据会有“粘包”的问题,例如你发送了2条时间间隔比较短的数据,如果没有协议对数据进行格式上的处理,那么网络层就可能将2个包打成一个包进行发送。为了解决这个问题,就要设计一种协议使用“消息头”+“消息体”的格式去重新包装要发的数据,而消息头里一般含有消息体的长度,通过这个长度可以去截取真正的消息体,这就是HTTP协议诞生的原因。但是HTTP协议设计之初,只考虑到了网页文本的场景,做到客户端发起请求再由服务器响应就够了,没有考虑网页游戏这种客户端和服务器之间都要互相主动发送大量数据的场景,所以为了支持这样的场景,我们需要一个基于TCP的新协议,于是新的应用层协议WebSocket诞生了。

20230816141052

?> 提示:HTTP协议、RPC协议、WebSocket协议都是将数据包装成“消息头”+“消息体”的格式,因此它们都可以解决粘包的问题。

WS协议

**WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议,它使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。**平时我们使用浏览器浏览网页,这时候使用的是HTTP协议,假如打开了网页游戏,这时候就得使用WebSocket协议,为了兼容这些使用场景,浏览器在TCP三次握手建立连接之后,都统一使用HTTP协议先进行一次通信,如果此时是普通的HTTP请求,那后续客户端和服务器会继续使用HTTP协议进行交互,如果此时是想建立WebSocket连接,客户端就会在HTTP请求里带上一些特殊的header头:

Connection: Upgrade  表明客户端想升级协议
Upgrade: websocket  表明客户端想升级成WebSocket协议
sec-websocket-key: T2a6wZIAwhgQNqruZ2YUyg  随机生成BASE64码

如果服务器正好支持WebSocket协议就会走WebSocket握手流程,同时根据客户端生成的BASE64码使用公开的算法变成另一段字符串,放在响应头的 Sec-WebSocket-Accept 中,并同时带上101状态码:

HTTP/1.1 101 Switching Protocols  状态码101指协议切换
Sec-WebSocket-Accept: iBJKv/ALIW2DobfoA4dmr3JHBCY=
Upgrade: websocket
Connection: Upgrade

浏览器得到响应后,也用同样使用公开的算法将BASE64码转成另一段字符串,如果这段字符串和服务器响应头中 Sec-WebSocket-Accept 的字符串一样,那验证通过,后续客户端和服务器都使用WebSocket协议进行交互。总结一下,进行WebSocket连接需要在经历了3次TCP握手之后,利用HTTP升级为WebSocket协议,后续双方创建持久性的WS连接,使用WebSocket格式数据进行双向数据传输。总之,WebSocket协议完美继承了TCP协议的全双工能力,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯,还提供了解决粘包的方案,它适用于需要服务器和客户端频繁交互的大部分场景,比如小程序游戏、网页游戏、网页聊天室、协同办公软件。

20230816135929

?> 提示:别被WebSocket名字带偏了,虽然名字中带有Socket,但其实Socket和WebSocket之间就与雷峰与雷峰塔一样,二者名称接近但毫无关系。

!> 注意:有种说法是WebSocket协议是基于HTTP的新协议,这其实并不对,因为WebSocket只有在建立连接时才用到了HTTP,升级完成之后就和HTTP没有任何关系了。

格式数据

上面提到建立WS连接之后,就会使用WebSocket格式数据进行通信,也就是说不同的协议发送信息类型以及所使用的格式是不一样的。具体如下:

  • HTTP协议是无状态、无连接的,也就是说每一个访问都是独立的,服务器处理完一个访问就断开连接,然后处理下一个新的访问,所发送的信息都是文本类型的,使用的格式有纯文本、json、xml等;
  • WebSocket协议是一个长连接协议,也就是客户端会等待服务器发送数据,当客户端接收到数据后也不会立马断开连接,就一直连接着,每个一段时间服务器会发送一个心跳包来确定客户端是否在线,如果客户端掉线或者客户端自己主动断开连接,服务器才会断开连接,其所发送的信息基本都是字节码类型的,使用的格式有protobuf(用于游戏,例如王者荣耀)、TLV(通用格式)。

底层实现

现在我们来讲WebSocket底层实现,首先需要创建一个WebSocket对象才能去使用,具体使用如下:

javascript
var Socket = new WebSocket(url, [protocol] );
/* 只读属性 readyState 表示连接状态,可以是以下值:
0 - 表示连接尚未建立。
1 - 表示连接已建立,可以进行通信。
2 - 表示连接正在进行关闭。
3 - 表示连接已经关闭或者连接不能打开。*/
Socket.readyState

// onopen连接建立时触发事件
Socket.onopen
// onmessage客户端接收服务端数据时触发事件
Socket.onmessage
// onerror通信发生错误时触发事件
Socket.onerror
// onclose连接关闭时触发事件
Socket.onclose

// send客户端发送给服务端数据时触发
Socket.send()
// close关闭连接
Socket.close()

!> 注意:WebSocket对象在我们的本地JS环境中的V8引擎是没有的。另外,WebSocket本身就是一个关键词。

WebSocket实战

蝌蚪聊天室

现在我们来抓包一个名称为蝌蚪聊天室的网站,进去后我们就是一个小蝌蚪的样子,这个网站就使用了WebSocket连接。针对WebSocket进行抓包就必须从连接的时候开始抓,也就是生成WebSocket对象那里开始抓起,所以打开F12后,必须要刷新页面,让其重新生成WebSocket对象。

20230816173225

页面刷新后,我们选择Network选项中的WS(WebSocket简称)选项,可以看到里面有一个WebSocket包,因为它是长连接,所以只有一个连接包。点击包后选择Messages选项,里面就可以看到客户端和服务器之间实时交互的数据,例如图中有红色向下箭头的就是服务器发送给客户端的数据,而绿色向上箭头的就是客户端发送给服务器的数据:

20230816173052

我们来看WebSocket包头部信息,只看特殊的具体如下:

# url不是以http、https开头,而是以ws开头,表明是WebSocket协议包
ws://kedou.workerman.net:8280/
# 101状态码表明协议切换
101 Switching Protocols

# 这4个请求头信息表明了当前的连接要升级成WebSocket
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: 23kVps/nKZzJ4g5gIFl2Xg==(key值可以随便写)
Sec-WebSocket-Version: 13(版本值必须和服务器版本值一致)
Upgrade: websocket(升级成websocket协议,固定值)

# 这4个响应头信息表明服务器已准备好进行Websocket连接
Connection: Upgrade
Sec-Websocket-Accept: lUgZCuJ0rmjgs0jBS26ht/qiiJk=(Websocket连接标识符,客户端需要保存)
Sec-Websocket-Version: 13
Upgrade: websocket

20230816174612

先来跟踪信息发送位置,全局搜索一下 .send( 发送信息的关键词,发现再 WebSocketServer.js 文件中出现了三处,我们都打上断点。每当我把鼠标移动到屏幕上时,第126行就断住了,里面就是一些明文的鼠标位置信息、性别信息等,说明此处就是发送基本信息的位置:

20230817141511

接着我在频道中发送信息,按下空格键,输入信息,回车发送时,断点断在了143行,里面的内容就是我要发送的明文信息,说明此处就是发送喊话信息的位置:

20230817142325

消息发出去后,我们回到请求当中,搜索一下我们发出的消息,可以看到结果中有一条我们发出的消息记录,还有一条服务器发送给我们的消息,这一条应该是服务器广播给所有的信息,表示我发送了消息:

20230817142858

最后我们来定位一下信息接收的位置,全局搜索一下 .onSocketMessage 接收消息的关键词,发现有一处使用,我们跳转过去就看到这里定义了 webSocket 对象,以及下面的一些触发事件,其中 webSocket.onSocketMessage 接收消息的触发事件由 app.onSocketMessage 进行赋值,搜索一下当前有两处使用:

20230817143827

我们来到另一处使用 app.onSocketMessage 地方,发现是一个函数,在其内部打上断点,立刻就断住了服务器发送过来的附近玩家的明文消息,说明此处就是接收服务器消息的位置:

20230817144610

由于整个流程发送的信息都是明文,至此整个网页的分析到此结束。

虎牙直播