日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

node 原生实现服务端 websocket

發布時間:2025/3/19 编程问答 13 豆豆
生活随笔 收集整理的這篇文章主要介紹了 node 原生实现服务端 websocket 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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

  • WebSocket
  • Buffer
  • 按位操作符
  • 了解二進制
  • 了解十六進制

首先我們看看 ws 數據幀格式:

0 1 2 30 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 就離不開上面這個圖,但是對數據幀不熟悉的,會完全搞不懂這個圖是表達的啥意思。所以我們先解釋下這個圖是干嘛的,我們應該看。

數據幀

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

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

  • 第一行(占 32 位)

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

    • Extended payload length continued, if payload len == 127擴展數據長度,這里為什么要分行呢?

      • 其實分行只是為了顯示方便而已,我們完全可以把第二行拼接到第一行后面,其實我們在處理數據時也是這么做的,沒有分行一說。
      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 的規則,我們按照上面的規則一步步解析就能得到我們想要的數據。

舉個例子:

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

FINRSV1RSV2RSV3opcode
10000001

數據幀格式詳解

  • 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;// 數據默認從第三個字節開始,默認數據長度小于125let 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 0this.frameOpcode = opcode;}// 獲取掩碼(MASK)和數據長度(payload length)let byte2 = buffer.readUInt8(1);// 定義“payload data”是否被添加掩碼// 如果置1, “Masking-key”就會被賦值// 所有從客戶端發往服務器的幀都會被置1let 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===4const 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+OPCODEbuffer.writeUInt8(byte1);// 從第二字節寫入MASK+payload lengthbuffer.writeUInt8(len, 1);// 從第三字節寫入真實數據payload.copy(buffer, 2);} else if (len < 1 << 16) {// 數據長度126~65535buffer.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

轉載于:https://juejin.im/post/5c923a8ae51d453ec10e563a

總結

以上是生活随笔為你收集整理的node 原生实现服务端 websocket的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。