WebSocket协定以及ws源码剖析

¿

本文包括以下内容:

  • WebSocket协定第四章 – 衔接握手
  • WebSocket协定第五章 – 数据帧
  • nodejs ws库源码剖析 – 衔接握手历程
  • nodejs ws库源码剖析 – 数据帧剖析历程

参考

WebSocket 协定深切探讨

ws – github

本文对WebSocket的观点、定义、诠释和用处等基本知识不会触及, 轻微偏干一点, 篇幅较长, markdown约莫800行, 浏览须要耐烦

1. 衔接握手历程

关于WebSocket有一句很罕见的话: Websocket复用了HTTP的握手通道, 它详细指的是:

客户端经由过程HTTP要求与WebSocket效劳器协商晋级协定, 协定晋级完成后, 后续的数据交流则遵循WebSocket协定

1.1 客户端: 请求协定晋级

起首由客户端换提议协定晋级要求, 依据WebSocket协定范例, 要求头必需包括以下的内容

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
  • 要求行: 要求要领必需是GET, HTTP版本至少是1.1
  • 要求必需含有Host
  • 如果要求来自浏览器客户端, 必需包括Origin
  • 要求必需含有Connection, 其值必需含有”Upgrade”暗号
  • 要求必需含有Upgrade, 其值必需含有”websocket”症结字
  • 要求必需含有Sec-Websocket-Version, 其值必需是13
  • 要求必需含有Sec-Websocket-Key, 用于供应基本的防护, 比方无意的衔接

1.2 效劳器: 相应协定晋级

效劳器返回的相应头必需包括以下的内容

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
  • 相应行: HTTP/1.1 101 Switching Protocols
  • 相应必需含有Upgrade, 其值为”weboscket”
  • 相应必需含有Connection, 其值为”Upgrade”
  • 相应必需含有Sec-Websocket-Accept, 依据要求首部的Sec-Websocket-key盘算出来

1.3 Sec-WebSocket-Key/Accept的盘算

范例提到:

Sec-WebSocket-Key值由一个随机天生的16字节的随机数经由过程base64(见RFC4648的第四章)编码取得的

比方, 随机挑选的16个字节为:

// 十六进制 数字1~16
0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10

经由过程base64编码后值为: AQIDBAUGBwgJCgsMDQ4PEA==

测试代码以下:

const list = Array.from({ length: 16 }, (v, index) => ++index)
const key = Buffer.from(list)
console.log(key.toString('base64'))
// AQIDBAUGBwgJCgsMDQ4PEA==

Sec-WebSocket-Accept值的盘算体式格局为:

  1. Sec-Websocket-Key的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
  2. 经由过程SHA1盘算出择要, 并转成base64字符串

此处不须要纠结奇异字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11, 它就是一个GUID, 没准儿是写RFC的时刻随机天生的

测试代码以下:

const crypto = require('crypto')

function hashWebSocketKey (key) {
  const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'

  return crypto.createHash('sha1')
    .update(key + GUID)
    .digest('base64')
}

console.log(hashWebSocketKey('w4v7O6xFTi36lq3RNcgctw=='))
// Oy4NRAQ13jhfONC7bP8dTKb4PTU=

1.4 Sec-WebSocket-Key的作用

前面简朴提到他的作用为: 供应基本的防护, 削减歹意衔接, 进一步论述以下:

  • Key能够防备效劳器收到不法的WebSocket衔接, 比方http要求衔接到websocket, 此时效劳端能够直接谢绝
  • Key能够用来开端确保效劳器熟悉ws协定, 但也不能消除有的http效劳器只处置惩罚Sec-WebSocket-Key, 并不完成ws协定
  • Key能够防备反向代办缓存
  • 在浏览器中提议ajax要求, Sec-Websocket-Key以及相干header是被制止的, 如许能够防备客户端发送ajax要求时, 不测要求协定晋级

终究须要强调的是: Sec-WebSocket-Key/Accept并非用来保证数据的安全性, 由于其盘算/转换公式都是公然的, 而且异常简朴, 最主要的作用是防备一些不测的状况

2. 数据帧

WebSocket通讯的最小单元是帧, 由一个或多个帧构成一条完全的音讯, 交流数据的历程当中, 发送端和吸收端须要做的事变以下:

  1. 发送端: 将音讯切割成多个帧, 并发送给效劳端
  2. 吸收端: 接收音讯帧, 并将关联的帧从新组装成完全的音讯

数据帧花样作为中心内容, 一眼看去好像难以明白, 但本文作者下死敕令了, 必需明白, 冲冲冲

2.1 数据帧花样详解

《WebSocket协定以及ws源码剖析》

  • FIN: 占1bit

    • 0示意不是音讯的末了一个分片
    • 1示意是音讯的末了一个分片
  • RSV1, RSV2, RSV3: 各占1bit, 平常状况下全为0, 与Websocket拓展有关, 如果涌现非零的值且没有采纳WebSocket拓展, 衔接失足
  • Opcode: 占4bit

    • %x0: 示意本次数据传输采纳了数据分片, 当前数据帧为个中一个数据分片
    • %x1: 示意这是一个文本帧
    • %x2: 示意这是一个二进制帧
    • %x3-7: 保存的操纵代码, 用于后续定义的非掌握帧
    • %x8: 示意衔接断开
    • %x9: 示意这是一个心跳要求(ping)
    • %xA: 示意这是一个心跳相应(pong)
    • %xB-F: 保存的操纵代码, 用于后续定义的非掌握帧
  • Mask: 占1bit

    • 0示意不对数据载荷举行掩码异或操纵
    • 1示意对数据载荷举行掩码异或操纵
  • Payload length: 占7或7+16或7+64bit

    • 0~125: 数据长度即是该值
    • 126: 后续的2个字节代表一个16位的无标记整数, 值为数据的长度
    • 127: 后续的8个字节代表一个64位的无标记整数, 值为数据的长度
  • Masking-key: 占0或4bytes

    • 1: 携带了4字节的Masking-key
    • 0: 没有Masking-key
    • 掩码的作用并非防备数据泄密,而是为了防备初期版本协定中存在的代办缓存污染进击等题目
  • payload data: 载荷数据

我想如果晓得byte和bit的区分, 这部份就没题目- –

2.2 数据通报

WebSocket的每条音讯能够被切分红多个数据帧, 当吸收到一个数据帧时,会依据FIN值来推断, 是不是为末了一个数据帧

数据帧通报示例:

  1. FIN=0, Opcode=0x1: 发送文本范例, 音讯还没有发送完成,另有后续帧
  2. FIN=0, Opcode=0x0: 音讯没有发送完成, 另有后续帧, 接在上一条背面
  3. FIN=1, Opcode=0x0: 音讯发送完成, 没有后续帧, 接在上一条背面构成完全音讯

3. ws库源码剖析: 衔接握手历程

虽然之前用的都是socket.io, 有时发现了ws, 运用量居然还挺大, 周下载量是socket.io的六倍

《WebSocket协定以及ws源码剖析》

NodeJS中, 每当碰到协商晋级要求时, 就会触发http模块的upgrade事宜, 这就是完成WebSocketServer的切入点, 原生示例代码以下:

// 建立 HTTP 效劳器。
const srv = http.createServer( (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('相应内容');
});
srv.on('upgrade', (req, socket, head) => {
  // 特定的处置惩罚, 以完成Websocket效劳
});

而且, 在平常的运用中, 都是在一个已有的httpServer基本上举行拓展, 以完成WebSocket, 而不是建立一个自力的WebSocketServer

在一个已有httpServer的基本上, ws运用的实例代码为

const http = require('http');
const WebSocket = require('ws');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

server.listen(8080);

已有的httpServer作为参数传给了WebSocket.Server组织函数, 所以源码剖析的中心切入点为:

new WebSocket.Server({ server });

经由过程这个切入点, 就能够完全复现衔接握手的历程

3.1 剖析WebSocketServer类

由于httpServer已作为参数通报进来, 因而其组织函数变得非常简朴:

class WebSocketServer extends EventEmitter {
  constructor(options, callback) {
    super()
    // 在供应了http server的基本上, 代码能够简化为
    if (options.server) {
      this._server = options.server
    }
    // 监听事宜
    if (this._server) {
      this._removeListeners = addListeners(this._server, {
        listening: this.emit.bind(this, 'listening'),
        error: this.emit.bind(this, 'error'),
        // 中心
        upgrade: (req, socket, head) => {
          // 下一步切入点
          this.handleUpgrade(req, socket, head, (ws) => {
            this.emit('connection', ws, req)
          })
        }
      })
    }
  }
}

// 这是一段异常带秀的代码, 在绑定多个事宜监听器的同时返回一个移除多个事宜监听器的函数
function addListeners(server, map) {
  for (const event of Object.keys(map)) server.on(event, map[event]);

  return function removeListeners() {
    for (const event of Object.keys(map)) {
      server.removeListener(event, map[event]);
    }
  };
}

能够看到, 在组织函数中, 为httpServer注册了upgrade事宜的监听器, 触发时, 会实行this.handleUpgrade函数, 这就是下一步的方向

3.2 过滤不法要求: handleUpgrade函数

这个函数主要用来过滤掉不正当的要求, 搜检的内容包括:

  • Sec-WebSocket-Key
  • Sec-WebSocket-Version
  • WebSocket要求的途径

症结代码以下:

const keyRegex = /^[+/0-9A-Za-z]{22}==$/;

handleUpgrade(req, socket, head, cb) {
  socket.on('error', socketOnError)

  // 猎取sec-websocket-key
  const key = req.headers['sec-websocket-key'] !== undefined
    ? req.headers['sec-websocket-key']
    : false

  // 猎取sec-websocket-version
  const version = +req.headers['sec-websocket-version']

  // 猎取协定拓展, 本篇不触及
  const extensions = {};

  // 关于不正当的要求, 中缀握手
  if (
    req.method !== 'GET' ||
    req.headers.upgrade.toLowerCase() !== 'websocket' ||
    !key ||
    !keyRegex.test(key) ||
    (version !== 8 && version !== 13) ||
    // 该函数是对Websocket要求途径的推断, 与option.path相干, 不睁开
    !this.shouldHandle(req)
  ) {
    return abortHandshake(socket, 400)
  }

  // 关于正当的要求, 给它晋级!
  this.completeUpgrade(key, extensions, req, socket, head, cb)
}

关于不正当的要求, 直接400 bad request了, abortHandshake以下:

const {  STATUS_CODES } = require('http');

function abortHandshake(socket, code, message, headers) {
  // net.Socket 也是双工流,因而它既可读也可写
  if (socket.writable) {
    message = message || STATUS_CODES

;
headers = {
Connection: 'close',
'Content-type': 'text/html',
'Content-Length': Buffer.byteLength(message),
...headers
};

socket.write(
`HTTP/1.1 ${code} ${STATUS_CODES

}\r\n` +
Object.keys(headers)
.map((h) => `${h}: ${headers[h]}`)
.join('\r\n') +
'\r\n\r\n' +
message
);
}
// 移除handleUpgrade中增加的error监听器
socket.removeListener('error', socketOnError);
// 确保在该 socket 上不再有 I/O 运动
socket.destroy();
}

如果一切顺利, 我们来到completeUpgrade函数

3.3 完成握手: completeUpgrade函数

这个函数主要用来, 返回准确的相应, 触发相干的事宜, 纪录值等, 代码比较简朴

const { createHash } = require('crypto');
const { GUID } = require('./constants');
const WebSocket = require('./websocket');

function completeUpgrade(key, extensions, req, socket, head, cb) {
  // Destroy the socket if the client has already sent a FIN packet.
  if (!socket.readable || !socket.writable) return socket.destroy()

  // 天生sec-websocket-accept
  const digest = createHash('sha1')
    .update(key + GUID)
    .digest('base64');

  // 组装Headers
  const headers = [
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: websocket',
    'Connection: Upgrade',
    `Sec-WebSocket-Accept: ${digest}`
  ];
  // 建立一个Websocket实例
  const ws = new Websocket(null)

  this.emit('headers', headers, req);
  // 返回相应
  socket.write(headers.concat('\r\n').join('\r\n'));
  socket.removeListener('error', socketOnError);

  // 下一步切入点
  ws.setSocket(socket, head, this.options.maxPayload);

  // 经由过程Set纪录处于衔接状况的客户端
  if (this.clients) {
    this.clients.add(ws);
    ws.on('close', () => this.clients.delete(ws));
  }
  // 触发connection事宜
  cb(ws);
}

到这里, 就完成了悉数握手阶段, 但还没触及到对数据帧的处置惩罚

4. ws库源码剖析: 数据帧处置惩罚

上一章末端, 启发下文的代码为completeUpgrade中的:

ws.setSocket(socket, head, this.options.maxPayload);

进入WebSocket类中的setSocket要领, 关于数据帧处置惩罚代码主要能够简化为:

Class WebSocket extends EventEmitter {
  ...
  setSocket(socket, head, maxPayload) {
    // 实例化一个可写流, 用于处置惩罚数据帧
    const receiver = new Receiver(
      this._binaryType,
      this._extensions,
      maxPayload
    );
    receiver[kWebSocket] = this;
    socket.on('data', socketOnData);
  }
}
function socketOnData(chunk) {
  if (!this[kWebSocket]._receiver.write(chunk)) {
    this.pause();
  }
}

此处疏忽了许多事宜处置惩罚, 比方error, end, close等, 由于他们与本文目的无关, 关于一些API, 也不做引见

所以中心切入点为Receiver类, 它就是用于处置惩罚数据帧的中心

4.1 Receiver类基本组织

Receiver类继续自可写流, 还须要明白两点基本观点:

  • stream一切的流都是EventEmitter的实例
  • 完成可写流须要完成writable._write要领, 该要领供内部运用
const { Writable } = require('stream')

class Recevier extends Writable {
  constructor(binaryType, extensions, maxPayload) {
    super()

    this._binaryType = binaryType || BINARY_TYPES[0]; // nodebuffer
    this[kWebSocket] = undefined; // WebSocket实例的援用
    this._extensions = extensions || {}; // WebSocket协定拓展
    this._maxPayload = maxPayload | 0; // 100 * 1024 * 1024

    this._bufferedBytes = 0; // 纪录buffer长度
    this._buffers = []; // 纪录buffer数据

    this._compressed = false; // 是不是紧缩
    this._payloadLength = 0; // 数据帧 PayloadLength
    this._mask = undefined; // 数据帧Mask Key
    this._fragmented = 0; // 数据帧是不是分片
    this._masked = false; // 数据帧 Mask
    this._fin = false; // 数据帧 FIN
    this._opcode = 0;  // 数据帧 Opcode

    this._totalPayloadLength = 0; // 载荷总长度
    this._messageLength = 0; // 载荷总长度, 与this._compressed有关
    this._fragments = []; // 载荷分片纪录数组

    this._state = GET_INFO; // 标志位, 用于startLoop函数
    this._loop = false; // 标志位, 用于startLoop函数
  }

  _write(chunk, encoding, cb) {
    if (this._opcode === 0x08 && this._state == GET_INFO) return cb();

    this._bufferedBytes += chunk.length;
    this._buffers.push(chunk);
    this.startLoop(cb);
  }
}

能够看到, 每当收到新的数据帧, 就会将其纪录在_buffers数组中, 并马上最先剖析流程startLoop

4.2 数据帧剖析流程: startLoop函数

startLoop(cb) {
  let err;
  this._loop = true;

  do {
    switch (this._state) {
      case GET_INFO:
        err = this.getInfo();
        break;
      case GET_PAYLOAD_LENGTH_16:
        err = this.getPayloadLength16();
        break;
      case GET_PAYLOAD_LENGTH_64:
        err = this.getPayloadLength64();
        break;
      case GET_MASK:
        this.getMask();
        break;
      case GET_DATA:
        err = this.getData(cb);
        break;
      default:
        // `INFLATING`
        this._loop = false;
        return;
    }
  } while (this._loop);

  cb(err);
}

剖析流程很简朴:

  • getInfo起首剖析FIN, RSV, OPCODE, MASK, PAYLOAD LENGTH等数据
  • 由于payload length分为三种状况(详细背面叙说, 此处只列出分支):

    • 0~125: 挪用haveLength要领
    • 126: 先触发getPayloadLength16要领, 再挪用haveLength要领
    • 127: 先出法getPayloadLength64要领, 再挪用haveLength要领
  • haveLength要领中, 如果存在掩码(mask), 先挪用getMask要领, 再挪用getData要领

团体流程和状况经由过程this._loopthis._state掌握, 比较直观

4.3 消耗Buffer的体式格局: consume要领

按理说第一步应当剖析getInfo要领, 不过内里触及到了consume要领, 这个函数供应了一种简约的体式格局消耗已猎取的Buffer, 这个函数接收一个参数n, 代表须要消耗的字节数, 末了返回消耗的字节

如果须要取得数据帧的第一个字节的数据(包括了 FIN + RSV + OPCODE), 只须要经由过程this.consume(1)即可

纪录值this._buffers是一个buffer数组, 最最先, 内里寄存完全的数据帧, 跟着消耗的举行, 数据则会逐步变小, 那末每次消耗存在三种能够:

  1. 消耗的字节数正好即是一个chunk的字节数
  2. 消耗的字节数小于一个chunk的字节数
  3. 消耗的字节数大于一个chunk的字节数

关于第一种状况, 只须要移出 + 返回即可

if (n === this._buffers[0].length) return this._buffers.shift()

关于第二种状况, 只须要裁剪 + 返回即可

if (n < this._buffers[0].length) {
  const buf = this._buffers[0]
  this._buffers[0] = buf.slice(n)
  return buf.slice(0, n)
}

关于第三种状况, 会轻微庞杂一点, 起首我们要请求一个大小为须要消耗字节数的buffer空间, 用于存储返回的buffer

// buffer空间是不是初始化并不主要, 由于终究他都会被悉数掩盖
const dst = Buffer.allocUnsafe(n)

在这类状况中, 能够保证他的长度大于第一个chunk, 但不能确定在消耗一个chunk以后, 是不是还大于第一个chunk(消耗以后索引前移), 因而须要轮回

// do...while能够防备一次无意义推断, 起首实行一次轮回体, 再推断前提
do {
  const buf = this._buffers[0]

  // 如果长度大于第一个chunk, 移除 + 复制即可
  if (n >= buf.length) {
    this._buffers.shift().copy(dst, dst.length - n);
  }
  // 如果长度小于一个chunk, 裁剪 + 复制即可
  else {
    // buf.copy这个api就本身温习一下嗷
    buf.copy(dst, dst.length - n, 0, n);
    this._buffers[0] = buf.slice(n);
  }
  n -= buf.length;
} while (n > 0)

4.4 剖析数据帧: getInfo要领

一个最小的数据帧必需包括以下的数据:

FIN (1 bit) + RSV (3 bit) + OPCODE (4 bit) + MASK (1 bit) + PAYLOADLENGTH (7 bit)

起码2个字节, 因而少于两个字节的数据帧是毛病的, 简化的getInfo以下

getInfo() {
  if (this._bufferedBytes < 2) {
    this._loop = false
    return
  }
  const buf = this.consume(2)

  // 只保存了数据帧中的几个症结数据
  this._fin = (buf[0] & 0x80) === 0x80
  this._opcode = buf[0] & 0x0f
  this._payloadLength = buf[1] & 0x7f
  this._masked = (buf[1] & 0x80) === 0x80

  // 对应Payload Length的三种状况
  if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16
  else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64
  else return this.haveLength()
}

此处的中心就是按位于运算符&的寄义, 先以FIN为例, FIN在数据帧中处于第一个bit

// FIN的值用[]指代, X代表第一个字节中的后续bit
[]xxxxxxx
// 十六进制数0x80代表二进制
10000000
// 二者按位与, 结果与背面7个bit无关
[]0000000
// 因而, 只须要比较[]0000000 和 10000000是不是相称即可, 简化即取得
this._fin = (buf[0] & 0x80) === 0x80

OPCODEPAYLOAD LENGTH同理

// OPCODE处于第一个字节的后四位, 与0000 1111按位与即可
xxxx[][][][] & 0000 1111 (也就是0x0f)

// PAYLOAD LENGTH处于第二个字节的后七为, 与0111 1111按位于即可
x[][][][][][][][] & 0111 1111 (也就是0x7f)

4.5 Payload Length三种状况与大小端

三种状况以下:

  • 0-125: 载荷现实长度就是0-125之间的某个数
  • 126: 载荷现实长度为随后2个字节代表的一个16位的无标记整数的数值
  • 127: 载荷现实长度为随后8个字节代表的一个64位的无标记整数的数值

能够听起来比较绕, 看代码, 以126分支为例:

getPayloadLength16() {
  if (this._bufferedBytes < 2) {
    this._loop = false;
    return;
  }

  this._payloadLength = this.consume(2).readUInt16BE(0);
  return this.haveLength();
}

能够看到, 处置惩罚长度的中心为readUInt16BE(0), 这便触及到大小端了:

  • 大端(Big endian)以为第一个字节是最高位字节, 和我们对十进制数字大小的认知类似
  • 小端(Little endian)以为第一个字节是最低位字节

那末, 范例中提到的随后2个字节代表的一个16位的无标记整数的数值, 天然指的是大端了

大端 vs 小端对照:

// 假定背面两个字节二进制值为
1111 1111 0000 0001
// 转为十六进制为
0xff 0x01
// 大端输出 65281
console.log(Buffer.from([0xff, 0x01]).readUInt16BE(0).toString(10))
// 小端输出 511
console.log(Buffer.from([0xff, 0x01]).readUInt16LE(0).toString(10))

除此之外, 7 + 64的形式另有一点分外的处置惩罚, 代码以下:

getPayloadLength64() {
  if (this._bufferedBytes < 8) {
    this._loop = false;
    return;
  }

  const buf = this.consume(8);
  const num = buf.readUInt32BE(0);

  //
  // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
  // if payload length is greater than this number.
  //
  if (num > Math.pow(2, 53 - 32) - 1) {
    this._loop = false;
    return error(
      RangeError,
      'Unsupported WebSocket frame: payload length > 2^53 - 1',
      false,
      1009
    );
  }

  this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
  return this.haveLength();
}

4.6 取得载荷数据: getData

在取得载荷之前, 如果getInfomask为1, 须要举行getMask操纵, 猎取Mask Key(一共四个字节)

getMask() {
  if (this._bufferedBytes < 4) {
    this._loop = false;
    return;
  }

  this._mask = this.consume(4);
  this._state = GET_DATA;
}

getData源码简化为以下

getData(cb) {
  // data为 Buffer.alloc(0)
  let data = EMPTY_BUFFER;

  // 消耗payload
  data = this.consume(this._payloadLength)
  // 如果有mask, 依据mask key举行解码, 此处不睁开
  if (this._masked) unmask(data, this._mask)
  // 将其纪录进分片数组
  this._fragments.push(data)
  // 如果该数据帧示意: 衔接断开, 心跳要求, 心跳相应
  if (this._opcode > 0x07) return this.controlMessage(data)
  // 如果该数据帧示意: 数据分片、文本帧、二进制帧
  return this.dataMessage()
}

4.7 组装载荷数据: dataMessage

接着剖析dataMessage()函数, 它用于将多个帧的数据兼并, 简化以后也比较简朴

dataMessage() {
  if (this._fin) {
    const messageLength = this._messageLength
    const fragments = this._fragments

    const buf = concat(fragments, messageLength)
    this.emit('message', buf.toString())
  }
}
// 简明易懂哦, 不诠释啦
function concat(list, totalLength) {
  if (list.length === 0) return EMPTY_BUFFER;
  if (list.length === 1) return list[0];

  const target = Buffer.allocUnsafe(totalLength);
  let offset = 0;

  for (let i = 0; i < list.length; i++) {
    const buf = list[i];
    buf.copy(target, offset);
    offset += buf.length;
  }

  return target;
}

5. 总结

本文篇幅较长且并非面试题那种小块的知识点, 浏览急需耐烦, 已只管防备贴大段代码, 能看到这里我都想给你打钱了

经由过程本篇剖析, 完全的引见以及复现了WebSocket中的两个症结阶段:

  • 衔接握手阶段
  • 数据交流极度

个人以为最症结就是: 触及到了对Node.js的buffer模块以及stream模块的运用, 这也是收成最大的一部份

    原文作者:Caaalabash
    原文地址: https://segmentfault.com/a/1190000019293037
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞