国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

全方位了解WebSocket?。ńㄗh收藏)

共 21372字,需瀏覽 43分鐘

 ·

2020-07-31 00:04


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

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

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

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

一、什么是 WebSocket

1.1 WebSocket 誕生背景

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

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

這種傳統(tǒng)的模式帶來(lái)很明顯的缺點(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 端口,可以繞過(guò)大多數(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)容之后,接下來(lái)我們來(lái)看一下 XHR Polling 與 WebSocket 之間的區(qū)別:

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

  • 較少的控制開(kāi)銷。在連接創(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ì)于前端開(kāi)發(fā)者來(lái)說(shuō),要想使用 WebSocket 提供的強(qiáng)大能力,就必須先掌握 WebSocket API,下面阿寶哥帶大家一起來(lái)認(rèn)識(shí)一下 WebSocket API。

二、WebSocket API

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

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

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

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

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

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

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

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

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

  • 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)閉或者沒(méi)有連接成功,對(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):該方法將需要通過(guò) WebSocket 鏈接傳輸至服務(wù)器的數(shù)據(jù)排入隊(duì)列,并根據(jù)所需要傳輸?shù)臄?shù)據(jù)的大小來(lái)增加 bufferedAmount 的值 。若數(shù)據(jù)無(wú)法傳輸(比如數(shù)據(jù)需要緩存而緩沖區(qū)已滿)時(shí),套接字會(huì)自行關(guān)閉。

2.4 事件

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

  • close:當(dāng)一個(gè) WebSocket 連接被關(guān)閉時(shí)觸發(fā),也可以通過(guò) onclose 屬性來(lái)設(shè)置。
  • error:當(dāng)一個(gè) WebSocket 連接因錯(cuò)誤而關(guān)閉時(shí)觸發(fā),也可以通過(guò) onerror 屬性來(lái)設(shè)置。
  • message:當(dāng)通過(guò) WebSocket 收到數(shù)據(jù)時(shí)觸發(fā),也可以通過(guò) onmessage 屬性來(lái)設(shè)置。
  • open:當(dāng)一個(gè) WebSocket 連接成功時(shí)觸發(fā),也可以通過(guò) onopen 屬性來(lái)設(shè)置。

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

2.5 發(fā)送普通文本

在以上示例中,我們?cè)陧?yè)面上創(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ù)交互過(guò)程,我們使用 Chrome 瀏覽器的開(kāi)發(fā)者工具來(lái)看一下相應(yīng)的過(guò)程:

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


<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)聽(tīng)連接成功事件
??????socket.addEventListener("open",?function?(event)?{
????????console.log("連接成功,可以開(kāi)始通訊");
??????});

??????//?監(jiān)聽(tīng)消息
??????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)行后,通過(guò) Chrome 開(kāi)發(fā)者工具,我們可以看到對(duì)應(yīng)的數(shù)據(jù)交互過(guò)程:

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

Blob(Binary Large Object)表示二進(jìn)制類型的大對(duì)象。在數(shù)據(jù)庫(kù)管理系統(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è)陧?yè)面上創(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)用自身來(lái)根據(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 瀏覽器的開(kāi)發(fā)者工具來(lái)看一下相應(yīng)的過(guò)程:

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

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


<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)聽(tīng)連接成功事件
??????socket.addEventListener("open",?function?(event)?{
????????console.log("連接成功,可以開(kāi)始通訊");
??????});

??????//?監(jiān)聽(tīng)消息
??????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 之后,覺(jué)得還不夠過(guò)癮。下面阿寶哥將帶大家來(lái)實(shí)現(xiàn)一個(gè)支持發(fā)送普通文本的 WebSocket 服務(wù)器。

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

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

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

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

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

3.1 握手協(xié)議

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

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

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

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)求頭

字段說(shuō)明

  • 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ù)來(lái)構(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ù)器通過(guò)返回相同的首部確認(rèn)自己支持一個(gè)或多個(gè)擴(kuò)展。
  • Origin 字段是可選的,通常用來(lái)表示在瀏覽器中發(fā)起此 WebSocket 連接所在的頁(yè)面,類似于 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" 來(lái)指示這是一個(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é)議,接下來(lái)阿寶哥將使用 Node.js 來(lái)開(kāi)發(fā)我們的 WebSocket 服務(wù)器。

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

要開(kāi)發(fā)一個(gè) WebSocket 服務(wù)器,首先我們需要先實(shí)現(xiàn)握手功能,這里阿寶哥使用 Node.js 內(nèi)置的 http 模塊來(lái)創(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 模塊,然后通過(guò)調(diào)用該模塊的 createServer() 方法創(chuàng)建一個(gè) HTTP 服務(wù)器,接著我們監(jiān)聽(tīng) 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” 頭的值,返回給客戶端。

上述的過(guò)程看起來(lái)好像有點(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");
}

開(kāi)發(fā)完握手功能之后,我們可以使用前面的示例來(lái)測(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)可以正常工作了。那么握手有沒(méi)有可能失敗呢?答案是肯定的。比如網(wǎng)絡(luò)問(wèn)題、服務(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ù)展開(kāi)介紹了。

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

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

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

3.3 消息通信基礎(chǔ)

在 WebSocket 協(xié)議中,數(shù)據(jù)是通過(guò)一系列數(shù)據(jù)幀來(lái)進(jìn)行傳輸?shù)摹榱吮苊庥捎诰W(wǎng)絡(luò)中介(例如一些攔截代理)或者一些安全問(wèn)題,客戶端必須在它發(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)容之后,就開(kāi)始有點(diǎn) “懵逼” 了。下面我們來(lái)結(jié)合實(shí)際的數(shù)據(jù)幀來(lái)進(jìn)一步分析一下:

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

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

  • 如果值為 0-125,那么就表示負(fù)載數(shù)據(jù)的長(zhǎng)度。
  • 如果是 126,那么接下來(lái)的 2 個(gè)字節(jié)解釋為 16 位的無(wú)符號(hào)整形作為負(fù)載數(shù)據(jù)的長(zhǎng)度。
  • 如果是 127,那么接下來(lái)的 8 個(gè)字節(jié)解釋為一個(gè) 64 位的無(wú)符號(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é)商過(guò)擴(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è)的。因此,掩碼必須來(lái)自強(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ì)算過(guò)程,我們來(lái)對(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?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)樗惴ū旧硎枪_(kāi)的,運(yùn)算也不復(fù)雜。那么為什么還要引入數(shù)據(jù)掩碼呢?引入數(shù)據(jù)掩碼是為了防止早期版本的協(xié)議中存在的代理緩存污染攻擊等問(wèn)題。

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

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

WebSocket 的每條消息可能被切分成多個(gè)數(shù)據(jù)幀。當(dāng) WebSocket 的接收方收到一個(gè)數(shù)據(jù)幀時(shí),會(huì)根據(jù) FIN 的值來(lái)判斷,是否已經(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)容,我們來(lái)看一個(gè)來(lá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ā)送了兩條消息。第一個(gè)消息在單個(gè)幀中發(fā)送,而第二個(gè)消息跨三個(gè)幀發(fā)送。

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

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

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

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

3.4.1 消息解析

利用消息通信基礎(chǔ)環(huán)節(jié)中介紹的相關(guān)知識(shí),阿寶哥實(shí)現(xiàn)了一個(gè) parseMessage 函數(shù),用來(lái)解析客戶端傳過(guò)來(lái)的 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:表示連接斷開(kāi);
???*?%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;
????}
????//?開(kāi)始讀取后面的payload,與掩碼計(jì)算,得到原來(lái)的字節(jié)內(nèi)容
????const?newBuffer?=?[];
????const?dataBuffer?=?buffer.slice(offset);
????for?(let?i?=?0,?j?=?0;?i?4)?{
??????const?nextBuf?=?dataBuffer[i];
??????newBuffer.push(nextBuf?^?MASK[j]);
????}
????return?Buffer.from(newBuffer).toString();
??}
??return?"";
}

創(chuàng)建完 parseMessage 函數(shù),我們來(lái)更新一下之前創(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ā)送普通文本” 的示例來(lái)測(cè)試消息解析功能。以下發(fā)送 “我是阿寶哥” 文本消息后,WebSocket 服務(wù)器輸出的信息。

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

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

3.4.2 消息響應(yīng)

要把數(shù)據(jù)返回給客戶端,我們的 WebSocket 服務(wù)器也得按照 WebSocket 數(shù)據(jù)幀的格式來(lái)封裝數(shù)據(jù)。與前面介紹的 parseMessage 函數(shù)一樣,阿寶哥也封裝了一個(gè) constructReply 函數(shù)用來(lái)封裝返回的數(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è)賮?lái)更新一下之前創(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);
??????//?新增以下?代碼
??????socket.write(constructReply({?message?}));
????}?else?if?(message?===?null)?{
??????console.log("WebSocket?connection?closed?by?the?client.");
????}
??});
});

到這里,我們的 WebSocket 服務(wù)器已經(jīng)開(kāi)發(fā)完成了,接下來(lái)我們來(lái)完整驗(yàn)證一下它的功能。

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

custom-websocket-server.js

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

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

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

server.on("upgrade",?function?(req,?socket)?{
??socket.on("data",?(buffer)?=>?{
????const?message?=?parseMessage(buffer);
????if?(message)?{
??????console.log("Message?from?client:"?+?message);
??????socket.write(constructReply({?message?}));
????}?else?if?(message?===?null)?{
??????console.log("WebSocket?connection?closed?by?the?client.");
????}
??});
??if?(req.headers["upgrade"]?!==?"websocket")?{
????socket.end("HTTP/1.1?400?Bad?Request");
????return;
??}
??//?讀取客戶端提供的Sec-WebSocket-Key
??const?secWsKey?=?req.headers["sec-websocket-key"];
??//?使用SHA-1算法生成Sec-WebSocket-Accept
??const?hash?=?generateAcceptValue(secWsKey);
??//?設(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}`)
);

util.js

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

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

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

function?parseMessage(buffer)?{
??//?第一個(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:表示連接斷開(kāi);
???*?%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;
????}
????//?開(kāi)始讀取后面的payload,與掩碼計(jì)算,得到原來(lái)的字節(jié)內(nèi)容
????const?newBuffer?=?[];
????const?dataBuffer?=?buffer.slice(offset);
????for?(let?i?=?0,?j?=?0;?i?4)?{
??????const?nextBuf?=?dataBuffer[i];
??????newBuffer.push(nextBuf?^?MASK[j]);
????}
????return?Buffer.from(newBuffer).toString();
??}
??return?"";
}

function?constructReply(data)?{
??const?json?=?JSON.stringify(data);
??const?jsonByteLength?=?Buffer.byteLength(json);
??//?目前只支持小于65535字節(jié)的負(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;
}

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

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

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

四、阿寶哥有話說(shuō)

4.1 WebSocket 與 HTTP 有什么關(guān)系

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

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

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

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

長(zhǎng)輪詢就是客戶端發(fā)起一個(gè)請(qǐng)求,服務(wù)器收到客戶端發(fā)來(lái)的請(qǐng)求后,服務(wù)器端不會(huì)直接進(jìn)行響應(yīng),而是先將這個(gè)請(qǐng)求掛起,然后判斷請(qǐng)求的數(shù)據(jù)是否有更新。如果有更新,則進(jìn)行響應(yīng),如果一直沒(méi)有數(shù)據(jù),則等待一定的時(shí)間后才返回。

長(zhǎng)輪詢的本質(zhì)還是基于 HTTP 協(xié)議,它仍然是一個(gè)一問(wèn)一答(請(qǐng)求 — 響應(yīng))的模式。而 WebSocket 在握手成功后,就是全雙工的 TCP 通道,數(shù)據(jù)可以主動(dòng)從服務(wù)端發(fā)送到客戶端。

4.3 什么是 WebSocket 心跳

網(wǎng)絡(luò)中的接收和發(fā)送數(shù)據(jù)都是使用 SOCKET 進(jìn)行實(shí)現(xiàn)。但是如果此套接字已經(jīng)斷開(kāi),那發(fā)送數(shù)據(jù)和接收數(shù)據(jù)的時(shí)候就一定會(huì)有問(wèn)題??墒侨绾闻袛噙@個(gè)套接字是否還可以使用呢?這個(gè)就需要在系統(tǒng)中創(chuàng)建心跳機(jī)制。所謂 “心跳” 就是定時(shí)發(fā)送一個(gè)自定義的結(jié)構(gòu)體(心跳包或心跳幀),讓對(duì)方知道自己 “在線”。以確保鏈接的有效性。

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

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

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

4.4 Socket 是什么

網(wǎng)絡(luò)上的兩個(gè)程序通過(guò)一個(gè)雙向的通信連接實(shí)現(xiàn)數(shù)據(jù)的交換,這個(gè)連接的一端稱為一個(gè) socket(套接字),因此建立網(wǎng)絡(luò)通信連接至少要一對(duì)端口號(hào)。socket 本質(zhì)是對(duì) TCP/IP 協(xié)議棧的封裝,它提供了一個(gè)針對(duì) TCP 或者 UDP 編程的接口,并不是另一種協(xié)議。通過(guò) socket,你可以使用 TCP/IP 協(xié)議。

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

在Internet 上的主機(jī)一般運(yùn)行了多個(gè)服務(wù)軟件,同時(shí)提供幾種服務(wù)。每種服務(wù)都打開(kāi)一個(gè)Socket,并綁定到一個(gè)端口上,不同的端口對(duì)應(yīng)于不同的服務(wù)。Socket 正如其英文原義那樣,像一個(gè)多孔插座。一臺(tái)主機(jī)猶如布滿各種插座的房間,每個(gè)插座有一個(gè)編號(hào),有的插座提供 220 伏交流電, 有的提供 110 伏交流電,有的則提供有線電視節(jié)目??蛻糗浖⒉孱^插到不同編號(hào)的插座,就可以得到不同的服務(wù)?!?百度百科

關(guān)于 Socket,可以總結(jié)以下幾點(diǎn):

  • 它可以實(shí)現(xiàn)底層通信,幾乎所有的應(yīng)用層都是通過(guò) socket 進(jìn)行通信的。
  • 對(duì) TCP/IP 協(xié)議進(jìn)行封裝,便于應(yīng)用層協(xié)議調(diào)用,屬于二者之間的中間抽象層。
  • TCP/IP 協(xié)議族中,傳輸層存在兩種通用協(xié)議: TCP、UDP,兩種協(xié)議不同,因?yàn)椴煌瑓?shù)的 socket 實(shí)現(xiàn)過(guò)程也不一樣。

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

五、參考資源

  • 維基百科 - WebSocket
  • MDN - WebSocket
  • MDN - Protocol_upgrade_mechanism
  • rfc6455
  • Web 性能權(quán)威指南

瀏覽 32
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)
評(píng)論
圖片
表情
推薦
點(diǎn)贊
評(píng)論
收藏
分享

手機(jī)掃一掃分享

分享
舉報(bào)

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 爽爽午国产浪潮AV性色www| 黃色级A片一級片| 探花在线综合| 91人人人人| 另类欧美| 人人操人人爽人人妻| 欧美香蕉在线| 青青草原视频在线免费观看| 日产久久久久久| 亚州精品国产精品乱码不99勇敢| 欧美日韩中文| 成人在线视频免费观看| 天天干天天射天天操| 大香蕉在线精品视频| 亚洲视频中文字幕| 久久综合伊人| 三级视频网| 成人免费视频国产免费麻豆,| 超小超嫩国产合集六部| 婷婷五月开心五月| 三级无码片| 成人做爰黄A片免费| 久久亚洲精品视频| 中文字幕AV在线免费观看| 欧美熟妇擦BBBB擦BBBB| 1024香蕉视频| 亚洲成人动漫在线| 国产操逼图片| 三级无码视频在线观看| 特一级黄A片| 成人A视频| 五月天在线电影| 大香蕉伊人成人| 人妻精品一区二区在线| 开心激情播播网| 性爱一级视频| A片地址| 久久99久久视频| gogogo高清在线完整免费播放韩国 | 亚洲无码在| 国产一级婬片A片| 国产福利91精品| 国内自拍第一页| 婷婷一区二区三区| 理论片熟女奶水哺乳| 久久性爱网| 自拍AV在线| 日本久久久| 人善交精品一区二区三区| 黄色日逼片| av资源在线播放| www.俺来也| 成人肏逼视频| 欧美日韩A| 亚洲va国产va天堂va久久| 山东wBBBB搡wBBBB| 久久大香| 欧美三级片在线| 亚洲成人黄色电影| 99性爱视频| 日韩性爱视频| 欧美级毛片高潮| 米奇7777狠狠狠狠| а√在线中文网新版地址在线| 日本少妇久久| 最美人妖系列国产Ts涵涵| 久艹在线观看视频| 中文字幕在线播放av| 91成人电影院| 亚洲AV无码乱码国产精品黑人| 人人摸人人看人人草| 无码一区视频| 在线观看成人18| 日本一级视频| 无码日韩视频| 狠狠躁夜夜躁人爽| 日韩操操| 无码一道本| 超碰中文字幕| 久久久久久国际四虎免费精品视频 | 爽好紧别夹喷水欧美| www.中文字幕| 亚洲影院中文字幕| 国内综合久久| 亚洲aaaaaa| 亚洲精品aaa| 日日久视频| 大香蕉综合网站| www高清无码| 成年人黄色视频在线观看| 亚洲性天堂| 性满足BBWBBWBBW| 亚洲无码精品在线| 欧美性爱一区二区| 亚洲免费视频在线观看| 亚洲AV无码乱码AV| 国产无码久久久| 一区二区三区四区无码在线| 91麻豆视频在线观看| 黄色精品网站| 偷拍亚洲欧美| 俺来也网| 婷婷五月天中文字幕| 天天天天天天操| 91露脸熟女四川熟女在线观看| 无码一区二区视频| 激情图区| 日韩欧美中文字幕视频| 羞羞涩漫无码免费网站入口| 视色AV| 日本aaaa片| 久久久久久久三级片| 全部免费黄色视频| 亚洲无码在线免费观看视频| www.干| 99热这里只有精品9| 亚洲欧美在线视频| 欧美性猛交一区二区三区精品| 午夜日韩乱伦| av在线观看网站| 亚洲永久天堂| 刘玥一级婬片A片AAA| 日本a在线免费观看| 综合网欧美| 亚洲a网| www亚洲无码| 99久久精品国产一区色| 日本无码一区二区三三| 北条麻妃99精彩视频| 欧美日韩四区| 嘿嘿av| 免费电影日本黄色| 性爱AV在线| 四川少妇BBBB| 无码在线电影| 日本无码一区二区三区| 婷婷五月天丁香| 日本成人中文字幕在线观看| 国产精品久久久久久最猛| a天堂在线| 91精彩视频在线观看| 日韩色在线| 亚洲欧美熟妇久久久久久久久 | 一区二区A片| 2022黄片| 国产青青| 蜜桃一区二区中午字幕| 444444在线观看免费高清电视剧木瓜一 | 欧美激情精品| 家庭乱伦av| 综合成人| 大香蕉尹人在线| 91九色麻豆| 欧美精品久久久久久久久| 欧亚免费视频| 中文无码日本一级A片久久影视| 91牛| 色av网| 99成人在线视频| 成人视频你懂的| 张柏芝BBw搡BBBB槡BBBBHDfree | 欧美日韩婷婷| 日韩性做爰免费A片AA片| 色婷婷大香蕉| 无码av无码AV| 996热re视频精品视频这里| 欧美老女人操逼视频| 亚洲色老板| 国产a√| 激情内射| 精品三级在线观看| 亚洲一区二区精品| 亚洲骚货| 成人伦理聚合| 亚洲丰满熟妇| 青青操在线| 精品国产99| 国产免费看| 中文字幕乱码视频| 午夜啊啊啊| 一级A片60分钟免费看| www.色999| 高清无码成人视频| 麻豆md0049免费| 神马午夜久久| 熟女456| 亚洲无码av电影| a级网站| 久久久久久亚洲Av无码精品专口 | 午夜精品久久久久久久91蜜桃| 亚洲人成色777777无码| av无码高清| 三上悠亚一区二区| 国产成人TV| 性爱福利视频| a无码| 成人一区二区三区四区五区| AV免费网址| 无码精品一区二区| 久久精品禁一区二区三区四区五区 | 大香蕉网址| 中文字幕精品久久久久人妻红杏Ⅰ| 青青草大香蕉伊人| 正在播放JUQ-878木下凛凛子| 先锋影音资源站| 18禁在线看| 日韩三级av| 中文字幕1区| 日韩91在线视频| 91麻豆视频在线观看| 日韩小视频在线观看| 成年人在线观看| 国产精品高清网站| 午夜福利aaa| 777在线视频| 欧美性爱内射| 少妇bbb搡bbbb搡bbbb| 国产激情在线播放| A片地址| 国产无遮挡又黄又爽又色视频 | A片免费网站| 一个色综合网| AV三级无码| 黄色大片在线播放| 2018人人操| 欧美A片视频| 日韩黄色电影在线观看| 国产精品毛片一区视频播| 1插菊花网| 刘玥91精一区二区三区| 亚洲人在线观看| 久久精品五月天| 西西444WWW无码大胆| 天天天天毛片| 亚洲白浆| 亚洲综合免费观看高清| 熟女一区二区三区| www.操| 日韩免费A片| 六月丁香久久| 久久精品一区二区三区不卡牛牛| 色女人天堂| 超碰毛片| 在线观看中文字幕AV| 8050网午夜| 日韩一级网| 中文有码在线观看| 欧美视频一区二区三区| 美女av网站| 蜜芽成人在线| 欧美人妻视频在线| 超碰超爽| 日韩欧美一区二区在线观看| 天天撸天天射| 性爱网站免费看| 成人精品一区二区三区无码视频| 国产成人无码免费看片| 高清中字无码| 伊人成人在线视频观看| 精品视频99| 久久人精品| 日韩欧美一区二区在线观看| 人人爽人人澡| 亚洲高清视屏| 欧美性爱69| 国产内射在线观看| 91成人在线影院| 天堂资源中文在线| 天天精品| 亚洲天堂一区在线观看| 中文字幕一区二区三区四区五区六区 | 一区二区三区电影高清电影免费观看 | 婷婷福利导航| 噜噜色色噜噜| 天天日天天操天天日| 国产亚洲天堂| 丰滿老婦BBwBBwBBw| 欧美va视频| 熟睡侵犯の奶水授乳在线| 久青草视频| 99爱在线| A片视频免费看| 中文字幕有码在线播放| 在线色网站| 在线看a片| 欧美一区二区三区成人| 中文字幕第315页| 一区二区三区四区五区六区高清无吗视频 | 国产精品久久久| 99在线精品视频观看| 日韩国产成人| 中文字幕一区二区三区人妻在线视频| 亚洲AV无码精品久久一区二区| 无码精品ThePorn| 国精品无码一区二区三区在线秋菊 | 天天做天天爽| 青娱乐三级在线免| 色色五月天网站| www.91久久| 闺蜜AV| 99久久99九九九99九他书对| 中文字幕高清在线中文字幕中文字幕 | 69性爱视频| 黄片免费观看网站| 国产精品无码成人AV电影| 午夜精品视频在线观看| 国产色五月视频| 欧美色图888| 夜夜操夜夜爽| 日韩加勒比在线| 国精品伦一区一区三区有限公司| 九月丁香| 毛片在线观看视频| 蜜臀av一区二区三区| 亚洲成年视频| 人人摸人人草| 熟女人妻在线| 成人精品在线| 人人看人人插| 囯产精品宾馆在线精品酒店| 精品秘一区性综合三区| 亚洲欧美性爱视频| 久久久精品无码| 综合色婷婷一区二区亚洲欧美国产 | 亚洲福利电影| 黄色免费av| 国产福利网站| 操b视频在线免费观看| 亚洲天堂视频在线| 日韩AV无码高清| 久久视频免费| 亚洲成人77777| 欧美一级免费观看| 一道本视频在线免费观看| 伊人大香蕉电影| 11孩岁女精品A片BBB| 欧美成人精品欧美一级| 小佟丽娅大战91哥| 天天操天天射天天日| 专业操美女视频网站| 国产三级在线免费观看| 中文字幕一级A片免费看| 中文字幕亚洲视频在线观看| 五月婷婷中文字幕| 中文无码在线观看中文字幕av中文| 午夜免费视频| 香蕉视频久久| 国产AV影视| 精产国品一区二区三区| 男人天堂网站| 国产乱子伦一区二区三区在线观看| 久久久久久国产精品| 亚洲GV成人无码久久精品 | 亚洲少妇网| 亚洲熟妇无码| 天天日天天干天天爽| 操逼在线免费观看| 中文字幕无码综合| 91人妻无码成人精品一区二区| 狼人综合视频| 欧美精品在线播放| 国产天堂在线观看| 成人怡红院| 天天撸在线| 日韩免费高清| 国产日逼网站| 日韩一级一级一级| 超碰2025| 国产精品扒开腿| 亚洲自拍电影| 99热青青| 18禁91| 88在线无码精品秘入口九色| 91一区| 影音先锋成人无码| 黄频美女日本免费| 99久久婷婷国产精品2020| 亚洲AV综合色区无码国产播放 | 色欲色欲一区二区三区| 亚洲AV第二区国产精品| 91九色蝌蚪91POR成人| 秋霞午夜成人无码精品| 无码av一区二区| 日本的黄色视频| 亚洲AV无码成人| 久久综合伊人7777777| 一区二区三区无码在线| 少妇BBB| 一级A片免费看| 亚洲精选中文字幕| 亚洲无码三级片在线观看| 欧美国产日本| AV在线大香蕉| 国产精品一二三区夜夜躁| 日韩日韩日韩| 四川美女网久草| 欧洲成人在线观看| 天堂av中文字幕| 91精品丝袜久久久久久久久久粉嫩 | 午夜福利剧场| 少妇高潮一区二区三区99| 91精品一区二区| 91亚洲精品国偷拍自产在线观看| 亚洲性精| 亚洲精品熟女| 91av导航| 中文无码在线播放| 精品人伦一区二区三区| 欧美日韩精品一区二区三区| 精品无码电影| 韩国gogogo高清在线完整版| 久久一级片| 日欧无码| 豆花视频一区| 日本爱爱网站| 各种BBwBBwBBwBBw| 亚洲一级免费在线观看| 黄色激情视频网站| 福利导航视频| 午夜操日在线| 岛国无码破解AV在线播放| 免费观看黄色在线视频| 插逼网站| 91国语又粗又大对白| 亚洲成人电影AV| 欧美在线一区二区三区| 亚洲精品美女| 一本久久A精品一合区久久久| 欧一美一婬一伦一区二区三区黑人 | 国产精品国产三级国产AⅤ| 久久人体| 搡女人视频国产一级午夜片| 国产精品欧美激情| 亚洲免费在线观看视频| 99看片| 国产欧美一| 俺来也俺也啪www色| 国产日韩一区二区三区| 中文字幕在线播放AV| 日韩极品视频| 俺也来www俺也色com| 在线观看视频日韩| 91麻豆精品传媒国产| 四川美人搡BBw搡BBw| 国产一区二区久久| 怡春院成人| 黄色a片视频| 一本色道精品久久一区二区三区 | 色色色色色色网站| 四川女人毛多水多A片| 91久久午夜无码鲁丝片久久人妻 | 亚洲无码播放| 午夜网页| 丁香五月综合啪啪| 思思久久高颜值| 成人免费在线观看| 蜜桃91在线观看| 国外亚洲成AV人片在线观看| 久久中文视频| 北京熟妇搡BBBB搡BBBB| 亚洲中文字幕无码爆乳av| 无码第一页| 9l农村站街老熟女| 超碰在线观看免费版| 欧美福利视频| 亚洲图片一区| 黄色毛片在线| 思思热99热| 女人天堂AV| 一本道视频在线| 色婷婷综合视频| 久久偷拍视频| 四川乱子伦95视频国产| 欧美熟妇一区二区| 靠逼国产| 亚洲黄色视频网站| 一区视频在线| 国产成人视频免费在线观看| 大香蕉免费中文| www.伊人| sese在线| 免费人成视频在线播放| 婷婷手机在线| 国产传媒三级| 中文字幕在线免费看线人| 久久无码人妻精品一区二区三区| 草久美女| 毛片视频免费观看| 强奸乱伦五月天| 亚洲人妖在线| 亚洲精品中文字幕在线| 婷婷五月天综合网| 亚洲天堂网在线视频| 欧美色图自拍| 韩国毛片基地久久| 蜜桃精品在线| 极品久久| 九七AV| 亚洲国产精品欧美久久| 精品视频一区二区三区| 亚洲一区在线免费观看| 波多野结衣在线观看一区二区| 天天操天天操天天操天天| 中文字幕日韩人妻| 人人操久久| 爱爱免费看片| 高清免费在线中文Av| 人妻无码一二三区免费| 午夜av影院| 三级片网站在线播放| 青青草在线观看视频| 国产精选在线| 国产一级内射| 黄色A片在线观看| 亚洲人成人无码一区二区三区| 亚洲videos| 777免费观看成人电影视频| 亚洲一级内射| 18久久| 怡春院院成人免费视频| 日本一区二区精品| 丁月婷婷五香天日五月天| 先锋资源AV| 一区二区三区免费在线| 国产激情在线观看视频| 欧美性爱内射| 久久丁香五月婷婷五月天激情视频 | 91精品国产成人www| 亚洲成人高清| 一本色道久久综合无码人妻| 91在线一区二区三区| 99无码精品| 日韩小电影在线观看| 国产视频福利| 另类激情| 中文字幕无码亚| 久久免费视频6| 色婷婷综合视频| 成人国产| 91久久久久久久久18| 欧美成人色图| www四虎com| 俺去也www俺去也com| 欧美性爱AAA| 在线亚洲日韩| 久草福利视频| 九九九色| 狠狠撸狠狠操| 成人精品电影| 久久熟妇| 99久久久精品久久久久久| 99久久精品一区二区成人| 人人妻人人操人人爽| 三级网站免费| 一曲二曲三曲在线观看中文字| 久久成人国产| 欧美性爱18| 亚洲日韩在线视频观看| 国产欧美在线观看不卡| 日本一级做a爱片| 91乱子伦国产乱子伦!| 中文字幕人妻互换av久久| 亚洲40p| 亚洲无码高清在线视频| 色老板综合| 波多野结衣被操| 777免费观看成人电影视频| 天堂中文在线观看| 91久久国产综合| 操骚屄视频| 欧美色图综合| 国产一区在线看| 黄色视频在线观看地址| 亚洲aaaaaa| 正在播放ADN156松下纱荣子| 日韩高清国产一区在线| 久久久国产精品黄毛片| 俺来也俺就去www色情网| 亚洲人妻无码视频| 日韩中文字幕在线免费观看| 国产人妖AV| 午夜老司机福利一二三区| 欧美日韩岛国| 米奇电影777无码| 黄色一区在线| 人人看人人摸人人操| 久久AV片| 久久穴| 亚洲午夜福利在线| 狠狠av| 人人干人人上| 日批动态图| 亚洲国产欧美在线| 日韩一区二区视频在线观看| 天天操操| 大香蕉日韩| 狠狠干狠狠撸| 久久精品国产99精品国产亚洲性色 | 天天干妹子| 国产熟女av| 国产在线一区二区三区四区| 无码激情18激情视频| 亚洲欧美久久久久久久久久久久 | 大香蕉福利视频导航| 日韩成人无码毛片| 性v天堂| 国产精品性爱| 色444| 黄色成人网站免费在线观看| 中国老女人操逼视频| 杨贵妃一级婬片90分钟| 日本无码成人| 免费无码一级A片大黄在线观看| 亚洲精品18禁| 青青操视频在线| 国产激情无码免费| 秘蜜桃色一区二区三区在线观看| 婷婷V亚洲V丁香月天V日韩V | 伊人99re| 国产A片电影| 久久精品苍井空免费一区| 北京熟妇搡BBBB搡BBBB电影 | 国产无套免费网站69| 久久久精品午夜人成欧洲亚洲韩国| 精品视频在线观看免费| 国产九九热视频| 肏屄视频网| 亚洲福利免费观看| 永久免费AV无码| 国产在线1| 天天日天天草天天干| 人人操夜夜爽| 久久久精品在线| 韩国一区二区在线观看| 欧美精品在线免费观看| 黄色成年人视频在线观看| 国产香蕉在线播放| 北条麻妃无码av| 欧美在线观看一区二区| 91麻豆天美传媒在线| 成人黄色毛片视频| 老司机福利在线视频| 豆花精品视频| 久久亚洲Aⅴ成人无码国产丝袜 | 午夜视频99| 黄色视频网站日本| 九月丁香| 亚洲中文字幕无码在线观看| 2024av在线| 热re99久久精品国产99热| 久操国产视频| 亚洲欧美日韩中文字幕在线观看 | 天天日天天拍| 日批国产| 天天天天天天操| seseav| 91香蕉国产| 自拍偷拍视频网址| 99热国产在线| 伊人午夜| 欧美成人精品无码网站| 午夜av免费在线| 午夜成人福利视频| 国产精品欧美综合亚洲| www.黄色在线| 日本在线视频不卡| 蜜桃久久精品成人无码AV| 热九九热| 四虎影院色| 日日日日日干| 亚洲激情五月| 超碰2025| HEZ-502搭讪绝品人妻系列| 少妇嫩搡BBBB搡BBBB| 一级电影网站| 亚洲免费无码视频| 国产小视频在线观看| 日本一级特黄电影| 欧美性爱天天| 国产精品51麻豆cm传媒| 一级a黄色片| 免费观看在线黄片| 青青操视频在线| 操操操综合网| 日本狠狠操| 成人国产精品在线看| 亚洲三级在线播放| 青青草在线视频免费观看| 秋霞福利视频| 亚洲无码高清视频| 国产黄片自拍| 亚洲综合成人在线| 无码一区二区区| 日韩一区二区三区无码| 女同久久另类99精品国产91 | 欧美又粗又长| 天天草天天| 狠狠撸在线观看| 强奷伦奷片91| 8050午夜| 亚洲成人免费福利| av天堂电影网| 日韩精品极品视频在线观看免费| 国产中文字幕波多| 91夜夜| 成人视频在线观看黄色18| 97精品综合久久| 中文无码在线播放| 朝鲜性感AV在线| 伊人精品大香蕉| 亚洲视频偷拍| 亚洲熟女一区二区| 免费无码婬片AAAA片老婦| 精品成人A片久久久久久不卡三区 免费看成人A片无码照片88hⅤ | 粉嫩99精品99久久久久久特污兔| 热九九精品| 成人理伦A级A片在线论坛| 婷婷深爱五月| 欧美亚洲天堂网| 无码第一页| 亚洲熟女一区二区三区妖精| 亚洲国产av一区| 日本黄色片在线播放| 黄片日逼视频| 大香蕉中文网| 白嫩外女BBWBBWBBW| 黄色片在线看| 翔田千里无码精品| 亚洲素人无码| 无码蜜桃吴梦梦| 狼人综合在线| 亚洲网站视频| 中国操逼网| 麻豆传媒猫爪| 亚洲成人在线一区| 成人在线激情| 日韩视频――中文字幕| www.91n| 大香蕉超碰| 日韩视频一二三| 国产精品一区一区三区| 新中文字幕| 91久久精品一区二区三| 色色9999| 69精品| 亚洲一本色道中文无码| 真人一级片| 欧美精品成人免费| 亚洲AV无码成人精品区| 77777色婷婷| 少妇456| 天天日天天噜| 成年人免费视频在线观看| 人妻天堂| 超碰成人福利| 国产精品乱码毛片在线人与| 瑟瑟免费视频| 午夜av在线免费观看| 日韩2区| 在线视频第一页| 小草久久95| 99视频自拍| 三浦恵子一级婬片A片| 国产情侣在线视频| 亚洲精品秘一区二区三线观看| 无码av亚洲一区二区毛片公司| 伊人热久久| 青青操成人在线视频| 国产无码免费在线观看| 国产福利av| 少妇高潮av久久久久久| 精品久久免费一区二区三区 | 日本精品码喷水在线看| 国产海角视频| a在线| 吹潮喷水高潮HD| 免费内射视频| 日韩av在线免费观看| 国产另类自拍| 国产午夜精品一区二区三区嫩A | 国产精品无码免费视频| 大香蕉伊人影院| 最近日韩中文字幕中文翻译歌词| 人人干AV| 精产国品一区二区三区| 国产激情视频在线播放| 久久久久久久国产| 久热这里| 中文字幕在线免费观看电影| 亚洲日逼视频| 黄色在线观看免费| 高清毛片AAAAAAAAA片| 国产白丝精品91爽爽久久| 色香蕉在线| 亚洲中文字幕一区| 国产精品51麻豆cm传媒| 日日搔AV一区二区三区| 亚洲日韩视频在线| 日韩91| 白嫩无码| 你懂的视频在线播放| 四川少妇BBBB槡BBBB槡| 国产欧美日韩| 大香蕉尹人在线视频| 国产不卡在线视频| 午夜理论片| 国产激倩都市一区二区三区欧美| 高清无码视频直接看| 国产做受91一片二片老头| 在线无码免费观看| 极品少妇视频| 国产久久久久久久久久| 日本爱爱视频免费| 国产精品123区| 久久久久中文字幕| 亚洲欧美日韩高清| 亚洲一卡二卡| 国产精品探花熟女AV| 久久成人福利| 国产精品久久久久久久久久九秃| 国产精品久久久| 777米奇视频| 精品亚洲无码视频| 国产成人视频在线观看| 久久三级片电影| 亚洲第一大网站| 青青草无码| 佐山爱人妻无码蜜桃| 日韩A∨| 国产在线一区二区三区| 秋霞一区二区三区无码| 北条麻妃中文字幕旡码| 亚洲美女视频在线观看| 欧美色图88| 日韩性生活| 内射免费网站| 国产夫妻在线| 天堂综合网久久| 婷婷精品免费久久| 日本一区二区不卡| 国产高潮视频在线观看| 在线观看黄A片免费网站| 亚洲AV中文| 99久操| www三级片| 伊人久久影院| 亚洲另类av| 水蜜桃网站在线观看| 亚洲无码电影网| 欧美大胆a| 亚洲精品一二三| 午夜国产在线| 久久艹综合网| 亚洲天堂视频在线观看免费| 久久精品中文字幕| 精品国产久久久久| 色婷婷一区二区三区久久午夜| 成人在线视频免费观看| 亚洲综合中文字幕在线播放| 久久久精品免费| 蜜桃视频| 午夜久| 少妇bbw搡bbbb搡bbbb| 国产乱伦不卡| 97成人在线视频| 国产视频你懂的| 一级片黄色免费| 亚洲无码视频看看| 欧美级毛片一进一出| 成人无码自拍| 69视频免费观看| 日韩人妻丰满无码区A片| 精品免费一区二区三区四区| 大香蕉在线伊| 秋霞精品一区二区三区| 欧美mv日韩mv国产| 日韩无码性爱| 91亚洲在线观看| 露脸老熟女91集合| 国产在线一区二区| 狠狠狠狠狠狠狠狠狠狠| 日韩欧美小视频| 影音先锋av色| 日韩在线成人视频| 亚洲黄色影院| 18性XXXXX性猛交| 操操网| 中文字幕淫乱视频欧美| 嫩小槡BBBB槡BBBB槡免费-百度| jizzjizzjizzjizz| 午夜无码久久| 天堂网2014| 激情性爱婷婷色五月| 亚洲免费a| 五月天中文字幕| 色哟哟一区| ThePorn日本无码| 九九热播精品| 国产18毛片18水多精品| 性爱免费视频|