午夜勾魂曲-午夜福利自怕-午夜福利在线观看6080-午夜福利院电影-国产精品毛片AV久久97-国产精品麻豆高潮刺激A片

你不知道的 WebSocket

2020-7-26    seo達人

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


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


一、什么是 WebSocket

1.1 WebSocket 誕生背景

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




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




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


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


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


ws://echo.websocket.org

wss://echo.websocket.org

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


1.2 WebSocket 簡介

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


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


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




1.3 WebSocket 優點

較少的控制開銷。在連接創建后,服務器和客戶端之間交換數據時,用于協議控制的數據包頭部相對較小。

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

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

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

可以支持擴展。WebSocket 定義了擴展,用戶可以擴展協議、實現部分自定義的子協議。

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


二、WebSocket API

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




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


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


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


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


2.1 構造函數

WebSocket 構造函數的語法為:


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

相關參數說明如下:


url:表示連接的 URL,這是 WebSocket 服務器將響應的 URL。

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

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


2.2 屬性

WebSocket 對象包含以下屬性:




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


binaryType:使用二進制的數據類型連接。

bufferedAmount(只讀):未發送至服務器的字節數。

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

onclose:用于指定連接關閉后的回調函數。

onerror:用于指定連接失敗后的回調函數。

onmessage:用于指定當從服務器接受到信息時的回調函數。

onopen:用于指定連接成功后的回調函數。

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

readyState(只讀):返回當前 WebSocket 的連接狀態,共有 4 種狀態:


CONNECTING — 正在連接中,對應的值為 0;

OPEN — 已經連接并且可以通訊,對應的值為 1;

CLOSING — 連接正在關閉,對應的值為 2;

CLOSED — 連接已關閉或者沒有連接成功,對應的值為 3。

url(只讀):返回值為當構造函數創建 WebSocket 實例對象時 URL 的絕對路徑。

2.3 方法

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

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

2.4 事件

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


close:當一個 WebSocket 連接被關閉時觸發,也可以通過 onclose 屬性來設置。

error:當一個 WebSocket 連接因錯誤而關閉時觸發,也可以通過 onerror 屬性來設置。

message:當通過 WebSocket 收到數據時觸發,也可以通過 onmessage 屬性來設置。

open:當一個 WebSocket 連接成功時觸發,也可以通過 onopen 屬性來設置。

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


2.5 發送普通文本



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


// 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("連接未建立,還不能發送消息");

   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 瀏覽器的開發者工具來看一下相應的過程:




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


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

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

   <title>WebSocket 發送普通文本示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿寶哥:WebSocket 發送普通文本示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>即將發送的數據:<button onclick="send()">發送</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");


     // 監聽連接成功事件

     socket.addEventListener("open", function (event) {

       console.log("連接成功,可以開始通訊");

     });


     // 監聽消息

     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("連接未建立,還不能發送消息");

         return;

       }

       if (message) socket.send(message);

     }

   </script>

 </body>

</html>

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


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

socket.onopen = function () {

 // 發送UTF-8編碼的文本信息

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

 // 發送UTF-8編碼的JSON數據

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

 

 // 發送二進制ArrayBuffer

 const buffer = new ArrayBuffer(128);

 socket.send(buffer);

 

 // 發送二進制ArrayBufferView

 const intview = new Uint32Array(buffer);

 socket.send(intview);


 // 發送二進制Blob

 const blob = new Blob([buffer]);

 socket.send(blob);

};

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




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


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

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


2.6 發送二進制數據



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


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


數據發送代碼


// 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("連接未建立,還不能發送消息");

   return;

 }

 const blob = new Blob([message], { type: "text/plain" });

 if (message) socket.send(blob);

 console.log(`未發送至服務器的字節數:${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 瀏覽器的開發者工具來看一下相應的過程:




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


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


<!DOCTYPE html>

<html>

 <head>

   <meta charset="UTF-8" />

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

   <title>WebSocket 發送二進制數據示例</title>

   <style>

     .block {

       flex: 1;

     }

   </style>

 </head>

 <body>

   <h3>阿寶哥:WebSocket 發送二進制數據示例</h3>

   <div style="display: flex;">

     <div class="block">

       <p>待發送的數據:<button onclick="send()">發送</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");


     // 監聽連接成功事件

     socket.addEventListener("open", function (event) {

       console.log("連接成功,可以開始通訊");

     });


     // 監聽消息

     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("連接未建立,還不能發送消息");

         return;

       }

       const blob = new Blob([message], { type: "text/plain" });

       if (message) socket.send(blob);

       console.log(`未發送至服務器的字節數:${socket.bufferedAmount}`);

     }

   </script>

 </body>

</html>

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


三、手寫 WebSocket 服務器

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




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


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


既然握手是 WebSocket 連接生命周期的第一個環節,接下來我們就先來分析 WebSocket 的握手協議。


3.1 握手協議

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


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


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


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 協議。

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 協議。

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

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

3.1.2 服務端響應

HTTP/1.1 101 Web Socket Protocol Handshake ①

Connection: Upgrade ②

Upgrade: websocket ③

Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④

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

① 101 響應碼確認升級到 WebSocket 協議。

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

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

④ 簽名的鍵值驗證協議支持。

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


3.2 實現握手功能

要開發一個 WebSocket 服務器,首先我們需要先實現握手功能,這里阿寶哥使用 Node.js 內置的 http 模塊來創建一個 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() 方法創建一個 HTTP 服務器,接著我們監聽 upgrade 事件,每次服務器響應升級請求時就會觸發該事件。由于我們的服務器只支持升級到 WebSocket 協議,所以如果客戶端請求升級的協議非 WebSocket 協議,我們將會返回 “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");

}

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


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




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


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


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

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


// 從請求頭中讀取子協議

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

// 如果包含子協議,則解析子協議

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


// 簡單起見,我們僅判斷是否含有JSON子協議

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

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

}

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


3.3 消息通信基礎

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


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

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

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




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


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


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

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

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

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


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


3.3.2 掩碼算法

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


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


j = i MOD 4

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

original-octet-i:為原始數據的第 i 字節。

transformed-octet-i:為轉換后的數據的第 i 字節。

masking-key-octet-j:為 mask key 第 j 字節。

為了讓小伙伴們能夠更好的理解上面掩碼的計算過程,我們來對示例中 “我是阿寶哥” 數據進行掩碼操作。這里 “我是阿寶哥” 對應的 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 < uint8.length; i++, j = 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 協議中,數據掩碼的作用是增強協議的安全性。但數據掩碼并不是為了保護數據本身,因為算法本身是公開的,運算也不復雜。那么為什么還要引入數據掩碼呢?引入數據掩碼是為了防止早期版本的協議中存在的代理緩存污染攻擊等問題。


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


3.3.3 數據分片

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


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


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

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


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


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


3.4 實現消息通信功能

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


3.4.1 消息解析

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


function parseMessage(buffer) {

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

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

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

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

  * %x8:表示連接斷開;

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

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

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

  */

 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位,表示是否使用了掩碼,在發送給服務端的數據幀里必須使用掩碼,而服務端返回時不需要掩碼

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

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

   const payloadLen = secondByte & 0x7f; // 低7位表示載荷字節長度

   offset += 1;

   // 四個字節的掩碼

   let MASK = [];

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

   if (payloadLen <= 0x7d) {

     // 載荷長度小于125

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

     offset += 4;

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

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

     // 如果這個值是126,則后面兩個字節(16位)內容應該,被識別成一個16位的二進制數表示數據內容大??;

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

     // 長度是126, 則后面兩個字節作為payload length,32位的掩碼

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

     offset += 6;

   } else {

     // 如果這個值是127,則后面的8個字節(64位)內容應該被識別成一個64位的二進制數表示數據內容大小

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

     offset += 12;

   }

   // 開始讀取后面的payload,與掩碼計算,得到原來的字節內容

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

}

創建完 parseMessage 函數,我們來更新一下之前創建的 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;

 }

 // 省略已有代碼

});

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


Server running at http://localhost:8888

isFIN:  true

use MASK:  true

payload length:  15

Message from client:我是阿寶哥

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


3.4.2 消息響應

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


function constructReply(data) {

 const json = JSON.stringify(data);

 const jsonByteLength = Buffer.byteLength(json);

 // 目前只支持小于65535字節的負載

 const lengthByteCount = jsonByteLength < 126 ? 0 : 2;

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

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

 // 設置數據幀首字節,設置opcode為1,表示文本幀

 buffer.writeUInt8(0b10000001, 0);

 buffer.writeUInt8(payloadLength, 1);

 // 如果payloadLength為126,則后面兩個字節(16位)內容應該,被識別成一個16位的二進制數表示數據內容大小

 let payloadOffset = 2;

 if (lengthByteCount > 0) {

   buffer.writeUInt16BE(jsonByteLength, 2);

   payloadOffset += lengthByteCount;

 }

 // 把JSON數據寫入到Buffer緩沖區中

 buffer.write(json, payloadOffset);

 return buffer;

}

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


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

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

   const message = parseMessage(buffer);

   if (message) {

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

     // 新增以下

日歷

鏈接

個人資料

藍藍設計的小編 http://www.gerard.com.cn

存檔

主站蜘蛛池模板: 欧美视频精品在线 | 国产无套粉嫩白浆内谢在线 | a级黄片毛片 | 催眠淫辱の教室3在线观看 村上凉子在线播放av88 | 91精品国产91久久久久 | 日批视频在线播放 | 免费精品99久久国产综合精品应用 | 日本三级全黄少妇三2020 | 午夜精品久久久久久久99热额 | 51久久成人国产精品 | a级在线观看视频 | 色综合免费视频 | 亚欧激情乱码久久久久久久久 | 成 人 黄 色 视频 免费观看 | 亚洲国产精品综合久久20 | 久久久久久久久久久久久女过产乱 | 男人天堂黄色 | 少妇一区二区三区四区 | 日女人免费视频 | 国产伦精品一区二区三区免费 | 日韩免费一区二区三区 | 欧美极品在线视频 | 中文字幕av一区二区三区人妻少妇 | 九九人人 | 国产乱人伦av在线a麻豆 | 村上凉子在线播放69xx | 国产成人亚洲综合无码精品 | 亚洲三级网址 | 性xxx18| 午夜视频在线观看入口 | 91高潮胡言乱语对白刺激国产 | 伊人色综合久久天天网 | 成人瑟瑟 | 激情久久一区 | 国产亚洲精品一区二区三区 | 国产精品久久久天天影视 | 日韩成人在线观看 | 久草在线免费资源 | aⅴ一区二区三区无卡无码 aⅴ在线免费观看 | 在线a视频网站 | 快色网站 | 公妇乱淫真实生活 | 国产欧美一区二区精品久导航 | 国产二级片| 荫蒂被男人添的好舒服爽免费视频 | 99re6热在线精品视频播放 | 亚洲精品国产精品乱码不99 | 人人操日日干 | 男人天堂1024 | 国产一区二区三区乱码 | 天天操网站 | 一个添下面两个吃奶把腿扒开 | 国产精品久久久久久免费播放 | 老牛嫩草一区二区三区消防 | 欧美九九 | 韩国三级毛片 | 一 级 黄 色 片免费网站 | aaaa黄色 | 九色丨蝌蚪pony蜜桃臀 | 2019中文字幕在线 | 成人性生生活性生交视频 | 18禁网站免费无遮挡无码中文 | 中文字幕在线第一页 | 怡红院a∨人人爰人人爽 | av怡红院 | 波多野42部无码喷潮 | 国产第一页在线播放 | 亚洲性色av私人影院无码 | 日本又色又爽又黄的a片吻戏 | 香蕉免费在线视频 | 性一交一乱一伦a片 | 影音先锋在线播放 | 国产综合自拍 | 蜜桃精品视频在线 | а√天堂8资源中文在线 | 国内自拍偷区亚洲综合伊人 | 一级α片免费看 | 女上男下激烈啪啪xx00免费 | 不卡免费av | 中文字幕乱码免费看电影 | 女人黄色片 | 超碰在线99 | 国产乱xxxxx987国语对白 | 亚洲免费在线 | 91亚洲国产精品 | 亚洲美女偷拍 | 偷偷久久| av中文字幕第一页 | 国产真实乱岳激情对白av | 成人精品美女隐私 | 91精品国产色综合久久不卡98最新章节 | 成人午夜在线观看视频 | 国产高清一级片 | 国产精品无码dvd在线观看 | 亚洲天堂日本 | 欧美日韩精品网站 | 草草影院精品一区二区三区 | 狠狠躁夜夜躁人人躁婷婷视频 | 国产精品国产三级国产普通话99 | 黄色片亚洲 | 久久亚洲精品久久国产一区二区 | 日本不卡1 | zzijzzij亚洲日本少妇熟睡 | 狠狠gao| 久久精品国产一区二区三区 | 一区二区国产精品视频 | 欧美人妖ⅹxxx极品另类 | 黄色毛片网站 | 涩涩视频免费看 | 精品免费二区三区三区高中清不卡 | 日韩欧美一中文字暮视频 | 77777五月色婷婷丁香视频 | 亚洲人成久久婷婷精品五码 | 四虎成人精品国产永久免费无码 | 久久精品123 | 吃奶呻吟打开双腿做受在线视频 | 欧美专区第一页 | 久热超碰 | 亚洲视频 中文字幕 | 久久久黄色片 | 91动态图 | 中文日韩欧美 | 好吊色国产欧美日韩免费观看 | 成片免费观看视频大全 | 免费的黄色的视频 | 亚洲妇女捆绑hd | 亚洲自拍网址 | 久久免费在线观看 | 亚洲熟妇av午夜无码不卡 | 国产精品毛片一区二区 | 亚洲国产精品久久久久 | 欧美在线一二三四区 | 91精品国产成人观看 | 少妇性l交大片免潘金莲 | 精品国精品自拍自在线 | 免费看a网站 | 香蕉视频在线看 | 日本另类αv欧美另类aⅴ | 熟女毛毛多熟妇人妻aⅴ在线毛片 | 无遮挡啪啪摇乳动态图gif | 91大神网址 | 男人女人黄 色视频免费 | 国产第69页 | 九色91视频 | 在线xxxx | 免费无码又爽又刺激高潮软件 | 国产农村乱子伦精品视频 | 色猫咪免费人成网站在线观看 | 啪啪网免费 | 一区二区国产精品精华液 | 欧美1314| 五月天精品视频 | 精品二区在线观看 | 色综合天天射 | 国产精品精华液网站 | 色护士极品影院 | 亚洲欧美日韩愉拍自拍 | 成人片在线免费看 | av网站导航| 久久人妻精品白浆国产 | 欧美亚洲精品suv一区 | 99久热在线精品视频成人一区 | 欧美精品videosex性欧美 | 欧美日韩精品久久久 | 亚洲码视频 | 第九色| 日韩av第一页 | 精品人妻无码一区二区三区蜜桃一 | 久久精品夜夜夜夜夜久久 | 水蜜桃亚洲一二三四在线 | 久久99国产精品视频 | 爱搞逼综合 | 成熟丰满熟妇高潮xxxxx | 亚洲欧洲精品成人 | 国产系列第一页 | 国产人19毛片水真多19精品 | 日本一区二区更新不卡 | 天天干天天色综合网 | 亚洲精品1区| 99久久久无码国产精品 | 乱色欧美 | 顶级毛茸茸aaahd极品 | 亚洲国产精品一区二区久久hs | 国产精品一区二区av不卡 | 久久精品a亚洲国产v高清不卡 | 久久久精品久久久久 | 国产亚洲欧洲 | 一区二区视频在线播放 | 床上激情网站 | 久久不见久久见免费影院视频 | 污污网站在线看 | 小柔的淫辱日记(1~7) | 少妇性bbb搡bbb爽爽爽 | 91精产国品一二三产区区 | 少妇熟女久久综合网色欲 | 中文在线a在线 | 日韩少妇白浆无码系列 | 中年两口子高潮呻吟 | 欧美性猛交一区二区三区精品 | 婷婷深爱激情 | 国产精品入口香蕉 | 亚洲成av人片在www色猫咪 | 欧美3p两根一起进高清免费视频 | 日韩欧美人妻一区二区三区 | 黄色成人免费网站 | 99热久久久久久久久久久174 | 日韩黄色一级 | 国产亚洲人成在线播放 | 老太婆性杂交欧美肥老太 | 中文字幕日韩伦理 | 美一女一无一伦一性一交 | 久久欧美亚洲另类专区91大神 | 日韩亚洲在线 | 性做久久久久久免费观看 | 久久中文字幕网 | 色网址在线观看 | 韩国主播青草55部完整 | 欧美三级少妇高潮 | 欧美乱大交做爰xxxⅹ性3 | 日韩av一级 | 国产福利观看 | 咪咪色影院 | 国产农村乱对白刺激视频 | 国产精品入口夜色视频大尺度 | 鲁丝一区二区三区免费 | 天天操夜夜操视频 | 夜夜撸av | 日本三级做a全过程在线观看 | 久久国产精品久久久久久电车 | 26uuu亚洲婷婷狠狠天堂 | 久草资源福利 | 激情久久一区 | 国产乱人伦av在线a麻豆 | 天堂а在线中文在线新版 | 亚洲色欲综合一区二区三区 | 羞羞色视频 | 今夜无人入睡在线观看 | www激情com | 亚洲成人网在线播放 | 午夜精品久久久久久久99热 | 国产九色91| 国产91在线播放 | 99视频免费观看 | 亚洲com| 乱码av| 国产成人午夜高潮毛片 | 国产女人精品视频国产灰线 | 免费涩涩视频 | 色橹橹欧美在线观看视频高清 | 国产91在线观看 | 97超碰伊人 | 亚洲欧美日韩精品成人 | 国产精品亚 | 男女性潮高清免费网站 | 欧美黄色xxx | 国产精品欧美综合 | 国产在线观看不卡 | 亚洲狠狠爱一区二区三区 | 日韩成人福利 | 国产精品自在线拍国产手机版 | 日韩欧美无 | 91精品国自产在线偷拍蜜桃 | 欧美日韩一区二区在线观看 | 亚洲日韩av在线观看 | 国产成人av一区二区三区在线观看 | 俺来也av| 999久久欧美人妻一区二区 | 国产国产小嫩模无套内谢 | 深夜免费福利 | 国产一二精品 | 久久九九视频 | 醉酒后少妇被疯狂内射视频 | 成人午夜视频在线免费观看 | 日本二区视频 | 免费看大片a | 做爰吃奶全过程免费的网站 | 99久久精品午夜一区二区 | 中文字幕乱码久久午夜不卡 | 男人和女人高潮免费网站 | 精品少妇一区二区30p | 色综合久久久久综合体桃花网 | 蜜桃色视频 | 秋霞特色aa大片 | 成人性生交大片免费7 | 久操国产视频 | 亚洲暴爽av天天爽日日碰 | 亚洲婷婷av | 婷婷精品国产一区二区三区日韩 | av久久天堂三区 | 狠狠色丁香久久婷婷综合五月 | 夜夜爽一区二区三区精品 | 国产一区二区不卡 | 色噜噜日韩精品欧美一区二区 | 黄色小视频在线看 | 久久免费看少妇高潮a | 精人妻无码一区二区三区 | 夜夜高潮夜夜爽夜夜爱爱一区 | 日本丰满熟妇bbxbbxhd | 男女www视频 | 久久精品国产成人av | 久久精品男人的天堂 | 男人的天堂欧美 | 欧美做受高潮中文字幕 | 亚洲射情| 色综合av综合无码综合网站 | 8x8ⅹ8成人免费视频观看 | 十二月综合缴缴情小说 | 国产情人综合久久777777 | 深夜福利视频网站 | 二级大黄大片高清在线视频 | 在线播放毛片 | 午夜乱码爽中文一区二区 | 国产大片黄在线观看 | 老子午夜影院 | 美女内射视频www网站午夜 | 色婷婷精品久久二区二区蜜臂av | 尤物av无码色av无码 | 成人免费一区二区三区视频 | 国产一区二区三区视频在线播放 | 娇喘顶撞深初h1v1 | 不卡av片| 美女二区 | 69sex久久精品国产麻豆 | 在线啪 | 91资源在线视频 | 久久精品首页 | 强行18分钟处破痛哭av | 安野由美中文一区二区 | 日本爽妇网| 久久99精品久久久久久秒播放器 | 嫩草视频在线观看免费 | 欧美日韩在线视频观看 | 黄色网址你懂的 | 人妻少妇av无码一区二区 | 三男一女吃奶添下面视频 | 少妇性bbb搡bbb爽爽爽欧美 | 午夜精品一区二区三区的区别 | 肉丝一区二区 | 无码免费一区二区三区免费播放 | 97香蕉碰碰人妻国产欧美 | 亚洲精品久久久久久中文传媒 | 在线黄色免费网站 | 国产精品欧美亚洲韩国日本久久 | 黄网站欧美内射 | 中文字幕人妻第一区 | 国产一区二区精华 | 五月天久久综合 | 热热色原网址 | 国产v综合v亚洲欧美久久 | 一本一本久久a久久精品综合麻豆 | 亚洲图片在线 | 婷婷超碰| 国产日韩精品在线 | av无码国产在线看免费网站 | bbbbbxxxxx性欧美 | 舌头伸进添得好爽高潮欧美 | 国产专区第一页 | 日产精品一区二区三区在线观看 | 大尺度裸体日韩羞羞xxx | 性夜久久一区国产9人妻 | 老女人任你躁久久久久久老妇 | 性一交一乱一乱一视频 | 丝袜美腿一区二区三区 | 亚洲欧美日韩专区 | 成人国产欧美大片一区 | 亚洲不卡网 | 日韩a∨精品日韩在线观看 免费特级黄毛片 | 五月在线 | 一级在线视频 | 青草青草久热精品视频在线观看 | 理论片中文 | 久久九九兔免费精品6 | 国产精品一在线观看 | 99国产精品无码 | 久久久久久99精品久久久 | 18禁裸男晨勃露j毛免费观看 | 色老头一区二区三区 | 国产91脚交调教 | 日韩wwww | 久久特级毛片 | 久久久久99精品国产片 | 亚洲人成亚洲人成在线观看 | 亚洲国产精品毛片av不卡在线 | 国产又大又黑又粗 | 直接观看黄网站免费视频 | h狠狠躁死你h出轨高h | 少妇搡bbbb搡bbb搡古装 | 天天躁日日躁狠狠躁伊人 | 一区二区激情日韩五月天 | 四虎最新网址在线观看 | 一级做人爱c黑人影片 | 少妇玉梅高潮久久久 | 久久99精品久久久久久 | 国产黄色一级网站 | 永久天堂网av手机版 | 青青青国产在线 | 久久久久久久久久久99 | 老女人综合网 | 亚洲日夜噜噜 | 性一交一乱一乱一视频 | 亚洲综合国产成人丁香五月激情 | 欧美性猛交aaaa片黑人 | 思热99re视热频这里只精品 | 免费无码专区毛片高潮喷水 | 亚洲精品一区二区五月天 | 91爱爱中文字幕 | 久久丁香五月天综合网 | 真实国产老熟女粗口对白 | 精产国品一区二区三区四区 | 日韩内射美女人妻一区二区三区 | 国产欧美精品一区二区 | 国产精品免费在线播放 | 一区二区三区91 | 亚洲精品一区中文字幕乱码 | 全部免费的毛片在线看 | 黑人操少妇| 四虎影视在线影院在线观看免费视频 | 国产成人手机在线 | 免费观看全黄做爰的视在线观看 | 免费黄色片网站 | 国产精品妇女一二三区 | 国产精品一区二区三区四区在线观看 | 亚洲成人aaa| 污污内射久久一区二区欧美日韩 | 欧美日本三级少妇三级久久 | 亚洲日韩精品a∨片无码 | 日韩三级黄色毛片 | 蜜桃精品噜噜噜成人av | 亚洲天天看 | www国产| 91popny丨九色丨国产 | 久久99精品国产麻豆 | 四虎院影亚洲永久 | 久久久久夜夜夜精品国产 | 神马久久网站 | 精品午夜福利无人区乱码一区 | 久久综合伊人中文字幕 | 免费国产又色又爽又黄的网站 | а√在线中文网新版地址在线 | 玩丰满熟妇xxxx视频 | 性高朝大尺度少妇大屁股 | 91亚洲人人在字幕国产 | 天海翼一二三区 | 免费一级做a爰片性色毛片 免费一级做a爰片性视频 | 国产粉嫩高中好第一次不戴 | 日日夜夜狠狠 | 亚洲精品久久久久玩吗 | www.九九热| 成人精品在线观看 | 亚洲人色婷婷成人网站在线观看 | 夜夜欢天天干 | 国产成人剧情av麻豆果冻 | 绿帽刺激高潮对白 | 精品国产粉嫩内射白浆内射双马尾 | 人妻少妇精品中文字幕av蜜桃 | 久久久免费在线观看 | 午夜激情一区二区 | 三上悠亚人妻中文字幕在线 | 国产伦精品一区二区三区精品视频 | 男女全黄一级高潮 | brazzers猛女系列 | 久久国产色 | 我和岳疯狂性做爰全过程视频 | 精品97国产免费人成视频 | 18成人片黄网站www | 国产精品夜夜春夜夜爽久久小 | 日韩欧美一区二区在线视频 | 一本一道久久a久久综合蜜桃 | 女女女女女裸体处开bbb | 麻豆精品视频在线观看 | 成人免费8888在线视频 | 少妇撒尿一区二区在线视频 | 成人精品一区二区三区 | 日本少妇高潮喷水xxxxxxx | 热播网| 亚洲成av人片天堂网无码 | 在线播放日韩 | 精品久久国产老人久久综合 | 亚洲免费视 | 香蕉视频色 | 欧美精品成人久久 | 最新毛片网站 | 欧美日批视频 | 人人射人人澡 | 国产在线拍偷自揄拍无码 | 无码熟妇人妻av | 天天爽天天爱 | 精品国产乱码久久久久久芒果 | 国产精品亚洲自拍 | 欧美无遮挡很黄裸交视频 | 中文人妻无码一区二区三区信息 | 国产xxxx做受性欧美88 | 日本美女逼 | tushy欧美激情在线观看 | 国产乱xxⅹxx国语对白 | 日本在线一区 | 久久亚洲中文字幕无码 | 久久91精品国产91久久久 | 免费观看又色又爽又黄的崩锅 | 激情丁香网 | 伊人久久精品无码麻豆一区 | 亚洲成年轻人电影网站www | 久操视频在线播放 | 91丝袜呻吟高潮美腿白嫩在线观看 | 日韩资源在线 | 人人妻人人澡av天堂香蕉 | 一个色在线 | 国产又粗又猛又爽又黄又 | 久久国产精品无码网站 | 国产成人精品综合 | 亚洲综合天堂 | 中文字幕有码无码人妻av蜜桃 | www.香蕉网 | 久久超碰av | 成人免费观看49www在线观看 | 中文字幕在线一区 | 91视频福利| 国产成人精品视频 | 在线视频欧美亚洲 | 亚洲性网站| 51精品国产人成在线观看 | 国产吞精囗交免费视频网站 | 亚洲高潮 | 国产精品成人免费一区久久羞羞 | 欧洲a老妇女黄大片 | 偷拍激情视频一区二区三区 | 91久久国产婷婷一区二区 | 亚洲综合区 | 新久久久| 欧美激情性生活 | 国产亚洲精久久久久久叶玉卿 | 九九在线精品视频 | 日本大香伊一区二区三区 | 日韩精品中文字幕一区二区三区 | 天天操天天操天天干 | 色情无码www视频无码区小黄鸭 | 精品国产乱码久久久久久夜甘婷婷 | 天天操综合网 | 国产精品久久久久久亚洲徐婉婉 | 久久久久无码精品亚洲日韩 | 人善性zzzzzo另类 | 欧美激情精品久久久久久免费 | 国产手机在线精品 | 91国自产精品中文字幕亚洲 | 深夜国产福利 | 欧美xx在线| 好吊妞视频988在线播放 | 欧美日韩69 | 色偷偷人人澡人人爽人人模 | 亚洲一区二区三区四区的 | 国产成人无码视频一区二区三区 | 男人天堂99 | 337p日本欧洲亚洲大胆色噜噜 | 男人扒开女人双腿猛进视频 | 日本人一69式jzzij | 日韩久久影视 | 美女黄色毛片 | 国产伦对白刺激精彩露脸 | 日本护士毛茸茸 | 欧美精品久久久久久久多人混战 | 久久久久国产a免费观看rela | 麻豆视频网址 | 撸啊撸av | 亚洲伊人久久大香线蕉综合图片 | 成人爱爱网站 | 草草影院av | 国产精品你懂的 | 欧美激情va永久在线播放 | 国产乱人乱精一区二视频国产精品 | 日韩 欧美 中文字幕 制服 | 亚洲日本区 | 亚欧洲精品在线视频免费观看 | 春色影视 | 久久人人爽人人爽人人片av高清 | 婷婷国产成人精品视频 | 九九九九热精品免费视频点播观看 | 性色网站 | tianlula成人精品 | 久久综合狠狠色综合伊人 | 少妇搡bbbb搡bbb搡澳门 | 国产精品一二 | 邻居少妇张开腿让我爽了在线观看 | 99精品福利| 欧美伊人 | 激情在线视频 | 国产一级美女视频 | 国产特黄aaa大片免费观看 | 91高跟黑色丝袜呻吟在线观看 | 日本黄色免费网站 | 永久精品| www春色| 九九热视频免费 | 国产精品久久久久久久久久久久久久久久久 | 精品国产一区二区三区日日嗨 | 三日本三级少妇三级99 | 日本中文在线观看 | 国产成人免费爽爽爽视频 | 漂亮少妇激烈床戏 | 韩国日本三级在线观看 | 狠狠色丁香婷婷综合欧美 | 性色av蜜臀av牛牛影院 | 春色校园综合人妻av | 久草免费网站 | 韩日av一区二区 | 日韩在线第二页 | 伊人yinren22综合开心 | 精品久久久中文字幕 | 国产精品对白交换绿帽视频 | av在线收看 | 四虎影音先锋 | 国产色在线 | 国产 狠狠色噜噜狠狠狠狠7777米奇 | 国产精品久久久久久久久久久久久 | av毛片久久| 91l九色lporny| 免费在线激情视频 |