WebSocket原理與實踐(三)--解析數據幀


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后查看效果

如下:

這樣服務器接收客戶端穿過了的數據就沒問題了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM