node 原生實現服務端 websocket

本文主要介紹 webSocket(下文簡寫爲 ws),並使用 node 原生實現基本功能,難點主要是解析和組裝數據。須要的知識點:javascript

首先咱們看看 ws 數據幀格式:html

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

複製代碼

要理解 ws 就離不開上面這個圖,可是對數據幀不熟悉的,會徹底搞不懂這個圖是表達的啥意思。因此咱們先解釋下這個圖是幹嗎的,咱們應該看。java

數據幀

  • 位(bit)
    • 計算機最小數據存儲單位是,簡稱 b,也稱比特(bit)。每一個 0 或 1 就是一個位。
  • 字節(Byte)
    • 八個位表示一個字節

有上面這兩個概念再看上面的圖:node

  • 第一行(佔 32 位)git

    • 表格左上角有個 FIN,這個就表示一個,在這個位上可能值就只能是 0 或者 1
    • 接下來是 RSV一、RSV二、RSV3,它們也分別佔用 1 位,
    • 再後面是opcode(4)這裏表示數據操做碼,佔據 4 位,取值返回是:0000-1111,注意是二進制
    • 而後是MASK掩碼標識,佔 1 位,
    • payload len(7),接受到的數據長度,佔 7 位。
    • Extended payload length(16/54)...第一行的最後一格,佔 8 位這裏的數據含義會有變化,稍後詳說。
  • 第二行(佔 32 位)github

    • Extended payload length continued, if payload len == 127擴展數據長度,這裏爲何要分行呢?web

      • 其實分行只是爲了顯示方便而已,咱們徹底能夠把第二行拼接到第一行後面,其實咱們在處理數據時也是這麼作的,沒有分行一說。
      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)           | Extended payload length continued, |
         |N|V|V|V|       | |             |                               |  if payload len == 127             |
         | | | | |       |S|             |   (if payload len==126/127)   |                                    |
         | |1|2|3|       |K|             |                               |                                    |
         +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +------------------------------------+
      複製代碼

因此後面幾行都是能夠以此拼接到後面。算法

若是客戶端(瀏覽器)要發送一個hello給服務器,咱們服務端收到的數據實際上是一個二進制數據一系列的 0 或者 1,就像這樣10001000111...,咱們要知道到底發給咱們的是啥,就須要對這一些列的 0/1 作解析,上面的圖就解析這系列 0/1 的規則,咱們按照上面的規則一步步解析就能獲得咱們想要的數據。api

舉個例子:瀏覽器

假如收到客戶端發來的數據10000001(這裏只是截取數據開始的一部分(第一個字節),後面還有不少),對應的值以下:

FIN RSV1 RSV2 RSV3 opcode
1 0 0 0 0001

數據幀格式詳解

  • FIN: 1bit

    表示這是一個消息的最後的一幀。第一個幀也多是最後一個。
    %x0 : 還有後續幀
    %x1 : 最後一幀

  • RSV1, RSV2, RSV3: 各佔1bit

    除非一個擴展通過協商賦予了非零值以某種含義,不然必須爲0 若是沒有定義非零值,而且收到了非零的RSV,則websocket連接會失敗

  • Opcode: 4bit

    解釋說明 「Payload data」 的用途/功能 若是收到了未知的opcode,最後會斷開連接 定義瞭如下幾個opcode值: %x0 : 表明連續的幀 %x1 : text幀 %x2 : binary幀 %x3-7 : 爲非控制幀而預留的 %x8 : 關閉握手幀 %x9 : ping幀 %xA : pong幀 %xB-F : 爲非控制幀而預留的

  • Mask: 1bit

    定義「payload data」(實際提交的數據)是否被添加掩碼若是置1, 「Masking-key」就會被賦值全部從客戶端發往服務器的幀都會被置1

  • Payload length: 7 bit | 7+16 bit | 7+64 bit

    若是是0~125,它就是「payload length」(收到數據的長度,好比收到的是hello,那麼就是5), 若是是126,緊隨其後的被表示爲16 bits無符號整型就是「payload length」, 若是是127,緊隨其後的被表示爲64 bits無符號整型就是「payload length」

    • 爲何會有這三種狀況呢? 因爲payload length只有7位,二級制最大是1111111轉換爲十進制就是127,若是「payload length」大於127了,就無法正確的表示。咱們須要更多的位來表示「payload length」,因此咱們在Payload length後面用另外的位來表示。那直接定義一個64位來表示不就好了麼?雖然這樣能行,可是也得考慮到性能問題,如上面說的hello長度只有「5」,轉換爲二進制是101,三位就能夠了,若是用64位就有點太浪費了。因此分別定義了這三種狀況。
  • Masking-key: 0 or 32bit

    全部從客戶端發送到服務器的幀都包含一個32 bits的掩碼(若是「mask bit」被設置成1),不然爲0 bit。一旦掩碼被設置,全部接收到的payload data都必須與該值以一種算法作異或運算來獲取真實值。

  • Payload data: (x+y) bytes

    它是"Extension data"和"Application data"的總和,通常擴展數據爲空。

  • Extension data: x bytes

    除非擴展被定義,不然就是0,任何擴展必須指定其Extension data的長度

  • Application data: y bytes

    佔據"Extension data"以後的剩餘幀的空間

實戰

知道了幀結構和含義,接下來就能夠按照規則解析數據

  • 解析數據
function parseFrams() {
    // buffer接受到的數據
    const buffer = this.buffer;
    // 數據默認從第三個字節開始,默認數據長度小於125
    let payloadIndex = 2;

    // 獲取第字節,包含FIN和操做碼(opcode)
    const byte1 = buffer.readUInt8(0);

    // 0:還有後續幀
    // 1:最後一幀
    const FIN = (byte1 >>> 7) & 0x1;

    // 獲取操做碼,後面會根據操做碼處理數據
    const opcode = byte1 & 0x0f;

    if (!FIN) {
      // 不是最後一幀須要暫存當前的操做碼,協議要求:
      // 必需要暫存第一幀的操做碼
      // 分片編號 0 1 ... N-2 N-1
      // FIN 0 0 ... 0 1
      // opcode !0 0 ... 0 0
      this.frameOpcode = opcode;
    }

    // 獲取掩碼(MASK)和數據長度(payload length)
    let byte2 = buffer.readUInt8(1);

    // 定義「payload data」是否被添加掩碼
    // 若是置1, 「Masking-key」就會被賦值
    // 全部從客戶端發往服務器的幀都會被置1
    let MASK = (byte2 >>> 7) & 0x1;

    // 獲取數據長度
    let payloadLength = byte2 & 0x7f;

    let mask_key;

    if (payloadLength === 126) {
      // 大於126小於65536,那麼後面字節表示的是數據的長度,那麼真實的數據就會後移兩字節
      payloadLength = buffer.readUInt16BE(payloadIndex);

      // 真實數據後移2位
      payloadIndex += 2;
    } else if (payloadLength === 127) {
      // 大於等於65536,那麼後面字節表示的是數據的長度,數據最長爲64位,可是數據太大就很差處理了,這裏限制最大爲32位
      // 因此第2-6字節的數據始終應該爲0,真實數據的長度在6-10字節
      // 4:2-6字節的位置
      payloadLength = buffer.readUInt32BE(payloadIndex + 4);
      // 8:數據長度佔據了8字節,真實數據就須要後移8字節
      payloadIndex += 8;
    }

    // 若是MASK位被置爲1那麼Mask_key將佔據4位 MASK_KEY_LENGTH===4
    const maskKeyLen = MASK ? MASK_KEY_LENGTH : 0;

    // 若是當前接受到的數據長度小於發送的數據總長度加上協議頭部的數據長度,表示數據沒有接受完,暫不處理,須要等到全部數據都接受到後再處理
    if (buffer.length < payloadIndex + maskKeyLen + payloadLength) {
      return;
    }

    // 若是有掩碼,那麼在真實數據以前會有四字節的掩碼key(Masking-key)
    let payload = Buffer.alloc(0);
    if (MASK) {
      // 獲取掩碼
      mask_key = buffer.slice(payloadIndex, payloadIndex + MASK_KEY_LENGTH);

      // 真實數據再次後移4位
      payloadIndex += MASK_KEY_LENGTH;

      // 有掩碼須要解碼,解碼算法是規定死的,可見文後源碼
      payload = unmask(mask_key, buffer.slice(payloadIndex));
    } else {
      // 沒有掩碼就直接截取數據
      payload = buffer.slice(payloadIndex);
    }

    // 多是分片傳輸,須要緩存數據幀,等待全部幀接受完畢後再處理完整數據
    this.payloadFrames = Buffer.concat([this.payloadFrames, payload]);
    this.buffer = Buffer.alloc(0);

    // 數據接受完畢
    if (FIN) {
      const _opcode = opcode || this.frameOpcode;
      const payloadFrames = this.payloadFrames.slice(0);
      this.payloadFrames = Buffer.alloc(0);
      this.frameOpcode = 0;

      // 根據不一樣opcode處理成不一樣的數據
      this.processPayload(_opcode, payloadFrames);
    }
  }
  
複製代碼
  • 構建返回數據,返回數據就是解析數據的逆操做
/** * * @param {number} opcode * @param {string|buffer} payload * @param {boolean} isFinal */
  function encodeMessage(opcode, payload, isFinal = true) {
    const len = payload.length;
    let buffer;
    let byte1 = (isFinal ? 0x80 : 0x00) | opcode;

    if (len < 126) {
      // 數據長度0~125

      // 構建返回數據容器
      buffer = Buffer.alloc(2 + len); // 2:[FIN+RSV1/2/3+OPCODE](佔1bytes) + [MASK+payload length](佔1bytes)

      // 寫入FIN+RSV1/2/3+OPCODE
      buffer.writeUInt8(byte1);

      // 從第二字節寫入MASK+payload length
      buffer.writeUInt8(len, 1);

      // 從第三字節寫入真實數據
      payload.copy(buffer, 2);
    } else if (len < 1 << 16) {
      // 數據長度126~65535
      buffer.Buffer.alloc(2 + 2 + len);
      buffer.writeUInt8(byte1);
      buffer.writeUInt8(126, 1);
      buffer.writeUInt16(len, 2);
      payload.copy(buffer, 4);
    } else {
      // 數據長度65536~..
      buffer.Buffer.alloc(2 + 8 + len);
      buffer.writeUInt8(byte1);
      buffer.writeUInt8(127, 1);
      buffer.writeUInt32(0, 2);
      buffer.writeUInt32(len, 6);
      payload.copy(buffer, 10);
    }
    return buffer;
  }
複製代碼

上面兩段代碼都有很詳細的註釋,應該能看懂,就再也不具體的解析,實現源碼見github

相關文章
相關標籤/搜索