共计 7452 个字符,预计需要花费 19 分钟才能阅读完成。
(如果您正巧因为首页推荐的功能点进此文章,由衷地建议您先回顾往期内容,这将有助您接下来的阅读体验。)
在上一篇文章中,我们一起回顾了本地存储的知识点,了解了 sessionStorage
和localStorage
的异同点。
在本篇文章中我们将会一起学习WebSocket
,并结合真实的面试题,掌握它。
WebSocket API 是一种先进的技术,可在 用户浏览器和服务器之间开启双向交互式通信会话。利用该 API,可以向服务器发送信息,并接收事件驱动的响应,而无需轮询服务器以获得回复。——MDN
它的定义我们一样可以拆成 2 部分来关注:
- 可在浏览器和服务器之间开启 双向交互式通信会话:双向?顾名思义,客户端(浏览器)可以向服务端发送消息,服务端也可以向客户端发送消息。
- 无需轮询服务器 以获得回复:为什么在
WebSocket
的介绍里会提一嘴轮询服务器的操作?看起来WebSocket
好像是作为更好的解决方案,替代轮询服务器 的方式,从而解决了某种业务需求?
带着由试图理解定义而产生的这些困惑,我们直接来做一道面试题:
WebSocket
与HTTP
有什么不同?
WebSocket
与 HTTP
有什么不同?
首先,它们都是 通信协议 ,我们在之前的文章中也提到过协议,比如file://
协议:
HTTP
:不加密:http://
,加密后:https://
WebSocket
:不加密:ws://
,加密后:wss://
OSI 七层模型
说得在细致一点,那就是它们都是 应用层 的协议。这里我们回顾一下 OSI(Open System Interconnect)七层模型:
每一层的数据都是不一样的,每一层用到的协议也都是不一样的,都聊到这里了,我们就把每一层回顾一下:
- 物理层:
- 功能:建立物理连接,实现比特流的传输
- 数据形式:比特流(
0
和1
的序列)
- 通信协议:属于是“物理意义”上的协议了,比如:光纤、电缆、双绞线、网卡等等。
- 数据链路层:
- 功能:将比特封装成数据帧,在相邻节点之间的线路上无差错地传送以帧为单位的数据,每一帧包括数据和必要的控制信息。
- 数据形式:帧(
Frame
),通常大小小于或等于MTU
(最大传输单元)(以太网标准是1500
字节)
- 通信协议:以太网协议(IEEE 802.3)、Wi-Fi(IEEE 802.11)、点对点协议(PPP)等。
- 网络层:
- 功能:分割和重新组合数据,使其变为数据包,然后基于网络层地址(也就是 IP 地址)实现不同网络之间的路径选择和数据包传输。
- 数据形式:数据包(
Packet
),即拆开帧头部和帧尾部,取出帧的数据部分。
- 通信协议:互联网协议(IP)、互联网控制消息协议(ICMP)、开放最短路径优先(OSPF)等。
- 传输层
- 功能:提供端到端的数据传输服务,确保数据的正确性和可靠性。
- 数据形式:段(
Segment
,对于 TCP)或数据报(Datagram
,对于 UDP)。
- 通信协议:传输控制协议(TCP)、用户数据报协议(UDP)等。
- 会话层:
-
功能:建立、管理和终止会话,比如我们在一个外卖 APP 上选择付款时,会自动跳到另一个付款 APP 上,这其实就是建立会话的行为。
-
数据形式:会话数据
-
通信协议:安全套接字层(SSL)、传输层安全性(TLS)
-
- 表示层:
-
功能:确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
-
数据形式:是对应用层上数据的编码形式,比如对图片使用 PNG、JPEG 编码、对音频使用 MP3、WAV 编码、对视频使用 MP4、AVI 编码。
-
通信协议:LPP、NBSSP 等等
-
- 应用层:
- 功能:为应用软件提供网络服务。
- 数据形式:各种应用数据,比如我们平常在软件中遇到的那些 文字 、 图片 、 视频 、 语音 等,都是数据在应用层的展现形式。
- 通信协议:超文本传输协议(HTTP)、文件传输协议(FTP)、远程登录协议(TELNET)、WebSocket 等等
好,回顾完毕之后,我们就要继续讲二者的区别。刚刚其实讲的是相同点。
全双工通信和半双工通信
接下来最明显的不同就是,WebSocket
能够实现服务器和客户端的 双向通信 ,而HTTP
是“单向”的,只能是客户端请求服务器,然后接收服务器返回的数据,而 不能够让服务器主动向客户端发送消息。
如果我们期望利用 HTTP
实现某些数据的实时更新,我们就需要通过 轮询 的操作,按照某个设置好的周期时间,不断地从客户端向服务端发送请求,从而获取数据,比如:
- 在线游戏:多个玩家的状态要实时同步
- 交通物流:路况信息肯定也要实时同步
- 金融交易:股票、外汇、期货市场的数据要实时同步
并且这种轮询并不是没有代价的,它 存在延迟 ,而且频繁的请求还会产生 不必要的网络流量 和服务器负载。
-
浪费资源 :轮询会导致大量的
HTTP
请求,而这些请求中,又有很多请求 返回空响应,- 就拿聊天软件来说,可能俩人有时候互相
10
秒钟就能发一条消息,此时通过轮询发起的大部分HTTP
请求返回的都是携带新消息的响应,没问题。但是当另一人可能过了很久,30
分钟还没有发消息,此时轮询操作还是会一直发起HTTP
请求,这时候,这段时间内发起的请求全都返回空响应。
浪费了服务器的资源和网络带宽。
- 就拿聊天软件来说,可能俩人有时候互相
-
延迟:轮询间隔决定了客户端感知服务器更新的最大延迟。间隔太短会导致资源浪费,太长则会导致用户体验不佳。
-
不实时:轮询无法提供真正的实时通信,因为客户端必须在等待间隔结束后才能检查更新。
而 WebSocket
不同,一旦 WebSocket
连接建立,数据就可以在服务器和客户端之间自由地、实时地 双向传输 ,我们称这种通信模式为: 全双工 通信模式。这里我们进行一个小扩展,也许面试官会追问一下。
- 单工:数据只能单向传输,一旦数据开始流动,它就不能改变方向。比如广播电台,只能是广播电台向听众发送信息,听众通过收音机接收信息。
- 半双工:数据可以双向传输,但是不能同时双向传输,比如对讲机,当有一方在说话时,另一方必须等待,只有对方说话完毕后,才能够回应。
- 全双工:数据能够双向传输,并且能够同时双向传输,比如打电话,双方能够同时说话以及听对方说话。
全双工 通信模式允许服务器不仅可以响应客户端的请求,还可以主动向客户端推送消息,极大地增强了通信的实时性和互动性。
为什么
WebSocket
就不会造成资源浪费?
-
减少请求次数:
- 在轮询中,客户端需要定期发送请求来检查是否有新数据,这可能导致大量无效的 HTTP 请求。
WebSocket
通过 持久连接 减少了请求次数。
- 在轮询中,客户端需要定期发送请求来检查是否有新数据,这可能导致大量无效的 HTTP 请求。
-
降低服务器负载:
- 由于
WebSocket
连接是 持久的,服务器不需要为每个客户端请求都进行完整的处理流程,从而降低了服务器的负载。
- 由于
-
按需推送:
- 服务器只在有新数据时才发送消息,而不是周期性地响应空查询,这减少了网络带宽和 CPU 资源的浪费。
-
控制消息推送:
- 服务器可以根据需要控制消息的推送,例如,只有在特定事件发生时才推送通知,这样可以更有效地利用资源。
这样看来,使用 WebSocket
确实能解决不少轮询服务器的弊端呢。那么在上面的内容,我们也看到了一个词被频繁地提及,那就是“持久性”,这就是我们接下来要聊的内容了:
持久性连接和非持久性连接
WebSocket:
- 持久连接 :
WebSocket
提供全双工通信通道,一旦建立连接,客户端和服务器之间就可以持续地进行数据交换,直到任意一方显式地关闭连接。
HTTP:
- 非持久连接 :传统的
HTTP
连接是无状态的,每次请求都需要建立新的连接,服务器在响应请求后即关闭连接。 - 请求 - 响应模式 :
HTTP
遵循请求 - 响应模式,客户端发送请求,服务器响应请求,然后关闭连接。
当我们查看一些讲 WebSocket
和HTTP
以及 TCP
的关系图时,我们往往能看到这样一张图:
我们会发现,HTTP
中会有一部分内容伸到了 WebSocket
里面,这是为什么呢?
因为当我们想要建立 WebSocket
与服务器之间的连接时,我们其实还是需要发起一次 HTTP
请求的。
这个请求用于将协议升级,从而正式建立 WebSocket
与服务器之间的连接。比如 豆包 AI,当我们使用它时,输入信息后,打开控制台,可以在【网络】标签页看到这样一个请求:
然后我们再看一下这个请求的请求头的内容:
最后,我们可以这个请求的响应头和状态码,可以看到状态码是101
,代表协议升级
不过这里是直接使用 wss://
协议发起连接的,现代浏览器和 WebSocket
客户端库内置了对 WebSocket
协议的支持,包括复杂的握手过程。这使得开发人员在使用时无需手动处理底层的 HTTP
握手细节,只需调用简单的构造函数并提供 URL
即可建立连接。
WebSocket
的使用
基本用法
使用 WebSocket()
构造函数可以创建一个实例对象,具体用法如下:
const myWebSocket = new WebSocket("ws://127.0.0.1:9501");
该构造函数接收 2 个参数:
-
url
: 目标WebSocket
服务器连接的 URL。URL 必须使用以下方案之一:ws、wss、http 或 https。 -
protocols
【可选】:一个字符串或字符串数组,代表客户端希望使用的子协议。如果省略,则默认使用空数组,即[]
。单个服务器可以实现多个
WebSocket
子协议,并根据指定的值处理不同类型的交互。允许的值是那些在Sec-WebSocket-Protocol
HTTP 头(响应头和请求头中均有)中指定的值。WebSocket
子协议 通常是特定应用程序或服务定义的,用于在WebSocket
连接上实现特定的通信协议。例如,知名的子协议有mqtt
(用于物联网通信)、xmpp
(用于即时消息传递)和soap
或wamp
(用于远程过程调用和发布 / 订阅消息传递)。- 这里我们看到豆包 AI 的子协议是
pbbp2
,这估计是他们内部定义的私有子协议。
需要注意的是,直到与服务器完成 协议协商 (如:确定使用哪些子协议、扩展等)后,连接才会建立。然后可以从
WebSocket.protocol
这个实例属性中读取选定的协议。
现在,我们已经知道了如何创建一个 WebSocket
实例对象,那接下来我们就要聊一聊它的生命周期了,这同样也是一道面试题。
请解释
WebSocket
的生命周期,包括如何建立连接、发送消息、断开连接的过程。
- 建立连接:客户端通过发送一个
HTTP Upgrade
请求与服务器协商升级协议至WebSocket
。 - 发送消息:一旦连接建立,客户端和服务器可以实时地互相发送消息。
- 断开连接:任何一方都可以发送一个关闭帧来终止连接,对方接收到关闭帧后也会发送一个关闭帧作为响应,然后连接被关闭。
生命周期
MDN 文档上并没有特别指明这一点,我们可以去 RFC 文档上看看,RFC 6455是 WebSocket
协议的官方规范,详细描述了 WebSocket
的生命周期,包括握手、消息交换、心跳 ping/pong 帧以及连接关闭等过程,是最权威的 WebSocket
生命周期描述资源。
当我们阅读此文档,可以根据章节标题,将 WebSocket
的生命周期分为以下三个部分:
-
开启握手 (
Opening Handshake
)、 建立连接 (Connection Establish
):一旦与服务器建立了连接(包括通过代理或通过 TLS 加密隧道建立的连接),客户端必须向服务器发送信息,示意开启握手。握手包括一个HTTP
协议升级请求,以及一系列必需的和可选的头部字段。同样的,服务端也要回应客户端,当客户端向服务器建立
WebSocket
连接时,服务器必须完成以下步骤以接受连接并发送服务器的开启握手:- 如果连接发生在
HTTPS
(基于TLS
的HTTP
)端口上,那么在连接上执行TLS
握手。如果握手失败(例如,客户端在扩展客户端问候的“server_name
”字段中指示了一个服务器不托管的主机名),那么关闭连接;否则,连接的所有后续通信(包括服务器的握手)必须通过加密隧道进行。 - 服务器可以执行额外的客户端认证,例如,通过返回带有相应
|WWW-Authenticate|
头字段的401
状态码。 - 服务器可以使用
3xx
状态码重定向客户端。注意,这一步可以与上面描述的可选认证步骤一起发生,也可以在其之前或之后发生。 - 验证一些信息,比如检查客户端握手中的
|Origin|
头字段,检查客户端的握手中的|Sec-WebSocket-Key|
头字段等等。 - 最后,如果服务端允许建立连接,则它必须以有效的
HTTP
响应进行回复,比如带有101
响应代码的状态行(表示协议升级)、值为“websocket
”的|Upgrade|
头字段、值为“Upgrade
”的|Connection|
头字段以及一个|Sec-WebSocket-Accept|
头字段。
- 如果连接发生在
-
发送和接收数据 (
Sending and Receiving Data
): 在WebSocket
中,数据以 数据帧 的形式进行传输,数据帧 可以在建立连接时的握手阶段完成之后,且在该端点发送 关闭帧 之前,由客户端或服务器随时传输。- 数据帧:
- 数据帧:
-
关闭握手 、 关闭连接(
Closing the Connection
):要关闭 WebSocket 连接,端点需要关闭底层的 TCP 连接。端点应该使用一种能干净地关闭 TCP 连接的方法,如果适用,还包括 TLS 会话,并丢弃可能已经接收到的任何剩余字节。在必要时,端点可以通过任何可用的方式关闭连接,例如在遭受攻击时。在大多数正常情况下,应该首先由服务器关闭底层的 TCP 连接,这样它就会处于
TIME_WAIT
状态,而不是客户端(因为这将阻止客户端在 2 个最大段生命周期(2MSL)内重新打开连接,而服务器作为TIME_WAIT
连接在新SYN
到来时立即重新打开,并不会受到影响)。在异常情况下(例如在合理的时间内没有从服务器接收到 TCP 关闭)客户端可以发起 TCP 关闭。因此,当服务器被指示关闭
WebSocket
连接时,它应该立即发起 TCP 关闭,而当客户端被指示执行相同的操作时,它应该等待来自服务器的 TCP 关闭。-
关闭连接中 :启动
WebSocket
关闭握手 ,端点必须发送一个 关闭控制帧 ,关闭帧就和我们在上文展示的数据帧一样,设置状态码为/code/
,关闭原因为/reason/
。一旦端点既发送又接收了关闭控制帧,它应该 按照规定的方式 (也就是这一小节一开始的内容,服务端和客户端处理关闭时的不同方式)关闭WebSocket
连接。当发送或接收关闭控制帧后,可以说
WebSocket
关闭握手已启动,并且WebSocket
连接处于CLOSING
状态。 -
彻底关闭连接 :当底层 TCP 连接关闭时,可以说
WebSocket
连接已关闭,并且WebSocket
连接处于CLOSED
状态。如果WebSocket
关闭握手完成后 TCP 连接被关闭,则说WebSocket
连接被 干净地关闭。
-
事件
聊完了生命周期,我们一起来看看 WebSocket
相关的事件。
- close:
WebSocket.onclose
属性返回一个事件监听器,这个事件监听器将在 WebSocket 连接的readyState
变为CLOSED
时被调用,它接收一个名字为“close”的CloseEvent
事件。
const myWebSocket = new WebSocket("ws://127.0.0.1:9501");
myWebSocket.onclose = (event) => {
// 检查连接是否是 干净关闭 的
// 这也是我们在聊生命周期的时候提到过的
if (event.wasClean) {
console.log(
"Connection closed cleanly, code=" +
event.code +
"reason=" +
event.reason
);
} else {
// 连接可能因为某些错误或网络问题而没有明确关闭
console.log(
"Connection died, code=" + event.code + "reason=" + event.reason
);
}
// 这里可以执行一些清理工作或尝试重新连接
};
- error:当
WebSocket
的连接由于一些错误事件的发生 (例如无法发送一些数据) 而被关闭时,一个error
事件将被引发。myWebSocket.onerror = function (event) { console.error( "WebSocket encountered error:", event.message ? event.message : "Unknown error" ); };
- message:
message
事件会在WebSocket
接收到新消息时被触发。myWebSocket.onmessage = function (event) { console.log("Received message:" + event.data); // 处理从服务器接收到的数据 };
- open:当与
WebSocket
建立连接时,会触发“open”事件。
心跳
在之前的章节里,我们提到了 WebSocket
的一个特点,那就是 持久性连接 。持久性是一个很好的特性,但是也是一个 需要我们去维护的特性,在遇到一些异常情况的时候,比如网络不稳定,可能连接就会中断了。
因此当我们使用 WebSocket
时,我们要选择一些合适的解决方案,去维护持久性连接。
安全
-
安全性:
WebSocket
使用ws://
和wss://
(WebSocket Secure)协议,后者是加密的。在使用WebSocket
时,必须确保使用wss://
以保证数据传输的安全性,避免中间人攻击。 -
跨域问题:
WebSocket
也存在跨域通信的问题,需要在服务器端设置适当的CORS
策略。- 在开启握手时,我们需要定义
Origin
头部字段,Origin
头部字段用于防止恶意脚本在浏览器中使用WebSocket API
进行未经授权的 跨源访问WebSocket
服务器。服务器会被告知发起WebSocket
连接请求的脚本来源。如果 服务器不想接受来自该来源的连接 ,可以选择通过发送适当的HTTP
错误代码来 拒绝连接。这个头部字段通常由浏览器客户端发送;对于非浏览器客户端,如果在其上下文中有意义,也可以发送这个头部字段。
- 在开启握手时,我们需要定义
在本篇文章中,我们学习了 WebSocket
,并将其和HTTP
对比,从各个方面了解了WebSocket
,比如它的特点、它的事件、它的安全性等等。
在学习这些通信协议的过程中,我们也回顾了经典的 OSI
七层模型,并且简单介绍了一下传输层的一些协议,比如 TCP
和UDP
。
其实这俩协议也有很多内容可以聊,比如 TCP
的 3 次握手 和 4 次挥手 、TCP
和UDP
有什么不同,以及 TCP
的特点(可靠的、有序的、基于字节流的),什么是“粘包”等等。有机会的话,会在网络专题的文章中和大家一起聊聊,敬请期待。