WebSocket原理與實踐(三)--解析資料幀

龍恩0707發表於2018-03-11

WebSocket原理與實踐(三)--解析資料幀

1-1 理解資料幀的含義:
   在WebSocket協議中,資料是通過幀序列來傳輸的。為了資料安全原因,客戶端必須掩碼(mask)它傳送到伺服器的所有幀,當它收到一個
沒有掩碼的幀時,伺服器必須關閉連線。不過伺服器端給客戶端傳送的所有幀都不是掩碼的,如果客戶端檢測到掩碼的幀時,也一樣必須關閉連線。
當幀被關閉的時候,可能傳送狀態碼1002(協議錯誤)。

基本幀協議如下:

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

如上是基本幀協議,它帶有操作碼(opcode)的幀型別,負載長度,和用於 "擴充套件資料" 與 "應用資料" 及 它們一起定義的 "負載資料"的指定位置,
某些位元組和操作碼保留用於未來協議的擴充套件。

FIN(1位): 是否為訊息的最後一個資料幀。
RSV1,RSV2,Rsv3(每個佔1位),必須是0,除非一個擴充套件協商為非零值定義的。
Opcode表示幀的型別(4位),例如這個傳輸的幀是文字型別還是二進位制型別,二進位制型別傳輸的資料可以是圖片或者語音之類的。(這4位轉換成16進位制值表示的意思如下):

0x0 表示附加資料幀
0x1 表示文字資料幀
0x2 表示二進位制資料幀
0x3-7 暫時無定義,為以後的非控制幀保留
0x8 表示連線關閉
0x9 表示ping
0xA 表示pong
0xB-F 暫時無定義,為以後的控制幀保留

Mask(佔1位): 表示是否經過掩碼處理, 1 是經過掩碼的,0是沒有經過掩碼的。

payload length (7位+16位,或者 7位+64位),定義負載資料的長度。
   1. 如果資料長度小於等於125的話,那麼該7位用來表示實際資料長度。
   2. 如果資料長度為126到65535(2的16次方)之間,該7位值固定為126,也就是 1111110,往後擴充套件2個位元組(16為,第三個區塊表示),用於儲存資料的實際長度。
   3. 如果資料長度大於65535, 該7位的值固定為127,也就是 1111111 ,往後擴充套件8個位元組(64位),用於儲存資料實際長度。

Masking-key(0或者4個位元組),該區塊用於儲存掩碼金鑰,只有在第二個子節中的mask為1,也就是訊息進行了掩碼處理時才有,否則沒有,
所以伺服器端向客戶端傳送訊息就沒有這一塊。

Payload data 擴充套件資料,是0位元組,除非已經協商了一個擴充套件。

1-2 客戶端到伺服器掩碼
WebSocket協議要求客戶端所傳送的幀必須掩碼,掩碼的金鑰是一個32位的隨機值。所有資料都需要與掩碼做一次異或運算。幀頭在第二個位元組的第一位表示該幀是否使用了掩碼。
WebSocket伺服器接收的每個載荷在處理之前首先需要處理掩碼,解除掩碼之後,伺服器將得到原始訊息內容。二進位制訊息可以直接交付。文字訊息將進行UTF-8解碼
並輸出到字串中。

二進位制位運算子知識擴充套件:

>> 含義是右移運算子,
   右移運算子是將一個二進位制位的運算元按指定移動的位數向右移動,移出位被丟棄,左邊移出的空位一律補0.
比如 11 >> 2, 意思是說將數字11右移2位。
首先將11轉換為二進位制數為 0000 0000 0000 0000 0000 0000 0000 1011 , 然後把低位的最後2個數字移出,因為該數字是正數,
所以在高位補零,則得到的最終結果為:0000 0000 0000 0000 0000 0000 0000 0010,轉換為10進位制是2.

<< 含義是左移運算子
    左移運算子是將一個二進位制位的運算元按指定移動的位數向左移位,移出位被丟棄,右邊的空位一律補0.
比如 3 << 2, 意思是說將數字3左移2位,
首先將3轉換為二進位制數為 0000 0000 0000 0000 0000 0000 0000 0011 , 然後把該數字高位(左側)的兩個零移出,其他的數字都朝左平移2位,
最後在右側的兩個空位補0,因此最後的結果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉換為十進位制是12(1100 = 1*2的3次方 + 1*2的2字方)

注意1: 在使用補碼作為機器數的機器中,正數的符號位為0,負數的符號位為1(一般情況下).
           比如:十進位制數13在計算機中表示為00001101,其中第一位0表示的是符號

注意2:負數的二進位制位如何計算?
          比如二進位制的原碼為 10010101,它的補碼怎麼計算呢?
          首先計算它的反碼是 01101010; 那麼補碼 = 反碼 + 1 = 01101011

再來看一個列子:
-7 >> 2 意思是將數字 -7 右移2位。
負數先用它的絕對值正數取它的二進位制程式碼,7的二進位制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那麼 -7的二進位制位就是 取反,
取反後再加1,就變成補碼。
因此-7的二進位制位: 1111 1111 1111 1111 1111 1111 1111 1001,
因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉換成十進位制的話 -7 >> 2 ,值就變成 -2了。

資料幀解析的程式如下程式碼:(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運算,最後轉換成Base64
      key = crypto.createHash('sha1').update(key+WS).digest('base64');

      // 輸出欄位資料,返回到客戶端,
      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頭結束
      o.write('\r\n');
    } else {
      // 資料處理
      onmessage(e);
    }
  })
}).listen(8000);
/* 
 >> 含義是右移運算子,
   右移運算子是將一個二進位制位的運算元按指定移動的位數向右移動,移出位被丟棄,左邊移出的空位一律補0.
 比如 11 >> 2, 意思是說將數字11右移2位。
 首先將11轉換為二進位制數為 0000 0000 0000 0000 0000 0000 0000 1011 , 然後把低位的最後2個數字移出,因為該數字是正數,
 所以在高位補零,則得到的最終結果為:0000 0000 0000 0000 0000 0000 0000 0010,轉換為10進位制是2.
  

 << 含義是左移運算子
   左移運算子是將一個二進位制位的運算元按指定移動的位數向左移位,移出位被丟棄,右邊的空位一律補0.
 比如 3 << 2, 意思是說將數字3左移2位,
 首先將3轉換為二進位制數為 0000 0000 0000 0000 0000 0000 0000 0011 , 然後把該數字高位(左側)的兩個零移出,其他的數字都朝左平移2位,
 最後在右側的兩個空位補0,因此最後的結果是 0000 0000 0000 0000 0000 0000 0000 1100,則轉換為十進位制是12(1100 = 1*2的3次方 + 1*2的2字方)

 注意1: 在使用補碼作為機器數的機器中,正數的符號位為0,負數的符號位為1(一般情況下). 
       比如:十進位制數13在計算機中表示為00001101,其中第一位0表示的是符號

 注意2:負數的二進位制位如何計算?
       比如二進位制的原碼為 10010101,它的補碼怎麼計算呢?
       首先計算它的反碼是 01101010; 那麼補碼 = 反碼 + 1 = 01101011

 再來看一個列子:
 -7 >> 2 意思是將數字 -7 右移2位。
 負數先用它的絕對值正數取它的二進位制程式碼,7的二進位制位為: 0000 0000 0000 0000 0000 0000 0000 0111 ,那麼 -7的二進位制位就是 取反,
 取反後再加1,就變成補碼。
 因此-7的二進位制位: 1111 1111 1111 1111 1111 1111 1111 1001,
 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此轉換成十進位制的話 -7 >> 2 ,值就變成 -2了。
*/
function decodeDataFrame(e) {

  var i = 0, j, s, arrs = [],
    frame = {
      // 解析前兩個位元組的基本資料
      FIN: e[i] >> 7,
      Opcode: e[i++] & 15,
      Mask: e[i] >> 7,
      PayloadLength: e[i++] & 0x7F
    };

    // 處理特殊長度126和127
    if (frame.PayloadLength === 126) {
      frame.PayloadLength = (e[i++] << 8) + e[i++];
    }
    if (frame.PayloadLength === 127) {
      i += 4; // 長度一般用4個位元組的整型,前四個位元組一般為長整型留空的。
      frame.PayloadLength = (e[i++] << 24)+(e[i++] << 16)+(e[i++] << 8) + e[i++];
    }
    // 判斷是否使用掩碼
    if (frame.Mask) {
      // 獲取掩碼實體
      frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]];
      // 對資料和掩碼做異或運算
      for(j = 0, arrs = []; j < frame.PayloadLength; j++) {
        arrs.push(e[i+j] ^ frame.MaskingKey[j%4]);
      }
    } else {
      // 否則的話 直接使用資料
      arrs = e.slice(i, i + frame.PayloadLength);
    }
    // 陣列轉換成緩衝區來使用
    arrs = new Buffer(arrs);
    // 如果有必要則把緩衝區轉換成字串來使用
    if (frame.Opcode === 1) {
      arrs = arrs.toString();
    }
    // 設定上資料部分
    frame.PayloadLength = arrs;
    // 返回資料幀
    return frame;
}

function onmessage(e) {
  console.log(e)
  e = decodeDataFrame(e);  // 解析資料幀
  console.log(e);  // 把資料幀輸出到控制檯
}

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, 先進入專案中對應的目錄後,使用node decodeDataFrame.js,  然後開啟index.html後檢視效果

如下:

這樣伺服器接收客戶端穿過了的資料就沒問題了。

相關文章