WebSocket原理與實踐(四)--生成數據幀


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"
}));


免責聲明!

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



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