日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

WebSocket原理與實(shí)踐

 waston 2019-05-10

(一) 基本原理

1. 了解現(xiàn)有的HTTP的架構(gòu)模式:
Http是客戶端/服務(wù)器模式中請(qǐng)求-響應(yīng)所用的協(xié)議,在這種模式中,客戶端(一般是web瀏覽器)向服務(wù)器提交HTTP請(qǐng)求,服務(wù)器響應(yīng)請(qǐng)求的資源
(例如HTML頁(yè)面)和關(guān)于頁(yè)面的附加信息。

1-1. HTTP的特點(diǎn)是:
HTTP是半雙工協(xié)議,也就是說(shuō),在同一時(shí)刻流量只能單向流動(dòng),客戶端向服務(wù)器發(fā)送請(qǐng)求(單向的),然后服務(wù)器響應(yīng)請(qǐng)求(單向的)。服務(wù)器不能主動(dòng)推送數(shù)據(jù)給瀏覽器。

當(dāng)初這么設(shè)計(jì)也是有原因的,假如服務(wù)器能主動(dòng)推送數(shù)據(jù)給瀏覽器的話,那么瀏覽器很容易受到攻擊,比如一些廣告商會(huì)主動(dòng)把一些廣告信息強(qiáng)行的傳輸給客戶端。
半雙工的缺點(diǎn)是:效率非常低下。
比如想實(shí)現(xiàn)股票的實(shí)時(shí)行情,火車票的剩余票數(shù)等這些,半雙工協(xié)議是無(wú)法做到的。

2. 了解HTTP輪詢,長(zhǎng)輪詢和流化。
一般情況下,瀏覽器訪問(wèn)網(wǎng)頁(yè)時(shí),會(huì)向頁(yè)面所在的服務(wù)器發(fā)送一個(gè)HTTP請(qǐng)求,Web服務(wù)器確認(rèn)請(qǐng)求并向?yàn)g覽器返回響應(yīng)。但是像股價(jià),實(shí)時(shí)新聞等到達(dá)瀏覽器
顯示頁(yè)面時(shí)已經(jīng)過(guò)時(shí)了,如果用戶想要得到最新的實(shí)時(shí)信息,他們就要不斷的刷新頁(yè)面,這種顯然是不切實(shí)際的。

輪詢: 輪詢是通過(guò)瀏覽器定時(shí)的向web服務(wù)器發(fā)送http的Get請(qǐng)求,服務(wù)器收到請(qǐng)求后,就把最新的數(shù)據(jù)發(fā)回給客戶端,客戶端得到數(shù)據(jù)后,將其顯示出來(lái),然后再定期的重復(fù)這一過(guò)程,雖然可以滿足需求,但是存在一些缺點(diǎn),比如某一段時(shí)間內(nèi)web服務(wù)器沒(méi)有更新的數(shù)據(jù),但是瀏覽器仍然需要定時(shí)的發(fā)送Get請(qǐng)求過(guò)來(lái)詢問(wèn),那么即浪費(fèi)了帶寬,又浪費(fèi)了cpu的利用率。
如下圖:

長(zhǎng)輪詢: 客戶端向服務(wù)器請(qǐng)求信息,并在設(shè)定的時(shí)間段內(nèi)打開(kāi)一個(gè)連接,服務(wù)器如果沒(méi)有任何信息,會(huì)保持請(qǐng)求打開(kāi),直到有客戶端可用的信息,或者直到
指定的超時(shí)時(shí)間用完為止。這個(gè)時(shí)候,客戶端會(huì)重新向服務(wù)器請(qǐng)求信息。長(zhǎng)輪詢也可以叫做Comet或反向ajax。它會(huì)延長(zhǎng)HTTP響應(yīng)的完成,直到服務(wù)器有
需要發(fā)送給客戶端的內(nèi)容,這種技術(shù)通??梢越?"掛起GET" 或 "擱置POST".

缺點(diǎn)是:
當(dāng)信息量非常大的時(shí)候,長(zhǎng)輪詢相對(duì)于http輪詢并沒(méi)有明顯的性能優(yōu)勢(shì),因?yàn)榭蛻舳吮仨氼l繁地重連到服務(wù)器以讀取新信息。造成網(wǎng)絡(luò)的表現(xiàn)和常規(guī)輪詢相同。

如下圖:

流化: 在流化技術(shù)中,客戶端發(fā)送一個(gè)請(qǐng)求,服務(wù)器發(fā)送并維護(hù)一個(gè)持續(xù)更新和保持打開(kāi)的開(kāi)放響應(yīng)。每當(dāng)服務(wù)器有需要交付給客戶端信息時(shí),它就更新響應(yīng)
。但是服務(wù)器從不發(fā)出完成HTTP響應(yīng)的請(qǐng)求,從而使鏈接一直保持打開(kāi)。在這種情況下,代理和防火墻可能緩存響應(yīng),導(dǎo)致信息交付的延遲增加。
因此許多流化對(duì)于存在防火墻和代理的網(wǎng)絡(luò)是不友好的。

3. 了解WebSocket
WebSocket是一種全雙工,雙向,單套接字連接,使用websocket,http請(qǐng)求變成打開(kāi)websocket的鏈接的單一請(qǐng)求,并且重用從客戶端到服務(wù)器以及
服務(wù)器到客戶端的同一連接。

WebSocket減少了延遲,因?yàn)橐坏┙⑵餡ebsocket連接,服務(wù)器可以在消息可用時(shí)發(fā)送他們。和輪詢不同的是:WebSocket只發(fā)出一個(gè)請(qǐng)求,服務(wù)器
不需要等待來(lái)自客戶端的請(qǐng)求,且客戶端可以在任何時(shí)候向服務(wù)器端發(fā)送消息,和輪詢相比的話,不管是否有可用消息,每隔一段時(shí)間都發(fā)送一個(gè)請(qǐng)求,
單一請(qǐng)求大大減少了延遲。

優(yōu)點(diǎn)有如下:
1. WebSocket使實(shí)時(shí)通信更加有效。能節(jié)約帶寬,CPU資源并減少延遲。
2. WebSocket使Web上客戶端和服務(wù)器之間的通信變得更加的簡(jiǎn)單。
3. Websocket是一個(gè)底層網(wǎng)絡(luò)協(xié)議,我們可以在它的基礎(chǔ)之上構(gòu)建其他的標(biāo)準(zhǔn)協(xié)議。

WebSocket的應(yīng)用場(chǎng)景?
比如聊天,大型多人在線游戲,股票交易應(yīng)用或?qū)崟r(shí)新聞等。

(二) WebSocket協(xié)議

    WebSocket協(xié)議是為了解決web即時(shí)應(yīng)用中服務(wù)器與客戶端瀏覽器全雙工通信問(wèn)題而設(shè)計(jì)的。協(xié)議定義ws和wss協(xié)議,分別為普通請(qǐng)求和基于SSL的安全傳輸, ws端口是80,wss的端口為443.

WebSocket協(xié)議由兩部分組成,握手和數(shù)據(jù)傳輸。

2-1 握手
WS的握手使用HTTP來(lái)實(shí)現(xiàn)的。客戶端的握手消息是一個(gè)普通的,帶有Upgrade頭的,HTTP Request的消息。
先來(lái)看看如下代碼:

<!DOCTYPE html><html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>websocket</title>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
  </head>
  <body>
    <script type="text/javascript">
      var wsUrl = "wss://echo.websocket.org";      var ws = new WebSocket(wsUrl);
      ws.onopen = function() {
        console.log('open');
      };
      ws.onmessage = function(msg) {
        console.log(msg.data);
      }
      ws.onclose = function() {
        console.log('已經(jīng)被關(guān)閉了');
      }    </script>
  </body></html>

頁(yè)面運(yùn)行后,我們可以看到鏈接到 wss://echo.websocket.org 期間記錄的一個(gè)握手協(xié)議。先來(lái)看看客戶端發(fā)送http的請(qǐng)求頭:

GET /chat HTTP/1.1Host:echo.websocket.org
Upgrade:websocket
Connection:Upgrade
Sec-WebSocket-Key:ALS2AoBJtUup67heKDgzFg==Origin:file://Sec-WebSocket-Version:13

服務(wù)器響應(yīng)的頭字段

Connection:Upgrade
Sec-WebSocket-Accept:qyzx/EgbRK15QNmr5PhpMQrPZMM=Server: Kaazing Gateway
Upgrade:websocket

下面是請(qǐng)求和響應(yīng)頭字段的含義:
Upgrade: websocket, 告訴服務(wù)器這個(gè)HTTP鏈接是升級(jí)的WebSocket協(xié)議。
Connection:Upgrade 告知服務(wù)器當(dāng)前請(qǐng)求鏈接是升級(jí)的。
Origin: 該字段是用來(lái)防止客戶端瀏覽器使用腳本進(jìn)行未授權(quán)的跨源攻擊,服務(wù)器要根據(jù)這個(gè)字段是否接受客戶端的socket鏈接。
可以返回一個(gè)HTTP錯(cuò)誤狀態(tài)碼來(lái)拒絕連接。

Sec-WebSocket-Key: ALS2AoBJtUup67heKDgzFg==
Sec-WebSocket-Accept: qyzx/EgbRK15QNmr5PhpMQrPZMM=

Sec-WebSocket-Key 的值是一串長(zhǎng)度為24的字符串是客戶端隨機(jī)生成的base64編碼的字符串,它發(fā)送給服務(wù)器,服務(wù)器需要使用它經(jīng)過(guò)一定的運(yùn)算規(guī)則生成服務(wù)器的key,然后把服務(wù)器的key發(fā)到客戶端去,客戶端驗(yàn)證正確后,握手成功。

握手的具體原理:當(dāng)我們客戶端執(zhí)行 new WebSocket(''wss://echo.websocket.org')的時(shí)候,客戶端就會(huì)發(fā)起請(qǐng)求報(bào)文進(jìn)行握手申請(qǐng),報(bào)文中有一個(gè)key就是
Sec-WebSocket-Key,服務(wù)器獲取到key,會(huì)將這個(gè)key與字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11相連,對(duì)新的字符串通過(guò)sha1安全散列算法計(jì)算出結(jié)果后,再進(jìn)行Base64編碼,并且將結(jié)果放在請(qǐng)求頭的"Sec-WebSocket-Accept",最后返回給客戶端,
客戶端進(jìn)行驗(yàn)證后,握手成功。握手成功后就可以開(kāi)始數(shù)據(jù)傳輸了。

下面是實(shí)現(xiàn)一個(gè)簡(jiǎn)單的握手協(xié)議的demo,代碼如下:

### 目錄結(jié)構(gòu)如下:

demo
  |--- hands.html
  |--- hands.js

hands.html 代碼如下:

<html><head>
  <title>WebSocket Demo</title></head><body>
  <script type="text/javascript">
    var ws = new WebSocket("ws://127.0.0.1:8000");
    ws.onerror = function(e) {
      console.log(e);
    };
    ws.onopen = function() {
      console.log('握手成功');
    }  </script></body></html>

hands.js 代碼如下:

var crypto = require('crypto');var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

require('net').createServer(function(o) {  var key;
  o.on('data', function(e) {    if (!key) {
      console.log(e);

      key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
      console.log(key);      
      // WS的字符串 加上 key, 變成新的字符串后做一次sha1運(yùn)算,最后轉(zhuǎn)換成Base64
      key = crypto.createHash('sha1').update(key+WS).digest('base64');
      console.log(key);      // 輸出字段數(shù)據(jù),返回到客戶端,
      o.write('HTTP/1.1 101 Switching Protocol\r\n');
      o.write('Upgrade: websocket\r\n');
      o.write('Connection: Upgrade\r\n');
      o.write('Sec-WebSocket-Accept:' +key+'\r\n');      // 輸出空行,使HTTP頭結(jié)束
      o.write('\r\n');
    } else {      // 數(shù)據(jù)處理    }
  })
}).listen(8000);

首先在命令行中 進(jìn)入相對(duì)應(yīng)項(xiàng)目目錄后,運(yùn)行 node hands.js, 然后打開(kāi) hands.html 運(yùn)行一下即可看到 命令行中打印出來(lái)如下信息:

$ node hands.js<Buffer 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 31 32 37 2e 30 2e 30 2e 31 3a 38 30 30 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 ... >
+iHlfGTolBaWYpnyTIw22g==W7IEsdQtwv8EP2204kssK/6pg+c=

然后在瀏覽器中查看請(qǐng)求頭如下信息:

Request Headers:

Connection:Upgrade
Host:127.0.0.1:8000Origin:file://Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits
Sec-WebSocket-Key:+iHlfGTolBaWYpnyTIw22g==Sec-WebSocket-Version:13Upgrade:websocket

響應(yīng)頭如下信息:

Response Headers:

Connection:Upgrade
Sec-WebSocket-Accept:W7IEsdQtwv8EP2204kssK/6pg+c=
Upgrade:websocket

如上信息可以看到,獲取報(bào)文中的key代碼:
key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
console.log(key); // 打印 +iHlfGTolBaWYpnyTIw22g==

和 Request Headers:中的 Sec-WebSocket-Key 值是一樣的,該值是瀏覽器自動(dòng)生成的,然后獲取該值后,與 '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',相連,對(duì)新的字符串通過(guò)sha1安全散列算法計(jì)算出結(jié)果后,再進(jìn)行Base64編碼,
并且將結(jié)果放在請(qǐng)求頭的"Sec-WebSocket-Accept",最后返回給客戶端,客戶端進(jìn)行驗(yàn)證后,握手成功。在瀏覽器中可以看到打印出 握手成功了。

(三) 解析數(shù)據(jù)幀

1-1 理解數(shù)據(jù)幀的含義:
   在WebSocket協(xié)議中,數(shù)據(jù)是通過(guò)幀序列來(lái)傳輸?shù)?。為了?shù)據(jù)安全原因,客戶端必須掩碼(mask)它發(fā)送到服務(wù)器的所有幀,當(dāng)它收到一個(gè)
沒(méi)有掩碼的幀時(shí),服務(wù)器必須關(guān)閉連接。不過(guò)服務(wù)器端給客戶端發(fā)送的所有幀都不是掩碼的,如果客戶端檢測(cè)到掩碼的幀時(shí),也一樣必須關(guān)閉連接。
當(dāng)幀被關(guān)閉的時(shí)候,可能發(fā)送狀態(tài)碼1002(協(xié)議錯(cuò)誤)。

基本幀協(xié)議如下:

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

如上是基本幀協(xié)議,它帶有操作碼(opcode)的幀類型,負(fù)載長(zhǎng)度,和用于 "擴(kuò)展數(shù)據(jù)" 與 "應(yīng)用數(shù)據(jù)" 及 它們一起定義的 "負(fù)載數(shù)據(jù)"的指定位置,
某些字節(jié)和操作碼保留用于未來(lái)協(xié)議的擴(kuò)展。

FIN(1位): 是否為消息的最后一個(gè)數(shù)據(jù)幀。
RSV1,RSV2,Rsv3(每個(gè)占1位),必須是0,除非一個(gè)擴(kuò)展協(xié)商為非零值定義的。
Opcode表示幀的類型(4位),例如這個(gè)傳輸?shù)膸俏谋绢愋瓦€是二進(jìn)制類型,二進(jìn)制類型傳輸?shù)臄?shù)據(jù)可以是圖片或者語(yǔ)音之類的。(這4位轉(zhuǎn)換成16進(jìn)制值表示的意思如下):

0x0 表示附加數(shù)據(jù)幀0x1 表示文本數(shù)據(jù)幀0x2 表示二進(jìn)制數(shù)據(jù)幀0x3-7 暫時(shí)無(wú)定義,為以后的非控制幀保留0x8 表示連接關(guān)閉0x9 表示ping0xA 表示pong0xB-F 暫時(shí)無(wú)定義,為以后的控制幀保留

Mask(占1位): 表示是否經(jīng)過(guò)掩碼處理, 1 是經(jīng)過(guò)掩碼的,0是沒(méi)有經(jīng)過(guò)掩碼的。

payload length (7位+16位,或者 7位+64位),定義負(fù)載數(shù)據(jù)的長(zhǎng)度。
   1. 如果數(shù)據(jù)長(zhǎng)度小于等于125的話,那么該7位用來(lái)表示實(shí)際數(shù)據(jù)長(zhǎng)度。
   2. 如果數(shù)據(jù)長(zhǎng)度為126到65535(2的16次方)之間,該7位值固定為126,也就是 1111110,往后擴(kuò)展2個(gè)字節(jié)(16為,第三個(gè)區(qū)塊表示),用于存儲(chǔ)數(shù)據(jù)的實(shí)際長(zhǎng)度。
   3. 如果數(shù)據(jù)長(zhǎng)度大于65535, 該7位的值固定為127,也就是 1111111 ,往后擴(kuò)展8個(gè)字節(jié)(64位),用于存儲(chǔ)數(shù)據(jù)實(shí)際長(zhǎng)度。

Masking-key(0或者4個(gè)字節(jié)),該區(qū)塊用于存儲(chǔ)掩碼密鑰,只有在第二個(gè)子節(jié)中的mask為1,也就是消息進(jìn)行了掩碼處理時(shí)才有,否則沒(méi)有,
所以服務(wù)器端向客戶端發(fā)送消息就沒(méi)有這一塊。

Payload data 擴(kuò)展數(shù)據(jù),是0字節(jié),除非已經(jīng)協(xié)商了一個(gè)擴(kuò)展。

1-2 客戶端到服務(wù)器掩碼
WebSocket協(xié)議要求客戶端所發(fā)送的幀必須掩碼,掩碼的密鑰是一個(gè)32位的隨機(jī)值。所有數(shù)據(jù)都需要與掩碼做一次異或運(yùn)算。幀頭在第二個(gè)字節(jié)的第一位表示該幀是否使用了掩碼。
WebSocket服務(wù)器接收的每個(gè)載荷在處理之前首先需要處理掩碼,解除掩碼之后,服務(wù)器將得到原始消息內(nèi)容。二進(jìn)制消息可以直接交付。文本消息將進(jìn)行UTF-8解碼
并輸出到字符串中。

二進(jìn)制位運(yùn)算符知識(shí)擴(kuò)展:

>> 含義是右移運(yùn)算符,
   右移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向右移動(dòng),移出位被丟棄,左邊移出的空位一律補(bǔ)0.
比如 11 >> 2, 意思是說(shuō)將數(shù)字11右移2位。
首先將11轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2個(gè)數(shù)字移出,因?yàn)樵摂?shù)字是正數(shù),
所以在高位補(bǔ)零,則得到的最終結(jié)果為:0000 0000 0000 0000 0000 0000 0000 0010,轉(zhuǎn)換為10進(jìn)制是2.

<< 含義是左移運(yùn)算符
    左移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向左移位,移出位被丟棄,右邊的空位一律補(bǔ)0.
比如 3 << 2, 意思是說(shuō)將數(shù)字3左移2位,
首先將3轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把該數(shù)字高位(左側(cè))的兩個(gè)零移出,其他的數(shù)字都朝左平移2位,
最后在右側(cè)的兩個(gè)空位補(bǔ)0,因此最后的結(jié)果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉(zhuǎn)換為十進(jìn)制是12(1100 = 1*2的3次方 + 1*2的2字方)

注意1: 在使用補(bǔ)碼作為機(jī)器數(shù)的機(jī)器中,正數(shù)的符號(hào)位為0,負(fù)數(shù)的符號(hào)位為1(一般情況下).
           比如:十進(jìn)制數(shù)13在計(jì)算機(jī)中表示為00001101,其中第一位0表示的是符號(hào)

注意2:負(fù)數(shù)的二進(jìn)制位如何計(jì)算?
          比如二進(jìn)制的原碼為 10010101,它的補(bǔ)碼怎么計(jì)算呢?
          首先計(jì)算它的反碼是 01101010; 那么補(bǔ)碼 = 反碼 + 1 = 01101011

再來(lái)看一個(gè)列子:
-7 >> 2 意思是將數(shù)字 -7 右移2位。
負(fù)數(shù)先用它的絕對(duì)值正數(shù)取它的二進(jìn)制代碼,7的二進(jìn)制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二進(jìn)制位就是 取反,
取反后再加1,就變成補(bǔ)碼。
因此-7的二進(jìn)制位: 1111 1111 1111 1111 1111 1111 1111 1001,
因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉(zhuǎn)換成十進(jìn)制的話 -7 >> 2 ,值就變成 -2了。

數(shù)據(jù)幀解析的程序如下代碼:(decodeDataFrame.js 代碼如下:)

var crypto = require('crypto');var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

require('net').createServer(function(o) {  var key;
  o.on('data', function(e) {    if (!key) {

      key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];      
      // WS的字符串 加上 key, 變成新的字符串后做一次sha1運(yùn)算,最后轉(zhuǎn)換成Base64
      key = crypto.createHash('sha1').update(key+WS).digest('base64');      // 輸出字段數(shù)據(jù),返回到客戶端,
      o.write('HTTP/1.1 101 Switching Protocol\r\n');
      o.write('Upgrade: websocket\r\n');
      o.write('Connection: Upgrade\r\n');
      o.write('Sec-WebSocket-Accept:' +key+'\r\n');      // 輸出空行,使HTTP頭結(jié)束
      o.write('\r\n');
    } else {      // 數(shù)據(jù)處理      onmessage(e);
    }
  })
}).listen(8000);/* 
 >> 含義是右移運(yùn)算符,
   右移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向右移動(dòng),移出位被丟棄,左邊移出的空位一律補(bǔ)0.
 比如 11 >> 2, 意思是說(shuō)將數(shù)字11右移2位。
 首先將11轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2個(gè)數(shù)字移出,因?yàn)樵摂?shù)字是正數(shù),
 所以在高位補(bǔ)零,則得到的最終結(jié)果為:0000 0000 0000 0000 0000 0000 0000 0010,轉(zhuǎn)換為10進(jìn)制是2.
  

 << 含義是左移運(yùn)算符
   左移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向左移位,移出位被丟棄,右邊的空位一律補(bǔ)0.
 比如 3 << 2, 意思是說(shuō)將數(shù)字3左移2位,
 首先將3轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把該數(shù)字高位(左側(cè))的兩個(gè)零移出,其他的數(shù)字都朝左平移2位,
 最后在右側(cè)的兩個(gè)空位補(bǔ)0,因此最后的結(jié)果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉(zhuǎn)換為十進(jìn)制是12(1100 = 1*2的3次方 + 1*2的2字方)

 注意1: 在使用補(bǔ)碼作為機(jī)器數(shù)的機(jī)器中,正數(shù)的符號(hào)位為0,負(fù)數(shù)的符號(hào)位為1(一般情況下). 
       比如:十進(jìn)制數(shù)13在計(jì)算機(jī)中表示為00001101,其中第一位0表示的是符號(hào)

 注意2:負(fù)數(shù)的二進(jìn)制位如何計(jì)算?
       比如二進(jìn)制的原碼為 10010101,它的補(bǔ)碼怎么計(jì)算呢?
       首先計(jì)算它的反碼是 01101010; 那么補(bǔ)碼 = 反碼 + 1 = 01101011

 再來(lái)看一個(gè)列子:
 -7 >> 2 意思是將數(shù)字 -7 右移2位。
 負(fù)數(shù)先用它的絕對(duì)值正數(shù)取它的二進(jìn)制代碼,7的二進(jìn)制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二進(jìn)制位就是 取反,
 取反后再加1,就變成補(bǔ)碼。
 因此-7的二進(jìn)制位: 1111 1111 1111 1111 1111 1111 1111 1001,
 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉(zhuǎn)換成十進(jìn)制的話 -7 >> 2 ,值就變成 -2了。*/function decodeDataFrame(e) {  var i = 0, j, s, arrs = [],
    frame = {      // 解析前兩個(gè)字節(jié)的基本數(shù)據(jù)
      FIN: e[i] >> 7,
      Opcode: e[i++] & 15,
      Mask: e[i] >> 7,
      PayloadLength: e[i++] & 0x7F
    };    // 處理特殊長(zhǎng)度126和127
    if (frame.PayloadLength === 126) {
      frame.PayloadLength = (e[i++] << 8) + e[i++];
    }    if (frame.PayloadLength === 127) {
      i += 4; // 長(zhǎng)度一般用4個(gè)字節(jié)的整型,前四個(gè)字節(jié)一般為長(zhǎng)整型留空的。
      frame.PayloadLength = (e[i++] << 24)+(e[i++] << 16)+(e[i++] << 8) + e[i++];
    }    // 判斷是否使用掩碼
    if (frame.Mask) {      // 獲取掩碼實(shí)體
      frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]];      // 對(duì)數(shù)據(jù)和掩碼做異或運(yùn)算
      for(j = 0, arrs = []; j < frame.PayloadLength; j++) {
        arrs.push(e[i+j] ^ frame.MaskingKey[j%4]);
      }
    } else {      // 否則的話 直接使用數(shù)據(jù)
      arrs = e.slice(i, i + frame.PayloadLength);
    }    // 數(shù)組轉(zhuǎn)換成緩沖區(qū)來(lái)使用
    arrs = new Buffer(arrs);    // 如果有必要?jiǎng)t把緩沖區(qū)轉(zhuǎn)換成字符串來(lái)使用
    if (frame.Opcode === 1) {
      arrs = arrs.toString();
    }    // 設(shè)置上數(shù)據(jù)部分
    frame.PayloadLength = arrs;    // 返回?cái)?shù)據(jù)幀
    return frame;
}function onmessage(e) {
  console.log(e)
  e = decodeDataFrame(e);  // 解析數(shù)據(jù)幀
  console.log(e);  // 把數(shù)據(jù)幀輸出到控制臺(tái)}

index.html代碼如下:

<html><head>
  <title>WebSocket Demo</title></head><body>
  <script type="text/javascript">
    var ws = new WebSocket("ws://127.0.0.1:8000");
    ws.onerror = function(e) {
      console.log(e);
    };
    ws.onopen = function(e) {
      console.log('握手成功');
      ws.send('次碳酸鈷');
    }  </script></body></html>

查看github上的源碼

demo還是一樣,decodeDataFrame.js 和 index.html, 先進(jìn)入項(xiàng)目中對(duì)應(yīng)的目錄后,使用node decodeDataFrame.js,  然后打開(kāi)index.html后查看效果

如下:

這樣服務(wù)器接收客戶端穿過(guò)了的數(shù)據(jù)就沒(méi)問(wèn)題了。

(四) 生成數(shù)據(jù)幀

    從服務(wù)器發(fā)往客戶端的數(shù)據(jù)也是同樣的數(shù)據(jù)幀,但是從服務(wù)器發(fā)送到客戶端的數(shù)據(jù)幀不需要掩碼的。我們自己需要去生成數(shù)據(jù)幀,解析數(shù)據(jù)幀的時(shí)候我們需要分片。

消息分片:
   有時(shí)候數(shù)據(jù)需要分成多個(gè)數(shù)據(jù)包發(fā)送,需要使用到分片,也就是說(shuō)多個(gè)數(shù)據(jù)幀來(lái)傳輸一個(gè)數(shù)據(jù)。比如將大數(shù)據(jù)分成多個(gè)數(shù)據(jù)包傳輸,分片的目的是允許發(fā)送未知長(zhǎng)度的消息。
這樣做的好處是:
  1. 大數(shù)據(jù)的傳輸可以分片傳輸,不用考慮到數(shù)據(jù)大小導(dǎo)致的長(zhǎng)度標(biāo)志位不夠的情況。
  2. 和http的chunk一樣,可以邊生成數(shù)據(jù)邊傳遞消息,可以提高傳輸效率。

如果大數(shù)據(jù)不能被碎片化,那么一端就必須將消息整個(gè)載入內(nèi)存緩沖之中,然后需要計(jì)算長(zhǎng)度等操作并發(fā)送,但是有了碎片化機(jī)制,服務(wù)器端或者中間件就可以選取適用的內(nèi)存緩沖長(zhǎng)度,然后當(dāng)緩沖滿了之后就發(fā)送一個(gè)消息碎片。

分片規(guī)則:
1. 如果一個(gè)消息不分片的話,那么該消息只有一幀(FIN為1,opcode非0);
2. 如果一個(gè)消息分片的話,它的構(gòu)成是由起始幀(FIN為0,opcode非0),然后若干(0個(gè)或多個(gè))幀(FIN為0,opcode為0),然后結(jié)束幀(FIN為1,opcode為0)。

注意:
   1. 當(dāng)前已經(jīng)定義了控制幀包括 0x8(close), 0x9(Ping), 0xA(Pong). 控制幀可以出現(xiàn)在分片消息中間,但是控制幀不允許分片,控制幀是通過(guò)它的opcode
的最高有效位是1去確定的。
   2. 組成消息的所有幀都是相同的數(shù)據(jù)類型,在第一幀中的opcode中指明。組成消息的碎片類型必須是文本,二進(jìn)制,或者其他的保留類型。

下面我們來(lái)理解下上面分片規(guī)則2中的話的含義:
  1. 開(kāi)始幀(1個(gè))---消息分片起始幀的構(gòu)成是 (FIN為0,opcode非0);即:FIN=0, Opcode > 0;
  2. 傳輸幀(0個(gè)或多個(gè))---是由若干個(gè)(0個(gè)或多個(gè))幀組成; 即 FIN = 0, Opcode = 0;
  3. 終止幀(1個(gè))--- FIN = 1, Opcode = 0;

還是看基本幀協(xié)議如下:

1                   2                   3
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 ...                |
 +---------------------------------------------------------------+

demo解析:
比如我們現(xiàn)在第三節(jié)我們講到的 "解析數(shù)據(jù)幀" 里面的代碼,我們發(fā)送的消息123456789后,返回的數(shù)據(jù)部分是:

<Buffer 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89>{ FIN: 1,
  Opcode: 1,
  Mask: 1,
  PayloadLength: '123456789',
  MaskingKey: [ 176, 35, 82, 90 ] 
}

上面返回的數(shù)據(jù)部分是16進(jìn)制,因此我們需要他們轉(zhuǎn)換成二進(jìn)制,有關(guān)16進(jìn)制,10進(jìn)制,2進(jìn)制的轉(zhuǎn)換表如下:
16進(jìn)制-->10進(jìn)制-->2進(jìn)制轉(zhuǎn)換查看

我們現(xiàn)在需要把 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89 這些16進(jìn)制先轉(zhuǎn)換成10進(jìn)制,然后轉(zhuǎn)換成二進(jìn)制,分析代碼如下:
16進(jìn)制(a=10, b=11, ... 依次類推)

16進(jìn)制          10進(jìn)制                           2進(jìn)制81          8*16的1次方 + 1*16的0次方 = 129      10000001
89          8*16的1次方 + 9*16的0次方 = 137      10001001b0          11*16的1次方 + 0*16的0次方 = 176     10110000 
23          2*16的1次方 + 3*16的0次方 = 35       00100011
52          5*16的1次方 + 2*16的0次方 = 82       010100105a          5*16的1次方 + 10*16的0次方 = 90      01011010
81          8*16的1次方 + 1*16的0次方 = 129      10000001
11          1*16的1次方 + 1*16的0次方 = 17       00010001
61          6*16的1次方 + 1*16的0次方 = 97       001111016e          6*16的1次方 + 14*16的0次方 = 110     01101110
85          8*16的1次方 + 5*16的0次方 = 133      10000101
15          1*16的1次方 + 5*16的0次方 = 21       00010101
65          6*16的1次方 + 5*16的0次方 = 101      01100101
62          6*16的1次方 + 2*16的0次方 = 98       01100010
89          8*16的1次方 + 9*16的0次方 = 137      10001001

我們把上面的轉(zhuǎn)換后的二進(jìn)制 對(duì)照上面的 基本幀協(xié)議表看下:
1. 先看 FIN 的含義是: 第一位是否為消息的最后一個(gè)數(shù)據(jù)幀,如果為1的話,說(shuō)明是,否則為0的話就不是,那說(shuō)明是最后一個(gè)數(shù)據(jù)幀。
2. 第2~4位都為0,對(duì)應(yīng)的RSV(1~3), 5~8為 0001,是屬于opcode的部分了,opcode是代表是幀的類型;它有如下類型:

   0x0 表示附加數(shù)據(jù)幀
   0x1 表示文本數(shù)據(jù)幀
   0x2 表示二進(jìn)制數(shù)據(jù)幀
   0x3-7 暫時(shí)無(wú)定義,為以后的非控制幀保留
   0x8 表示連接關(guān)閉
   0x9 表示ping
   0xA 表示pong
   0xB-F 暫時(shí)無(wú)定義,為以后的控制幀保留

注意:其中8進(jìn)制是以0開(kāi)頭的,16進(jìn)制是以0x開(kāi)頭的。

0001,是文本數(shù)據(jù)幀了。

3.  第九位是1,那么對(duì)應(yīng)的幀協(xié)議表就是MASK部分了,Mask(占1位): 表示是否經(jīng)過(guò)掩碼處理, 1 是經(jīng)過(guò)掩碼的,0是沒(méi)有經(jīng)過(guò)掩碼的。說(shuō)明是經(jīng)過(guò)掩碼處理的,
也就是說(shuō)可以理解為是客戶端向服務(wù)器端發(fā)送數(shù)據(jù)的。(因?yàn)榉?wù)器端給客戶端是不需要掩碼的,否則連接中斷)。

4. 第10~16位是 0001001 = 9 < 125, 對(duì)應(yīng)幀協(xié)議中的 payload length的部分了,數(shù)據(jù)長(zhǎng)度為9,因此小于125位,因此使用7位來(lái)表示實(shí)際數(shù)據(jù)長(zhǎng)度。

5. b0, 23, 52, 5a 對(duì)應(yīng)的部分是 屬于Masking-key(0或者4個(gè)字節(jié)),該區(qū)塊用于存儲(chǔ)掩碼密鑰,只有在第二個(gè)子節(jié)中的mask為1,也就是消息進(jìn)行了掩碼處理時(shí)才有。

6. 81 11 61 6e 85 15 65 62 89 這些就是對(duì)應(yīng)表中的數(shù)據(jù)部分了。

下面我們?cè)賮?lái)理解下 消息 123456789 怎么通過(guò)掩碼加密成 81 11 61 6e 85 15 65 62 89 這些數(shù)據(jù)了。

數(shù)字字符1的ASCLL碼的16進(jìn)制為31,轉(zhuǎn)換成10進(jìn)制就是49了。其他的數(shù)字依次類推+1;

數(shù)字           10進(jìn)制          二進(jìn)制1             49              00110001
2             50              00110010
3             51              00110011
4             52              00110100
5             53              00110101
6             54              00110110
7             55              00110111
8             56              00111000
9             57              00111001

6-1: 其中字符1的二進(jìn)制位 00110001,掩碼b0的二進(jìn)制位 10110000, 因此:

00110001
10110000

進(jìn)行交配的話,二進(jìn)制就變成:10000001,轉(zhuǎn)換成10進(jìn)制為 129了,那么轉(zhuǎn)換成16進(jìn)制就是 81了。

6-2:字符2的二進(jìn)制位 00110010,掩碼23的二進(jìn)制位 00100011,因此:

00110010
00100011

進(jìn)行交配的話,二進(jìn)制就變成 00010001,轉(zhuǎn)換10進(jìn)制為17,那么轉(zhuǎn)換成16進(jìn)制就是 11了。

6-3: 字符3的二進(jìn)制位 00110011,掩碼52的二進(jìn)制位 01010010,因此:

00110011
01010010

進(jìn)行交配的話,二進(jìn)制就變成:01100001,轉(zhuǎn)換成10進(jìn)制為 97,那么轉(zhuǎn)換成16進(jìn)制就是 61了。

6-4: 字符4的二進(jìn)制位 00110100,掩碼 5a 的二進(jìn)制位 01011010,因此:

00110100
01011010

進(jìn)行交配的話,二進(jìn)制就變成 01101110,轉(zhuǎn)換成10進(jìn)制為 110,那么轉(zhuǎn)換成16進(jìn)制為 6e.

6-5: 字符5的二進(jìn)制位 00110101,掩碼b0的二進(jìn)制位 10110000, 因此:

00110101
10110000

進(jìn)行交配的話,二進(jìn)制就變成:10000101,轉(zhuǎn)換成10進(jìn)制為 133,那么轉(zhuǎn)換成16進(jìn)制就是 85了。

6-6: 字符6的二進(jìn)制位 00110110,掩碼23的二進(jìn)制位 00100011,因此:

00110110
00100011

進(jìn)行交配的話,二進(jìn)制就變成:00010101,轉(zhuǎn)換成10進(jìn)制為 21,那么轉(zhuǎn)換成16進(jìn)制就是 15了。

6-7: 字符7的二進(jìn)制位 00110111,掩碼52的二進(jìn)制位 01010010,因此:

00110111
01010010

進(jìn)行交配的話,二進(jìn)制就變成:01100101,轉(zhuǎn)換成10進(jìn)制為 101,那么轉(zhuǎn)換成16進(jìn)制就是 65了。

6-8: 字符8的二進(jìn)制位 00111000,掩碼 5a 的二進(jìn)制位 01011010,因此:

00111000
01011010

進(jìn)行交配的話,二進(jìn)制就變成:01100010,轉(zhuǎn)換成10進(jìn)制為 98,那么轉(zhuǎn)換成16進(jìn)制就是 62了。

6-9: 字符9的二進(jìn)制位 00111001,掩碼b0的二進(jìn)制位 10110000, 因此:

00111001
10110000

進(jìn)行交配的話,二進(jìn)制就變成:10001001,轉(zhuǎn)換成10進(jìn)制為 137,那么轉(zhuǎn)換成16進(jìn)制就是 89了。

字符123456789與掩碼加密的整個(gè)過(guò)程如上面分析,可以看到,字符分別依次與掩碼交配,如果掩碼不夠的話,依次從頭循環(huán)即可。

因此我們可以編寫如下encodeDataFrame.js代碼:

var crypto = require('crypto');var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

require('net').createServer(function(o) {  var key;
  o.on('data', function(e) {    if (!key) {

      key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];      
      // WS的字符串 加上 key, 變成新的字符串后做一次sha1運(yùn)算,最后轉(zhuǎn)換成Base64
      key = crypto.createHash('sha1').update(key+WS).digest('base64');      // 輸出字段數(shù)據(jù),返回到客戶端,
      o.write('HTTP/1.1 101 Switching Protocol\r\n');
      o.write('Upgrade: websocket\r\n');
      o.write('Connection: Upgrade\r\n');
      o.write('Sec-WebSocket-Accept:' +key+'\r\n');      // 輸出空行,使HTTP頭結(jié)束
      o.write('\r\n');      // 握手成功后給客戶端發(fā)送數(shù)據(jù)      o.write(encodeDataFrame({
        FIN: 1,
        Opcode: 1,
        PayloadData: "123456789"
      }))
    } else {
      
    }
  })
}).listen(8001);/* 
 >> 含義是右移運(yùn)算符,
   右移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向右移動(dòng),移出位被丟棄,左邊移出的空位一律補(bǔ)0.
 比如 11 >> 2, 意思是說(shuō)將數(shù)字11右移2位。
 首先將11轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2個(gè)數(shù)字移出,因?yàn)樵摂?shù)字是正數(shù),
 所以在高位補(bǔ)零,則得到的最終結(jié)果為:0000 0000 0000 0000 0000 0000 0000 0010,轉(zhuǎn)換為10進(jìn)制是2.

 << 含義是左移運(yùn)算符
   左移運(yùn)算符是將一個(gè)二進(jìn)制位的操作數(shù)按指定移動(dòng)的位數(shù)向左移位,移出位被丟棄,右邊的空位一律補(bǔ)0.
 比如 3 << 2, 意思是說(shuō)將數(shù)字3左移2位,
 首先將3轉(zhuǎn)換為二進(jìn)制數(shù)為 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把該數(shù)字高位(左側(cè))的兩個(gè)零移出,其他的數(shù)字都朝左平移2位,
 最后在右側(cè)的兩個(gè)空位補(bǔ)0,因此最后的結(jié)果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉(zhuǎn)換為十進(jìn)制是12(1100 = 1*2的3次方 + 1*2的2字方)

 注意1: 在使用補(bǔ)碼作為機(jī)器數(shù)的機(jī)器中,正數(shù)的符號(hào)位為0,負(fù)數(shù)的符號(hào)位為1(一般情況下). 
       比如:十進(jìn)制數(shù)13在計(jì)算機(jī)中表示為00001101,其中第一位0表示的是符號(hào)

 注意2:負(fù)數(shù)的二進(jìn)制位如何計(jì)算?
       比如二進(jìn)制的原碼為 10010101,它的補(bǔ)碼怎么計(jì)算呢?
       首先計(jì)算它的反碼是 01101010; 那么補(bǔ)碼 = 反碼 + 1 = 01101011

 再來(lái)看一個(gè)列子:
 -7 >> 2 意思是將數(shù)字 -7 右移2位。
 負(fù)數(shù)先用它的絕對(duì)值正數(shù)取它的二進(jìn)制代碼,7的二進(jìn)制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二進(jìn)制位就是 取反,
 取反后再加1,就變成補(bǔ)碼。
 因此-7的二進(jìn)制位: 1111 1111 1111 1111 1111 1111 1111 1001,
 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉(zhuǎn)換成十進(jìn)制的話 -7 >> 2 ,值就變成 -2了。*/function decodeDataFrame(e) {  var i = 0, j, s, arrs = [],
    frame = {      // 解析前兩個(gè)字節(jié)的基本數(shù)據(jù)
      FIN: e[i] >> 7,
      Opcode: e[i++] & 15,
      Mask: e[i] >> 7,
      PayloadLength: e[i++] & 0x7F
    };    // 處理特殊長(zhǎng)度126和127
    if (frame.PayloadLength === 126) {
      frame.PayloadLength = (e[i++] << 8) + e[i++];
    }    if (frame.PayloadLength === 127) {
      i += 4; // 長(zhǎng)度一般用4個(gè)字節(jié)的整型,前四個(gè)字節(jié)一般為長(zhǎng)整型留空的。
      frame.PayloadLength = (e[i++] << 24)+(e[i++] << 16)+(e[i++] << 8) + e[i++];
    }    // 判斷是否使用掩碼
    if (frame.Mask) {      // 獲取掩碼實(shí)體
      frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]];      // 對(duì)數(shù)據(jù)和掩碼做異或運(yùn)算
      for(j = 0, arrs = []; j < frame.PayloadLength; j++) {
        arrs.push(e[i+j] ^ frame.MaskingKey[j%4]);
      }
    } else {      // 否則的話 直接使用數(shù)據(jù)
      arrs = e.slice(i, i + frame.PayloadLength);
    }    // 數(shù)組轉(zhuǎn)換成緩沖區(qū)來(lái)使用
    arrs = new Buffer(arrs);    // 如果有必要?jiǎng)t把緩沖區(qū)轉(zhuǎn)換成字符串來(lái)使用
    if (frame.Opcode === 1) {
      arrs = arrs.toString();
    }    // 設(shè)置上數(shù)據(jù)部分
    frame.PayloadLength = arrs;    // 返回?cái)?shù)據(jù)幀
    return frame;
}function encodeDataFrame(e) {  var arrs = [],
    o = new Buffer(e.PayloadData),
    l = o.length;  // 處理第一個(gè)字節(jié)
  arrs.push((e.FIN << 7)+e.Opcode);  // 處理第二個(gè)字節(jié),判斷它的長(zhǎng)度并放入相應(yīng)的后溪長(zhǎng)度
  if (l < 126) {
    arrs.push(l);
  } else if(l < 0x0000) {
    arrs.push(126, (1&0xFF00) >> 8, 1&0xFF);
  } else {
    arrs.push(127, 0, 0, 0, 0, 
      (l&0xFF000000)>>24,(l&0xFF0000)>>16,(l&0xFF00)>>8,l&0xFF 
    );
  }  // 返回頭部分和數(shù)據(jù)部分的合并緩沖區(qū)
  return Buffer.concat([new Buffer(arrs), o]);
}

然后index.html代碼如下:

<html><head>
  <title>WebSocket Demo</title></head><body>
  <script type="text/javascript">
    var ws = new WebSocket("ws://127.0.0.1:8001");
    ws.onerror = function(e) {
      console.log(e);
    };
    ws.onopen = function(e) {
      console.log('握手成功');
      ws.send('123456789');
    }
    ws.onmessage = function(e) {
      console.log(e);
    }  </script></body></html>

進(jìn)入目錄后,運(yùn)行node encodeDataFrame.js后,打開(kāi)index.html頁(yè)面,在控制臺(tái)看待效果圖如下:

查看git上代碼

使用分片的方式重新修改代碼:

上面是基本的使用方法,但是有時(shí)候我們需要將一個(gè)大的數(shù)據(jù)包需要分成多個(gè)數(shù)據(jù)幀來(lái)傳輸,因此分片它分為3個(gè)部分:

1個(gè)開(kāi)始幀:FIN=0, Opcode > 0;
零個(gè)或多個(gè)傳輸幀: FIN=0, Opcode=0;
1個(gè)終止幀:FIN=1, Opcode=0;

因此之前的握手成功后發(fā)送的數(shù)據(jù)代碼:

o.write(encodeDataFrame({
  FIN: 1,
  Opcode: 1,
  PayloadData: "123456789"}))

需要分成三部分來(lái)發(fā)送了;

改成如下代碼:

// 握手成功后給客戶端發(fā)送數(shù)據(jù)o.write(encodeDataFrame({
  FIN: 0,
  Opcode: 1,
  PayloadData: "123"}));
o.write(encodeDataFrame({
  FIN: 0,
  Opcode: 0,
  PayloadData: "456"}));
o.write(encodeDataFrame({
  FIN: 1,
  Opcode: 0,
  PayloadData: "789"}));

(五) 心跳及重連機(jī)制

    在使用websocket的過(guò)程中,有時(shí)候會(huì)遇到網(wǎng)絡(luò)斷開(kāi)的情況,但是在網(wǎng)絡(luò)斷開(kāi)的時(shí)候服務(wù)器端并沒(méi)有觸發(fā)onclose的事件。這樣會(huì)有:服務(wù)器會(huì)繼續(xù)向客戶端發(fā)送多余的鏈接,并且這些數(shù)據(jù)還會(huì)丟失。所以就需要一種機(jī)制來(lái)檢測(cè)客戶端和服務(wù)端是否處于正常的鏈接狀態(tài)。因此就有了websocket的心跳了。還有心跳,說(shuō)明還活著,沒(méi)有心跳說(shuō)明已經(jīng)掛掉了。

1. 為什么叫心跳包呢?
它就像心跳一樣每隔固定的時(shí)間發(fā)一次,來(lái)告訴服務(wù)器,我還活著。

2. 心跳機(jī)制是?
心跳機(jī)制是每隔一段時(shí)間會(huì)向服務(wù)器發(fā)送一個(gè)數(shù)據(jù)包,告訴服務(wù)器自己還活著,同時(shí)客戶端會(huì)確認(rèn)服務(wù)器端是否還活著,如果還活著的話,就會(huì)回傳一個(gè)數(shù)據(jù)包給客戶端來(lái)確定服務(wù)器端也還活著,否則的話,有可能是網(wǎng)絡(luò)斷開(kāi)連接了。需要重連~

那么需要怎么去實(shí)現(xiàn)它呢?如下所有代碼:

<html>
<head>
  <meta charset="utf-8">
  <title>WebSocket Demo</title>
</head>
<body>
  <script type="text/javascript">    // var ws = new WebSocket("wss://echo.websocket.org");
    /*
    ws.onerror = function(e) {
      console.log('已關(guān)閉');
    };
    ws.onopen = function(e) {
      console.log('握手成功');
      ws.send('123456789');
    }
    ws.onclose = function() {
      console.log('已關(guān)閉');
    }
    ws.onmessage = function(e) {
      console.log('收到消息');
      console.log(e);
    }    */
    
    var lockReconnect = false;//避免重復(fù)連接
    var wsUrl = "wss://echo.websocket.org";    var ws;    var tt;    function createWebSocket() {      try {
        ws = new WebSocket(wsUrl);
        init();
      } catch(e) {
        console.log('catch');
        reconnect(wsUrl);
      }
    }    function init() {
      ws.onclose = function () {
        console.log('鏈接關(guān)閉');
        reconnect(wsUrl);
      };
      ws.onerror = function() {
        console.log('發(fā)生異常了');
        reconnect(wsUrl);
      };
      ws.onopen = function () {        //心跳檢測(cè)重置        heartCheck.start();
      };
      ws.onmessage = function (event) {
        //拿到任何消息都說(shuō)明當(dāng)前連接是正常的
        console.log('接收到消息');
        heartCheck.start();
      }
    }    function reconnect(url) {      if(lockReconnect) {        return;
      };
      lockReconnect = true;      //沒(méi)連接上會(huì)一直重連,設(shè)置延遲避免請(qǐng)求過(guò)多
      tt && clearTimeout(tt);
      tt = setTimeout(function () {
        createWebSocket(url);
        lockReconnect = false;
      }, 4000);
    }    //心跳檢測(cè)
    var heartCheck = {
      timeout: 3000,
      timeoutObj: null,
      serverTimeoutObj: null,
      start: function(){
        console.log('start');        var self = this;        this.timeoutObj && clearTimeout(this.timeoutObj);        this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);        this.timeoutObj = setTimeout(function(){          //這里發(fā)送一個(gè)心跳,后端收到后,返回一個(gè)心跳消息,
          console.log('55555');
          ws.send("123456789");
          self.serverTimeoutObj = setTimeout(function() {
            console.log(111);
            console.log(ws);
            ws.close();            // createWebSocket();          }, self.timeout);

        }, this.timeout)
      }
    }
    createWebSocket(wsUrl);  </script>
</body>
</html>

具體的思路如下:
1. 第一步頁(yè)面初始化,先調(diào)用createWebSocket函數(shù),目的是創(chuàng)建一個(gè)websocket的方法:new WebSocket(wsUrl);因此封裝成函數(shù)內(nèi)如下代碼:

function createWebSocket() {  try {
    ws = new WebSocket(wsUrl);
    init();
  } catch(e) {
    console.log('catch');
    reconnect(wsUrl);
  }
}

2. 第二步調(diào)用init方法,該方法內(nèi)把一些監(jiān)聽(tīng)事件封裝如下:

function init() {
  ws.onclose = function () {
    console.log('鏈接關(guān)閉');
    reconnect(wsUrl);
  };
  ws.onerror = function() {
    console.log('發(fā)生異常了');
    reconnect(wsUrl);
  };
  ws.onopen = function () {    //心跳檢測(cè)重置    heartCheck.start();
  };
  ws.onmessage = function (event) {
    //拿到任何消息都說(shuō)明當(dāng)前連接是正常的
    console.log('接收到消息');
    heartCheck.start();
  }
}

3. 如上第二步,當(dāng)網(wǎng)絡(luò)斷開(kāi)的時(shí)候,會(huì)先調(diào)用onerror,onclose事件可以監(jiān)聽(tīng)到,會(huì)調(diào)用reconnect方法進(jìn)行重連操作。正常的情況下,是先調(diào)用
onopen方法的,當(dāng)接收到數(shù)據(jù)時(shí),會(huì)被onmessage事件監(jiān)聽(tīng)到。

4. 重連操作 reconnect代碼如下:

var lockReconnect = false;//避免重復(fù)連接function reconnect(url) {  if(lockReconnect) {    return;
  };
  lockReconnect = true;  //沒(méi)連接上會(huì)一直重連,設(shè)置延遲避免請(qǐng)求過(guò)多
  tt && clearTimeout(tt);
  tt = setTimeout(function () {
    createWebSocket(url);
    lockReconnect = false;
  }, 4000);
}

如上代碼,如果網(wǎng)絡(luò)斷開(kāi)的話,會(huì)執(zhí)行reconnect方法,使用了一個(gè)定時(shí)器,4秒后會(huì)重新創(chuàng)建一個(gè)新的websocket鏈接,重新調(diào)用createWebSocket函數(shù),
重新會(huì)執(zhí)行及發(fā)送數(shù)據(jù)給服務(wù)器端。

5. 最后一步就是實(shí)現(xiàn)心跳檢測(cè)的代碼:如下:

//心跳檢測(cè)var heartCheck = {
  timeout: 3000,
  timeoutObj: null,
  serverTimeoutObj: null,
  start: function(){
    console.log('start');    var self = this;    this.timeoutObj && clearTimeout(this.timeoutObj);    this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);    this.timeoutObj = setTimeout(function(){      //這里發(fā)送一個(gè)心跳,后端收到后,返回一個(gè)心跳消息,
      //onmessage拿到返回的心跳就說(shuō)明連接正常
      console.log('55555');
      ws.send("123456789");
      self.serverTimeoutObj = setTimeout(function() {
        console.log(111);
        console.log(ws);
        ws.close();        // createWebSocket();      }, self.timeout);

    }, this.timeout)
  }
}

實(shí)現(xiàn)心跳檢測(cè)的思路是:每隔一段固定的時(shí)間,向服務(wù)器端發(fā)送一個(gè)ping數(shù)據(jù),如果在正常的情況下,服務(wù)器會(huì)返回一個(gè)pong給客戶端,如果客戶端通過(guò)
onmessage事件能監(jiān)聽(tīng)到的話,說(shuō)明請(qǐng)求正常,這里我們使用了一個(gè)定時(shí)器,每隔3秒的情況下,如果是網(wǎng)絡(luò)斷開(kāi)的情況下,在指定的時(shí)間內(nèi)服務(wù)器端并沒(méi)有返回心跳響應(yīng)消息,因此服務(wù)器端斷開(kāi)了,因此這個(gè)時(shí)候我們使用ws.close關(guān)閉連接,在一段時(shí)間后(在不同的瀏覽器下,時(shí)間是不一樣的,firefox響應(yīng)更快),
可以通過(guò) onclose事件監(jiān)聽(tīng)到。因此在onclose事件內(nèi),我們可以調(diào)用 reconnect事件進(jìn)行重連操作。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多