伊人久久大香线蕉综合影视_日韩精品少妇无码受不了_71pao成人国产永久免费视频_国产伦片中文免费观看_国产高清无码麻豆精品_九色综合伊人久久富二代_日韩黄色精品_日韩A∨精品日韩精品无码

你不知道的 WebSocket

2020-7-26    seo達人

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


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


一、什么是 WebSocket

1.1 WebSocket 誕生背景

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




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




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


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


在這種情況下,HTML5 定義了 WebSocket 協(xié)議,能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實時地進行通訊。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 端口;若運行在 TLS 之上時,默認(rèn)使用 443 端口。


1.2 WebSocket 簡介

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


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


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




1.3 WebSocket 優(yōu)點

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

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

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

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

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

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


二、WebSocket API

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




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


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


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


使用 WebSocket 構(gòu)造函數(shù),我們就能輕易地構(gòu)造一個 WebSocket 對象。接下來我們將從 WebSocket 構(gòu)造函數(shù)、WebSocket 對象的屬性、方法及 WebSocket 相關(guān)的事件四個方面來介紹 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(可選):一個協(xié)議字符串或者一個包含協(xié)議字符串的數(shù)組。這些字符串用于指定子協(xié)議,這樣單個服務(wù)器可以實現(xiàn)多個 WebSocket 子協(xié)議。比如,你可能希望一臺服務(wù)器能夠根據(jù)指定的協(xié)議(protocol)處理不同類型的交互。如果不指定協(xié)議字符串,則假定為空字符串。

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


2.2 屬性

WebSocket 對象包含以下屬性:




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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

2.3 方法

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

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

2.4 事件

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


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

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

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

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

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


2.5 發(fā)送普通文本



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


// 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ù)端返回的消息之后,會把對應(yīng)的文本內(nèi)容保存到 接收的數(shù)據(jù) 對應(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)的過程:




以上示例對應(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>

其實 WebSocket 除了支持發(fā)送普通的文本之外,它還支持發(fā)送二進制數(shù)據(jù),比如 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數(shù)據(jù)

 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ā)者工具,我們可以看到對應(yīng)的數(shù)據(jù)交互過程:




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


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

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


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



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


當(dāng)瀏覽器接收到新消息后,如果是文本數(shù)據(jù),會自動將其轉(zhuǎn)換成 DOMString 對象,如果是二進制數(shù)據(jù)或 Blob 對象,會直接將其轉(zhuǎn)交給應(yīng)用,由應(yīng)用自身來根據(jù)返回的數(shù)據(jù)類型進行相應(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ù)端返回的消息之后,會判斷返回的數(shù)據(jù)類型,如果是 Blob 類型的話,會調(diào)用 Blob 對象的 text() 方法,獲取 Blob 對象中保存的 UTF-8 格式的內(nèi)容,然后把對應(yīng)的文本內(nèi)容保存到 接收的數(shù)據(jù) 對應(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 對象時,Data 欄位的信息顯示的是 Binary Message,而對于發(fā)送普通文本來說,Data 欄位的信息是直接顯示發(fā)送的文本消息。


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


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

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

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

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿寶哥:WebSocket 發(fā)送二進制數(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 之后,覺得還不夠過癮。下面阿寶哥將帶大家來實現(xiàn)一個支持發(fā)送普通文本的 WebSocket 服務(wù)器。


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

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




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


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


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


3.1 握手協(xié)議

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


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


下面我們以前面已經(jīng)演示過的發(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 必須設(shè)置 Upgrade,表示客戶端希望連接升級。

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

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

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

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

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

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)升級到 WebSocket 協(xié)議。

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

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

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

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


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

要開發(fā)一個 WebSocket 服務(wù)器,首先我們需要先實現(xiàn)握手功能,這里阿寶哥使用 Node.js 內(nèi)置的 http 模塊來創(chuàng)建一個 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}`,

 ];

 // 返回握手請求的響應(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)建一個 HTTP 服務(wù)器,接著我們監(jiān)聽 upgrade 事件,每次服務(wù)器響應(yīng)升級請求時就會觸發(fā)該事件。由于我們的服務(wù)器只支持升級到 WebSocket 協(xié)議,所以如果客戶端請求升級的協(xié)議非 WebSocket 協(xié)議,我們將會返回 “400 Bad Request”。


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


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


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




從上圖可知,我們實現(xiàn)的握手功能已經(jīng)可以正常工作了。那么握手有沒有可能失敗呢?答案是肯定的。比如網(wǎng)絡(luò)問題、服務(wù)器異?;?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 服務(wù)器要支持子協(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é)議相關(guān)的內(nèi)容基本已經(jīng)介紹完了。下一步我們來介紹開發(fā)消息通信功能需要了解的一些基礎(chǔ)知識。


3.3 消息通信基礎(chǔ)

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


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

要實現(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)容之后,就開始有點 “懵逼” 了。下面我們來結(jié)合實際的數(shù)據(jù)幀來進一步分析一下:




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


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


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

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

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

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


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


3.3.2 掩碼算法

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


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


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é)。

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


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

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


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

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


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

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




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


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


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

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


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


為了讓大家能夠更好地理解上述的內(nèi)容,我們來看一個來自 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ā)送了兩條消息。第一個消息在單個幀中發(fā)送,而第二個消息跨三個幀發(fā)送。


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


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


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

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


3.4.1 消息解析

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


function parseMessage(buffer) {

 // 第一個字節(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:表示一個延續(xù)幀。當(dāng) Opcode 為 0 時,表示本次數(shù)據(jù)傳輸采用了數(shù)據(jù)分片,當(dāng)前收到的數(shù)據(jù)幀為其中一個數(shù)據(jù)分片;

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

  * %x2:表示這是一個二進制幀(binary frame);

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

  * %x8:表示連接斷開;

  * %x9:表示這是一個心跳請求(ping);

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

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

  */

 const opcode = firstByte & 0x0f;

 if (opcode === 0x08) {

   // 連接關(guān)閉

   return;

 }

 if (opcode === 0x02) {

   // 二進制幀

   return;

 }

 if (opcode === 0x01) {

   // 目前只處理文本幀

   let offset = 1;

   const secondByte = buffer.readUInt8(offset);

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

   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位)就應(yīng)該被直接識別成掩碼;

   if (payloadLen <= 0x7d) {

     // 載荷長度小于125

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

     offset += 4;

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

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

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

     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位)內(nèi)容應(yīng)該被識別成一個64位的二進制數(shù)表示數(shù)據(jù)內(nèi)容大小

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

     offset += 12;

   }

   // 開始讀取后面的payload,與掩碼計算,得到原來的字節(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;

 }

 // 省略已有代碼

});

更新完成之后,我們重新啟動服務(wù)器,然后繼續(xù)使用 “發(fā)送普通文本” 的示例來測試消息解析功能。以下發(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ù)幀,下一步我們來實現(xiàn)消息響應(yīng)的功能。


3.4.2 消息響應(yīng)

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


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

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

 buffer.writeUInt8(0b10000001, 0);

 buffer.writeUInt8(payloadLength, 1);

 // 如果payloadLength為126,則后面兩個字節(jié)(16位)內(nèi)容應(yīng)該,被識別成一個16位的二進制數(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ù),我們再來更新一下之前創(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);

     // 新增以下

日歷

鏈接

個人資料

藍藍設(shè)計的小編 http://m.cqzjtgb.com

存檔

丰满饥渴人妻一区二区三| 激情视频va一区二区三区| 男女之事视频高清在线观看| 国产精品综合久久久久久久免费 | 久久久久国产一级毛片高清牌| 51午夜福利影视在线观看| 欧美黑人精品巨大| 午夜视频精品福利| 不卡av一区二区三区| 最新美女视频免费是黄的| 欧美人与性动交α欧美精品济南到| 中文字幕人妻丝袜制服| 午夜福利乱码中文字幕| 妹子高潮喷水视频| 色播在线永久视频| 大码成人一级视频| 亚洲av欧美aⅴ国产| 久久久久久久午夜电影 | 老司机亚洲免费影院| 欧美丝袜亚洲另类 | 亚洲成国产人片在线观看| 日韩欧美一区视频在线观看| 亚洲精品一卡2卡三卡4卡5卡| √禁漫天堂资源中文www| 制服诱惑二区| 99国产精品一区二区蜜桃av | a级毛片在线看网站| 另类亚洲欧美激情| 亚洲全国av大片| 欧美日韩av久久| 50天的宝宝边吃奶边哭怎么回事| 久久久国产一区二区| 久久久久久亚洲精品国产蜜桃av| 日本一区二区免费在线视频| 母亲3免费完整高清在线观看| 久久中文字幕一级| videosex国产| 露出奶头的视频| 欧美日本中文国产一区发布| 在线观看免费视频网站a站| 91字幕亚洲| 777久久人妻少妇嫩草av网站| 啦啦啦在线免费观看视频4| 我的亚洲天堂| 最新美女视频免费是黄的| 亚洲成a人片在线一区二区| 99久久国产精品久久久| 久久天躁狠狠躁夜夜2o2o| 国产av精品麻豆| 19禁男女啪啪无遮挡网站| 亚洲第一av免费看| 99久久人妻综合| 日本黄色视频三级网站网址 | 日韩熟女老妇一区二区性免费视频| 91麻豆精品激情在线观看国产 | 亚洲少妇的诱惑av| 村上凉子中文字幕在线| 国产野战对白在线观看| 日韩欧美国产一区二区入口| 大型黄色视频在线免费观看| 午夜免费成人在线视频| 亚洲精品在线观看二区| 美女视频免费永久观看网站| 又紧又爽又黄一区二区| 亚洲欧美精品综合一区二区三区| 国产精品国产高清国产av | 日韩欧美免费精品| 黄色视频,在线免费观看| 成在线人永久免费视频| 日韩视频一区二区在线观看| a在线观看视频网站| 国产成人影院久久av| 超色免费av| 国产精品偷伦视频观看了| 中文欧美无线码| 99热国产这里只有精品6| 亚洲人成伊人成综合网2020| 亚洲免费av在线视频| 成熟少妇高潮喷水视频| √禁漫天堂资源中文www| 黑人欧美特级aaaaaa片| 欧美 亚洲 国产 日韩一| 人人妻人人添人人爽欧美一区卜| 亚洲 欧美一区二区三区| 免费观看精品视频网站| 黄色毛片三级朝国网站| 淫妇啪啪啪对白视频| 啦啦啦视频在线资源免费观看| 中文字幕av电影在线播放| 亚洲国产欧美网| 99久久99久久久精品蜜桃| 又黄又爽又免费观看的视频| 亚洲色图av天堂| 麻豆成人av在线观看| 精品国产超薄肉色丝袜足j| 国产亚洲av高清不卡| 一级a爱视频在线免费观看| 国产99白浆流出| 国产亚洲精品久久久久久毛片 | 午夜福利免费观看在线| 香蕉丝袜av| 午夜亚洲福利在线播放| 麻豆av在线久日| 欧美乱妇无乱码| 国产av又大| 亚洲五月天丁香| 日韩大码丰满熟妇| av福利片在线| 青草久久国产| 亚洲欧美激情综合另类| 国产亚洲欧美98| 伦理电影免费视频| 免费一级毛片在线播放高清视频 | 一进一出好大好爽视频| 亚洲男人天堂网一区| 国产一区二区三区综合在线观看| 黄色视频,在线免费观看| 高清黄色对白视频在线免费看| 欧美乱妇无乱码| 国产精品亚洲av一区麻豆| 美女扒开内裤让男人捅视频| 黄色怎么调成土黄色| 国产成人免费无遮挡视频| 一本综合久久免费| 亚洲欧美日韩另类电影网站| 女人被躁到高潮嗷嗷叫费观| 亚洲专区国产一区二区| 女人被躁到高潮嗷嗷叫费观| av欧美777| 成年人免费黄色播放视频| 国产精品一区二区在线观看99| 精品福利永久在线观看| 国产欧美日韩一区二区精品| 国产成人免费观看mmmm| 国产精品永久免费网站| 国精品久久久久久国模美| 国产成+人综合+亚洲专区| 亚洲色图av天堂| 黑人巨大精品欧美一区二区蜜桃| 色在线成人网| 久久精品aⅴ一区二区三区四区| 国产又色又爽无遮挡免费看| 国产欧美日韩一区二区三| 国产成人系列免费观看| 搡老岳熟女国产| 国产一区二区三区综合在线观看| 丝袜美腿诱惑在线| а√天堂www在线а√下载 | 两性午夜刺激爽爽歪歪视频在线观看 | 757午夜福利合集在线观看| 亚洲精品一卡2卡三卡4卡5卡| 天天躁夜夜躁狠狠躁躁| 免费少妇av软件| 日韩熟女老妇一区二区性免费视频| 亚洲精品自拍成人| aaaaa片日本免费| 国产日韩一区二区三区精品不卡| 久久 成人 亚洲| 91九色精品人成在线观看| 一区二区三区激情视频| 精品亚洲成a人片在线观看| 天堂√8在线中文| 免费人成视频x8x8入口观看| 欧美人与性动交α欧美精品济南到| 法律面前人人平等表现在哪些方面| 一级毛片精品| 在线观看66精品国产| 在线视频色国产色| 亚洲中文av在线| 真人做人爱边吃奶动态| netflix在线观看网站| 999久久久精品免费观看国产| 精品一区二区三卡| 国产精品九九99| 亚洲av日韩在线播放| 国产欧美日韩一区二区精品| 首页视频小说图片口味搜索| xxxhd国产人妻xxx| 成年人午夜在线观看视频| 少妇粗大呻吟视频| 精品人妻1区二区| 日本精品一区二区三区蜜桃| 亚洲国产精品一区二区三区在线| 久久久精品国产亚洲av高清涩受| 国产精品永久免费网站| 亚洲精品久久成人aⅴ小说| 日韩免费av在线播放| 国产99白浆流出| 天堂俺去俺来也www色官网| 午夜激情av网站| 无限看片的www在线观看| 丝袜美足系列| 人人澡人人妻人| 久久草成人影院| x7x7x7水蜜桃| 超色免费av| 精品国产一区二区三区久久久樱花| 亚洲精品美女久久久久99蜜臀| 国产又色又爽无遮挡免费看| 久久精品国产a三级三级三级| 每晚都被弄得嗷嗷叫到高潮| 黑人巨大精品欧美一区二区mp4| 高清欧美精品videossex| 一级毛片高清免费大全| 欧美大码av| 国产男女内射视频| 热99re8久久精品国产| 香蕉国产在线看| 18禁裸乳无遮挡动漫免费视频| 午夜免费鲁丝| 国产精品99久久99久久久不卡| 亚洲国产毛片av蜜桃av| 国产午夜精品久久久久久| 亚洲人成电影观看| 免费久久久久久久精品成人欧美视频| 亚洲av片天天在线观看| 国产精品久久久久久精品古装| 一区福利在线观看| 91成年电影在线观看| 最近最新免费中文字幕在线| 久久久久国产一级毛片高清牌| 又紧又爽又黄一区二区| 一区二区三区国产精品乱码| 咕卡用的链子| 中文亚洲av片在线观看爽 | 久久狼人影院| 老司机影院毛片| 久久九九热精品免费| 丰满迷人的少妇在线观看| a级毛片黄视频| 99香蕉大伊视频| 精品国产乱码久久久久久男人| 日韩熟女老妇一区二区性免费视频| 视频区欧美日本亚洲| 国产精品av久久久久免费| 午夜福利免费观看在线| 人人妻人人澡人人爽人人夜夜| 国产亚洲欧美在线一区二区| 91精品三级在线观看| 久久久久精品人妻al黑| 18禁国产床啪视频网站| 一进一出好大好爽视频| 黄片小视频在线播放| 国产一区二区三区综合在线观看| 美女高潮到喷水免费观看| 国产一区二区激情短视频| 国产精品.久久久| 久久九九热精品免费| 国产高清国产精品国产三级| 两个人看的免费小视频| 99精品久久久久人妻精品| 一本一本久久a久久精品综合妖精| 国产在线精品亚洲第一网站| 不卡av一区二区三区| 日本黄色日本黄色录像| 欧美黑人欧美精品刺激| 久久精品国产清高在天天线| 国产精品国产高清国产av | 亚洲av成人一区二区三| 美女视频免费永久观看网站| 一a级毛片在线观看| 久久久久精品人妻al黑| 国产精品自产拍在线观看55亚洲 | 国产成+人综合+亚洲专区| 变态另类成人亚洲欧美熟女 | 午夜福利免费观看在线| 免费黄频网站在线观看国产| 深夜精品福利| 欧美精品人与动牲交sv欧美| 免费不卡黄色视频| 成人18禁高潮啪啪吃奶动态图| 亚洲自偷自拍图片 自拍| www.自偷自拍.com| 丰满的人妻完整版| 精品免费久久久久久久清纯 | 成人亚洲精品一区在线观看| 大香蕉久久成人网| 日韩欧美在线二视频 | 久久精品国产亚洲av香蕉五月 | 亚洲一卡2卡3卡4卡5卡精品中文| 99riav亚洲国产免费| 如日韩欧美国产精品一区二区三区| 丰满的人妻完整版| 国产日韩欧美亚洲二区| 亚洲欧美色中文字幕在线| 欧美黑人精品巨大| 女人久久www免费人成看片| 亚洲人成77777在线视频| 欧美日韩亚洲国产一区二区在线观看 | 丰满人妻熟妇乱又伦精品不卡| 国产精品综合久久久久久久免费 | 亚洲国产欧美日韩在线播放| av不卡在线播放| 国产激情欧美一区二区| 真人做人爱边吃奶动态| 亚洲一区二区三区不卡视频| 一级毛片精品| 国产一区二区三区视频了| www.自偷自拍.com| 亚洲少妇的诱惑av| 村上凉子中文字幕在线| 十八禁高潮呻吟视频| 午夜免费成人在线视频| 国产无遮挡羞羞视频在线观看| 日日摸夜夜添夜夜添小说| 国产精品 欧美亚洲| 亚洲人成电影免费在线| 日本撒尿小便嘘嘘汇集6| 亚洲一卡2卡3卡4卡5卡精品中文| 男人操女人黄网站| 亚洲人成电影观看| 欧美色视频一区免费| 久久九九热精品免费| 麻豆国产av国片精品| 国产欧美日韩一区二区精品| 后天国语完整版免费观看| bbb黄色大片| e午夜精品久久久久久久| 亚洲avbb在线观看| 淫妇啪啪啪对白视频| 咕卡用的链子| 91九色精品人成在线观看| 国产乱人伦免费视频| 国产成人欧美| 亚洲 国产 在线| 在线观看一区二区三区激情| 日本精品一区二区三区蜜桃| 18禁美女被吸乳视频| 精品福利永久在线观看| 久久久国产成人免费| 老司机午夜十八禁免费视频| 国产高清国产精品国产三级| 亚洲 欧美一区二区三区| av不卡在线播放| 久久久久久人人人人人| 欧美av亚洲av综合av国产av| 免费在线观看完整版高清| 欧美黄色片欧美黄色片| 丝袜人妻中文字幕| 国产一区二区三区综合在线观看| 久久久久国内视频| 久久久久视频综合| 成人精品一区二区免费| 欧美日韩国产mv在线观看视频| 757午夜福利合集在线观看| av免费在线观看网站| 纯流量卡能插随身wifi吗| 精品一区二区三卡| 五月开心婷婷网| 大陆偷拍与自拍| 国产精品久久视频播放| 国产欧美日韩一区二区精品| 亚洲avbb在线观看| 在线国产一区二区在线| 欧美性长视频在线观看| 亚洲中文字幕日韩| 国产精品久久久av美女十八| 国产不卡av网站在线观看| 国产伦人伦偷精品视频| 男人操女人黄网站| 法律面前人人平等表现在哪些方面| 国产成人免费无遮挡视频| a级毛片黄视频| 成人av一区二区三区在线看| 精品亚洲成国产av| 中文字幕人妻丝袜一区二区| 免费观看人在逋| 热99re8久久精品国产| 免费av中文字幕在线| 91九色精品人成在线观看| 悠悠久久av| 国产一卡二卡三卡精品| 啪啪无遮挡十八禁网站| 在线观看午夜福利视频| 久久久久国产一级毛片高清牌| 窝窝影院91人妻| 日本黄色视频三级网站网址 | 99riav亚洲国产免费| 女人被狂操c到高潮| 高潮久久久久久久久久久不卡| 岛国在线观看网站| 女人被躁到高潮嗷嗷叫费观| 在线视频色国产色| 亚洲精品粉嫩美女一区| 精品福利永久在线观看| 欧美丝袜亚洲另类 | 两性午夜刺激爽爽歪歪视频在线观看 | 在线观看www视频免费| 亚洲第一av免费看| 欧美日韩亚洲综合一区二区三区_| 久99久视频精品免费| 淫妇啪啪啪对白视频| 成人特级黄色片久久久久久久| 国产一卡二卡三卡精品| 国产精品 国内视频| 欧美丝袜亚洲另类 | 国产成人系列免费观看| 19禁男女啪啪无遮挡网站| 久久性视频一级片| 一进一出抽搐动态| 国产精品一区二区免费欧美| 一边摸一边抽搐一进一出视频| 亚洲全国av大片| 欧美午夜高清在线| 黄频高清免费视频| 国产成人一区二区三区免费视频网站| 精品第一国产精品| 水蜜桃什么品种好| 在线视频色国产色| 国内毛片毛片毛片毛片毛片| 亚洲人成电影免费在线| 精品久久久久久久久久免费视频 | 亚洲一卡2卡3卡4卡5卡精品中文| 国产高清videossex| 久久久久久久国产电影| cao死你这个sao货| 一级毛片高清免费大全| 一a级毛片在线观看| 69精品国产乱码久久久| 一边摸一边抽搐一进一出视频| 岛国毛片在线播放| 午夜成年电影在线免费观看| av天堂在线播放| a在线观看视频网站| av电影中文网址| 高清视频免费观看一区二区| 老司机福利观看| 精品电影一区二区在线| 母亲3免费完整高清在线观看| 亚洲午夜理论影院| 国产精品免费一区二区三区在线 | 人人澡人人妻人| 亚洲国产毛片av蜜桃av| 国产精品美女特级片免费视频播放器 | 欧美+亚洲+日韩+国产| 99热只有精品国产| 国产单亲对白刺激| 悠悠久久av| 日韩人妻精品一区2区三区| 亚洲av片天天在线观看| 国产高清激情床上av| 日本wwww免费看| 亚洲五月色婷婷综合| 一夜夜www| 免费一级毛片在线播放高清视频 | 亚洲成人手机| 亚洲av美国av| 亚洲国产中文字幕在线视频| 天天躁狠狠躁夜夜躁狠狠躁| 中文欧美无线码| 一边摸一边抽搐一进一出视频| 91av网站免费观看| 怎么达到女性高潮| 丰满的人妻完整版| 亚洲avbb在线观看| 精品国产一区二区三区四区第35| 国产97色在线日韩免费| 18禁观看日本| 欧美日韩一级在线毛片| 手机成人av网站| 久久国产精品大桥未久av| 一级毛片女人18水好多| 精品人妻在线不人妻| 欧美丝袜亚洲另类 | 天天影视国产精品| 桃红色精品国产亚洲av| 亚洲午夜精品一区,二区,三区| 中文字幕另类日韩欧美亚洲嫩草| 美女福利国产在线| 国产91精品成人一区二区三区| 在线av久久热| 久久精品国产a三级三级三级| 亚洲色图 男人天堂 中文字幕| 九色亚洲精品在线播放| 免费高清在线观看日韩| 亚洲色图综合在线观看| 国产不卡一卡二| 国产97色在线日韩免费| 丝瓜视频免费看黄片| 青草久久国产| 777久久人妻少妇嫩草av网站| 成在线人永久免费视频| 精品少妇久久久久久888优播| 热99久久久久精品小说推荐| 人妻久久中文字幕网| av中文乱码字幕在线| 国产av又大| 久久久精品区二区三区| av网站免费在线观看视频| 9191精品国产免费久久| 大香蕉久久网| av国产精品久久久久影院| 他把我摸到了高潮在线观看| 国产单亲对白刺激| 涩涩av久久男人的天堂| 精品一区二区三区四区五区乱码| 九色亚洲精品在线播放| 免费人成视频x8x8入口观看| 久久精品国产综合久久久| videosex国产| 亚洲 欧美一区二区三区| 亚洲av成人av| 91老司机精品| 十八禁网站免费在线| 亚洲精品国产色婷婷电影| 国产一区在线观看成人免费| 女人精品久久久久毛片| 婷婷精品国产亚洲av在线 | 午夜精品国产一区二区电影| 大香蕉久久成人网| 欧美日韩精品网址| 亚洲欧美一区二区三区黑人| 好看av亚洲va欧美ⅴa在| 亚洲性夜色夜夜综合| 中文字幕av电影在线播放| 啦啦啦 在线观看视频| 精品国内亚洲2022精品成人 | 国产精品成人在线| 在线观看66精品国产| 最新的欧美精品一区二区| 麻豆成人av在线观看| 这个男人来自地球电影免费观看| 国产视频一区二区在线看| 一级片免费观看大全| 国产精品98久久久久久宅男小说| 久久人妻熟女aⅴ| 91九色精品人成在线观看| 亚洲av电影在线进入| 黄色毛片三级朝国网站| 亚洲成av片中文字幕在线观看| 淫妇啪啪啪对白视频| 91精品三级在线观看| 欧美大码av| 久久精品国产清高在天天线| 国产高清videossex| 国产欧美日韩一区二区三| 丰满迷人的少妇在线观看| 91麻豆精品激情在线观看国产 | 国产精品一区二区在线不卡| avwww免费| 欧美中文综合在线视频| 免费少妇av软件| 少妇的丰满在线观看| 一级a爱片免费观看的视频| 欧美成人免费av一区二区三区 | 亚洲av熟女| 欧美不卡视频在线免费观看 | 国产一卡二卡三卡精品| 久久人妻av系列| 亚洲,欧美精品.| xxx96com| 美女高潮喷水抽搐中文字幕| 亚洲精品久久午夜乱码| 国产一区二区三区综合在线观看| 欧美最黄视频在线播放免费 | 欧美av亚洲av综合av国产av| 自拍欧美九色日韩亚洲蝌蚪91| 国产不卡一卡二| 国产片内射在线| 18禁国产床啪视频网站| 午夜免费鲁丝| 国产欧美亚洲国产| 99久久99久久久精品蜜桃| 国产精品免费一区二区三区在线 | 久久久精品区二区三区| 久久性视频一级片| 国产精品综合久久久久久久免费 | 激情视频va一区二区三区| 香蕉国产在线看| 中文亚洲av片在线观看爽 | 亚洲专区国产一区二区| 午夜精品在线福利| 久久久久久人人人人人| 极品人妻少妇av视频| 国产日韩欧美亚洲二区| 国产成人欧美在线观看 | 久久久国产成人精品二区 | 又大又爽又粗| 国产视频一区二区在线看| 成人黄色视频免费在线看| 自线自在国产av| 国产精品亚洲av一区麻豆| 一级a爱片免费观看的视频| 午夜久久久在线观看| 亚洲精品一二三| 精品久久久久久久久久免费视频 | 久久国产精品男人的天堂亚洲| 欧洲精品卡2卡3卡4卡5卡区| 国产91精品成人一区二区三区| 欧美最黄视频在线播放免费 | 天天影视国产精品| 成人18禁在线播放| 久久精品国产清高在天天线| 人人澡人人妻人| 欧美日韩中文字幕国产精品一区二区三区 | 国产乱人伦免费视频| 国产精品成人在线| 丝瓜视频免费看黄片| 精品久久蜜臀av无| 在线观看免费视频网站a站| 国产人伦9x9x在线观看| 在线天堂中文资源库| 搡老熟女国产l中国老女人| 51午夜福利影视在线观看| 少妇 在线观看| 99香蕉大伊视频| 亚洲少妇的诱惑av| 欧美日韩亚洲国产一区二区在线观看 | 亚洲片人在线观看| 夜夜躁狠狠躁天天躁| 叶爱在线成人免费视频播放| 激情在线观看视频在线高清 | 亚洲三区欧美一区| 亚洲人成电影免费在线| 亚洲成人免费av在线播放|