¿
Эта статья включает в себя следующее:
-
WebSocket
Глава 4 Протокола - Квитирование соединения -
WebSocket
Соглашение, глава 5 — фрейм данных nodejs ws
nodejs ws
Ссылаться на
Эта статья о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
Значение рассчитывается как:
- Буду
Sec-Websocket-Key
стоимость и258EAFA5-E914-47DA-95CA-C5AB0DC85B11
соединение - пройти через
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
Наименьшей единицей связи является фрейм, который состоит из одного или нескольких фреймов для формирования полного сообщения.В процессе обмена данными отправитель и получатель должны сделать следующее:
- Отправитель: Разрежьте сообщение на несколько кадров и отправьте его на сервер.
- Получение: принять кадр сообщения и повторно скомбинировать связанный кадр в полное сообщение.
Поскольку основное содержание формата фрейма данных кажется трудным для понимания на первый взгляд, но автор этой статьи дал мертвый приказ, должен понять, спешить спешить
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
Пример передачи фрейма данных:
-
FIN=0, Opcode=0x1
: Тип текста отправки, сообщение еще не отправлено, а есть последующие кадры -
FIN=0, Opcode=0x0
: Сообщение не отправлено, а есть последующие кадры, за которыми следует предыдущий -
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);
}
-
getInfo
FIN
,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
Это буферный массив, изначально, сохраненный внутри полной кадры,Поскольку данные потребления постепенно станут меньше, то есть три возможности для каждого потребления:
-
Количество потребляемых байтов точно равно одному
chunk
байты -
Количество потребляемых байтов меньше одного
chunk
байты -
Количество байтов потребляется более одного
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
Например,FIN
bit
// 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
серединаmask
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