<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【Web技術】672- 你不知道的 WebSocket

          共 21374字,需瀏覽 43分鐘

           ·

          2020-08-03 17:21


          本文阿寶哥將從多個方面入手,全方位帶你一起探索 WebSocket 技術。閱讀完本文,你將了解以下內容:

          • 了解 WebSocket 的誕生背景、WebSocket 是什么及它的優(yōu)點;
          • 了解 WebSocket 含有哪些 API 及如何使用 WebSocket API 發(fā)送普通文本和二進制數據;
          • 了解 WebSocket 的握手協(xié)議和數據幀格式、掩碼算法等相關知識;
          • 了解如何實現一個支持發(fā)送普通文本的 WebSocket 服務器。

          在最后的 阿寶哥有話說 環(huán)節(jié),阿寶哥將介紹 WebSocket 與 HTTP 之間的關系、WebSocket 與長輪詢有什么區(qū)別、什么是 WebSocket 心跳及 Socket 是什么等內容。

          下面我們進入正題,為了讓大家能夠更好地理解和掌握 WebSocket 技術,我們先來介紹一下什么是 WebSocket。

          一、什么是 WebSocket

          1.1 WebSocket 誕生背景

          早期,很多網站為了實現推送技術,所用的技術都是輪詢。輪詢是指由瀏覽器每隔一段時間向服務器發(fā)出 HTTP 請求,然后服務器返回最新的數據給客戶端。常見的輪詢方式分為輪詢與長輪詢,它們的區(qū)別如下圖所示:

          為了更加直觀感受輪詢與長輪詢之間的區(qū)別,我們來看一下具體的代碼:

          這種傳統(tǒng)的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發(fā)出請求,然而 HTTP 請求與響應可能會包含較長的頭部,其中真正有效的數據可能只是很小的一部分,所以這樣會消耗很多帶寬資源。

          比較新的輪詢技術是 Comet。這種技術雖然可以實現雙向通信,但仍然需要反復發(fā)出請求。而且在 Comet 中普遍采用的 HTTP 長連接也會消耗服務器資源。

          在這種情況下,HTML5 定義了 WebSocket 協(xié)議,能更好的節(jié)省服務器資源和帶寬,并且能夠更實時地進行通訊。Websocket 使用 ws 或 wss 的統(tǒng)一資源標志符(URI),其中 wss 表示使用了 TLS 的 Websocket。如:

          ws://echo.websocket.org
          wss://echo.websocket.org

          WebSocket 與 HTTP 和 HTTPS 使用相同的 TCP 端口,可以繞過大多數防火墻的限制。默認情況下,WebSocket 協(xié)議使用 80 端口;若運行在 TLS 之上時,默認使用 443 端口。

          1.2 WebSocket 簡介

          WebSocket 是一種網絡傳輸協(xié)議,可在單個 TCP 連接上進行全雙工通信,位于 OSI 模型的應用層。WebSocket 協(xié)議在 2011 年由 IETF 標準化為 RFC 6455,后由 RFC 7936 補充規(guī)范。

          WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只需要完成一次握手,兩者之間就可以創(chuàng)建持久性的連接,并進行雙向數據傳輸。

          介紹完輪詢和 WebSocket 的相關內容之后,接下來我們來看一下 XHR Polling 與 WebSocket 之間的區(qū)別:

          1.3 WebSocket 優(yōu)點

          • 較少的控制開銷。在連接創(chuàng)建后,服務器和客戶端之間交換數據時,用于協(xié)議控制的數據包頭部相對較小。
          • 更強的實時性。由于協(xié)議是全雙工的,所以服務器可以隨時主動給客戶端下發(fā)數據。相對于 HTTP 請求需要等待客戶端發(fā)起請求服務端才能響應,延遲明顯更少。
          • 保持連接狀態(tài)。與 HTTP 不同的是,WebSocket 需要先創(chuàng)建連接,這就使得其成為一種有狀態(tài)的協(xié)議,之后通信時可以省略部分狀態(tài)信息。
          • 更好的二進制支持。WebSocket 定義了二進制幀,相對 HTTP,可以更輕松地處理二進制內容。
          • 可以支持擴展。WebSocket 定義了擴展,用戶可以擴展協(xié)議、實現部分自定義的子協(xié)議。

          由于 WebSocket 擁有上述的優(yōu)點,所以它被廣泛地應用在即時通信、實時音視頻、在線教育和游戲等領域。對于前端開發(fā)者來說,要想使用 WebSocket 提供的強大能力,就必須先掌握 WebSocket API,下面阿寶哥帶大家一起來認識一下 WebSocket API。

          二、WebSocket API

          在介紹 WebSocket API 之前,我們先來了解一下它的兼容性:

          (圖片來源:https://caniuse.com/#search=WebSocket)

          從上圖可知,目前主流的 Web 瀏覽器都支持 WebSocket,所以我們可以在大多數項目中放心地使用它。

          在瀏覽器中要使用 WebSocket 提供的能力,我們就必須先創(chuàng)建 WebSocket 對象,該對象提供了用于創(chuàng)建和管理 WebSocket 連接,以及可以通過該連接發(fā)送和接收數據的 API。

          使用 WebSocket 構造函數,我們就能輕易地構造一個 WebSocket 對象。接下來我們將從 WebSocket 構造函數、WebSocket 對象的屬性、方法及 WebSocket 相關的事件四個方面來介紹 WebSocket API,首先我們從 WebSocket 的構造函數入手:

          2.1 構造函數

          WebSocket 構造函數的語法為:

          const?myWebSocket?=?new?WebSocket(url?[,?protocols]);

          相關參數說明如下:

          • url:表示連接的 URL,這是 WebSocket 服務器將響應的 URL。
          • protocols(可選):一個協(xié)議字符串或者一個包含協(xié)議字符串的數組。這些字符串用于指定子協(xié)議,這樣單個服務器可以實現多個 WebSocket 子協(xié)議。比如,你可能希望一臺服務器能夠根據指定的協(xié)議(protocol)處理不同類型的交互。如果不指定協(xié)議字符串,則假定為空字符串。

          當嘗試連接的端口被阻止時,會拋出 SECURITY_ERR 異常。

          2.2 屬性

          WebSocket 對象包含以下屬性:

          每個屬性的具體含義如下:

          • binaryType:使用二進制的數據類型連接。
          • bufferedAmount(只讀):未發(fā)送至服務器的字節(jié)數。
          • extensions(只讀):服務器選擇的擴展。
          • onclose:用于指定連接關閉后的回調函數。
          • onerror:用于指定連接失敗后的回調函數。
          • onmessage:用于指定當從服務器接受到信息時的回調函數。
          • onopen:用于指定連接成功后的回調函數。
          • protocol(只讀):用于返回服務器端選中的子協(xié)議的名字。
          • readyState(只讀):返回當前 WebSocket 的連接狀態(tài),共有 4 種狀態(tài):
            • CONNECTING — 正在連接中,對應的值為 0;
            • OPEN — 已經連接并且可以通訊,對應的值為 1;
            • CLOSING — 連接正在關閉,對應的值為 2;
            • CLOSED — 連接已關閉或者沒有連接成功,對應的值為 3。
          • url(只讀):返回值為當構造函數創(chuàng)建 WebSocket 實例對象時 URL 的絕對路徑。

          2.3 方法

          • close([code[, reason]]):該方法用于關閉 WebSocket ?連接,如果連接已經關閉,則此方法不執(zhí)行任何操作。
          • send(data):該方法將需要通過 WebSocket 鏈接傳輸至服務器的數據排入隊列,并根據所需要傳輸的數據的大小來增加 bufferedAmount 的值 。若數據無法傳輸(比如數據需要緩存而緩沖區(qū)已滿)時,套接字會自行關閉。

          2.4 事件

          使用 addEventListener() 或將一個事件監(jiān)聽器賦值給 WebSocket 對象的 oneventname 屬性,來監(jiān)聽下面的事件。

          • close:當一個 WebSocket 連接被關閉時觸發(fā),也可以通過 onclose 屬性來設置。
          • error:當一個 WebSocket 連接因錯誤而關閉時觸發(fā),也可以通過 onerror 屬性來設置。
          • message:當通過 WebSocket 收到數據時觸發(fā),也可以通過 onmessage 屬性來設置。
          • open:當一個 WebSocket 連接成功時觸發(fā),也可以通過 onopen 屬性來設置。

          介紹完 WebSocket API,我們來舉一個使用 WebSocket 發(fā)送普通文本的示例。

          2.5 發(fā)送普通文本

          在以上示例中,我們在頁面上創(chuàng)建了兩個 textarea,分別用于存放 待發(fā)送的數據服務器返回的數據。當用戶輸入完待發(fā)送的文本之后,點擊 發(fā)送 按鈕時會把輸入的文本發(fā)送到服務端,而服務端成功接收到消息之后,會把收到的消息原封不動地回傳到客戶端。

          //?const?socket?=?new?WebSocket("ws://echo.websocket.org");
          //?const?sendMsgContainer?=?document.querySelector("#sendMessage");
          function?send()?{
          ??const?message?=?sendMsgContainer.value;
          ??if?(socket.readyState?!==?WebSocket.OPEN)?{
          ????console.log("連接未建立,還不能發(fā)送消息");
          ????return;
          ??}
          ??if?(message)?socket.send(message);
          }

          當然客戶端接收到服務端返回的消息之后,會把對應的文本內容保存到 接收的數據 對應的 textarea 文本框中。

          //?const?socket?=?new?WebSocket("ws://echo.websocket.org");
          //?const?receivedMsgContainer?=?document.querySelector("#receivedMessage");????
          socket.addEventListener("message",?function?(event)?{
          ??console.log("Message?from?server?",?event.data);
          ??receivedMsgContainer.value?=?event.data;
          });

          為了更加直觀地理解上述的數據交互過程,我們使用 Chrome 瀏覽器的開發(fā)者工具來看一下相應的過程:

          以上示例對應的完整代碼如下所示:


          <html>
          ??<head>
          ????<meta?charset="UTF-8"?/>
          ????<meta?name="viewport"?content="width=device-width,?initial-scale=1.0"?/>
          ????<title>WebSocket?發(fā)送普通文本示例title>
          ????<style>
          ??????.block?{
          ????????flex:?1;
          ??????}
          ????
          style>
          ??head>
          ??<body>
          ????<h3>阿寶哥:WebSocket 發(fā)送普通文本示例h3>
          ????<div?style="display:?flex;">
          ??????<div?class="block">
          ????????<p>即將發(fā)送的數據:<button?onclick="send()">發(fā)送button>p>
          ????????<textarea?id="sendMessage"?rows="5"?cols="15">textarea>
          ??????div>
          ??????<div?class="block">
          ????????<p>接收的數據:p>
          ????????<textarea?id="receivedMessage"?rows="5"?cols="15">textarea>
          ??????div>
          ????div>

          ????<script>
          ??????const?sendMsgContainer?=?document.querySelector("#sendMessage");
          ??????const?receivedMsgContainer?=?document.querySelector("#receivedMessage");
          ??????const?socket?=?new?WebSocket("ws://echo.websocket.org");

          ??????//?監(jiān)聽連接成功事件
          ??????socket.addEventListener("open",?function?(event)?{
          ????????console.log("連接成功,可以開始通訊");
          ??????});

          ??????//?監(jiān)聽消息
          ??????socket.addEventListener("message",?function?(event)?{
          ????????console.log("Message?from?server?",?event.data);
          ????????receivedMsgContainer.value?=?event.data;
          ??????});

          ??????function?send()?{
          ????????const?message?=?sendMsgContainer.value;
          ????????if?(socket.readyState?!==?WebSocket.OPEN)?{
          ??????????console.log("連接未建立,還不能發(fā)送消息");
          ??????????return;
          ????????}
          ????????if?(message)?socket.send(message);
          ??????}
          ????
          script>
          ??body>
          html>

          其實 WebSocket 除了支持發(fā)送普通的文本之外,它還支持發(fā)送二進制數據,比如 ArrayBuffer 對象、Blob 對象或者 ArrayBufferView 對象:

          const?socket?=?new?WebSocket("ws://echo.websocket.org");
          socket.onopen?=?function?()?{
          ??//?發(fā)送UTF-8編碼的文本信息
          ??socket.send("Hello?Echo?Server!");
          ??//?發(fā)送UTF-8編碼的JSON數據
          ??socket.send(JSON.stringify({?msg:?"我是阿寶哥"?}));
          ??
          ??//?發(fā)送二進制ArrayBuffer
          ??const?buffer?=?new?ArrayBuffer(128);
          ??socket.send(buffer);
          ??
          ??//?發(fā)送二進制ArrayBufferView
          ??const?intview?=?new?Uint32Array(buffer);
          ??socket.send(intview);

          ??//?發(fā)送二進制Blob
          ??const?blob?=?new?Blob([buffer]);
          ??socket.send(blob);
          };

          以上代碼成功運行后,通過 Chrome 開發(fā)者工具,我們可以看到對應的數據交互過程:

          下面阿寶哥以發(fā)送 Blob 對象為例,來介紹一下如何發(fā)送二進制數據。

          Blob(Binary Large Object)表示二進制類型的大對象。在數據庫管理系統(tǒng)中,將二進制數據存儲為一個單一個體的集合。Blob 通常是影像、聲音或多媒體文件。在 JavaScript 中 Blob 類型的對象表示不可變的類似文件對象的原始數據。

          對 Blob 感興趣的小伙伴,可以閱讀 “你不知道的 Blob” 這篇文章。

          2.6 發(fā)送二進制數據

          在以上示例中,我們在頁面上創(chuàng)建了兩個 textarea,分別用于存放 待發(fā)送的數據服務器返回的數據。當用戶輸入完待發(fā)送的文本之后,點擊 發(fā)送 按鈕時,我們會先獲取輸入的文本并把文本包裝成 Blob 對象然后發(fā)送到服務端,而服務端成功接收到消息之后,會把收到的消息原封不動地回傳到客戶端。

          當瀏覽器接收到新消息后,如果是文本數據,會自動將其轉換成 DOMString 對象,如果是二進制數據或 Blob 對象,會直接將其轉交給應用,由應用自身來根據返回的數據類型進行相應的處理。

          數據發(fā)送代碼

          //?const?socket?=?new?WebSocket("ws://echo.websocket.org");
          //?const?sendMsgContainer?=?document.querySelector("#sendMessage");
          function?send()?{
          ??const?message?=?sendMsgContainer.value;
          ??if?(socket.readyState?!==?WebSocket.OPEN)?{
          ????console.log("連接未建立,還不能發(fā)送消息");
          ????return;
          ??}
          ??const?blob?=?new?Blob([message],?{?type:?"text/plain"?});
          ??if?(message)?socket.send(blob);
          ??console.log(`未發(fā)送至服務器的字節(jié)數:${socket.bufferedAmount}`);
          }

          當然客戶端接收到服務端返回的消息之后,會判斷返回的數據類型,如果是 Blob 類型的話,會調用 Blob 對象的 text() 方法,獲取 Blob 對象中保存的 UTF-8 格式的內容,然后把對應的文本內容保存到 接收的數據 對應的 textarea 文本框中。

          數據接收代碼

          //?const?socket?=?new?WebSocket("ws://echo.websocket.org");
          //?const?receivedMsgContainer?=?document.querySelector("#receivedMessage");
          socket.addEventListener("message",?async?function?(event)?{
          ??console.log("Message?from?server?",?event.data);
          ??const?receivedData?=?event.data;
          ??if?(receivedData?instanceof?Blob)?{
          ????receivedMsgContainer.value?=?await?receivedData.text();
          ??}?else?{
          ????receivedMsgContainer.value?=?receivedData;
          ??}
          ?});

          同樣,我們使用 Chrome 瀏覽器的開發(fā)者工具來看一下相應的過程:

          通過上圖我們可以很明顯地看到,當使用發(fā)送 Blob 對象時,Data 欄位的信息顯示的是 Binary Message,而對于發(fā)送普通文本來說,Data 欄位的信息是直接顯示發(fā)送的文本消息。

          以上示例對應的完整代碼如下所示:


          <html>
          ??<head>
          ????<meta?charset="UTF-8"?/>
          ????<meta?name="viewport"?content="width=device-width,?initial-scale=1.0"?/>
          ????<title>WebSocket?發(fā)送二進制數據示例title>
          ????<style>
          ??????.block?{
          ????????flex:?1;
          ??????}
          ????
          style>
          ??head>
          ??<body>
          ????<h3>阿寶哥:WebSocket 發(fā)送二進制數據示例h3>
          ????<div?style="display:?flex;">
          ??????<div?class="block">
          ????????<p>待發(fā)送的數據:<button?onclick="send()">發(fā)送button>p>
          ????????<textarea?id="sendMessage"?rows="5"?cols="15">textarea>
          ??????div>
          ??????<div?class="block">
          ????????<p>接收的數據:p>
          ????????<textarea?id="receivedMessage"?rows="5"?cols="15">textarea>
          ??????div>
          ????div>

          ????<script>
          ??????const?sendMsgContainer?=?document.querySelector("#sendMessage");
          ??????const?receivedMsgContainer?=?document.querySelector("#receivedMessage");
          ??????const?socket?=?new?WebSocket("ws://echo.websocket.org");

          ??????//?監(jiān)聽連接成功事件
          ??????socket.addEventListener("open",?function?(event)?{
          ????????console.log("連接成功,可以開始通訊");
          ??????});

          ??????//?監(jiān)聽消息
          ??????socket.addEventListener("message",?async?function?(event)?{
          ????????console.log("Message?from?server?",?event.data);
          ????????const?receivedData?=?event.data;
          ????????if?(receivedData?instanceof?Blob)?{
          ??????????receivedMsgContainer.value?=?await?receivedData.text();
          ????????}?else?{
          ??????????receivedMsgContainer.value?=?receivedData;
          ????????}
          ??????});

          ??????function?send()?{
          ????????const?message?=?sendMsgContainer.value;
          ????????if?(socket.readyState?!==?WebSocket.OPEN)?{
          ??????????console.log("連接未建立,還不能發(fā)送消息");
          ??????????return;
          ????????}
          ????????const?blob?=?new?Blob([message],?{?type:?"text/plain"?});
          ????????if?(message)?socket.send(blob);
          ????????console.log(`未發(fā)送至服務器的字節(jié)數:${socket.bufferedAmount}`);
          ??????}
          ????
          script>
          ??body>
          html>

          可能有一些小伙伴了解完 WebSocket API 之后,覺得還不夠過癮。下面阿寶哥將帶大家來實現一個支持發(fā)送普通文本的 WebSocket 服務器。

          三、手寫 WebSocket 服務器

          在介紹如何手寫 WebSocket 服務器前,我們需要了解一下 WebSocket 連接的生命周期。

          從上圖可知,在使用 WebSocket 實現全雙工通信之前,客戶端與服務器之間需要先進行握手(Handshake),在完成握手之后才能開始進行數據的雙向通信。

          握手是在通信電路創(chuàng)建之后,信息傳輸開始之前。握手用于達成參數,如信息傳輸率,字母表,奇偶校驗,中斷過程,和其他協(xié)議特性。 ?握手有助于不同結構的系統(tǒng)或設備在通信信道中連接,而不需要人為設置參數。

          既然握手是 WebSocket 連接生命周期的第一個環(huán)節(jié),接下來我們就先來分析 WebSocket 的握手協(xié)議。

          3.1 握手協(xié)議

          WebSocket 協(xié)議屬于應用層協(xié)議,它依賴于傳輸層的 TCP 協(xié)議。WebSocket 通過 HTTP/1.1 協(xié)議的 101 狀態(tài)碼進行握手。為了創(chuàng)建 WebSocket 連接,需要通過瀏覽器發(fā)出請求,之后服務器進行回應,這個過程通常稱為 “握手”(Handshaking)。

          利用 HTTP 完成握手有幾個好處。首先,讓 WebSocket 與現有 HTTP 基礎設施兼容:使得 WebSocket 服務器可以運行在 80 和 443 端口上,這通常是對客戶端唯一開放的端口。其次,讓我們可以重用并擴展 HTTP 的 Upgrade 流,為其添加自定義的 WebSocket 首部,以完成協(xié)商。

          下面我們以前面已經演示過的發(fā)送普通文本的例子為例,來具體分析一下握手過程。

          3.1.1 客戶端請求
          GET ws://echo.websocket.org/ HTTP/1.1
          Host: echo.websocket.org
          Origin: file://
          Connection: Upgrade
          Upgrade: websocket
          Sec-WebSocket-Version: 13
          Sec-WebSocket-Key: Zx8rNEkBE4xnwifpuh8DHQ==
          Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

          備注:已忽略部分 HTTP 請求頭

          字段說明

          • Connection 必須設置 Upgrade,表示客戶端希望連接升級。
          • Upgrade 字段必須設置 websocket,表示希望升級到 WebSocket 協(xié)議。
          • Sec-WebSocket-Version 表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均應當棄用。
          • Sec-WebSocket-Key 是隨機的字符串,服務器端會用這些數據來構造出一個 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一個特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進行 Base64 編碼,將結果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。如此操作,可以盡量避免普通 HTTP 請求被誤認為 WebSocket 協(xié)議。
          • Sec-WebSocket-Extensions 用于協(xié)商本次連接要使用的 WebSocket 擴展:客戶端發(fā)送支持的擴展,服務器通過返回相同的首部確認自己支持一個或多個擴展。
          • Origin 字段是可選的,通常用來表示在瀏覽器中發(fā)起此 WebSocket 連接所在的頁面,類似于 Referer。但是,與 Referer 不同的是,Origin 只包含了協(xié)議和主機名稱。
          3.1.2 服務端響應
          HTTP/1.1 101 Web Socket Protocol Handshake ①
          Connection: Upgrade ②
          Upgrade: websocket ③
          Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④

          備注:已忽略部分 HTTP 響應頭

          • ① 101 響應碼確認升級到 WebSocket 協(xié)議。
          • ② 設置 Connection 頭的值為 "Upgrade" 來指示這是一個升級請求。HTTP 協(xié)議提供了一種特殊的機制,這一機制允許將一個已建立的連接升級成新的、不相容的協(xié)議。
          • ③ Upgrade 頭指定一項或多項協(xié)議名,按優(yōu)先級排序,以逗號分隔。這里表示升級為 WebSocket 協(xié)議。
          • ④ ?簽名的鍵值驗證協(xié)議支持。

          介紹完 WebSocket 的握手協(xié)議,接下來阿寶哥將使用 Node.js 來開發(fā)我們的 WebSocket 服務器。

          3.2 實現握手功能

          要開發(fā)一個 WebSocket 服務器,首先我們需要先實現握手功能,這里阿寶哥使用 Node.js 內置的 http 模塊來創(chuàng)建一個 HTTP 服務器,具體代碼如下所示:

          const?http?=?require("http");

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

          const?server?=?http.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"]?!==?"websocket")?{
          ????socket.end("HTTP/1.1?400?Bad?Request");
          ????return;
          ??}
          ??//?讀取客戶端提供的Sec-WebSocket-Key
          ??const?secWsKey?=?req.headers["sec-websocket-key"];
          ??//?使用SHA-1算法生成Sec-WebSocket-Accept
          ??const?hash?=?generateAcceptValue(secWsKey);
          ??//?設置HTTP響應頭
          ??const?responseHeaders?=?[
          ????"HTTP/1.1?101?Web?Socket?Protocol?Handshake",
          ????"Upgrade:?WebSocket",
          ????"Connection:?Upgrade",
          ????`Sec-WebSocket-Accept:?${hash}`,
          ??];
          ??//?返回握手請求的響應信息
          ??socket.write(responseHeaders.join("\r\n")?+?"\r\n\r\n");
          });

          server.listen(port,?()?=>
          ??console.log(`Server?running?at?http://localhost:${port}`)
          );

          在以上代碼中,我們首先引入了 http 模塊,然后通過調用該模塊的 createServer() 方法創(chuàng)建一個 HTTP 服務器,接著我們監(jiān)聽 upgrade 事件,每次服務器響應升級請求時就會觸發(fā)該事件。由于我們的服務器只支持升級到 WebSocket 協(xié)議,所以如果客戶端請求升級的協(xié)議非 WebSocket 協(xié)議,我們將會返回 “400 Bad Request”。

          當服務器接收到升級為 WebSocket 的握手請求時,會先從請求頭中獲取 “Sec-WebSocket-Key” 的值,然后把該值加上一個特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計算 SHA-1 摘要,之后進行 Base64 編碼,將結果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。

          上述的過程看起來好像有點繁瑣,其實利用 Node.js 內置的 crypto 模塊,幾行代碼就可以搞定了:

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

          function?generateAcceptValue(secWsKey)?{
          ??return?crypto
          ????.createHash("sha1")
          ????.update(secWsKey?+?MAGIC_KEY,?"utf8")
          ????.digest("base64");
          }

          開發(fā)完握手功能之后,我們可以使用前面的示例來測試一下該功能。待服務器啟動之后,我們只要對 “發(fā)送普通文本” 示例,做簡單地調整,即把先前的 URL 地址替換成 ws://localhost:8888,就可以進行功能驗證。

          感興趣的小伙們可以試試看,以下是阿寶哥本地運行后的結果:

          從上圖可知,我們實現的握手功能已經可以正常工作了。那么握手有沒有可能失敗呢?答案是肯定的。比如網絡問題、服務器異常或 Sec-WebSocket-Accept 的值不正確。

          下面阿寶哥修改一下 “Sec-WebSocket-Accept” 生成規(guī)則,比如修改 MAGIC_KEY 的值,然后重新驗證一下握手功能。此時,瀏覽器的控制臺會輸出以下異常信息:

          WebSocket?connection?to?'ws://localhost:8888/'?failed:?Error?during?WebSocket?handshake:?Incorrect?'Sec-WebSocket-Accept'?header?value

          如果你的 WebSocket 服務器要支持子協(xié)議的話,你可以參考以下代碼進行子協(xié)議的處理,阿寶哥就不繼續(xù)展開介紹了。

          //?從請求頭中讀取子協(xié)議
          const?protocol?=?req.headers["sec-websocket-protocol"];
          //?如果包含子協(xié)議,則解析子協(xié)議
          const?protocols?=?!protocol???[]?:?protocol.split(",").map((s)?=>?s.trim());

          //?簡單起見,我們僅判斷是否含有JSON子協(xié)議
          if?(protocols.includes("json"))?{
          ??responseHeaders.push(`Sec-WebSocket-Protocol:?json`);
          }

          好的,WebSocket 握手協(xié)議相關的內容基本已經介紹完了。下一步我們來介紹開發(fā)消息通信功能需要了解的一些基礎知識。

          3.3 消息通信基礎

          在 WebSocket 協(xié)議中,數據是通過一系列數據幀來進行傳輸的。為了避免由于網絡中介(例如一些攔截代理)或者一些安全問題,客戶端必須在它發(fā)送到服務器的所有幀中添加掩碼。服務端收到沒有添加掩碼的數據幀以后,必須立即關閉連接。

          3.3.1 數據幀格式

          要實現消息通信,我們就必須了解 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
          +-+-+-+-+-------+-+-------------+-------------------------------+
          |F|R|R|R|?opcode|M|?Payload?len?|????Extended?payload?length????|
          |I|S|S|S|??(4)??|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??|
          +?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?+-------------------------------+
          |???????????????????????????????|Masking-key,?if?MASK?set?to?1??|
          +-------------------------------+-------------------------------+
          |?Masking-key?(continued)???????|??????????Payload?Data?????????|
          +--------------------------------?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?+
          :?????????????????????Payload?Data?continued?...????????????????:
          +?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?-?+
          |?????????????????????Payload?Data?continued?...????????????????|
          +---------------------------------------------------------------+

          可能有一些小伙伴看到上面的內容之后,就開始有點 “懵逼” 了。下面我們來結合實際的數據幀來進一步分析一下:

          在上圖中,阿寶哥簡單分析了 “發(fā)送普通文本” 示例對應的數據幀格式。這里我們來進一步介紹一下 Payload length,因為在后面開發(fā)數據解析功能的時候,需要用到該知識點。

          Payload length 表示以字節(jié)為單位的 “有效負載數據” 長度。它有以下幾種情形:

          • 如果值為 0-125,那么就表示負載數據的長度。
          • 如果是 126,那么接下來的 2 個字節(jié)解釋為 16 位的無符號整形作為負載數據的長度。
          • 如果是 127,那么接下來的 8 個字節(jié)解釋為一個 64 位的無符號整形(最高位的 bit 必須為 0)作為負載數據的長度。

          多字節(jié)長度量以網絡字節(jié)順序表示,有效負載長度是指 “擴展數據” + “應用數據” 的長度。“擴展數據” 的長度可能為 0,那么有效負載長度就是 “應用數據” 的長度。

          另外,除非協(xié)商過擴展,否則 “擴展數據” 長度為 0 字節(jié)。在握手協(xié)議中,任何擴展都必須指定 “擴展數據” 的長度,這個長度如何進行計算,以及這個擴展如何使用。如果存在擴展,那么這個 “擴展數據” 包含在總的有效負載長度中。

          3.3.2 掩碼算法

          掩碼字段是一個由客戶端隨機選擇的 32 位的值。掩碼值必須是不可被預測的。因此,掩碼必須來自強大的熵源(entropy),并且給定的掩碼不能讓服務器或者代理能夠很容易的預測到后續(xù)幀。掩碼的不可預測性對于預防惡意應用的作者在網上暴露相關的字節(jié)數據至關重要。

          掩碼不影響數據荷載的長度,對數據進行掩碼操作和對數據進行反掩碼操作所涉及的步驟是相同的。掩碼、反掩碼操作都采用如下算法:

          j?=?i?MOD?4
          transformed-octet-i?=?original-octet-i?XOR?masking-key-octet-j
          • original-octet-i:為原始數據的第 i 字節(jié)。
          • transformed-octet-i:為轉換后的數據的第 i 字節(jié)。
          • masking-key-octet-j:為 mask key 第 j 字節(jié)。

          為了讓小伙伴們能夠更好的理解上面掩碼的計算過程,我們來對示例中 “我是阿寶哥” 數據進行掩碼操作。這里 “我是阿寶哥” 對應的 UTF-8 編碼如下所示:

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

          而對應的 Masking-Key 為 0x08f6efb1,根據上面的算法,我們可以這樣進行掩碼運算:

          let?uint8?=?new?Uint8Array([0xE6,?0x88,?0x91,?0xE6,?0x98,?0xAF,?0xE9,?0x98,?
          ??0xBF,?0xE5,?0xAE,?0x9D,?0xE5,?0x93,?0xA5]);
          let?maskingKey?=?new?Uint8Array([0x08,?0xf6,?0xef,?0xb1]);
          let?maskedUint8?=?new?Uint8Array(uint8.length);

          for?(let?i?=?0,?j?=?0;?i?4)?{
          ??maskedUint8[i]?=?uint8[i]?^?maskingKey[j];
          }

          console.log(Array.from(maskedUint8).map(num=>Number(num).toString(16)).join('?'));

          以上代碼成功運行后,控制臺會輸出以下結果:

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

          上述結果與 WireShark 中的 Masked payload 對應的值是一致的,具體如下圖所示:

          在 WebSocket 協(xié)議中,數據掩碼的作用是增強協(xié)議的安全性。但數據掩碼并不是為了保護數據本身,因為算法本身是公開的,運算也不復雜。那么為什么還要引入數據掩碼呢?引入數據掩碼是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊等問題。

          了解完 WebSocket 掩碼算法和數據掩碼的作用之后,我們再來介紹一下數據分片的概念。

          3.3.3 數據分片

          WebSocket 的每條消息可能被切分成多個數據幀。當 WebSocket 的接收方收到一個數據幀時,會根據 FIN 的值來判斷,是否已經收到消息的最后一個數據幀。

          利用 FIN 和 Opcode,我們就可以跨幀發(fā)送消息。操作碼告訴了幀應該做什么。如果是 0x1,有效載荷就是文本。如果是 0x2,有效載荷就是二進制數據。但是,如果是 0x0,則該幀是一個延續(xù)幀。這意味著服務器應該將幀的有效負載連接到從該客戶機接收到的最后一個幀。

          為了讓大家能夠更好地理解上述的內容,我們來看一個來自?MDN?上的示例:

          Client:?FIN=1,?opcode=0x1,?msg="hello"
          Server:?(process?complete?message?immediately)?Hi.
          Client:?FIN=0,?opcode=0x1,?msg="and?a"
          Server:?(listening,?new?message?containing?text?started)
          Client:?FIN=0,?opcode=0x0,?msg="happy?new"
          Server:?(listening,?payload?concatenated?to?previous?message)
          Client:?FIN=1,?opcode=0x0,?msg="year!"
          Server:?(process?complete?message)?Happy?new?year?to?you?too!

          在以上示例中,客戶端向服務器發(fā)送了兩條消息。第一個消息在單個幀中發(fā)送,而第二個消息跨三個幀發(fā)送。

          其中第一個消息是一個完整的消息(FIN=1 且 opcode != 0x0),因此服務器可以根據需要進行處理或響應。而第二個消息是文本消息(opcode=0x1)且 FIN=0,表示消息還沒發(fā)送完成,還有后續(xù)的數據幀。該消息的所有剩余部分都用延續(xù)幀(opcode=0x0)發(fā)送,消息的最終幀用 FIN=1 標記。

          好的,簡單介紹了數據分片的相關內容。接下來,我們來開始實現消息通信功能。

          3.4 實現消息通信功能

          阿寶哥把實現消息通信功能,分解為消息解析與消息響應兩個子功能,下面我們分別來介紹如何實現這兩個子功能。

          3.4.1 消息解析

          利用消息通信基礎環(huán)節(jié)中介紹的相關知識,阿寶哥實現了一個 parseMessage 函數,用來解析客戶端傳過來的 WebSocket 數據幀。出于簡單考慮,這里只處理文本幀,具體代碼如下所示:

          function?parseMessage(buffer)?{
          ??//?第一個字節(jié),包含了FIN位,opcode,?掩碼位
          ??const?firstByte?=?buffer.readUInt8(0);
          ??//?[FIN,?RSV,?RSV,?RSV,?OPCODE,?OPCODE,?OPCODE,?OPCODE];
          ??//?右移7位取首位,1位,表示是否是最后一幀數據
          ??const?isFinalFrame?=?Boolean((firstByte?>>>?7)?&?0x01);
          ??console.log("isFIN:?",?isFinalFrame);
          ??//?取出操作碼,低四位
          ??/**
          ???*?%x0:表示一個延續(xù)幀。當 Opcode 為?0?時,表示本次數據傳輸采用了數據分片,當前收到的數據幀為其中一個數據分片;
          ???*?%x1:表示這是一個文本幀(text frame);
          ???*?%x2:表示這是一個二進制幀(binary frame);
          ???*?%x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀;
          ???*?%x8:表示連接斷開;
          ???*?%x9:表示這是一個心跳請求(ping);
          ???*?%xA:表示這是一個心跳響應(pong);
          ???*?%xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。
          ???*/

          ??const?opcode?=?firstByte?&?0x0f;
          ??if?(opcode?===?0x08)?{
          ????//?連接關閉
          ????return;
          ??}
          ??if?(opcode?===?0x02)?{
          ????//?二進制幀
          ????return;
          ??}
          ??if?(opcode?===?0x01)?{
          ????//?目前只處理文本幀
          ????let?offset?=?1;
          ????const?secondByte?=?buffer.readUInt8(offset);
          ????//?MASK:?1位,表示是否使用了掩碼,在發(fā)送給服務端的數據幀里必須使用掩碼,而服務端返回時不需要掩碼
          ????const?useMask?=?Boolean((secondByte?>>>?7)?&?0x01);
          ????console.log("use?MASK:?",?useMask);
          ????const?payloadLen?=?secondByte?&?0x7f;?//?低7位表示載荷字節(jié)長度
          ????offset?+=?1;
          ????//?四個字節(jié)的掩碼
          ????let?MASK?=?[];
          ????//?如果這個值在0-125之間,則后面的4個字節(jié)(32位)就應該被直接識別成掩碼;
          ????if?(payloadLen?<=?0x7d)?{
          ??????//?載荷長度小于125
          ??????MASK?=?buffer.slice(offset,?4?+?offset);
          ??????offset?+=?4;
          ??????console.log("payload?length:?",?payloadLen);
          ????}?else?if?(payloadLen?===?0x7e)?{
          ??????//?如果這個值是126,則后面兩個字節(jié)(16位)內容應該,被識別成一個16位的二進制數表示數據內容大小;
          ??????console.log("payload?length:?",?buffer.readInt16BE(offset));
          ??????//?長度是126,?則后面兩個字節(jié)作為payload?length,32位的掩碼
          ??????MASK?=?buffer.slice(offset?+?2,?offset?+?2?+?4);
          ??????offset?+=?6;
          ????}?else?{
          ??????//?如果這個值是127,則后面的8個字節(jié)(64位)內容應該被識別成一個64位的二進制數表示數據內容大小
          ??????MASK?=?buffer.slice(offset?+?8,?offset?+?8?+?4);
          ??????offset?+=?12;
          ????}
          ????//?開始讀取后面的payload,與掩碼計算,得到原來的字節(jié)內容
          ????const?newBuffer?=?[];
          ????const?dataBuffer?=?buffer.slice(offset);
          ????for?(let?i?=?0,?j?=?0;?i?4)?{
          ??????const?nextBuf?=?dataBuffer[i];
          ??????newBuffer.push(nextBuf?^?MASK[j]);
          ????}
          ????return?Buffer.from(newBuffer).toString();
          ??}
          ??return?"";
          }

          創(chuàng)建完 parseMessage 函數,我們來更新一下之前創(chuàng)建的 WebSocket 服務器:

          server.on("upgrade",?function?(req,?socket)?{
          ??socket.on("data",?(buffer)?=>?{
          ????const?message?=?parseMessage(buffer);
          ????if?(message)?{
          ??????console.log("Message?from?client:"?+?message);
          ????}?else?if?(message?===?null)?{
          ??????console.log("WebSocket?connection?closed?by?the?client.");
          ????}
          ??});
          ??if?(req.headers["upgrade"]?!==?"websocket")?{
          ????socket.end("HTTP/1.1?400?Bad?Request");
          ????return;
          ??}
          ??//?省略已有代碼
          });

          更新完成之后,我們重新啟動服務器,然后繼續(xù)使用 “發(fā)送普通文本” 的示例來測試消息解析功能。以下發(fā)送 “我是阿寶哥” 文本消息后,WebSocket 服務器輸出的信息。

          Server?running?at?http://localhost:8888
          isFIN:??true
          use?MASK:??true
          payload?length:??15
          Message?from?client:我是阿寶哥

          通過觀察以上的輸出信息,我們的 WebSocket 服務器已經可以成功解析客戶端發(fā)送包含普通文本的數據幀,下一步我們來實現消息響應的功能。

          3.4.2 消息響應

          要把數據返回給客戶端,我們的 WebSocket 服務器也得按照 WebSocket 數據幀的格式來封裝數據。與前面介紹的 parseMessage 函數一樣,阿寶哥也封裝了一個 constructReply 函數用來封裝返回的數據,該函數的具體代碼如下:

          function?constructReply(data)?{
          ??const?json?=?JSON.stringify(data);
          ??const?jsonByteLength?=?Buffer.byteLength(json);
          ??//?目前只支持小于65535字節(jié)的負載
          ??const?lengthByteCount?=?jsonByteLength?126???0?:?2;
          ??const?payloadLength?=?lengthByteCount?===?0???jsonByteLength?:?126;
          ??const?buffer?=?Buffer.alloc(2?+?lengthByteCount?+?jsonByteLength);
          ??//?設置數據幀首字節(jié),設置opcode為1,表示文本幀
          ??buffer.writeUInt8(0b10000001,?0);
          ??buffer.writeUInt8(payloadLength,?1);
          ??//?如果payloadLength為126,則后面兩個字節(jié)(16位)內容應該,被識別成一個16位的二進制數表示數據內容大小
          ??let?payloadOffset?=?2;
          ??if?(lengthByteCount?>?0)?{
          ????buffer.writeUInt16BE(jsonByteLength,?2);
          ????payloadOffset?+=?lengthByteCount;
          ??}
          ??//?把JSON數據寫入到Buffer緩沖區(qū)中
          ??buffer.write(json,?payloadOffset);
          ??return?buffer;
          }

          創(chuàng)建完 constructReply 函數,我們再來更新一下之前創(chuàng)建的 WebSocket 服務器:

          server.on("upgrade",?function?(req,?socket)?{
          ??socket.on("data",?(buffer)?=>?{
          ????const?message?=?parseMessage(buffer);
          ????if?(message)?{
          ??????console.log("Message?from?client:"?+?message);
          ??????//?新增以下?代碼
          ??????socket.write(constructReply({?message?}));
          ????}?else?if?(message?===?null)?{
          ??????console.log("WebSocket?connection?closed?by?the?client.");
          ????}
          ??});
          });

          到這里,我們的 WebSocket 服務器已經開發(fā)完成了,接下來我們來完整驗證一下它的功能。

          從圖中可知,我們的開發(fā)的簡易版 WebSocket 服務器已經可以正常處理普通文本消息了。最后我們來看一下完整的代碼:

          custom-websocket-server.js

          const?http?=?require("http");

          const?port?=?8888;
          const?{?generateAcceptValue,?parseMessage,?constructReply?}?=?require("./util");

          const?server?=?http.createServer((req,?res)?=>?{
          ??res.writeHead(200,?{?"Content-Type":?"text/plain;?charset=utf-8"?});
          ??res.end("大家好,我是阿寶哥。感謝你閱讀“你不知道的WebSocket”");
          });

          server.on("upgrade",?function?(req,?socket)?{
          ??socket.on("data",?(buffer)?=>?{
          ????const?message?=?parseMessage(buffer);
          ????if?(message)?{
          ??????console.log("Message?from?client:"?+?message);
          ??????socket.write(constructReply({?message?}));
          ????}?else?if?(message?===?null)?{
          ??????console.log("WebSocket?connection?closed?by?the?client.");
          ????}
          ??});
          ??if?(req.headers["upgrade"]?!==?"websocket")?{
          ????socket.end("HTTP/1.1?400?Bad?Request");
          ????return;
          ??}
          ??//?讀取客戶端提供的Sec-WebSocket-Key
          ??const?secWsKey?=?req.headers["sec-websocket-key"];
          ??//?使用SHA-1算法生成Sec-WebSocket-Accept
          ??const?hash?=?generateAcceptValue(secWsKey);
          ??//?設置HTTP響應頭
          ??const?responseHeaders?=?[
          ????"HTTP/1.1?101?Web?Socket?Protocol?Handshake",
          ????"Upgrade:?WebSocket",
          ????"Connection:?Upgrade",
          ????`Sec-WebSocket-Accept:?${hash}`,
          ??];
          ??//?返回握手請求的響應信息
          ??socket.write(responseHeaders.join("\r\n")?+?"\r\n\r\n");
          });

          server.listen(port,?()?=>
          ??console.log(`Server?running?at?http://localhost:${port}`)
          );

          util.js

          const?crypto?=?require("crypto");

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

          function?generateAcceptValue(secWsKey)?{
          ??return?crypto
          ????.createHash("sha1")
          ????.update(secWsKey?+?MAGIC_KEY,?"utf8")
          ????.digest("base64");
          }

          function?parseMessage(buffer)?{
          ??//?第一個字節(jié),包含了FIN位,opcode,?掩碼位
          ??const?firstByte?=?buffer.readUInt8(0);
          ??//?[FIN,?RSV,?RSV,?RSV,?OPCODE,?OPCODE,?OPCODE,?OPCODE];
          ??//?右移7位取首位,1位,表示是否是最后一幀數據
          ??const?isFinalFrame?=?Boolean((firstByte?>>>?7)?&?0x01);
          ??console.log("isFIN:?",?isFinalFrame);
          ??//?取出操作碼,低四位
          ??/**
          ???*?%x0:表示一個延續(xù)幀。當 Opcode 為?0?時,表示本次數據傳輸采用了數據分片,當前收到的數據幀為其中一個數據分片;
          ???*?%x1:表示這是一個文本幀(text frame);
          ???*?%x2:表示這是一個二進制幀(binary frame);
          ???*?%x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀;
          ???*?%x8:表示連接斷開;
          ???*?%x9:表示這是一個心跳請求(ping);
          ???*?%xA:表示這是一個心跳響應(pong);
          ???*?%xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。
          ???*/

          ??const?opcode?=?firstByte?&?0x0f;
          ??if?(opcode?===?0x08)?{
          ????//?連接關閉
          ????return;
          ??}
          ??if?(opcode?===?0x02)?{
          ????//?二進制幀
          ????return;
          ??}
          ??if?(opcode?===?0x01)?{
          ????//?目前只處理文本幀
          ????let?offset?=?1;
          ????const?secondByte?=?buffer.readUInt8(offset);
          ????//?MASK:?1位,表示是否使用了掩碼,在發(fā)送給服務端的數據幀里必須使用掩碼,而服務端返回時不需要掩碼
          ????const?useMask?=?Boolean((secondByte?>>>?7)?&?0x01);
          ????console.log("use?MASK:?",?useMask);
          ????const?payloadLen?=?secondByte?&?0x7f;?//?低7位表示載荷字節(jié)長度
          ????offset?+=?1;
          ????//?四個字節(jié)的掩碼
          ????let?MASK?=?[];
          ????//?如果這個值在0-125之間,則后面的4個字節(jié)(32位)就應該被直接識別成掩碼;
          ????if?(payloadLen?<=?0x7d)?{
          ??????//?載荷長度小于125
          ??????MASK?=?buffer.slice(offset,?4?+?offset);
          ??????offset?+=?4;
          ??????console.log("payload?length:?",?payloadLen);
          ????}?else?if?(payloadLen?===?0x7e)?{
          ??????//?如果這個值是126,則后面兩個字節(jié)(16位)內容應該,被識別成一個16位的二進制數表示數據內容大小;
          ??????console.log("payload?length:?",?buffer.readInt16BE(offset));
          ??????//?長度是126,?則后面兩個字節(jié)作為payload?length,32位的掩碼
          ??????MASK?=?buffer.slice(offset?+?2,?offset?+?2?+?4);
          ??????offset?+=?6;
          ????}?else?{
          ??????//?如果這個值是127,則后面的8個字節(jié)(64位)內容應該被識別成一個64位的二進制數表示數據內容大小
          ??????MASK?=?buffer.slice(offset?+?8,?offset?+?8?+?4);
          ??????offset?+=?12;
          ????}
          ????//?開始讀取后面的payload,與掩碼計算,得到原來的字節(jié)內容
          ????const?newBuffer?=?[];
          ????const?dataBuffer?=?buffer.slice(offset);
          ????for?(let?i?=?0,?j?=?0;?i?4)?{
          ??????const?nextBuf?=?dataBuffer[i];
          ??????newBuffer.push(nextBuf?^?MASK[j]);
          ????}
          ????return?Buffer.from(newBuffer).toString();
          ??}
          ??return?"";
          }

          function?constructReply(data)?{
          ??const?json?=?JSON.stringify(data);
          ??const?jsonByteLength?=?Buffer.byteLength(json);
          ??//?目前只支持小于65535字節(jié)的負載
          ??const?lengthByteCount?=?jsonByteLength?126???0?:?2;
          ??const?payloadLength?=?lengthByteCount?===?0???jsonByteLength?:?126;
          ??const?buffer?=?Buffer.alloc(2?+?lengthByteCount?+?jsonByteLength);
          ??//?設置數據幀首字節(jié),設置opcode為1,表示文本幀
          ??buffer.writeUInt8(0b10000001,?0);
          ??buffer.writeUInt8(payloadLength,?1);
          ??//?如果payloadLength為126,則后面兩個字節(jié)(16位)內容應該,被識別成一個16位的二進制數表示數據內容大小
          ??let?payloadOffset?=?2;
          ??if?(lengthByteCount?>?0)?{
          ????buffer.writeUInt16BE(jsonByteLength,?2);
          ????payloadOffset?+=?lengthByteCount;
          ??}
          ??//?把JSON數據寫入到Buffer緩沖區(qū)中
          ??buffer.write(json,?payloadOffset);
          ??return?buffer;
          }

          module.exports?=?{
          ??generateAcceptValue,
          ??parseMessage,
          ??constructReply,
          };

          其實服務器向瀏覽器推送信息,除了使用 WebSocket 技術之外,還可以使用 SSE(Server-Sent Events)。它讓服務器可以向客戶端流式發(fā)送文本消息,比如服務器上生成的實時消息。為實現這個目標,SSE 設計了兩個組件:瀏覽器中的 EventSource API 和新的 “事件流” 數據格式(text/event-stream)。其中,EventSource 可以讓客戶端以 DOM 事件的形式接收到服務器推送的通知,而新數據格式則用于交付每一次數據更新。

          實際上,SSE 提供的是一個高效、跨瀏覽器的 XHR 流實現,消息交付只使用一個長 HTTP 連接。然而,與我們自己實現 XHR 流不同,瀏覽器會幫我們管理連接、 解析消息,從而讓我們只關注業(yè)務邏輯。篇幅有限,關于 SSE 的更多細節(jié),阿寶哥就不展開介紹了,對 SSE 感興趣的小伙伴可以自行查閱相關資料。

          四、阿寶哥有話說

          4.1 WebSocket 與 HTTP 有什么關系

          WebSocket 是一種與 HTTP 不同的協(xié)議。兩者都位于 OSI 模型的應用層,并且都依賴于傳輸層的 TCP 協(xié)議。雖然它們不同,但是 RFC 6455 中規(guī)定:WebSocket 被設計為在 HTTP 80 和 443 端口上工作,并支持 HTTP 代理和中介,從而使其與 HTTP 協(xié)議兼容。為了實現兼容性,WebSocket 握手使用 HTTP Upgrade 頭,從 HTTP 協(xié)議更改為 WebSocket 協(xié)議。

          既然已經提到了 OSI(Open System Interconnection Model)模型,這里阿寶哥來分享一張很生動、很形象描述 OSI 模型的示意圖:

          (圖片來源:https://www.networkingsphere.com/2019/07/what-is-osi-model.html)

          4.2 WebSocket 與長輪詢有什么區(qū)別

          長輪詢就是客戶端發(fā)起一個請求,服務器收到客戶端發(fā)來的請求后,服務器端不會直接進行響應,而是先將這個請求掛起,然后判斷請求的數據是否有更新。如果有更新,則進行響應,如果一直沒有數據,則等待一定的時間后才返回。

          長輪詢的本質還是基于 HTTP 協(xié)議,它仍然是一個一問一答(請求 — 響應)的模式。而 WebSocket 在握手成功后,就是全雙工的 TCP 通道,數據可以主動從服務端發(fā)送到客戶端。

          4.3 什么是 WebSocket 心跳

          網絡中的接收和發(fā)送數據都是使用 SOCKET 進行實現。但是如果此套接字已經斷開,那發(fā)送數據和接收數據的時候就一定會有問題。可是如何判斷這個套接字是否還可以使用呢?這個就需要在系統(tǒng)中創(chuàng)建心跳機制。所謂 “心跳” 就是定時發(fā)送一個自定義的結構體(心跳包或心跳幀),讓對方知道自己 “在線”。以確保鏈接的有效性。

          而所謂的心跳包就是客戶端定時發(fā)送簡單的信息給服務器端告訴它我還在而已。代碼就是每隔幾分鐘發(fā)送一個固定信息給服務端,服務端收到后回復一個固定信息,如果服務端幾分鐘內沒有收到客戶端信息則視客戶端斷開。

          在 WebSocket 協(xié)議中定義了 心跳 Ping心跳 Pong 的控制幀:

          • 心跳 Ping 幀包含的操作碼是 0x9。如果收到了一個心跳 Ping 幀,那么終端必須發(fā)送一個心跳 Pong 幀作為回應,除非已經收到了一個關閉幀。否則終端應該盡快回復 Pong 幀。
          • 心跳 Pong 幀包含的操作碼是 0xA。作為回應發(fā)送的 Pong 幀必須完整攜帶 Ping 幀中傳遞過來的 “應用數據” 字段。如果終端收到一個 Ping 幀但是沒有發(fā)送 Pong 幀來回應之前的 Ping 幀,那么終端可以選擇僅為最近處理的 Ping 幀發(fā)送 Pong 幀。此外,可以自動發(fā)送一個 Pong 幀,這用作單向心跳。

          4.4 Socket 是什么

          網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為一個 socket(套接字),因此建立網絡通信連接至少要一對端口號。socket 本質是對 TCP/IP 協(xié)議棧的封裝,它提供了一個針對 TCP 或者 UDP 編程的接口,并不是另一種協(xié)議。通過 socket,你可以使用 TCP/IP 協(xié)議。

          Socket 的英文原義是“孔”或“插座”。作為 BSD UNIX 的進程通信機制,取后一種意思。通常也稱作"套接字",用于描述IP地址和端口,是一個通信鏈的句柄,可以用來實現不同虛擬機或不同計算機之間的通信。

          在Internet 上的主機一般運行了多個服務軟件,同時提供幾種服務。每種服務都打開一個Socket,并綁定到一個端口上,不同的端口對應于不同的服務。Socket 正如其英文原義那樣,像一個多孔插座。一臺主機猶如布滿各種插座的房間,每個插座有一個編號,有的插座提供 220 伏交流電, 有的提供 110 伏交流電,有的則提供有線電視節(jié)目。客戶軟件將插頭插到不同編號的插座,就可以得到不同的服務。—— 百度百科

          關于 Socket,可以總結以下幾點:

          • 它可以實現底層通信,幾乎所有的應用層都是通過 socket 進行通信的。
          • 對 TCP/IP 協(xié)議進行封裝,便于應用層協(xié)議調用,屬于二者之間的中間抽象層。
          • TCP/IP 協(xié)議族中,傳輸層存在兩種通用協(xié)議: TCP、UDP,兩種協(xié)議不同,因為不同參數的 socket 實現過程也不一樣。

          下圖說明了面向連接的協(xié)議的套接字 API 的客戶端/服務器關系。

          五、參考資源

          • 維基百科 - WebSocket
          • MDN - WebSocket
          • MDN - Protocol_upgrade_mechanism
          • rfc6455
          • Web 性能權威指南
          推薦閱讀
          你不知道的 Web Workers (上)

          你不知道的 Web Workers (上)

          你不知道的 Blob

          你不知道的 Blob

          你不知道的 WeakMap

          你不知道的 WeakMap

          聚焦全棧,專注分享 TypeScript、Web API、Deno 等技術干貨。

          瀏覽 50
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  五月天在线欧美日韩在线 | 伊人大香蕉伊人在线 | 亚洲午夜影院在线 | 日韩成人区 | 真实亲子乱一区二区 |