WebSocket原理與實踐(四)--生成資料幀

龍恩0707發表於2018-03-18

WebSocket原理與實踐(四)--生成資料幀

    從伺服器發往客戶端的資料也是同樣的資料幀,但是從伺服器傳送到客戶端的資料幀不需要掩碼的。我們自己需要去生成資料幀,解析資料幀的時候我們需要分片。

訊息分片:
   有時候資料需要分成多個資料包傳送,需要使用到分片,也就是說多個資料幀來傳輸一個資料。比如將大資料分成多個資料包傳輸,分片的目的是允許傳送未知長度的訊息。
這樣做的好處是:
  1. 大資料的傳輸可以分片傳輸,不用考慮到資料大小導致的長度標誌位不夠的情況。
  2. 和http的chunk一樣,可以邊生成資料邊傳遞訊息,可以提高傳輸效率。

如果大資料不能被碎片化,那麼一端就必須將訊息整個載入記憶體緩衝之中,然後需要計算長度等操作併傳送,但是有了碎片化機制,伺服器端或者中介軟體就可以選取適用的記憶體緩衝長度,然後當緩衝滿了之後就傳送一個訊息碎片。

分片規則:
1. 如果一個訊息不分片的話,那麼該訊息只有一幀(FIN為1,opcode非0);
2. 如果一個訊息分片的話,它的構成是由起始幀(FIN為0,opcode非0),然後若干(0個或多個)幀(FIN為0,opcode為0),然後結束幀(FIN為1,opcode為0)。

注意:
   1. 當前已經定義了控制幀包括 0x8(close), 0x9(Ping), 0xA(Pong). 控制幀可以出現在分片訊息中間,但是控制幀不允許分片,控制幀是通過它的opcode
的最高有效位是1去確定的。
   2. 組成訊息的所有幀都是相同的資料型別,在第一幀中的opcode中指明。組成訊息的碎片型別必須是文字,二進位制,或者其他的保留型別。

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

還是看基本幀協議如下:

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解析:
比如我們現在第三節我們講到的 "解析資料幀" 裡面的程式碼,我們傳送的訊息123456789後,返回的資料部分是:

<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 ] 
}

上面返回的資料部分是16進位制,因此我們需要他們轉換成二進位制,有關16進位制,10進位制,2進位制的轉換表如下:
16進位制-->10進位制-->2進位制轉換檢視

我們現在需要把 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89 這些16進位制先轉換成10進位制,然後轉換成二進位制,分析程式碼如下:
16進位制(a=10, b=11, ... 依次類推)

16進位制          10進位制                           2進位制
81          8*16的1次方 + 1*16的0次方 = 129      10000001
89          8*16的1次方 + 9*16的0次方 = 137      10001001
b0          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       01010010
5a          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       00111101
6e          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

我們把上面的轉換後的二進位制 對照上面的 基本幀協議表看下:
1. 先看 FIN 的含義是: 第一位是否為訊息的最後一個資料幀,如果為1的話,說明是,否則為0的話就不是,那說明是最後一個資料幀。
2. 第2~4位都為0,對應的RSV(1~3), 5~8為 0001,是屬於opcode的部分了,opcode是代表是幀的型別;它有如下型別:

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

注意:其中8進位制是以0開頭的,16進位制是以0x開頭的。

0001,是文字資料幀了。

3.  第九位是1,那麼對應的幀協議表就是MASK部分了,Mask(佔1位): 表示是否經過掩碼處理, 1 是經過掩碼的,0是沒有經過掩碼的。說明是經過掩碼處理的,
也就是說可以理解為是客戶端向伺服器端傳送資料的。(因為伺服器端給客戶端是不需要掩碼的,否則連線中斷)。

4. 第10~16位是 0001001 = 9 < 125, 對應幀協議中的 payload length的部分了,資料長度為9,因此小於125位,因此使用7位來表示實際資料長度。

5. b0, 23, 52, 5a 對應的部分是 屬於Masking-key(0或者4個位元組),該區塊用於儲存掩碼金鑰,只有在第二個子節中的mask為1,也就是訊息進行了掩碼處理時才有。

6. 81 11 61 6e 85 15 65 62 89 這些就是對應表中的資料部分了。

下面我們再來理解下 訊息 123456789 怎麼通過掩碼加密成 81 11 61 6e 85 15 65 62 89 這些資料了。

數字字元1的ASCLL碼的16進製為31,轉換成10進位制就是49了。其他的數字依次類推+1;

數字           10進位制          二進位制
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的二進位制位 00110001,掩碼b0的二進位制位 10110000, 因此:

00110001
10110000

進行交配的話,二進位制就變成:10000001,轉換成10進製為 129了,那麼轉換成16進位制就是 81了。

6-2:字元2的二進位制位 00110010,掩碼23的二進位制位 00100011,因此:

00110010
00100011

進行交配的話,二進位制就變成 00010001,轉換10進製為17,那麼轉換成16進位制就是 11了。

6-3: 字元3的二進位制位 00110011,掩碼52的二進位制位 01010010,因此:

00110011
01010010

進行交配的話,二進位制就變成:01100001,轉換成10進製為 97,那麼轉換成16進位制就是 61了。

6-4: 字元4的二進位制位 00110100,掩碼 5a 的二進位制位 01011010,因此:

00110100
01011010

進行交配的話,二進位制就變成 01101110,轉換成10進製為 110,那麼轉換成16進製為 6e.

6-5: 字元5的二進位制位 00110101,掩碼b0的二進位制位 10110000, 因此:

00110101
10110000

進行交配的話,二進位制就變成:10000101,轉換成10進製為 133,那麼轉換成16進位制就是 85了。

6-6: 字元6的二進位制位 00110110,掩碼23的二進位制位 00100011,因此:

00110110
00100011

進行交配的話,二進位制就變成:00010101,轉換成10進製為 21,那麼轉換成16進位制就是 15了。

6-7: 字元7的二進位制位 00110111,掩碼52的二進位制位 01010010,因此:

00110111
01010010

進行交配的話,二進位制就變成:01100101,轉換成10進製為 101,那麼轉換成16進位制就是 65了。

6-8: 字元8的二進位制位 00111000,掩碼 5a 的二進位制位 01011010,因此:

00111000
01011010

進行交配的話,二進位制就變成:01100010,轉換成10進製為 98,那麼轉換成16進位制就是 62了。

6-9: 字元9的二進位制位 00111001,掩碼b0的二進位制位 10110000, 因此:

00111001
10110000

進行交配的話,二進位制就變成:10001001,轉換成10進製為 137,那麼轉換成16進位制就是 89了。

字元123456789與掩碼加密的整個過程如上面分析,可以看到,字元分別依次與掩碼交配,如果掩碼不夠的話,依次從頭迴圈即可。

因此我們可以編寫如下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運算,最後轉換成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');

      // 握手成功後給客戶端傳送資料
      o.write(encodeDataFrame({
        FIN: 1,
        Opcode: 1,
        PayloadData: "123456789"
      }))
    } else {
      
    }
  })
}).listen(8001);
/* 
 >> 含義是右移運算子,
   右移運算子是將一個二進位制位的運算元按指定移動的位數向右移動,移出位被丟棄,左邊移出的空位一律補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 encodeDataFrame(e) {
  var arrs = [],
    o = new Buffer(e.PayloadData),
    l = o.length;
  // 處理第一個位元組
  arrs.push((e.FIN << 7)+e.Opcode);
  // 處理第二個位元組,判斷它的長度並放入相應的後溪長度
  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 
    );
  }
  // 返回頭部分和資料部分的合併緩衝區
  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>

進入目錄後,執行node encodeDataFrame.js後,開啟index.html頁面,在控制檯看待效果圖如下:

檢視git上程式碼

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

上面是基本的使用方法,但是有時候我們需要將一個大的資料包需要分成多個資料幀來傳輸,因此分片它分為3個部分:

1個開始幀:FIN=0, Opcode > 0;
零個或多個傳輸幀: FIN=0, Opcode=0;
1個終止幀:FIN=1, Opcode=0;

因此之前的握手成功後傳送的資料程式碼:

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

需要分成三部分來傳送了;

改成如下程式碼:

// 握手成功後給客戶端傳送資料
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"
}));

相關文章