我所知道的HTML——WebSocketWebsocket是一个比较常用的Web API,它的出现解决了HTTP轮询方案

2,674次阅读
没有评论

共计 7452 个字符,预计需要花费 19 分钟才能阅读完成。

(如果您正巧因为首页推荐的功能点进此文章,由衷地建议您先回顾往期内容,这将有助您接下来的阅读体验。)

在上一篇文章中,我们一起回顾了本地存储的知识点,了解了 sessionStoragelocalStorage的异同点。

在本篇文章中我们将会一起学习WebSocket,并结合真实的面试题,掌握它。

WebSocket API 是一种先进的技术,可在 用户浏览器和服务器之间开启双向交互式通信会话。利用该 API,可以向服务器发送信息,并接收事件驱动的响应,而无需轮询服务器以获得回复。——MDN

它的定义我们一样可以拆成 2 部分来关注:

  • 可在浏览器和服务器之间开启 双向交互式通信会话:双向?顾名思义,客户端(浏览器)可以向服务端发送消息,服务端也可以向客户端发送消息。
  • 无需轮询服务器 以获得回复:为什么在 WebSocket 的介绍里会提一嘴轮询服务器的操作?看起来 WebSocket 好像是作为更好的解决方案,替代轮询服务器 的方式,从而解决了某种业务需求?

带着由试图理解定义而产生的这些困惑,我们直接来做一道面试题:

WebSocketHTTP有什么不同?

WebSocketHTTP有什么不同?

首先,它们都是 通信协议 ,我们在之前的文章中也提到过协议,比如file:// 协议:

  • HTTP:不加密:http://,加密后:https://
  • WebSocket:不加密:ws://,加密后:wss://

OSI 七层模型

说得在细致一点,那就是它们都是 应用层 的协议。这里我们回顾一下 OSI(Open System Interconnect)七层模型:

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

每一层的数据都是不一样的,每一层用到的协议也都是不一样的,都聊到这里了,我们就把每一层回顾一下:

  • 物理层:
    • 功能:建立物理连接,实现比特流的传输
    • 数据形式:比特流(01 的序列)
      我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
    • 通信协议:属于是“物理意义”上的协议了,比如:光纤、电缆、双绞线、网卡等等。
  • 数据链路层:
    • 功能:将比特封装成数据帧,在相邻节点之间的线路上无差错地传送以帧为单位的数据,每一帧包括数据和必要的控制信息。
    • 数据形式:帧(Frame),通常大小小于或等于 MTU(最大传输单元)(以太网标准是1500 字节)
      我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
    • 通信协议:以太网协议(IEEE 802.3)、Wi-Fi(IEEE 802.11)、点对点协议(PPP)等。
  • 网络层:
    • 功能:分割和重新组合数据,使其变为数据包,然后基于网络层地址(也就是 IP 地址)实现不同网络之间的路径选择和数据包传输。
    • 数据形式:数据包(Packet),即拆开帧头部和帧尾部,取出帧的数据部分。
      我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
    • 通信协议:互联网协议(IP)、互联网控制消息协议(ICMP)、开放最短路径优先(OSPF)等。
  • 传输层
    • 功能:提供端到端的数据传输服务,确保数据的正确性和可靠性。
    • 数据形式:段(Segment,对于 TCP)或数据报(Datagram,对于 UDP)。
      我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
    • 通信协议:传输控制协议(TCP)、用户数据报协议(UDP)等。
  • 会话层:
    • 功能:建立、管理和终止会话,比如我们在一个外卖 APP 上选择付款时,会自动跳到另一个付款 APP 上,这其实就是建立会话的行为。

    • 数据形式:会话数据

    • 通信协议:安全套接字层(SSL)、传输层安全性(TLS)

  • 表示层:
    • 功能:确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。

    • 数据形式:是对应用层上数据的编码形式,比如对图片使用 PNG、JPEG 编码、对音频使用 MP3、WAV 编码、对视频使用 MP4、AVI 编码。

    • 通信协议:LPP、NBSSP 等等

  • 应用层:
    • 功能:为应用软件提供网络服务。
    • 数据形式:各种应用数据,比如我们平常在软件中遇到的那些 文字 图片 视频 语音 等,都是数据在应用层的展现形式。
      我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
    • 通信协议:超文本传输协议(HTTP)、文件传输协议(FTP)、远程登录协议(TELNET)、WebSocket 等等

好,回顾完毕之后,我们就要继续讲二者的区别。刚刚其实讲的是相同点。

全双工通信和半双工通信

接下来最明显的不同就是,WebSocket能够实现服务器和客户端的 双向通信 ,而HTTP 是“单向”的,只能是客户端请求服务器,然后接收服务器返回的数据,而 不能够让服务器主动向客户端发送消息

如果我们期望利用 HTTP 实现某些数据的实时更新,我们就需要通过 轮询 的操作,按照某个设置好的周期时间,不断地从客户端向服务端发送请求,从而获取数据,比如:

  • 在线游戏:多个玩家的状态要实时同步
  • 交通物流:路况信息肯定也要实时同步
  • 金融交易:股票、外汇、期货市场的数据要实时同步

并且这种轮询并不是没有代价的,它 存在延迟 ,而且频繁的请求还会产生 不必要的网络流量 服务器负载

  • 浪费资源 :轮询会导致大量的HTTP 请求,而这些请求中,又有很多请求 返回空响应

    • 就拿聊天软件来说,可能俩人有时候互相 10 秒钟就能发一条消息,此时通过轮询发起的大部分 HTTP 请求返回的都是携带新消息的响应,没问题。但是当另一人可能过了很久,30分钟还没有发消息,此时轮询操作还是会一直发起 HTTP 请求,这时候,这段时间内发起的请求全都返回空响应。

    浪费了服务器的资源和网络带宽。

  • 延迟:轮询间隔决定了客户端感知服务器更新的最大延迟。间隔太短会导致资源浪费,太长则会导致用户体验不佳。

  • 不实时:轮询无法提供真正的实时通信,因为客户端必须在等待间隔结束后才能检查更新。

WebSocket 不同,一旦 WebSocket 连接建立,数据就可以在服务器和客户端之间自由地、实时地 双向传输 ,我们称这种通信模式为: 全双工 通信模式。这里我们进行一个小扩展,也许面试官会追问一下。

  • 单工:数据只能单向传输,一旦数据开始流动,它就不能改变方向。比如广播电台,只能是广播电台向听众发送信息,听众通过收音机接收信息。

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

  • 半双工:数据可以双向传输,但是不能同时双向传输,比如对讲机,当有一方在说话时,另一方必须等待,只有对方说话完毕后,才能够回应。

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

  • 全双工:数据能够双向传输,并且能够同时双向传输,比如打电话,双方能够同时说话以及听对方说话。

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
全双工 通信模式允许服务器不仅可以响应客户端的请求,还可以主动向客户端推送消息,极大地增强了通信的实时性和互动性。

为什么 WebSocket 就不会造成资源浪费?

  1. 减少请求次数

    • 在轮询中,客户端需要定期发送请求来检查是否有新数据,这可能导致大量无效的 HTTP 请求。WebSocket通过 持久连接 减少了请求次数。
  2. 降低服务器负载

    • 由于 WebSocket 连接是 持久的,服务器不需要为每个客户端请求都进行完整的处理流程,从而降低了服务器的负载。
  3. 按需推送

    • 服务器只在有新数据时才发送消息,而不是周期性地响应空查询,这减少了网络带宽和 CPU 资源的浪费。
  4. 控制消息推送

    • 服务器可以根据需要控制消息的推送,例如,只有在特定事件发生时才推送通知,这样可以更有效地利用资源。

这样看来,使用 WebSocket 确实能解决不少轮询服务器的弊端呢。那么在上面的内容,我们也看到了一个词被频繁地提及,那就是“持久性”,这就是我们接下来要聊的内容了:

持久性连接和非持久性连接

WebSocket:

  1. 持久连接 WebSocket 提供全双工通信通道,一旦建立连接,客户端和服务器之间就可以持续地进行数据交换,直到任意一方显式地关闭连接。

HTTP:

  1. 非持久连接 :传统的HTTP 连接是无状态的,每次请求都需要建立新的连接,服务器在响应请求后即关闭连接。
  2. 请求 - 响应模式 HTTP 遵循请求 - 响应模式,客户端发送请求,服务器响应请求,然后关闭连接。

当我们查看一些讲 WebSocketHTTP以及 TCP 的关系图时,我们往往能看到这样一张图:

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

我们会发现,HTTP中会有一部分内容伸到了 WebSocket 里面,这是为什么呢?

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

因为当我们想要建立 WebSocket 与服务器之间的连接时,我们其实还是需要发起一次 HTTP 请求的。

这个请求用于将协议升级,从而正式建立 WebSocket 与服务器之间的连接。比如 豆包 AI,当我们使用它时,输入信息后,打开控制台,可以在【网络】标签页看到这样一个请求:

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

然后我们再看一下这个请求的请求头的内容:

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

最后,我们可以这个请求的响应头和状态码,可以看到状态码是101,代表协议升级

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

不过这里是直接使用 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 头(响应头和请求头中均有)中指定的值。

    我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

    • WebSocket子协议 通常是特定应用程序或服务定义的,用于在 WebSocket 连接上实现特定的通信协议。例如,知名的子协议有 mqtt(用于物联网通信)、xmpp(用于即时消息传递)和soapwamp(用于远程过程调用和发布 / 订阅消息传递)。
    • 这里我们看到豆包 AI 的子协议是pbbp2,这估计是他们内部定义的私有子协议。

    需要注意的是,直到与服务器完成 协议协商 (如:确定使用哪些子协议、扩展等)后,连接才会建立。然后可以从WebSocket.protocol 这个实例属性中读取选定的协议。

现在,我们已经知道了如何创建一个 WebSocket 实例对象,那接下来我们就要聊一聊它的生命周期了,这同样也是一道面试题。

请解释 WebSocket 的生命周期,包括如何建立连接、发送消息、断开连接的过程。

  • 建立连接:客户端通过发送一个 HTTP Upgrade 请求与服务器协商升级协议至WebSocket
  • 发送消息:一旦连接建立,客户端和服务器可以实时地互相发送消息。
  • 断开连接:任何一方都可以发送一个关闭帧来终止连接,对方接收到关闭帧后也会发送一个关闭帧作为响应,然后连接被关闭。

生命周期

MDN 文档上并没有特别指明这一点,我们可以去 RFC 文档上看看,RFC 6455WebSocket 协议的官方规范,详细描述了 WebSocket 的生命周期,包括握手、消息交换、心跳 ping/pong 帧以及连接关闭等过程,是最权威的 WebSocket 生命周期描述资源。

当我们阅读此文档,可以根据章节标题,将 WebSocket 的生命周期分为以下三个部分:

  • 开启握手 Opening Handshake)、 建立连接 Connection Establish):一旦与服务器建立了连接(包括通过代理或通过 TLS 加密隧道建立的连接),客户端必须向服务器发送信息,示意开启握手。握手包括一个HTTP 协议升级请求,以及一系列必需的和可选的头部字段。

    同样的,服务端也要回应客户端,当客户端向服务器建立 WebSocket 连接时,服务器必须完成以下步骤以接受连接并发送服务器的开启握手:

    1. 如果连接发生在 HTTPS(基于TLSHTTP)端口上,那么在连接上执行 TLS 握手。如果握手失败(例如,客户端在扩展客户端问候的“server_name”字段中指示了一个服务器不托管的主机名),那么关闭连接;否则,连接的所有后续通信(包括服务器的握手)必须通过加密隧道进行。
    2. 服务器可以执行额外的客户端认证,例如,通过返回带有相应 |WWW-Authenticate| 头字段的 401 状态码。
    3. 服务器可以使用 3xx 状态码重定向客户端。注意,这一步可以与上面描述的可选认证步骤一起发生,也可以在其之前或之后发生。
    4. 验证一些信息,比如检查客户端握手中的 |Origin| 头字段,检查客户端的握手中的 |Sec-WebSocket-Key| 头字段等等。
    5. 最后,如果服务端允许建立连接,则它必须以有效的 HTTP 响应进行回复,比如带有 101 响应代码的状态行(表示协议升级)、值为“websocket”的 |Upgrade| 头字段、值为“Upgrade”的 |Connection| 头字段以及一个 |Sec-WebSocket-Accept| 头字段。
  • 发送和接收数据 Sending and Receiving Data): 在WebSocket 中,数据以 数据帧 的形式进行传输,数据帧 可以在建立连接时的握手阶段完成之后,且在该端点发送 关闭帧 之前,由客户端或服务器随时传输。

    • 数据帧:
      我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案
  • 关闭握手 关闭连接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 连接被 干净地关闭

我所知道的 HTML——WebSocketWebsocket 是一个比较常用的 Web API,它的出现解决了 HTTP 轮询方案

事件

聊完了生命周期,我们一起来看看 WebSocket 相关的事件。

  • closeWebSocket.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"
      );
    
    };
    
  • messagemessage 事件会在 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 七层模型,并且简单介绍了一下传输层的一些协议,比如 TCPUDP

其实这俩协议也有很多内容可以聊,比如 TCP 3 次握手 4 次挥手 TCPUDP有什么不同,以及 TCP 的特点(可靠的、有序的、基于字节流的),什么是“粘包”等等。有机会的话,会在网络专题的文章中和大家一起聊聊,敬请期待。

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-09-18发表,共计7452字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)