• <center id="sm46c"></center>
  • <dfn id="sm46c"></dfn>
  • <strike id="sm46c"></strike>
  • <cite id="sm46c"><source id="sm46c"></source></cite>
    • <strike id="sm46c"><source id="sm46c"></source></strike>
      <option id="sm46c"></option>
      国产精品天天看天天狠,女高中生强奷系列在线播放,久久无码免费的a毛片大全,国产日韩综合av在线,亚洲国产中文综合专区在,特殊重囗味sm在线观看无码,中文字幕一区二区三区四区在线,无码任你躁久久久久久老妇蜜桃

      你不知道的 WebSocket

      2020-7-26    seo達(dá)人

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


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


      一、什么是 WebSocket

      1.1 WebSocket 誕生背景

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




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




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


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


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


      ws://echo.websocket.org

      wss://echo.websocket.org

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


      1.2 WebSocket 簡(jiǎn)介

      WebSocket 是一種網(wǎng)絡(luò)傳輸協(xié)議,可在單個(gè) TCP 連接上進(jìn)行全雙工通信,位于 OSI 模型的應(yīng)用層。WebSocket 協(xié)議在 2011 年由 IETF 標(biāo)準(zhǔn)化為 RFC 6455,后由 RFC 7936 補(bǔ)充規(guī)范。


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


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




      1.3 WebSocket 優(yōu)點(diǎn)

      較少的控制開銷。在連接創(chuàng)建后,服務(wù)器和客戶端之間交換數(shù)據(jù)時(shí),用于協(xié)議控制的數(shù)據(jù)包頭部相對(duì)較小。

      更強(qiáng)的實(shí)時(shí)性。由于協(xié)議是全雙工的,所以服務(wù)器可以隨時(shí)主動(dòng)給客戶端下發(fā)數(shù)據(jù)。相對(duì)于 HTTP 請(qǐng)求需要等待客戶端發(fā)起請(qǐng)求服務(wù)端才能響應(yīng),延遲明顯更少。

      保持連接狀態(tài)。與 HTTP 不同的是,WebSocket 需要先創(chuàng)建連接,這就使得其成為一種有狀態(tài)的協(xié)議,之后通信時(shí)可以省略部分狀態(tài)信息。

      更好的二進(jìn)制支持。WebSocket 定義了二進(jìn)制幀,相對(duì) HTTP,可以更輕松地處理二進(jìn)制內(nèi)容。

      可以支持?jǐn)U展。WebSocket 定義了擴(kuò)展,用戶可以擴(kuò)展協(xié)議、實(shí)現(xiàn)部分自定義的子協(xié)議。

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


      二、WebSocket API

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




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


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


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


      使用 WebSocket 構(gòu)造函數(shù),我們就能輕易地構(gòu)造一個(gè) WebSocket 對(duì)象。接下來我們將從 WebSocket 構(gòu)造函數(shù)、WebSocket 對(duì)象的屬性、方法及 WebSocket 相關(guān)的事件四個(gè)方面來介紹 WebSocket API,首先我們從 WebSocket 的構(gòu)造函數(shù)入手:


      2.1 構(gòu)造函數(shù)

      WebSocket 構(gòu)造函數(shù)的語法為:


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

      相關(guān)參數(shù)說明如下:


      url:表示連接的 URL,這是 WebSocket 服務(wù)器將響應(yīng)的 URL。

      protocols(可選):一個(gè)協(xié)議字符串或者一個(gè)包含協(xié)議字符串的數(shù)組。這些字符串用于指定子協(xié)議,這樣單個(gè)服務(wù)器可以實(shí)現(xiàn)多個(gè) WebSocket 子協(xié)議。比如,你可能希望一臺(tái)服務(wù)器能夠根據(jù)指定的協(xié)議(protocol)處理不同類型的交互。如果不指定協(xié)議字符串,則假定為空字符串。

      當(dāng)嘗試連接的端口被阻止時(shí),會(huì)拋出 SECURITY_ERR 異常。


      2.2 屬性

      WebSocket 對(duì)象包含以下屬性:




      每個(gè)屬性的具體含義如下:


      binaryType:使用二進(jìn)制的數(shù)據(jù)類型連接。

      bufferedAmount(只讀):未發(fā)送至服務(wù)器的字節(jié)數(shù)。

      extensions(只讀):服務(wù)器選擇的擴(kuò)展。

      onclose:用于指定連接關(guān)閉后的回調(diào)函數(shù)。

      onerror:用于指定連接失敗后的回調(diào)函數(shù)。

      onmessage:用于指定當(dāng)從服務(wù)器接受到信息時(shí)的回調(diào)函數(shù)。

      onopen:用于指定連接成功后的回調(diào)函數(shù)。

      protocol(只讀):用于返回服務(wù)器端選中的子協(xié)議的名字。

      readyState(只讀):返回當(dāng)前 WebSocket 的連接狀態(tài),共有 4 種狀態(tài):


      CONNECTING — 正在連接中,對(duì)應(yīng)的值為 0;

      OPEN — 已經(jīng)連接并且可以通訊,對(duì)應(yīng)的值為 1;

      CLOSING — 連接正在關(guān)閉,對(duì)應(yīng)的值為 2;

      CLOSED — 連接已關(guān)閉或者沒有連接成功,對(duì)應(yīng)的值為 3。

      url(只讀):返回值為當(dāng)構(gòu)造函數(shù)創(chuàng)建 WebSocket 實(shí)例對(duì)象時(shí) URL 的絕對(duì)路徑。

      2.3 方法

      close([code[, reason]]):該方法用于關(guān)閉 WebSocket 連接,如果連接已經(jīng)關(guān)閉,則此方法不執(zhí)行任何操作。

      send(data):該方法將需要通過 WebSocket 鏈接傳輸至服務(wù)器的數(shù)據(jù)排入隊(duì)列,并根據(jù)所需要傳輸?shù)臄?shù)據(jù)的大小來增加 bufferedAmount 的值 。若數(shù)據(jù)無法傳輸(比如數(shù)據(jù)需要緩存而緩沖區(qū)已滿)時(shí),套接字會(huì)自行關(guān)閉。

      2.4 事件

      使用 addEventListener() 或?qū)⒁粋€(gè)事件監(jiān)聽器賦值給 WebSocket 對(duì)象的 oneventname 屬性,來監(jiān)聽下面的事件。


      close:當(dāng)一個(gè) WebSocket 連接被關(guān)閉時(shí)觸發(fā),也可以通過 onclose 屬性來設(shè)置。

      error:當(dāng)一個(gè) WebSocket 連接因錯(cuò)誤而關(guān)閉時(shí)觸發(fā),也可以通過 onerror 屬性來設(shè)置。

      message:當(dāng)通過 WebSocket 收到數(shù)據(jù)時(shí)觸發(fā),也可以通過 onmessage 屬性來設(shè)置。

      open:當(dāng)一個(gè) WebSocket 連接成功時(shí)觸發(fā),也可以通過 onopen 屬性來設(shè)置。

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


      2.5 發(fā)送普通文本



      在以上示例中,我們?cè)陧撁嫔蟿?chuàng)建了兩個(gè) textarea,分別用于存放 待發(fā)送的數(shù)據(jù) 和 服務(wù)器返回的數(shù)據(jù)。當(dāng)用戶輸入完待發(fā)送的文本之后,點(diǎn)擊 發(fā)送 按鈕時(shí)會(huì)把輸入的文本發(fā)送到服務(wù)端,而服務(wù)端成功接收到消息之后,會(huì)把收到的消息原封不動(dòng)地回傳到客戶端。


      // 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);

      }

      當(dāng)然客戶端接收到服務(wù)端返回的消息之后,會(huì)把對(duì)應(yīng)的文本內(nèi)容保存到 接收的數(shù)據(jù) 對(duì)應(yīng)的 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;

      });

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




      以上示例對(duì)應(yīng)的完整代碼如下所示:


      <!DOCTYPE html>

      <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ā)送的數(shù)據(jù):<button onclick="send()">發(fā)送</button></p>

             <textarea id="sendMessage" rows="5" cols="15"></textarea>

           </div>

           <div class="block">

             <p>接收的數(shù)據(jù):</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>

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


      const socket = new WebSocket("ws://echo.websocket.org");

      socket.onopen = function () {

       // 發(fā)送UTF-8編碼的文本信息

       socket.send("Hello Echo Server!");

       // 發(fā)送UTF-8編碼的JSON數(shù)據(jù)

       socket.send(JSON.stringify({ msg: "我是阿寶哥" }));

       

       // 發(fā)送二進(jìn)制ArrayBuffer

       const buffer = new ArrayBuffer(128);

       socket.send(buffer);

       

       // 發(fā)送二進(jìn)制ArrayBufferView

       const intview = new Uint32Array(buffer);

       socket.send(intview);


       // 發(fā)送二進(jìn)制Blob

       const blob = new Blob([buffer]);

       socket.send(blob);

      };

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




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


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

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


      2.6 發(fā)送二進(jìn)制數(shù)據(jù)



      在以上示例中,我們?cè)陧撁嫔蟿?chuàng)建了兩個(gè) textarea,分別用于存放 待發(fā)送的數(shù)據(jù) 和 服務(wù)器返回的數(shù)據(jù)。當(dāng)用戶輸入完待發(fā)送的文本之后,點(diǎn)擊 發(fā)送 按鈕時(shí),我們會(huì)先獲取輸入的文本并把文本包裝成 Blob 對(duì)象然后發(fā)送到服務(wù)端,而服務(wù)端成功接收到消息之后,會(huì)把收到的消息原封不動(dòng)地回傳到客戶端。


      當(dāng)瀏覽器接收到新消息后,如果是文本數(shù)據(jù),會(huì)自動(dòng)將其轉(zhuǎn)換成 DOMString 對(duì)象,如果是二進(jìn)制數(shù)據(jù)或 Blob 對(duì)象,會(huì)直接將其轉(zhuǎn)交給應(yīng)用,由應(yīng)用自身來根據(jù)返回的數(shù)據(jù)類型進(jìn)行相應(yīng)的處理。


      數(shù)據(jù)發(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ā)送至服務(wù)器的字節(jié)數(shù):${socket.bufferedAmount}`);

      }

      當(dāng)然客戶端接收到服務(wù)端返回的消息之后,會(huì)判斷返回的數(shù)據(jù)類型,如果是 Blob 類型的話,會(huì)調(diào)用 Blob 對(duì)象的 text() 方法,獲取 Blob 對(duì)象中保存的 UTF-8 格式的內(nèi)容,然后把對(duì)應(yīng)的文本內(nèi)容保存到 接收的數(shù)據(jù) 對(duì)應(yīng)的 textarea 文本框中。


      數(shù)據(jù)接收代碼


      // 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ā)者工具來看一下相應(yīng)的過程:




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


      以上示例對(duì)應(yīng)的完整代碼如下所示:


      <!DOCTYPE html>

      <html>

       <head>

         <meta charset="UTF-8" />

         <meta name="viewport" content="width=device-width, initial-scale=1.0" />

         <title>WebSocket 發(fā)送二進(jìn)制數(shù)據(jù)示例</title>

         <style>

           .block {

             flex: 1;

           }

         </style>

       </head>

       <body>

         <h3>阿寶哥:WebSocket 發(fā)送二進(jìn)制數(shù)據(jù)示例</h3>

         <div style="display: flex;">

           <div class="block">

             <p>待發(fā)送的數(shù)據(jù):<button onclick="send()">發(fā)送</button></p>

             <textarea id="sendMessage" rows="5" cols="15"></textarea>

           </div>

           <div class="block">

             <p>接收的數(shù)據(jù):</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ā)送至服務(wù)器的字節(jié)數(shù):${socket.bufferedAmount}`);

           }

         </script>

       </body>

      </html>

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


      三、手寫 WebSocket 服務(wù)器

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




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


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


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


      3.1 握手協(xié)議

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


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


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


      3.1.1 客戶端請(qǐng)求

      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 請(qǐng)求頭

      字段說明


      Connection 必須設(shè)置 Upgrade,表示客戶端希望連接升級(jí)。

      Upgrade 字段必須設(shè)置 websocket,表示希望升級(jí)到 WebSocket 協(xié)議。

      Sec-WebSocket-Version 表示支持的 WebSocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均應(yīng)當(dāng)棄用。

      Sec-WebSocket-Key 是隨機(jī)的字符串,服務(wù)器端會(huì)用這些數(shù)據(jù)來構(gòu)造出一個(gè) SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一個(gè)特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后計(jì)算 SHA-1 摘要,之后進(jìn)行 Base64 編碼,將結(jié)果做為 “Sec-WebSocket-Accept” 頭的值,返回給客戶端。如此操作,可以盡量避免普通 HTTP 請(qǐng)求被誤認(rèn)為 WebSocket 協(xié)議。

      Sec-WebSocket-Extensions 用于協(xié)商本次連接要使用的 WebSocket 擴(kuò)展:客戶端發(fā)送支持的擴(kuò)展,服務(wù)器通過返回相同的首部確認(rèn)自己支持一個(gè)或多個(gè)擴(kuò)展。

      Origin 字段是可選的,通常用來表示在瀏覽器中發(fā)起此 WebSocket 連接所在的頁面,類似于 Referer。但是,與 Referer 不同的是,Origin 只包含了協(xié)議和主機(jī)名稱。

      3.1.2 服務(wù)端響應(yīng)

      HTTP/1.1 101 Web Socket Protocol Handshake ①

      Connection: Upgrade ②

      Upgrade: websocket ③

      Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④

      備注:已忽略部分 HTTP 響應(yīng)頭

      ① 101 響應(yīng)碼確認(rèn)升級(jí)到 WebSocket 協(xié)議。

      ② 設(shè)置 Connection 頭的值為 "Upgrade" 來指示這是一個(gè)升級(jí)請(qǐng)求。HTTP 協(xié)議提供了一種特殊的機(jī)制,這一機(jī)制允許將一個(gè)已建立的連接升級(jí)成新的、不相容的協(xié)議。

      ③ Upgrade 頭指定一項(xiàng)或多項(xiàng)協(xié)議名,按優(yōu)先級(jí)排序,以逗號(hào)分隔。這里表示升級(jí)為 WebSocket 協(xié)議。

      ④ 簽名的鍵值驗(yàn)證協(xié)議支持。

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


      3.2 實(shí)現(xiàn)握手功能

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


      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);

       // 設(shè)置HTTP響應(yīng)頭

       const responseHeaders = [

         "HTTP/1.1 101 Web Socket Protocol Handshake",

         "Upgrade: WebSocket",

         "Connection: Upgrade",

         `Sec-WebSocket-Accept: ${hash}`,

       ];

       // 返回握手請(qǐng)求的響應(yīng)信息

       socket.write(responseHeaders.join("\r\n") + "\r\n\r\n");

      });


      server.listen(port, () =>

       console.log(`Server running at http://localhost:${port}`)

      );

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


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


      上述的過程看起來好像有點(diǎn)繁瑣,其實(shí)利用 Node.js 內(nèi)置的 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ā)完握手功能之后,我們可以使用前面的示例來測(cè)試一下該功能。待服務(wù)器啟動(dòng)之后,我們只要對(duì) “發(fā)送普通文本” 示例,做簡(jiǎn)單地調(diào)整,即把先前的 URL 地址替換成 ws://localhost:8888,就可以進(jìn)行功能驗(yàn)證。


      感興趣的小伙們可以試試看,以下是阿寶哥本地運(yùn)行后的結(jié)果:




      從上圖可知,我們實(shí)現(xiàn)的握手功能已經(jīng)可以正常工作了。那么握手有沒有可能失敗呢?答案是肯定的。比如網(wǎng)絡(luò)問題、服務(wù)器異常或 Sec-WebSocket-Accept 的值不正確。


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


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

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


      // 從請(qǐng)求頭中讀取子協(xié)議

      const protocol = req.headers["sec-websocket-protocol"];

      // 如果包含子協(xié)議,則解析子協(xié)議

      const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim());


      // 簡(jiǎn)單起見,我們僅判斷是否含有JSON子協(xié)議

      if (protocols.includes("json")) {

       responseHeaders.push(`Sec-WebSocket-Protocol: json`);

      }

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


      3.3 消息通信基礎(chǔ)

      在 WebSocket 協(xié)議中,數(shù)據(jù)是通過一系列數(shù)據(jù)幀來進(jìn)行傳輸?shù)摹榱吮苊庥捎诰W(wǎng)絡(luò)中介(例如一些攔截代理)或者一些安全問題,客戶端必須在它發(fā)送到服務(wù)器的所有幀中添加掩碼。服務(wù)端收到?jīng)]有添加掩碼的數(shù)據(jù)幀以后,必須立即關(guān)閉連接。


      3.3.1 數(shù)據(jù)幀格式

      要實(shí)現(xiàn)消息通信,我們就必須了解 WebSocket 數(shù)據(jù)幀的格式:


      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 ...                |

      +---------------------------------------------------------------+

      可能有一些小伙伴看到上面的內(nèi)容之后,就開始有點(diǎn) “懵逼” 了。下面我們來結(jié)合實(shí)際的數(shù)據(jù)幀來進(jìn)一步分析一下:




      在上圖中,阿寶哥簡(jiǎn)單分析了 “發(fā)送普通文本” 示例對(duì)應(yīng)的數(shù)據(jù)幀格式。這里我們來進(jìn)一步介紹一下 Payload length,因?yàn)樵诤竺骈_發(fā)數(shù)據(jù)解析功能的時(shí)候,需要用到該知識(shí)點(diǎn)。


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


      如果值為 0-125,那么就表示負(fù)載數(shù)據(jù)的長(zhǎng)度。

      如果是 126,那么接下來的 2 個(gè)字節(jié)解釋為 16 位的無符號(hào)整形作為負(fù)載數(shù)據(jù)的長(zhǎng)度。

      如果是 127,那么接下來的 8 個(gè)字節(jié)解釋為一個(gè) 64 位的無符號(hào)整形(最高位的 bit 必須為 0)作為負(fù)載數(shù)據(jù)的長(zhǎng)度。

      多字節(jié)長(zhǎng)度量以網(wǎng)絡(luò)字節(jié)順序表示,有效負(fù)載長(zhǎng)度是指 “擴(kuò)展數(shù)據(jù)” + “應(yīng)用數(shù)據(jù)” 的長(zhǎng)度。“擴(kuò)展數(shù)據(jù)” 的長(zhǎng)度可能為 0,那么有效負(fù)載長(zhǎng)度就是 “應(yīng)用數(shù)據(jù)” 的長(zhǎng)度。


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


      3.3.2 掩碼算法

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


      掩碼不影響數(shù)據(jù)荷載的長(zhǎng)度,對(duì)數(shù)據(jù)進(jìn)行掩碼操作和對(duì)數(shù)據(jù)進(jìn)行反掩碼操作所涉及的步驟是相同的。掩碼、反掩碼操作都采用如下算法:


      j = i MOD 4

      transformed-octet-i = original-octet-i XOR masking-key-octet-j

      original-octet-i:為原始數(shù)據(jù)的第 i 字節(jié)。

      transformed-octet-i:為轉(zhuǎn)換后的數(shù)據(jù)的第 i 字節(jié)。

      masking-key-octet-j:為 mask key 第 j 字節(jié)。

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


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

      而對(duì)應(yīng)的 Masking-Key 為 0x08f6efb1,根據(jù)上面的算法,我們可以這樣進(jìn)行掩碼運(yùn)算:


      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 < uint8.length; i++, j = i % 4) {

       maskedUint8[i] = uint8[i] ^ maskingKey[j];

      }


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

      以上代碼成功運(yùn)行后,控制臺(tái)會(huì)輸出以下結(jié)果:


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

      上述結(jié)果與 WireShark 中的 Masked payload 對(duì)應(yīng)的值是一致的,具體如下圖所示:




      在 WebSocket 協(xié)議中,數(shù)據(jù)掩碼的作用是增強(qiáng)協(xié)議的安全性。但數(shù)據(jù)掩碼并不是為了保護(hù)數(shù)據(jù)本身,因?yàn)樗惴ū旧硎枪_的,運(yùn)算也不復(fù)雜。那么為什么還要引入數(shù)據(jù)掩碼呢?引入數(shù)據(jù)掩碼是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊等問題。


      了解完 WebSocket 掩碼算法和數(shù)據(jù)掩碼的作用之后,我們?cè)賮斫榻B一下數(shù)據(jù)分片的概念。


      3.3.3 數(shù)據(jù)分片

      WebSocket 的每條消息可能被切分成多個(gè)數(shù)據(jù)幀。當(dāng) WebSocket 的接收方收到一個(gè)數(shù)據(jù)幀時(shí),會(huì)根據(jù) FIN 的值來判斷,是否已經(jīng)收到消息的最后一個(gè)數(shù)據(jù)幀。


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


      為了讓大家能夠更好地理解上述的內(nèi)容,我們來看一個(gè)來自 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!

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


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


      好的,簡(jiǎn)單介紹了數(shù)據(jù)分片的相關(guān)內(nèi)容。接下來,我們來開始實(shí)現(xiàn)消息通信功能。


      3.4 實(shí)現(xiàn)消息通信功能

      阿寶哥把實(shí)現(xiàn)消息通信功能,分解為消息解析與消息響應(yīng)兩個(gè)子功能,下面我們分別來介紹如何實(shí)現(xiàn)這兩個(gè)子功能。


      3.4.1 消息解析

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


      function parseMessage(buffer) {

       // 第一個(gè)字節(jié),包含了FIN位,opcode, 掩碼位

       const firstByte = buffer.readUInt8(0);

       // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE];

       // 右移7位取首位,1位,表示是否是最后一幀數(shù)據(jù)

       const isFinalFrame = Boolean((firstByte >>> 7) & 0x01);

       console.log("isFIN: ", isFinalFrame);

       // 取出操作碼,低四位

       /**

        * %x0:表示一個(gè)延續(xù)幀。當(dāng) Opcode 為 0 時(shí),表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片,當(dāng)前收到的數(shù)據(jù)幀為其中一個(gè)數(shù)據(jù)分片;

        * %x1:表示這是一個(gè)文本幀(text frame);

        * %x2:表示這是一個(gè)二進(jìn)制幀(binary frame);

        * %x3-7:保留的操作代碼,用于后續(xù)定義的非控制幀;

        * %x8:表示連接斷開;

        * %x9:表示這是一個(gè)心跳請(qǐng)求(ping);

        * %xA:表示這是一個(gè)心跳響應(yīng)(pong);

        * %xB-F:保留的操作代碼,用于后續(xù)定義的控制幀。

        */

       const opcode = firstByte & 0x0f;

       if (opcode === 0x08) {

         // 連接關(guān)閉

         return;

       }

       if (opcode === 0x02) {

         // 二進(jìn)制幀

         return;

       }

       if (opcode === 0x01) {

         // 目前只處理文本幀

         let offset = 1;

         const secondByte = buffer.readUInt8(offset);

         // MASK: 1位,表示是否使用了掩碼,在發(fā)送給服務(wù)端的數(shù)據(jù)幀里必須使用掩碼,而服務(wù)端返回時(shí)不需要掩碼

         const useMask = Boolean((secondByte >>> 7) & 0x01);

         console.log("use MASK: ", useMask);

         const payloadLen = secondByte & 0x7f; // 低7位表示載荷字節(jié)長(zhǎng)度

         offset += 1;

         // 四個(gè)字節(jié)的掩碼

         let MASK = [];

         // 如果這個(gè)值在0-125之間,則后面的4個(gè)字節(jié)(32位)就應(yīng)該被直接識(shí)別成掩碼;

         if (payloadLen <= 0x7d) {

           // 載荷長(zhǎng)度小于125

           MASK = buffer.slice(offset, 4 + offset);

           offset += 4;

           console.log("payload length: ", payloadLen);

         } else if (payloadLen === 0x7e) {

           // 如果這個(gè)值是126,則后面兩個(gè)字節(jié)(16位)內(nèi)容應(yīng)該,被識(shí)別成一個(gè)16位的二進(jìn)制數(shù)表示數(shù)據(jù)內(nèi)容大小;

           console.log("payload length: ", buffer.readInt16BE(offset));

           // 長(zhǎng)度是126, 則后面兩個(gè)字節(jié)作為payload length,32位的掩碼

           MASK = buffer.slice(offset + 2, offset + 2 + 4);

           offset += 6;

         } else {

           // 如果這個(gè)值是127,則后面的8個(gè)字節(jié)(64位)內(nèi)容應(yīng)該被識(shí)別成一個(gè)64位的二進(jìn)制數(shù)表示數(shù)據(jù)內(nèi)容大小

           MASK = buffer.slice(offset + 8, offset + 8 + 4);

           offset += 12;

         }

         // 開始讀取后面的payload,與掩碼計(jì)算,得到原來的字節(jié)內(nèi)容

         const newBuffer = [];

         const dataBuffer = buffer.slice(offset);

         for (let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) {

           const nextBuf = dataBuffer[i];

           newBuffer.push(nextBuf ^ MASK[j]);

         }

         return Buffer.from(newBuffer).toString();

       }

       return "";

      }

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


      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;

       }

       // 省略已有代碼

      });

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


      Server running at http://localhost:8888

      isFIN:  true

      use MASK:  true

      payload length:  15

      Message from client:我是阿寶哥

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


      3.4.2 消息響應(yīng)

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


      function constructReply(data) {

       const json = JSON.stringify(data);

       const jsonByteLength = Buffer.byteLength(json);

       // 目前只支持小于65535字節(jié)的負(fù)載

       const lengthByteCount = jsonByteLength < 126 ? 0 : 2;

       const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126;

       const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength);

       // 設(shè)置數(shù)據(jù)幀首字節(jié),設(shè)置opcode為1,表示文本幀

       buffer.writeUInt8(0b10000001, 0);

       buffer.writeUInt8(payloadLength, 1);

       // 如果payloadLength為126,則后面兩個(gè)字節(jié)(16位)內(nèi)容應(yīng)該,被識(shí)別成一個(gè)16位的二進(jìn)制數(shù)表示數(shù)據(jù)內(nèi)容大小

       let payloadOffset = 2;

       if (lengthByteCount > 0) {

         buffer.writeUInt16BE(jsonByteLength, 2);

         payloadOffset += lengthByteCount;

       }

       // 把JSON數(shù)據(jù)寫入到Buffer緩沖區(qū)中

       buffer.write(json, payloadOffset);

       return buffer;

      }

      創(chuàng)建完 constructReply 函數(shù),我們?cè)賮砀乱幌轮皠?chuàng)建的 WebSocket 服務(wù)器:


      server.on("upgrade", function (req, socket) {

       socket.on("data", (buffer) => {

         const message = parseMessage(buffer);

         if (message) {

           console.log("Message from client:" + message);

           // 新增以下

      日歷

      鏈接

      個(gè)人資料

      存檔

      主站蜘蛛池模板: 亚洲区日韩精品中文字幕| 55夜色66夜色国产精品视频| 黄网站色成年片在线观看| 欧美日韩综合在线精品| 中文字幕有码在线第十页| 中文字幕av无码免费一区| 国产成人精品午夜二三区波多野 | 亚洲国产超清无码专区| 成人国产精品日本在线观看| 强行糟蹋人妻hd中文| 亚洲欧美综合在线天堂| 人人妻人人超人人| 国产成人夜色91| 亚洲精品无码日韩国产不卡av| 人人妻在人人| 日本在线看片免费人成视频1000 | 另类专区一区二区三区| 亚洲AV无码一二区三区在线播放| 精品 无码 国产观看| 久久婷婷人人澡人人爽人人喊| 无码人妻丰满熟妇区五十路在线| 亚洲爆乳中文字幕无码专区网站| 亚洲色五月| 小13箩利洗澡无码视频免费网站| 亚洲性视频网站| 国产精品lululu在线观看| 在厨房拨开内裤进入毛片| 中文字幕无线乱码人妻| 日本一卡二卡不卡视频查询| 日韩福利视频导航| 97人摸人人澡人人人超碰| 蜜桃av久久久一区二区三区麻豆| 亚洲一区二区中文字幕| 殴美国产中文字幕视频在线观看 | 欧美三级视频| 午夜自产精品一区二区三区| 自拍亚洲综合在线精品| 亚洲色图综合在线| 日本乱人伦aⅴ精品潮喷| 国产大片黄在线观看| 久激情内射婷内射蜜桃 |