Протокол WebSocket и анализ исходного кода ws

Node.js

¿

Эта статья включает в себя следующее:

  • WebSocketГлава 4 Протокола - Квитирование соединения
  • WebSocketСоглашение, глава 5 — фрейм данных
  • nodejs ws
  • nodejs ws

Ссылаться на

ws - github

Эта статья оWebSocketБазовые знания о концепциях, определениях, объяснениях и использовании не будут освещены, просто немного сухо,Длина большая, уценка около 800 строк, чтение требует терпения.

1. Процесс установления соединения

оWebSocketЕсть очень распространенная поговорка:Websocket повторно использует канал рукопожатия HTTP, что конкретно относится к:

Клиент согласовывает протокол обновления с сервером WebSocket через HTTP-запрос.После завершения обновления протокола последующий обмен данными следует протоколу 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
  • Происхождение должно быть включено, если запрос исходит от клиента браузера.
  • Запрос должен содержать 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.

1.3 Расчет Sec-WebSocket-Key/Accept

В спецификации упоминается:

Значение Sec-WebSocket-Key кодируется случайно сгенерированным 16-байтовым случайным числом с помощью base64 (см. главу 4 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И связанные заголовки запрещены, что позволяет избежать случайного обновления протокола, когда клиент отправляет запрос ajax.

В заключение необходимо подчеркнуть, что:Sec-WebSocket-Key/Accept не используется для обеспечения безопасности данных, потому что его формулы расчета/преобразования общедоступны и очень просты, а основная функция — предотвращение некоторых непредвиденных ситуаций

2. Фрейм данных

WebSocketНаименьшей единицей связи является фрейм, который состоит из одного или нескольких фреймов для формирования полного сообщения.В процессе обмена данными отправитель и получатель должны сделать следующее:

  1. Отправитель: Разрежьте сообщение на несколько кадров и отправьте его на сервер.
  2. Получение: принять кадр сообщения и повторно скомбинировать связанный кадр в полное сообщение.

Поскольку основное содержание формата фрейма данных кажется трудным для понимания на первый взгляд, но автор этой статьи дал мертвый приказ, должен понять, спешить спешить

2.1 Подробное объяснение формата фрейма данных

  • FIN: занимать 1 бит

    • 0Указывает, что это не последний фрагмент сообщения
    • 1Указывает, что это последний фрагмент сообщения
  • RSV1, RSV2, RSV3: каждый занимает 1 бит, вообще все 0, связанные с расширением WebSocket, если есть ненулевое значение и расширение WebSocket не используется, ошибка подключения

  • Opcode: занимать 4 бита

    • %x0: данные представляют собой передачу с использованием слайса данных, при этом текущий кадр данных является слайсом данных.
    • %x1: указывает, что это текстовый фрейм
    • %x2: указывает, что это двоичный кадр
    • %x3-7: зарезервированные коды операций для определенных впоследствии неуправляющих кадров
    • %x8: указывает на отключение
    • %x9: Указывает, что это запрос сердцебиения (ping)
    • %xA: Указывает, что это ответ сердцебиения (pong)
    • %xB-F: зарезервированные коды операций для определенных впоследствии неуправляющих кадров
  • Mask: занимать 1 бит

    • 0Указывает, что операция XOR по маске не выполняется для полезной нагрузки данных.
    • 1Указывает, что маскированная операция XOR выполняется над полезными данными.
  • Payload length: 7 или 7 + 16 или 7 + 64 бит

    • 0~125: длина данных равна этому значению
    • 126: следующие 2 байта представляют собой 16-битное целое число без знака, значение представляет собой длину данных
    • 127: Следующие 8 байтов представляют собой 64-битное целое число без знака, значение представляет собой длину данных
  • Masking-key: занимают 0 или 4 байта

    • 1: несет 4-байтовый маскирующий ключ
    • 0: Нет маскирующего ключа
    • Роль маски заключается не в предотвращении утечки данных, а в предотвращении таких проблем, как атаки с загрязнением кеша прокси, которые существовали в более ранних версиях протокола.
  • payload data: Payload Data.

2.2 передача данных

WebSocket

Пример передачи фрейма данных:

  1. FIN=0, Opcode=0x1: Тип текста отправки, сообщение еще не отправлено, а есть последующие кадры
  2. FIN=0, Opcode=0x0: Сообщение не отправлено, а есть последующие кадры, за которыми следует предыдущий
  3. FIN=1, Opcode=0x0: Сообщение отправлено, следующего кадра нет, за ним следует предыдущий для формирования полного сообщения.

3. Анализ исходного кода библиотеки ws: процесс установления соединения

Хотя предыдущийsocket.io, наткнулся наws, использование довольно велико, а еженедельный объем загрузки составляетsocket.ioшесть раз

существует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[code];
    headers = {
      Connection: 'close',
      'Content-type': 'text/html',
      'Content-Length': Buffer.byteLength(message),
      ...headers
    };

    socket.write(
      `HTTP/1.1 ${code} ${STATUS_CODES[code]}\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

Таким образом, основной точкой входа является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

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

  • getInfoFIN, RSV, OPCODE, MASK, PAYLOAD LENGTHи т.д. данные

  • потому чтоpayload lengthЕсть три случая (описаны позже, здесь перечислены только ответвления):

    • 0~125: вызовhaveLengthметод

    • 126: сначала запуститьgetPayloadLength16метод, позвоните еще разhaveLengthметод

    • 127: Первый вышелgetPayloadLength64метод, позвоните еще разhaveLengthметод

  • haveLengthВ методе, если есть маска (mask), вызовите сначалаgetMaskметод, позвоните еще разgetDataметод

Общий процесс и статус пройденыthis._loopа такжеthis._stateКонтроль, более интуитивно понятен

4.3 Способ потребления буфера: метод потребления

Понятно, что первым шагом должен стать анализgetInfoметод, но он предполагаетconsumeметод, функцияПредоставляет краткий способ использования полученного буфераэта функция принимает один параметрnпредставляет число байтов, которые нужно потреблять, и, наконец, возвращает потребляемые байты

Если вам нужно получить данные первого байта фрейма данных (включая FIN+RSV+OPCODE), просто передайтеthis.consume(1)Только что

рекордное значениеthis._buffersЭто буферный массив, изначально, сохраненный внутри полной кадры,Поскольку данные потребления постепенно станут меньше, то есть три возможности для каждого потребления:

  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空间是否初始化并不重要, 因为最终他都会被全部覆盖
const dst = Buffer.allocUnsafe(n)

В этом случае,Можно гарантировать, что его длина больше, чем у первого чанка, но нельзя определить, больше ли он, чем первый чанк после потребления чанка (индекс движется вперед после потребления), поэтому он должен зацикливаться

// 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Например,FINbit

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

OPCODEа такжеPAYLOAD LENGTHпо аналогии

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

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

4.5 Длина полезной нагрузки для трех корпусов и размер концов

Эти три случая следующие:

  • 0-125: Фактическая длина полезной нагрузки представляет собой число от 0 до 125.

  • 126: Фактическая длина грузаЦелочисленное значение без знака, за которым следует 16-битный, представленный 2 байтами

  • 127: Фактическая длина грузаЗначение 64-битного целого числа без знака, представленное следующими 8 байтами.

Это может показаться запутанным, посмотрите на код, чтобы126Пример ветки:

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

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

Видно, что ядро ​​длины обработки равноreadUInt16BE(0), который включает в себя большой и малый концы:

  • Большой эндПервый байт считается самым значащим байтом, и наша осведомленность о десятичных цифрах одинакового размера

  • Маленький порядок байтовЯ думаю, что первый байт является наименее значащим байтом

Затем указанная спецификация

// 假设后面两个字节二进制值为
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

До получения нагрузки, еслиgetInfoсерединаmask1, это нужно сделать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