likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 70

往期回顾

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

前言

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

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

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”事件。
    • 用法:
       myWebSocket.onopen = function (event) {
        console.log("WebSocket is open now.");
        // 连接成功后,可以发送消息到服务器
        myWebSocket.send("Hello, Server!");
      };
      

心跳

在之前的章节里,我们提到了WebSocket的一个特点,那就是持久性连接。持久性是一个很好的特性,但是也是一个需要我们去维护的特性,在遇到一些异常情况的时候,比如网络不稳定,可能连接就会中断了。

因此当我们使用WebSocket时,我们要选择一些合适的解决方案,去维护持久性连接。

  • 重连机制:我们可以通过WebSocket的事件监听,在客户端采用重连的解决方案。

    • 自动重连:客户端检测到连接断开后,可以自动尝试重新连接到服务器。
    • 指数退避策略:重连尝试可以使用指数退避策略,即每次重连的等待时间逐渐增加,避免频繁的无效尝试。
  • 心跳保活

    • 发送心跳:客户端和服务器定期发送心跳消息(通常是简单的文本消息或特定的控制字符),以维持连接的活跃。
    • 检测心跳响应:如果客户端发送心跳消息后,在一定时间内没有收到服务器的响应,可以认为连接已经断开,并触发重连机制。
    • PingPing帧的opcode0x9,可以在建立连接后和关闭连接前的任何时间发送。收到Ping帧后,除非已经收到关闭帧,否则必须发送Pong帧作为响应,并且应该尽快回复。
    • PongPong帧的opcode0xA,其“应用数据”部分必须与对应的Ping帧相同。如果收到多个Ping帧但还未回复Pong帧,可以选择只对最后一个Ping帧发送Pong帧。此外,Pong帧也可以未经请求主动发送,作为单向的心跳检查,此时不需要回复。

安全

  • 安全性WebSocket 使用 ws://wss://(WebSocket Secure)协议,后者是加密的。在使用 WebSocket 时,必须确保使用 wss:// 以保证数据传输的安全性,避免中间人攻击。

    • TSL(Transport Layer Security,传输层安全)加密是一种安全协议,用于在互联网上提供通信的安全性。它的前身是SSL(Secure Sockets Layer)。TSL加密的原理可以分为几个关键步骤:

      1. 握手阶段:当客户端(例如浏览器)与服务器建立连接时,它们会进行一个握手过程。在这个过程中,双方会交换安全参数和加密密钥。
      2. 密钥交换:在这个阶段,客户端和服务器会协商一个共同的加密方法,并生成一对密钥——一个公钥和一个私钥。公钥可以公开分享,而私钥必须保密。
      3. 证书验证:服务器会向客户端提供一个证书,这个证书包含了服务器的公钥和由第三方信任的证书颁发机构(CA)签发的数字签名。客户端使用CA的公钥来验证服务器的证书,确保它是在与正确的服务器通信。
      4. 加密通信:一旦密钥交换完成,并且证书验证通过,客户端和服务器会使用这些密钥来加密它们之间的所有通信。即使数据被截获,没有私钥的第三方也无法解密。
      5. 数据完整性:除了加密,TSL还提供数据完整性保护。它使用哈希函数来创建数据的唯一指纹,确保在传输过程中数据没有被篡改。
  • 跨域问题WebSocket 也存在跨域通信的问题,需要在服务器端设置适当的 CORS 策略。

    • 在开启握手时,我们需要定义Origin头部字段,Origin头部字段用于防止恶意脚本在浏览器中使用WebSocket API进行未经授权的跨源访问 WebSocket服务器。服务器会被告知发起WebSocket连接请求的脚本来源。如果服务器不想接受来自该来源的连接,可以选择通过发送适当的HTTP错误代码来拒绝连接。这个头部字段通常由浏览器客户端发送;对于非浏览器客户端,如果在其上下文中有意义,也可以发送这个头部字段。

结语

在本篇文章中,我们学习了WebSocket,并将其和HTTP对比,从各个方面了解了WebSocket,比如它的特点、它的事件、它的安全性等等。

在学习这些通信协议的过程中,我们也回顾了经典的OSI七层模型,并且简单介绍了一下传输层的一些协议,比如TCPUDP

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

转载自:https://juejin.cn/post/7409503443004653583
评论
请登录