你不知道的 WebSocket


本文阿宝哥将从多个方面下手,全方位带你一起探究 WebSocket 技能。阅览完本文,你将了解以下内容:

  • 了解 WebSocket 的诞生布景、WebSocket 是什么及它的长处;
  • 了解 WebSocket 含有哪些 API 及怎么运用 W? O l t O )ebSocket API 发送一般文本和二进制数据;
  • 了解 WebSocket 的握手协议和数据帧格局、掩码算法等相关常识;
  • 了解怎么完结一个支撑发送一般文本的I T z WebSocket 服务器。

在终究的 阿宝哥有话说 环节,H ` ^ [ y r阿宝哥将介绍 WebSocket 与 HTTP 之间的联系、WebSocket 与长轮询有什么差异、什么是 WebSocketk 2 8 心跳及 SockJ ! Q B ( )et 是什么等内容。

推荐阅览(感谢掘友的鼓励与支撑):F D ! p ] 7 1 6 U

  • 你不知道的 Web Workers (上)[7.8K 字 | 多图预警](424+ 个)
  • 你不知道的 Blob(215+ 个)
  • 你不知道的 WeakMap(55+ 个)
  • 玩转前端 Video 播放器 | 多图预警(708+ 个)
  • 玩转前端二进制(359+ 个)

下面咱们进入正题,为了让大家能够更好地了解和把握 WebSocket 技能,咱们先来介绍一下什么是 WebSocket。

一、什么是 WebSocket

1.1 WebSocket 诞生布景

前期,许多网站为了完结推送技能,所用的技能都是轮询Z a e Q h T Z l。轮询是W ; a q u指由浏览器每隔一段时间向服务器宣布 HTTP 恳求,然后服务器回来最新的数据给客户端。常见的轮询方式分为轮询与长轮询,它们的差异如下图所示:

你不知道的 WebSocket

为了愈加直观感受轮询与长轮询之间的差异,咱们来看一下详细的代码:

你不知道的 WebSocket

这种i ? # N Y传统的形式带来很明显的缺点,即浏览器需求不断的向服务器宣布恳求,可是 HTTP 恳求与呼应或许会包括较长的头部,其间真实有用的数据或许只是很小的一部分,所以这样会消耗许多带宽资源。

比较新的轮询技能是 Comet。这种技能尽管能够完结双向通讯,但仍然需求重复宣布恳B V I % Z Y求。而且在 Comet 中普遍选用的 HTTP 长衔接也 $ P t会消耗? f ( 1 k 0 Q K服务器资源。

在这种情况下,HTML5 界说了 WebSocket 协议,能更好的节约服务器资源和带宽,而且能够更实时地进行通讯。WebsD 8 k @ 5 Bocket 运用 ws 或 wss 的统一资源标志符(URI),其间 wss 表明运用了 TLS 的 Websocket。如:

ws://echo.websocket.org
wss://echo.web6 % ) [ Psocket3 ~ t E P j v.org

WebSocket 与 HTTP 和 HTTPS 运用相同的 TCP 端口,能够绕过大多数防火墙的限制。默许情W Q g * n q / k况下,WebSocket 协议运用 80 端口;若运转在 TLS 之上时,默许运用 443 端口。

1.2 WebSo5 1 ( –cket 简介

WebSocket 是一种网络传输协议,可在单个 TCP 衔接上进行全双工通讯,位于 OSI 模型的运用层。We% O # 6bSocket 协议在 2011Y v – | 3 r[ * U b {由 IETF 规范化为 RFC 6455,后由 RFC 7936 弥补规范。

WebSocket 使得客户端和服务器之间的数据交换变得愈加简略,答应服务端主意向客户端推送数据。在 WebSocket2 – O y & API 中,浏览器和服务器只需求完结一次握手,两者之间就能够创立持久性的衔接,并进行双向数据传输。

介绍完轮询和 WebSocket 的相关内容之后,接下来咱们来看一下 XHR Polling 与 WebSo4 ) L 2 .cket 之间的差异:

你不知道的 WebSocket

1.3 WebSocket 长处

  • 较少的操控开支。在衔接创立后,服务器和客户端之间交换数据时,用于协议操控的数据包头部相对较小。
  • 更强的实时性n ? { a g c o。由于协议# ^ }是全双工的,所以服务器能够随时自动给客户端下发数据。相关于 HTTP 恳求需求等候客户端建议恳求服务端才干呼应,推迟明显更少。
  • 保持衔接状况。与 HTTP 不同的是,WebSocket 需求先创立衔接,这就使& o * M得其成为一种有状况的协议,之后通讯时r Z D h能够省掉部分状况信息。
  • 更好的二进制支撑。WebSoj p N { J C ? ^ Zcketx 7 u ? 界说了二进制帧,相对 HTTP,能够更轻松地a T s g ? & p F *处理二进制内容。
  • 能够支撑扩展。WebSocket 界说了扩展,用户能够扩展协议、完结部分自界B . C I m说的子协议。

由于 WebSocket 拥有上述的长处,所以它被广泛地运用在即时通讯、实时音视频、在线教育g [ W = H h U和游戏等领域。关于前端开发者来说,要想运用 WebSocket 供给的强大才干,就有必要先把握 WT N 7ebSocket API,下面阿宝哥带大家一起来认识一下 WebSocket API。

二、WebSocket API

在介绍 WebSock9 [ 4 | $ O jet API 之前,咱们先来了解一下它的兼容性:

你不知道的 WebSocket

(图片来历:https://z w E m 9 % Jcaniuse.com/#search=WebSocket)

从上图可知,现在干流的 Web 浏览g 1 B v h 4器都支撑 WebSocket,所以咱们能够在大多数项目中放心肠运用它。

在浏览器中要运用 WebSocket 供给的才干,咱w X B ^们就d Q , o ? q U b有必要先创^ X i ; j立 WebSocket 方针,该方针供给了用于创立和管理 W! 6 : S 1 q S 3ebSocket 衔接,以及能够经过该衔接发送和接纳数据的 API。

运用 WebSocket 结构函数,咱们就能轻易地结构一个 WebSocket 方针。接下来咱们将从 WebSocket 结构函: 2 U x &数、WebSocket 方针的特点、办法[ u l V及 WebSocke i ` G E W m Dt 相关的事情四J + | [ C个方面来介绍 WebSp o M gocket API,首要咱们从 WebSocket 的U / 7 I 1 _结构函数8 e S下手:

2.1 结构函数

WebSocket 结构函数的语法为:

const myWebSocket = new WebS7 - H )ocket(url [, protocols]);

相关参数阐明如下:

  • url:表明衔接的 URL,这是 WebSocket 服务器将呼应的 URL。
  • protocols(可选):一个协议字符串或许一个包括协议字符串的数组。这些字符串用于指定子协议,这样单个服务器能够完结多个 WebSocket 子协议。比方,你或许期望一台服务器能够依据指Y N L C . i定的协议(protocol)处理不同类型的交互。假如不指定协I _ 8 2 ) O议字符串,则假定为空字符串。

当测验衔接的端口被阻止时,会抛出 SECURITY_ERR 反常。

2.2 特点

WebSocket 方i { l { * b针包b 8 W o _括以下特点:

你不知道的 WebSocket

每个特点的详细意义如下:

  • binaryType:运用二进制的数据类型衔接。
  • bufferedAmount(只读):未发送至服务器的字节数。
  • extensions(只读):服务器选择的扩展。
  • onclose:用于指定衔接封闭后的+ m O ] [ 6 =回调函数。
  • onerrV h b D G y H C Wor:用于指定衔接失利后的回调函数。L m } * F B
  • od I 1nmessage:用于指定L 0 /当从服务器接受到信息时的回调函数。
  • onopen:用于指定衔接成功后的回调函数。
  • protocol(只读):用于回来服务器端选中的子协议的名字。
  • readyState(只读):回来当时 WebSocw y )ket 的衔接状况,/ n G 7 W 4 o共有 4 种状况:

    • CONNECTING — 正在衔接中,对应的值为 0;
    • OPEN — 现已衔接而且能够通讯,对应的值i ? p f + Y O% ^ c 3 N x 1;
    • CLOSING — 衔接正r ] q } a在封闭,对应的值为 2;
    • CLOSED —6 S S 3 5 N 1 ! 衔接已封闭或许没有衔接成功,对应的值为 3。
  • url(只读):回来值为当结构函数创立 WebSocket 实例方针时 URL 的绝对路径。

2.3 办法

  • close([code[, reason]]):该办法用于封闭 WebSocket 衔接,0 @ w V y 假如衔接现已封闭,则此办法不履行任何操作。
  • senY E { q E W h }d(data):该办法将需求经A E k _过 WebSocket 链接传输至服务器的数据排入行列,并依据所需求传输的数据的巨细来增加 bufferedAmount 的– * ! ; L y g值 。若数据无法传输(比方数据需求缓存而缓冲区已满F x W E S # J j)时,套接字会自行封闭。

2.4 事情

运用 addo } /EventListener() 或将一个事情监听器赋值给 WebSocket 方针的 oneventname 特点,来监v Q k A 6听下面的事情。

  • close:当一个 WebSocket 衔接被封闭时触发,也能够经过 oncU Y r ` g 1 8 mlose 特点来设置。
  • error:当一个 WebSocket 衔接因错误而封闭m r 4 c时触发,也能够经过 onerror 特点来设置。
  • messagD @ _ %e:当经过 WebSocket 收到数据时触发,也能够经过 onmessage 特点来设置。
  • open:当一个 WebSocket 衔接成功时触发,也能够经过 onoH | ; W } M ? k epen 特点来设置。

介绍完 WebSocket API,咱们来举一个运用 WebSocket 发送一般文本的示例。

2.5 发送一般文本

你不知道的 WebSocket

在以上示例中,咱们在页面上创立了两j v 9 m C ? T个 textarea,别离用于寄存 待发送的数据服务器回来的数据。当用户输入完待发送的文本之后,点击 发送 按钮时会把输入的文本发送到服务端,而服务端成功接纳到音讯之后,会把收到的音讯原封不动地回传到客户端。

// const socket = new WebSocQ w [ $ket("ws://echo.websocket.org");
// const sendMsgContainer = documentW ^ 5 P w O.querySelector("#sendMessage")@ 1  # G J : Q;
functE 7 [ e w ^ 8 B Uion send() {
  const message = sC V S @ S C 6 r -endMsgContainer.value;
  if (socket.readyState !== WebS% M ^ a x cocket.OPEN) {A ` r 
    console.log("衔接未树立,还不能发送音讯");
    return;
  }
  if (message) socket.send(message);
}

当然客户端接纳到服务端回来的音讯之后,会把对} ? @应的文本内容保存到 接纳的数据 对应的 textarea 文本框中。

// const socketd a t u . G C + { = new WebSocket("ws://echo.websocket.org");
// const receiveE s s W / p N ] RdMsgContainer = document.querySelector("#receivedMessage");    
socket.addEventListener("message", function (event) {
  console.log("Message from server ", event.data);
  receivedMsgContainer.value = event, F s _ : U.data;
});

为了愈加直观地了解上述的数据交互! l 8 )进程,咱们运用 ChromeG # + M Q H 浏览器的开发者东西来看一下相W , J 8 ^ M B应的进程:

你不知道的 WebSocket

以上示例对应的完好代码如下所示:

<!DOCTYPE html>
<1 G F ^ 8 r N;html>
  <head>
    <meta c; M 9 g J bharset="UTF-8" />
    <meta name="viewport" conI Q  A Stent="wid_ s 3th=device-width, initial-scale=1.0" />
    <title>WebSocket 发送一般P / ) F v W H文本示例</title>
    <style>
      .block {
        flex: 1;
      }
</sty[ R ] | V ble>
  </head>
  <body>
    <h3>0 , q z { W n w ]宝哥:WebSocket 发送一般文本示T c +</h3>
    <div style="display: flex;">
      <div class="block">
        <p>行将发送的数据:&lS 7 gt;+ a gbutto : x & 6 ^ S G Don onclick="send()">发送</button></p>
        <textarea id="sendMessa1 0 x M z 4 w . &ge" rows="5" cols="15">&S 5 W 4 6 Z [lt;/textarea>
      </div>
      <div class=I @ ? . 1"block">
        <p>! j Q o $ ( 8 | $;接纳的数据:</p>
        <textam 2 u A C - m } =rea id="receivedMessage" rows="5" cols="15"></textarea>
      </div>D k D c = , ; { ^
    &~ q c 5 d . ` 0lt;/div>

    <script>4 D 8 q R + Q W w;
      const seu Z q _ I 3ndMsgContainer = document.querySelector(Z 6 P Q b ( B c"#sendMessp F / J 0 Yage");
      const receivedMsgContainer = document.querySelector("#receivedMessage");
      const socket = new WebSocket("wsX D % w g J://echo.websocket.org");

      // 监听衔接成功事情
      socket.addEventListener("oJ 7  - 5 = q 9 Dpen", function (event) {
        console.log("衔接成功,能够开端通讯");
      });

      /, L  t - ?/ 监听音讯
      socket.addEventListener("message", function (event) {
        console.log("MeJ # _ 2 k a Z T Qssage from serve? f k Er ", event.daB & { 5 q T V 8 Yta);
        receivedMsgContainer.value = event.dO | : q N k O 8 (ata;
      });

      function send() {
        const message = sendMsgContainer.value;
        if (socket.readyState !== WebSocket.OPEN) {
          console.lo7 - o O L Bg("H B 9 }衔接未树立,还不% s @ g 8 }能发送音讯");
          return;
        }
        if (message) socket) @ : , U x }.m U Y 2 [send(message);
      }
</script>
  </body>
</y M 8 P G M q Chtml>

其实 WebSocket 除了支撑发送一般的文本之外,它还支撑发送二进制数据,比方 ArrayBuffer 方针、Blob 方针或许 ArrayBufferView 方针:

constQ 9 | E socket = new WebSocV i 1 f { 0 x Sket("ws://echo.websocket.orga V x m");
socket.onopen = function () {
  //p w c # x 发送UTF-8编码的文本信息
  socket.send("Hello Echo Server!");
  // 发送UR d D g * R Q A }TF-8编码的JSON数据
  socket.send(JSON.stringify({ msg: "我是阿宝哥" }));

  /= _ 3 }/ 发送二进k T + (制ArrayBuffer
  const buffer = new ArrayBuffer(128);
  socke# D I qt.send(buffer);

  // 发送二进制ArrayBufferView
  const intview = new Uin4 - * # i 6 U # {t32Array$ ~ Q(buf2 2 Efer);
  socket.send(intview);J & T 1 V r

  // 发送二进制Blob
  const blob = new Blob([buffer]);
  socket.send(blob);
};

以上代码成功运转后,经过 Cr C ^ v lhrome 开发者东西,P 2 2 g X = 3 e咱们能够看到对应的数据交互进程:

你不知道的 WebSocket

下面阿宝哥以发送 Blob 方针为例,来介绍一下怎么发送二进制数据。

Bl5 E g x o q C f :ob(Binary Large Object)表明二进制类型的大方针。在数据库管理体系中,将二进制数n p 5 ] ? 3 – ~据存储为一个N # F ` + p单一个别的调集。Blob 通常是印象、声响或多媒体文件。在 JavaScript 中 Blob 类型的方针表明不行变: : 7的相似文件方针的原始数据。

对 Blob 感兴趣的小伙伴H X $,能够阅览 “你不知道的 Blob” 这篇文章。

2.6 发送二进制数据

你不知道的 WebSocket

在以上示例中,咱们在页面上创立了两个 textarea,别离用于寄存 待发送的数据服务器回来的数据。当用户输入完待发送的文本之后,点击 发送 按钮时,咱们会先获取输入的文本并( q $ 0 J y 2 9 把文本包装成 Blob 方针然后发送到服u h @务端,而服务端成功接纳到音讯之_ i Z m u G后,会[ v h把收到的音讯原封不动地回传到客户端。

当浏览器接纳O D q @ X Y 2到新音讯后,假如是文K ? ` h :本数据,会自动将其转化成 DOMString 方针,假如是二R b P l B进制数据或 Blob 方针,会直接将其转交给运用,由运用自身~ n J M # + ]来依据回来的数据类4 q q g J型进行相应的处理。

数据. & O ^ s 5发送代码

// const socket = new WebSocket("ws://echo.websock| * ~ O O X / ) uet.org");
// const sendMsgContainer = document.querySelector("#sendMessage");
function send() {
  const message = sen# / z A o ! ! O |dMsgContainer.value;
  if (socket.readyState !== WebSocket.OPEN) {
    console.log("衔接未树立,还不能发送音讯");
    re1 Q a / :turn;
  }
  const blob = new Blob([messag; y  n E ve], { type: "text/plain" });
  if (message) socket.send(blob);
  console.log(`未发送至服务器的字节数:${socket.buffe? W u B { Q TredAmount}`);
}

当然客户端接纳到服务端回来的音讯之后,会判断回来的数据类型,假如是 Bl: k Z iob 类4 B ,型的话,会调用 Blob 方针的 text() 办法,获取 Blob 方针中保存3 ; Q s ( W的 UTF-8 格局的内容,然后, s w ! ! 1把对应的文本内容保存到 接纳的数据 对应的 textarea 文本M : 8 V V _ b 框中。

数据接纳代码

// const socket = new WebSocket("ws://echo.webso# d / 3 v _ Ccket.org");
//6 j a ) K 9 9 F = const receivedMsgContainer = doc; I c M @ Rument.querySelector("#receivedMessage");
socket.addEventListener("message", async function (event) {
  console.log("Message from server ", event.data);
  constv V X ; recei| ) } ,vedData = event.data;
  if (receiv? 6 T = a 8 xedData instanceof Blob) {
    receivedF  r  g B E + 6MsgContainer.vald $ _ bue = await receivedData.+ 8 c B G  [text();
  } else {
    receivedMsgContainer.valuw L y L } n R | ]e =2 m | 7 receivedData;
  }
 });

相同,咱们运用 Chrome 浏览器的开发者东西来看一下相应的进程:

你不知道的 WebSocket

经过上图咱们能够w 6 ~ H _ ) 很明显地看到,当 R M W运用发送 Blob 方针时,Data 栏位的信息X j c显现的是 Binary Me[ L R Gssage,而关于发送一般文本来说1 O L,Data 栏位的信息是直接显现发送的文本音讯。

% n + ` ^上示例对应的完好代码如下所示:

<!DOCTU v 3 O G |YPE html>
<html>
  <he= M f R S nad>
    <meta charset="UTF-8" /&{ % % p s Ggt;
    <meta name="viewport" content="width=deviceK ~ q ` k Q e-width, initial-scale=1.0" />
    <title>WebSocket 发送二进制数据示例</title>
    <style>
      .block {
        flex: 1;
      }
</styY . = / K t F B cle>
  </head>
  <body>
    <h3>阿宝哥:WebSocket 发送二进制数据示例</h3>
    <div style="display: flex;"+ j V $ 2 b>
      <div class="block">
        <p>待发送的数据:<button on- ; { . !click="send()">发送</button></p>
        <textarea id="sen| h O 0 E Z ZdMessage" rows="5" cols="15">& ~ f g</textarea>
      </div>
      <div class="block">
        <p>f I ~ -纳的数据:</p>
        <textarea id="receA G 4ivedMessage" rows="5" cols="15"><. i G c u;/textarea>
      </div>
    </div>

    <script>
      const sV b d uendMsgContainer = document.querySelector("#sendMessage");
      const receivedMsgContainer = document.querySelector("#receivedMessage");
      const socket = new WebSockeB ! ~ *t("ws://echo.websocket.org");

      //z { k p 监听衔接成功事情
      socket.addEventListener("open", function (event) {
        conso C u } 2 nole.log("衔接成功,能够开端通讯");
      });

      // 监听音讯
      soc% N D u F s & o nket.addEventListener("message", async functio0 ; Z M : $ $n (evenc ? G ct) {
        console.log("MesN m 8 t : dsage from server ", event.data);
        const receivedDat9 T )a = event.dataT 7 ,;
        if (receivedData instanceof Blob) {
          receivedMsgContainer.value = await receivedDatE z w N ra.text(h ( z 6);
        } else {
          received7 F  @ X E G kMsgContaiO r p H % T 9 @ner.value = receivedData;
        }
      });

      function send() {
        const message = sendMsgContainer.value;
        if (socket.re/ U ; h ladyState !== WebSocket.OPEN) {
          consn K 0ole.log("衔接未树立,还不能发送音讯")y K A a R f B ];
          return;m G 8 ` Y
        }
        const blobc t C % K Z . p = new Blob([mesz ] %sage], { type: "text/plain" });
        if (s m w ` v 4 ~ .message) socket.se, b - D E and(blob);
        console.log(`未发送至服务器的字节数:${socket.bufferen | 6dAmount}`);
      }
</y v $ J g D [ 9 |script>
  </body>
</html>

或许有一些小伙伴了解完 WebSocket API 之后,觉得还不行过瘾。下面阿宝哥将带大家来完结一个支撑发送一般文本的 WebSQ d y | a 1 z Kocket 服务器。

三、手写 WebSocket 服务器

在介绍怎么手写 WebSocket 服务器前,咱们需求了解一下 WebSocket 衔接的生命周期。

你不知道的 WebSocket

从上图可知,在运用 WebSocket 完结全双工通讯之前,客户端与x ] t @ H & ] { D服务器之间需求先进行握手(Handsh6 @ t D S T uake),在完结握手之后才干开端进行数据的双向通讯。r # q

握手是在通讯电路创立之后,信息传输开端之前B R e Q /握手用于达成参数,如信息传输率,字母表,奇偶校验,中断进程,和其他协议特性。 握手有助于不同结构的体系或设备在通讯信道中衔接,而不需求人为设置参数。

已然握手是 WebSockeI 5 N dt 衔接生命周期的第一个环节,接下来咱们就先来剖析 WebSocket 的握手c h _ C F [ 5 *协议。

3.1 握手协议

WebSocket 协议归于运用层协议,它依赖于传输层的 TCP 协议。WebSocketz ( M H B W w ~ 经过 HTTP/1.1 协议的 101 状况码进行握手。为了创立 WebSocket 衔接,需求经过9 Z Q {浏览器宣布恳求,之后服务器进行回应,这个进程通常称为 “握手v v F G # . i &”(Handshaking)。

运用 HTTP 完结握手有几个优点。首要,让 WebSocket 与现有 HTTP 基础设施兼容:使得 WL i = d L {ebSocket 服务器能够运转在 80 和 443 端口上,这通常是对客户端仅有开放的端口。其i ) s V a I ) y &次,让咱们能够重用并扩) p L W 5 . ` g L展 HTTP 的 Upgrade 流,为其增加自界说的 WebSocket 首部,以完结洽谈。

下面咱们以前面现已演示过的发送一般文本的例子为例,来详细剖析一下握4 D [ @ % . : u手进程。

3.1.1 客户端恳求
GET ws://echo.websocket.org/ HTTP/1.1
Host: echoC 5 Q.websocket.org
Origin: file://
Connection: Upgrade
Upgrade: websocket
SeX U + [ p ?c-WebSo. ~ T 1 P [cket-Version: 13
Sec-WebSt a M  ) z  oocket-Key: Zx8rf b ? q * NEkBE4xnwifpuh8DHQ==
Sec-WebSocket-Extensions: permessI { 8 : @ eage-deflate;K  ] B client_max_wi: / w ndow_bits

备注:已忽略D n m Y & [部分 HTTP 恳求, E { c 8 %

字段阐明

  • Connection{ : L 有必要设置 Upgrade,表明客户端期望衔接晋级。
  • Upgrade. i z 字段有必要设置 webW . K 7 L B dsocket,表明期望晋级到 WebSocket 协议。
  • Sec-WebSocket-Version 表明支撑的 WebSocket 版别。RF) 1 / = q @ UC6455 要求运用的版别是 13,之前草案的版R | z别均应当弃用。
  • Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来结构出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一t / , W C & I 3 r个特别字符串 “258EAFA5-EK K ! ; _ Z p914-4q # X d 97DA-95CA-C5AB0DC8U p – 4 B a 4 L5B11”,然后核算 SHA-1 摘要 1 ^ ,,之后进行 Base64 编码,将成果做为 “Sec-WebSocket-Accept” 头的值,回来给客户端。如此操l F k作,能够尽量防止一般 HTTP 恳求被误认为 WebSocket 协议。
  • Sec-WebSocket-Extensions 用于洽谈本次衔接要运用的 WebSocket 扩展:客户端发送支撑4 n A q g F 5的扩展,服务器经过回来相同的首部确认自己支撑一个或多个扩展。
  • Origin 字段是可选的,通常用来表明在浏览器中建议此 WebSocket 衔接所在的页面,相似于 Referer。? % c . , 3可是,与 Referer 不同的是,Origin 只包括了协议和主机名称。
3.1.2 服务端呼应
HTTP/1./ 7 p @ 5 o * I b1 101 Web Socket Protocol Handshake ①
ConnecN d O q 4tion: Upgrade ②2 G _
Upgrade: websocket ③
Sec-WebSocket-Accept0 r / : 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④

备注:已忽略部分 HTTP 呼应头

  • ① 101 呼应码o b ~确认晋级到 WebSocT A ` /ket 协议。
  • ② 设置 Connection 头的值为 “Upgrade” 来指示这是一个晋级恳求。HTTP 协议供给L f E G了一种特别的机制,这一机制答应将一个已树立的衔接晋级成新的、不相容的协议p 9 y
  • ③ Upgrade 头指定一项或多项协议名,按优/ d 5 S } v * b E先级排序,以逗号分隔。这儿表明晋级为 WebSocket 协议。
  • ④ 签名的键值验证协议支撑。

a : y绍完 WebSocket 的握手协议,接下来阿宝哥将运用 Node.js 来开发咱们的 WebSocket 服务器。

3.2 完结握手功用

要开发一个 WebSocket 服务器,首要咱们需求i 7 Z r v h先完结握手功用,这儿阿宝哥运用 Node.js 内置的 http 模块来创立一个 HTTP 服务器,详细代码如y E 7 5 O下所示:

const http = requE ^ M * )ire("httpD u C , H 1 V");

const port = 8888;
const { generateAcceptValue } = require("./util");

constY r 1 C 4 e o : server = http.D i + G P ` createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
  res.end("大家好,我是阿宝哥。感谢你阅览“你不知道的WebSocket”");
});

server.on("upgrade", function (req, socket) {
  if (req.headers["upgrade"] !== "wK q a {ebsocket") {
    socket.end("HTTP/1.1 400 Bad Request");
    return;! I , [ B Z 8
  }
  // 读取客户端供给的Sec- ~ I $ NWebSocket-Key
  const secWsKey = req.headm  t e 1 : l w 1ers["sec-websocket-keo K f N [ 3 H Gy"];
  //H 6 A t 6 8 K 9 运用SHA-1算法生成Sec-WebSocket-Accept
  const hash = ge( K , ) s J n -nerateAcceptValue(secWsKey);
  // 设置HTTP呼应头
  const responseHeaders = [
    "HTTP/1.1 101 Web Socket Protocolm q ( Handshak] 7 B I $ / u we",
    "Upgrade: WebSocket",
    "Connection: Upgrade",
    `Sec-WebSocket-I H ] 0 i u [ eAccept: ${hash}`,
  ];
  /D g C d  m r F ;/ 回来握手恳求的呼应信息
  socket.write(responseHeaders.jo_ 0 i A e 1in("\r\n") + "\r\n\r\n");
});

server.listen(port, () =>
  console.log(`Server running at htt^ a v fp://localhost:${port}`)
);

在以上代码中,咱们首要引进了 http 模块,然后经过调用该模块的 createServer()q A } Y ) I w @法创立一q x q g + E个 HTTP 服务器,接着咱们监听 upM ) 6 i hgrade 事情,每次服务器呼应晋级恳求时就会触发该事情。由于咱们的服务器只支撑晋级到A ) 8 WebSocket 协议,所以假如客户端恳求晋级的协议非 WebSocket 协议,咱们将会回来 “400 Bad Request”。

当服务器接纳到晋级为 WebSocket 的握手恳求时,会先从恳求头中获取 “Sec-WebSocket-Key” 的值,然后把该值加上一个特别字符串 “258EAFA5-E914-47DA-95CA-C5ABV T n H0DC85B11”,然后核算 SHA-1 摘要,之后进行 Base64 编码,将成果做为 “Sec-WebSocket-Accept” 头的值,回来给客户端。

上述的进程看起来好像有点繁琐,其实运用 Node.js 内置的 crypto 模块,几行代码就能够搞定了N _ 2 d 4

// util.js
const crypto = requiS ) q xre("crypto");
const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

function generateAcceptVM I X G J m |alue(secWsKey) {
  return crypto
    .createHash("sha1")
    .update(secWsKey + MAGIC_KEY, $ , G = p E + U"utf8")
    .digest(q r  e :"base64");
}

开发完握手功用之后,咱们能够运用前面的示例来测试一下} ; 0 e 9 f W –该功用。待7 U 8 0 d 3服务器发动之后,咱们只要对 “发送一般文本” 示例,做简略Y N ` e D地调整,即把先前的 URL 地址替换成 ws://localhost:8888,就能够进行功用验证。

感兴趣的小伙们能够试试看,以下6 6 ^ * B S i是阿宝哥本地运转后的成果:

你不知道的 WebSocket

从上图可知,咱们完结的q n b #握手功用现已能够正常工作了。那么握手有没有或许失利呢?答案是必定的。比方网络问题、服务器反常或 Sec-WebSocket-Acc / pcept 的值不正确。

下面阿宝哥修正一下 “Sec-WebSocket-AccI ) ) a B M $ ) Mept” 生成规则,比方修正 MAGIC_KEY 的值,然后从头验证一下y Q D握手Z = ] @ a * Q P功用。此时,浏览器的操控台会输出以下反8 t ! p ^ C常信息:

WebSocket connection to 'ws://localhost:8888/' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSa E  f + * , (ocket-Accept' header value

假如你的 WebSocket 服务器要支撑子协议的话,你能够参阅以下代码进行子协议的处理,阿宝哥就不继续展开介绍了。

// 从恳求头中读取子协议
const protocol = req.headers["sec-websocket-proS ] ? . 2tocol"];
// 假如包括子协议,则解析子协议
constJ I S Q n _ p j ^ prot] K ` E =ocols = !protocol ? [] : protocol.split(",").map((s) => s.trim());

// 简略起见,咱们仅判断是否含R X A l有JSON子协议
if (protocols.includes("json] ] 9 ] ; d ; m f")) {
  responseHeaders.push(`Sec-WebSocket-Protocol: json`);
}

好的,WebSocket 握手协议相关的内容基本现已介绍完了。下一步咱们来介绍开发音讯通讯功用需求了解的一些基础{ 1 n B [ j t 3 ?常识。

3.3 音讯通讯基础

在 WebSocket 协议中,数据是经过一系列数据帧来进行传输的。为了防止由于网络中介(例如一些阻拦署理)或许一些安全问题,H – J f X 2 n &客户端有必要在它发送到服务器的一切帧中增加掩码。服务端收到没有增加掩码的数据帧今后,有必要立即封闭衔接。

3.3.1 数据w v B 8 p帧格局

要完结音讯通讯,咱们E h k ~ K ~ @就有必要了解 WebSocket 数据帧的格局:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-# J [ X W ^------+-+------------` 9 f o ~-+---------e F W #----------------------+
|F|R|R|R| opcode|M| PaT @ K p B 6 * P Uyload len |    Extended payload length    |
|I|S|S|S|  (4k H V E)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - -{ # $ - +----5 t _ 8 w v---------------------------+
|                               |MaskingZ / 3 ` * ;-keyD z E W $ -, if MASK set to 1  |3 ` j ? D L K G z
+----------------Z H j Z z X k Z---------------+-------------------i _ U T .------------+
| Masking-key (continued; @ 1)       |          Payload Data         |
+--------# ` K } ; | - & T------------------------ - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - -y B y W - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

或许有一些小伙伴看到上面的内容之后,就开端有点 “懵逼” 了。下面咱们来结合实际的数据帧来进一步剖析一下:

你不知道的 WebSocket

在上图中,阿宝哥简略剖析了 “发送一般文本” 示例对应的数据帧格局。这儿咱们来进一步介绍一下 Payload length,由于在后边开发` m R B数据解析功用的时分,需求用到该常识点。

Payload length 表明以字节为单位的 “有用负载数据” 长度。= w K ! y它有以下几种情形:

  • 假如值为 0-125,那么就表明负载数据的4 % N [ ^ =长度。
  • 假如是 126,那么接下来的 2 个字节解释为 16 位的无符号整形作为负载数据C O % 3 _ – e @ –的长度。
  • 假如是 127,那么接下来的 8 个字节解释为一个 6N I H4 位的无符号整形(最高位的 bit 有必要为 0)作为负载数据的长度。

多字节长度量以网络字节次序表明,有用负H v # Y ) & & – pw m m = 5 4长度是指 “扩展数据” + “运用数据” 的长度。“扩展数据” 的长度或许为 0,那么有用负载长度便是 “运用数据” 的长度。

另外,除非洽谈过扩展,不然 “扩展数据” 长度为1 – S K Z G _ ] p 0 字节。i = i %在握手协议中,任何扩展都有必要指定 “扩展数据” 的长度,这个长度怎么进行核算,以及这个扩展怎么运用。假如存在扩展,那么这个 “扩展数据” 包括在总的有用负载长度中。

3.e P b : { H d3.2 掩码算法

掩码字段是一个由客户端随机选择的 32 位的值。掩码值有必要是不行X 8 x K / U u被猜E 3 . E F t * |测的。因而,掩码有必要来自强大的熵源(entropy)@ – 6 ;,而且给定的掩码不能让服务器或许署理能够很容易的猜测到后续帧。掩码的不行猜测性关j D .于防备歹意运用的作者在网上暴露相关的字节数据至关重要。

掩码不影响数Y v W 2 ] Y o %据荷载的长度,7 7 l对数据进行掩码操作和对数据进行反掩码操作所涉及的进程是相同的。掩码、反掩码操( & F 4作都选用如下算法:

j = i MOD 4
transformed-octet-i = original; + ~ {-octet-i XOR masking-key-octet-j
  • original-octet-i:为原始数据的第 i 字节。
  • transformed-octek , ) , &t-i:为转化后的数据的第 i 字节。
  • masking-key-octet-O I ( Wj:为 mask key 第 j 字节。

为了让小伙伴们能够更好e d | T的了解上面掩码的核算进程,咱们来对示例中 “我是阿宝哥” 数据进行掩码操作。这儿 “我是阿宝哥” 对应的 UTF-8 编码如下所示:

E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5

而对应的 Masking-Key 为 0x08f6efb1,依据上面的算r G X法,咱们能够这样进行掩码运算:

leE  ~ =t uint8 = new Uint8Array([0xE6, 0x88, 0x91, 0xE6, 0x9( = z !8, 0xAF, 0xE9, 0x98,
  0xBF, 0xE5, 0xAE, 0x9D, 0xE5, 0x93, 0xA5]);
let maskingKey = new Uint8Array([0x08, 0xf6, 0xef, 0xb1]);
let maskedUint8 = new Ui2 ~ s 8 8nt8Array(uint8.length);

for (let i = 0, j = 0; i < uint8.length; i++, j; j 4  . ^ t X ~ = i % 4) {
  maskedUint8[i] = uint8[i] ^ maskingKey[j];
}

console.log(Array.fn , f = d &rom(maskedUint8).map(num=>N# Y qumber(num).toS| O Wtring(16)).joio Z . B Mn(' '));

以上代码成功运转后,操控台会输出以下成果:

ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a

上述成果与 WireShark 中的 Masked payload 对9 9 ( + x V H 1 (应的O C h 0 S t值是共同的,详` X d细如下图所示:

你不知道的 WebSocket

在 WebSocket 协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据自身,由于算法自身是公开的,运算也不杂乱。那么为什么还要引进数1 6 5 x u o r )据掩码呢?引进数据掩码是为了防止前期版别的协议中存在的署理缓存污染攻击等问题。

了解完 WebSocket 掩码算法和z u # * m y 8数据掩码的作用之后,咱们再来介绍一下数据分片的概念。

3.3.3 数n F F据分片

WebSocket 的每4 ] U + ,条音讯或许被切分红多个数据帧。当 WebSocket5 2 r | $ x d r b 的接纳方收到一个数据帧时,会依据 FIN 的值来判断,是否o ] u ! . ^ % *现已收到音讯的终究一个数据帧。

运用 FIN 和 Opcode,咱们就能够跨帧发送音讯。操作码告知了帧应该做什么。假如是 0x1,有用载荷便是文本。假如是 0x2,有用载荷便是二进制数据。可是,假如是 0x0,则该帧是一个连续4 F } V ` v帧。这意味着服务器应; L 9 B V W Q该将帧的有用负载 } A S P n a Y 0衔接到从该客户机接纳到的终究一个帧。

为了让大家能够更好地了解上述的内容,咱们来看一个来自 MDN 上的示例:

Client: FIN=1, opcode=0x1, msg="} 8 g dhello"
Server: (process complete message immedia. ^ stely) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
S_ ^ e q J o w u Oerver: (listening, payload concatenated to previou5 9 4 * 8 : 5 : xs message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happn 4 g H ( b g ry new year to you too!

在以上示例中,客户端向服务器发送了两条音讯。第一个音讯在单个帧中发送,而第二个音讯跨三个帧发送。

其间第一个音讯是一个完好的音讯(FIN=1 且 opcode != 0x0),因而服务器能够依据需求进行处理或呼应。而第二个音讯是文本音讯(opcode=0x1)且 FIN=0,表明音讯还没发送完结,还有后续的数据帧。该音讯的一切剩下部分u d x $ 0 ~ f J 7都用连续帧(opcode=0x0)发送,音讯的终究帧用 FIN=1 标记。

好的,简略介绍了数据分片的相关内容。接下来,咱们来开端完结音讯通讯功用。

3.4 完结音讯通讯功用

阿宝哥把完结音A q w x H讯通U : c T P ^讯功用,分解为音讯解析与音讯呼应两个子功用,下面咱们别离来介@ Y P绍怎么完结这两个子功用。

3.4.1 音讯解析

运用音讯通讯基础环节中介绍@ d S G的相关常识,阿宝哥完结了一个 parseMessage 函数,用来x = G解析客户端传过来的 WebSocket 数据帧。出于简略考虑,这儿只处理文本帧,详细代码如下所示:

function parseQ ] D R e hMessage(buffer) {
  // 第一个字节,包括了FIN位,opcode, 掩码位
  const firstByte = buffer.readUInt8(0);
  // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE];4 1 | X | F 8 = i
  // 右移7位取首位,1位,表明是否是终究一帧数据
  const isFinalFrame = Bool_ c a & Tean((firstByte >>> 7) &# w D  N d 2 0x01);
  console.log("isFIN: ", isFinalFrame);
  // 取出操作码,低四位
  /**
   * %x0:表明一个连续帧。当 Opcode 为 0 时,表明本次数据传输选用了数据分片,当时收到的数据帧为其间一个数据分片;
   * %xK = H } N 7 `1:表明这是一个文本帧(text frame);
   * %x2:表明这是一个二进制帧(binary frame);
   * %x3-7:保留的操作代码,用于后续界说的非操控帧;
   * %x8:表明衔接断开;
   * %x9:表明这是一个心跳恳求(ping);
   * %xAg h * H n o 8 2 e:表明这是一个心跳呼应(pong);
   * %xB-F:保留的操作代码,用于后续界说的操控帧. U Y I q 5   */
  const opcode = firstByte & 0x0f;
  if (( e T 2 + -opcode ==} w x q r ~ + , w= 0x08) {
    // 衔接封闭
    return;
  }
  if (opcode === 0x02) {
    // 二进制帧
    return;
  }
  if (opcode === 0x01) {
    // 现在只处理文本帧
    let offset = 1;
    const$ l ( E % 4 u secondByte = buffer.readUInt8(offset);
    // MASK: 1位,表明是否运用了掩码,在发送给服务端的数据帧里有必要运用掩码,而服务端回来时不需求掩码
    const useMask = Boolean((secondByte >>> 7)*  J [  & 0x01);
    console.log("use MASK: ", useMask);
    const payloadLen = secondByte &! 0 k R; 0x7f; // 低7位表明载荷_ X s ~ D @字节长度
    offset += 1;
    // 四个字节P j R B f的掩码
    let MASK = [];
    // 假如这个值在0-125之间,则后边的4个字节(32位)就应该被直接识别成掩码;
    if (payloadLen <= 0x7d) {
      // 载荷长度小于125
      MASK = buffer.slice(offset, 4 + offq h ` N ; | G set);
      offset += 4;. L P a 1 ?  g *
      console.log("pj w U Aayload length: ", payloadLen);
    } else if (payloadLen === 0x7e) {
      // 假如这个值是126,则后边两个字节(16位)内容应该,被识别成一个16位的二进制数表明数据内容巨细;
      consol1 * . m fe.G M ? j T n l Ulog("payload length: | $ h $ w v ] ~", buffer.readInt16BE(offseJ * $t));
      // 长度是126, 则后边两个字节作为payload length,32位的掩码
      MASs l 7 N i h ~ 5 aK = buffer.slice(offset + 2, offset + 2 + 4);
      offset += 6;
    } else {
      // 假如这个值是D w * q 8 R j127,则后边的8个字节(64位)内容应该被识别成一个64位的二进制数表明数据内容巨细
      MASK = buffer.slice(offset + 8, offset + 8 + 4);
      offsT  T bet += 12;
    }
    // 开端读取后边的2 / y Zpayload,与掩码核算,得到本来的字节内容
    const newBuffer = [];
    const dataBuffer = buffer.sl~ I i vice(offset);
    for (let i = 0, j = 0; i < dataBuH M | q X :ffer.length; i+` + L a } Q+, j = i % 4) {
      const nextBuf = dataBuffer[i];
      newBuffer.push(nextBuf ^ MASK[j]);
    }
    return BuffA b ~er.from(newBuffer).toString();
  }
  r/ / J w x b 4 Leturn "";
}

创立完 p= h TarseMessage 函数,咱们来更新一下之前创立的 WebSocket 服务器:

seD t H X irver.on("upgrade", function (req, socket) {
  socket.on("data", (buffer) => {
    const message = parseMessage(buff j c vfer);
    if (message) {
      console.log(6 K ~ B s  A o"Message from client:" + message);
    } else if (message === null) {
      console.log("WebSocket connection closed by the client.b B #");
    }
  });
  if (req.he3 ) . D D _ y caders["upgrade"] !== "websocke7 U w i t C pt") {
    socket.end("HTTP/1.1 400 Bad Request5 j ^ ( u");
    return;
  }
  // 省T m 1 D *掉已有代码
});

更新完结之后,咱们从头发动服务器,然后继续运用 “发送一般文本” 的示例来测试音讯解析功用。以下发送 “a @ = E我是阿宝哥” 文本音讯后,WebSocket 服务器输出的信息。

Server running at hK ` Jttp://localhost:8888
isFIN:  true
use MASK:  true
payload length:  15
Message from client:我是阿宝哥

经过观察以上的输出信息,咱们的 WebSocket 服务器现已能够# X w M @ U Z ? ^成功解析客户端发送包括一般文本的数据帧,下一步咱们来完结音讯呼应的功用。

3.4.2 音讯呼# Z S 3 g ] B

要把数据回来给客户端,咱们的 WebSocket 服务器也得按照 WebSocket 数据帧的格局来封装数据。与前面介绍的 parseMessage 函数一样,阿宝哥也封装了一个 construct` ` H K zReply 函数用来封装回来的数据,该函数的详细代码如下:

function constructReply(data) {
  const json = JSON.strim ~ s u  { Angify(data);
  const jsonByteLength = Buffer.byteLength(json);
  // 现在只支撑小于i b % v ] G ^65535字节的负载
  const lengthByteCount = jsonByteLength < 126 ? 0 : 2;
  const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;
  const buffer = Buffer.alloc(2 + lengthByt8 1 v & } 6eCount + jsonByteLength);
  /T M 5 Y ? 2 ] s/ 设置数据帧首字节,设置opcode为1,表明文本, ( S l j ! v ~
  buffer.writeUInt8(0b10000001, 0);
  buffer.writeUInt8(payloadLength, 1);
  // 假如payloadLeL e  d +ngth为126,则后边两个字节(16位)内容应该,被识别成一个16位的二进制数表明数据内容巨细
  let payloadOffset = 2;
  if (lengthByteCount >~ H [; 0) {
    buffer.writ7 t F f 4 P Q / 7eUInt16BE(jsonBy) v OteLength, 2);
    payloadOffset += lengthByteCount;
  }
  // 把JSON数据写入到Buffer缓冲区中
  buffer.write(json, payloadOffset);
  return buffer;
}

创立完 constructReply 函数,咱们| _ k 1 ( ! p =再来更新一下之前创立的 WebSocket 服务器:

server.on("upgrade", function (req, socket) {
  socket.on("data", (b. # F 3uffer) => {
    const message = parseMessage(buffer);
    if (m4 2 y B i . ` pessage) {
      console.log("Message from client:" + message);
      // 新增以下代码
      socket.write(constructReply({ message } a r Y h p R));
    } else if (message === null) {
      console.log("WebSocket connectiQ M i { ; ^ qon closed by the client.");
    }
  });
});

到这儿,咱们的 WebSocket 服务器现已开发完结了,接下来咱们来完好验证一下它的功用。

你不知道的 WebSocket

从图中可知,咱们的开发的简易版 WebSocket 服务器现已能够正常处理一般文本音讯了。终J c * { D d R c 5咱们来看一下完好的代码:

custom-websm n ( w : t |ocket-server.js

const httw . & Z ` 4 t l &p = require("http");

conN 3 Wst port = 8888;
const { generateAcceptValue, parseMessagV N 4 - [ m / ue, constructRe1 h `ply } = requ] H q d & M x ~ Tire("./= 3 G K 5 D 3util");

const server = http.createServer((req, res) => {
  res.writeY  J C 0 5 ;Head(200, { "Content-Ty0 Z m N D 5pe": "text/plain; charso ` K O N z 4  ket=utf-8" });
  res.end("大家好,我是阿宝哥。感谢你阅览“你不知道的WebSocket”");
});

server.on("z @ upgrade", function (req, socket) {
  socket.on3 y ( R 7 2 = R ("data", (buffer) => {
    const message = parse3 _ U & Q z | dMessage(buffer);
    if (message) {
      console.log("Message from client:" + mes= F * N hsage);
      socket x _ H.write(constructReply({ message }));
    } else if (message === null) {
      console.log("WebSocket connection closed by the client.");
    }
  }x ) W ` @ [ B m q);
  if (req.headers["upgrade"] !=- w &  I ) # C= "websocket") {
    socket.end("HTTP/1.1 400 Bad Request");
    return;
  }
  // 读取客s | @ D - _ g户端n E S i供给的Sec-WebSocket-Key
  conF c Q _ + [ 8 ist secWsKey = req.headers["sec-websocket-key"];
  // 运用SHA-1算法生成Sec-WebSocket-Accept
  const h4 } I N W K Bash = generaM % gteAcceptValue(secWsKey; y U 1 =);
  // 设置HTTP呼应头
  const responseHeaders = [
    "HTTP/1.1 101 Web Socket Protocol Handshake",
    "Upgrade: WebSocket",
    "Connection: Upgrade",
    `Sec-WebSocket^ : q x W  a-Accept: ${hash}`,
  ];
  // 回来握手恳求的呼应信息
  socket.write(responseHeaders.join("\rd E + Y B O\n") + "C r Tr\n\r\nR - [ 8 Q q 3 _");
});

server.listen(port, () =>
  console.log(S + ; m C 4 z`Serve? 6 { B  d xr running at http://localhost:${port}`)
)e # u  e / } &;

util.js

const crypto = require("crypto");

const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

function generateAcceptValue(secWsKey) {
  returu v o T / wn crL 3 9 { =  p -ypto
    .createHash("sha1")
    .update(secWsKey + MAGIC_KEY, "utf8")
    .digest("base64");
}

function parseMessage(b0 C H } 9 t 7 juffer) {
  // 第一个字节,D I ` . D .包括了FIN位,opcode, 掩码位
  const firstByte = buffer.readUInt8(0);
  // [FIN, RSV, RSV, RSV, OP4 X #CODE, OPCODE, OPCODE, OPCODE];
  // 右移7位取首位,1位,表明是否是终究一帧数据
  const isFinalFrame = Boolean((firstByt_ g O B 9  f % 9e >>> 7) & 0x01);
  cons{ - P j x ,ole.log("isFIN: ", isFinalFrame);
  // 取出操作码,低四位
  /**
   * %x0:表明一个连续帧。当 Opcode 为 0 时,表明本次数据传输选用了数据分片,当时收到的数据帧为其间一个数据分片;
   * %x1:表明这是一个文本帧(text frame);
   * %x2:表明这是一个二进制帧(binarq  R $y frame)2 D f H T   * %x3-7:保留的操作代码,用于后续界说的非操控帧;
   * %x8:表明衔接断开;
   * %x9:表明这是一个心跳恳求(ping);
   * %xA:表明这是一个心跳呼应(pong);} k [ o 6 / n + y
   * %xB-F:保留的操作代码,用于后续界说的操控帧。
   */
  const opcode = firstByt~ r f * M D (e & 0x0f;
  if (opcode === 0x08) {
    // 衔接封闭
    return;
  }
  if (opcode === 0x02) {
    // 二进制帧
    return;
  }
  if (opcode === 0x01) {
    // 现在只处理文本] m ) T
    let offset = 1;
    const sx ^ S c ?econdByte = buffer.readUz e 8 = 8 KInt0 R J O h & e x g8(offset);
    // MASK: 1位,表明是否运用了掩码,在发送给服务端的数据帧里有必要运用掩码,而服务端( - R回来时不需求掩码
    const us~ $ b n m :eMask = Boolean((secondByte >>> 7D  P & } 5 * t o) & 0x01);
    console.log("use MASK: ", us^ t } ;eMasQ # F = i |k);
    const payloadLen = secondByte &am= z W Tp; 0x7f; // 低7位表明载荷字节长度
    off1 $ U vset += 1;
    // 四个字节的掩码
    let MASK = [];
    // 假如这个值在0-125之间,则后边的4个字节(32位)就应该被直接识别成掩码;
    if (payloadLen <= 0x7d) {
      // 载荷长度小于125
      MASA P F t u K = buffer.slice(offset, 4 + offset);
      offset += 4;
      console.log("payload length: ", payloadLen);
    } elseV * R [ C ! M w G if (payloadLen === 0x7e) {
      // 假如这个值是126,则后边两个字节(16位)内容应该,被识别成一个16位的二进制数表明数据内容巨细;
      console.log("payload lengg y A R / p g Q nth: ", bufg U n C * a { e fer.readInt16BE(offset));
      // 长度是126, 则后边两个字节作为payload length,32位的掩码
      MASK = buffer.slice(offset + 2, offset + 2 + 4);
      offset += 6;
    } else {
      // 假如这个值是127,则后边的8个字节(64位)内容应该, 5 D , n被识别成一个64位的二进制数表明数据内容巨细
      MASK = buffer.slice(offset + 8, offset + 8J } T d d : 1 + 4);
      offset += 12;
    }
    // 开端读取后边的payload,与掩码核算,得到本来的字节内容
    const newBuffer = [];
    const dataBuffer = buffer.slice(offset);
    for (let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) {
      col $ [ -nst nextBuf = dataBuffer[i];
      nf r ?ewBuffer.push(nextBuf ^ MASK[j]);
    }
    return Buffer.from(newBuffer).toString();
  }
  return ""R M U t D T K };
}

function construcp ~ Q 2 U J x ItReply(data) {
  const json = JSON.stringify(data);
  const jsonByteLength = Buffer.byteLength(json)I 0 O Y;
  // 现在只支撑小于65535字节的负载
  const lengthByteCou) Z - % Gnt = jsonByteLength < 126 ? 0 : 2;9 R G
  const payloadF } 7 Z b / .Length = lengthByteCg K Y b P ount === 0 ? jsonByteLength : 126;
  const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);
  // 设置数据帧首字节,设置opcode为1,表明文本帧
  buffer.writeUInE P D K M p { T 9t8(0bA E Q w F h  C10000001, 0);
  buffN j ) M ser.writeUInt8(payloadLength,& { ? / [ [ P h 1);
  // 假如pas ^ A [yloadLen2 ~ 1 Jgth为1X ~ t ? O Q d I26,则后边两个字节(16位)内容应该,被识+ m Q : 3 V别成一个16位L f K 2的二进制数表明数据内容巨细
  let payloadOffset = 2;
  if (lengthByteCount > 0) {
    buffer.writeUInt16BE(jsoi Y * UnByteLength, 2);
    payloa& y q j r 8dOffset += lengthByteCount;
  }
  // 把JSON数据写入到Buffer缓冲区中
  buffer.write(json, payloadOffset);
  returL K $n buffer;
}

module.exports = {
  generateA? k L G G E bcceptValue,
  parseMessage,
  constructReply,
};

其实服务器向浏览器推送信息,除了运用 Web| M Y U RSocket 技能之外,还能够运用 SSE(Server-Sent Eg T A ( { c 3 zvents)。它让服务器能够向客户端流式发送文本音讯,比方服务器上生成的实时音讯。为完结这个方针,SSE 规划了两个组件:浏览器中的 EventSource API 和新的 “事情流” 数据格局(text/event-stream)。其间,EventSource 能够让客户端以 DOM 事情的方式接纳到服务器推送的告诉,而新数据格局则用于交给每一次数据l @ r ^ m _ v更新~ Z c s Y

实际上,SSE 供给的是一个高效、跨浏览器的 XHR 流完结,音讯交给只运用一个长 HTTP 衔接。可是,与咱们自己完结 XHR 流不同,浏览器会u 4 f g帮咱们管理衔接、 解析音讯,然后让咱们只关注事务逻辑。篇幅有限,关于 SSE 的更多% O 9 | ? 6细节,阿宝哥就不展开介绍了,对 SSE 感兴趣的小伙伴能够自行查阅相关材料。

四、阿宝哥有话说

4.1 Wg 1 ! u U $ L , ebSocket 与 HTTP 有什么联系

WebSocket 是一种与# d E Z X w s G HTTP 不同的协议。两者都位于 OSI 模型的运用层,而且都依赖于传输层的 TCP 协议。 尽管它们不同,可是 RFC 6455 中规定:WebSocket 被规划为在 HTTP 80 和 443 端口上工作,并支撑 HTTP 署理和中介,然后使其与 HTTP 协议兼容。 为了完结0 T v兼容性,WebSocket 握手运用 HTE 0 oTP Upgrade 头,从 HTTP{ a $ 协议更改为 WebSocket 协议。

已然现已提到了 OSI(Open System Interconnection Mody x c z t & ) ] ?el)U g K模型= ; f H | C .,这儿阿宝哥来共享一张很生动、很形象描绘 OSI 模型的示意图:

你不知道的 WebSocket

(图片来历:~ B ( H Y Jhttps://www.networkingsphere.com/20 C . 8 Q M j C019/07/what-isx y u Y B 0 & P-osi-model.html)

4.2A s ~ r ^ B WebSocket 与长轮询有什么差异

长轮询便是客户端建议一个恳求,服务器收到客户端发来g h f C 3 , # #的恳求后,服务器端不会直接进行呼应,6 s S V I M W _而是先将这个恳求挂起,然后判断恳求的数据是否有更4 . B 5 $新。假如有更新,则进行呼应,假如一向没有数据,则等候必定的时间a S L = ~ _ {后才回来。

长轮询的实质仍是基于 HTTP 协议,d d m x y { s 9它仍然是一个一问一答(恳求 — 呼应)的形式。而 WebSo6 q M U & v ? Gcket 在握手成功后,便是全双工的 TCP 通道,数据能够自动从服务端发送到客户端。

你不知道的 WebSocket

4.3– : D D j 什么是 WebSocket 心跳

网络中的接纳和发送数据都是运用 SOCKET 进行完结。可是假如此套接字[ # L H c现已断开,那发送数据和接纳数据的时分就必定会有问题。可是怎么判断这个套接字是否还能够运用呢?这个就需求在体系中创– 1 { m | 0 ^ *立心跳机x a O # : ; 9制。所谓 “心跳” 便是守时发送一个自界说的结构体(心跳包或心跳^ , | S * ) P U帧),让对方知道自己 “在线”。 以保证链接的有用性。

而所谓的心跳包便是客户端守时发送简略的信息给服务器端告知它我还在罢了。代码便是每隔几分钟发送一n 4 W个固定信u t 9 f p R息给服务端,服务端收到后回复一个固定8 + } T @信息,假如服务端几分钟内没有收到客户端信息则视客户端断开。

在 WebSo6 + 6 _ f W Rcket 协议中界说了 心跳 Ping心跳 Pong 的操控帧:

  • 心跳 Ping 帧包括的操作码是 0x9。假如收到了一个心跳 PG d 6 A { 6 E T iing 帧,那么终端有必要发送一个心跳 Pong 帧作为回应,除非现已收到了一个封闭帧。不然终端应该尽快回复 Pong 帧。
  • 心跳 Pong 帧包括的操作码是 0xA。作为回应发送的 Pong 帧有必要完好带着 Ping 帧X e X ` Y !中传递过来的} W s O | o “运用数据” 字段。假如终端收到一个 Ping 帧可是没有发送 Pong 帧来回应之前的 Ping 帧,那么终端能够选择仅为最近处理的 Pi3 n r p 5 9ng 帧发送 Pong 帧。此外,能够自动发送一个 Pong 帧,这用作单向心跳。

4.4 Socket 是什么

Y s E e络上的两个程序F J 0经过一个双向的通讯[ 0 * O V f衔接完结数据的交换y X O v t E M i,这个衔接的一端称为一个 socketZ U U q N C(套接字),因而s = { d x | y树立网络通讯衔接至少要一对端口号。socket 实质是对 TCP/IP 协议栈的封装,它供给了一个针对 TCP 或许 UDP 编程的接口,并不是另一种协议。经过 s: j – @ ~ock ^ 0 / u met,你能够运用 TCP/IP 协议。

Socket 的英文原义是“孔”或“插座”。作为 BSD UNIX 的进程通讯机制,取后一种意思。通常也称作”套接字”,用于描绘IP地址和端口,是一个通讯链的句柄,能够用来完结不同虚拟机或不同核算机之间的通讯。

在Intee 1 ( Zrnet 上的主机一般运转了多个服务软件,同时供给几种服务。每种服务都翻开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket 正如其英文原义] ? F那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座供给 220 伏交流电, 有的供给 110 伏交流电,有的则供给有线电视节f % , ) Z @ . r目。 客户软件将插头插到不同编号的插座,就能够得到不同的服务。—— 百度百科

关于 Socket,能够总结以下几点:

  • 它能够完结底层通讯,简直一切的运用层都是经过 socket 进行通讯的。
  • 对 TCP/IP 协议进行封装,便于运用层协议调用,归于二者之间的2 7 O ; u q 6中心笼统层。
  • TCP/IP 协议族中,传输层存在两种通用协议: TCP、UDP,两种协议不同,由于不同参& R 7 ? D x % x 数的 socket 完结进程也不一样。

下图阐明晰面向衔接的协议的套接字 API 的客户端/服务器联系。

你不知道的 WebSocket

五、参阅资源

  • 维基K l ~百科 – WebSocket
  • MDN – WebSocket
  • MDN – Protocol_upgrade_mechanism
  • MDN – 编B T 8 ) a O写 WebSocket 服务器
  • rfc6455
  • Web 功能威望攻略
你不知道的 WebSocket

本文运用 mdnice 排版

发表评论

提供最优质的资源集合

立即查看 了解详情