
Node.js是用于构建客户端服务器应用程序的流行工具。正确使用Node.js可以仅使用一个线程即可处理大量网络请求。毫无疑问,网络I / O是该平台的优势之一。看起来,当使用Node.js为积极使用各种网络协议的应用程序编写服务器端代码时,开发人员应该知道这些协议的工作方式,但通常并非如此。这是由于Node.js的另一个强项,那就是它的NPM软件包管理器,您可以在其中找到针对几乎所有任务的现成解决方案。使用现成的软件包,我们可以简化我们的生活,重复使用代码(这是正确的),但是与此同时,我们将自己隐藏在库的屏幕后面,隐藏了正在进行的流程的本质。在本文中,我们将尝试通过实现规范的一部分而不使用外部依赖项来理解WebSocket协议。欢迎来到猫。
, , WebSocket . , , http, , . http . Http request/reply — , . (, http 2.0). , . , , http, . RFC6202, , . WebSocket 2008 , . , WebSocket 2011 13 RFC6455. OSI http tcp. WebSocket http. WebSocket , , , . . , WebSocket 2009 , , Google Chrome 4 . , , . WebSocket :
- (handshake)
, , WebSocket, http . , GET . , , , , . http , . typescript ts-node.
import * as http from 'http';
import * as stream from 'stream';
export class SocketServer {
constructor(private port: number) {
http
.createServer()
.on('request', (request: http.IncomingMessage, socket: stream.Duplex) => {
console.log(request.headers);
})
.listen(this.port);
console.log('server start on port: ', this.port);
}
}
new SocketServer(8080);
8080. .
const socket = new WebSocket('ws://localhost:8080');
WebSocket, . readyState. :
- 0 —
- 1 — .
- 2 —
- 3 —
readyState, 0, 3. , . WebSocket API
:
{
host: 'localhost:8080',
connection: 'Upgrade',
pragma: 'no-cache',
'cache-control': 'no-cache',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
upgrade: 'websocket',
origin: 'chrome-search://local-ntp',
'sec-websocket-version': '13',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
'sec-websocket-key': 'h/k2aB+Gu3cbgq/GoSDOqQ==',
'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits'
}
, http RFC2616. http GET, upgrade , . , 101, — . WebSocket , :
- sec-websocket-version . 13
- sec-websocket-extensions , . ,
- sec-websocket-protocol , . , , . — , .
- sec-websocket-key . . .
, , 101, sec-websocket-accept, , sec-websocket-key :
- sec-websocket-key 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
- sha-1
- base64
Upgrade: WebSocket Connection: Upgrade. , . sec-websocket-key node.js crypto. .
import * as crypto from 'crypto';
SocketServer
private HANDSHAKE_CONSTANT = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
constructor(private port: number) {
http
.createServer()
.on('upgrade', (request: http.IncomingMessage, socket: stream.Duplex) => {
const clientKey = request.headers['sec-websocket-key'];
const handshakeKey = crypto
.createHash('sha1')
.update(clientKey + this.HANDSHAKE_CONSTANT)
.digest('base64');
const responseHeaders = [
'HTTP/1.1 101',
'upgrade: websocket',
'connection: upgrade',
`sec-webSocket-accept: ${handshakeKey}`,
'\r\n',
];
socket.write(responseHeaders.join('\r\n'));
})
.listen(this.port);
console.log('server start on port: ', this.port);
}
http Node.js upgrade , . , , 1. . .
. — . . , , , .. . , , , ( ). , , , . .

, , . .
. 2
| 0 | 1 | 2 | 3 | 4 5 6 7 | 0 | 1 2 3 4 5 6 7 |
|---|---|---|---|---|---|---|
| FIN | RSV1 | RSV2 | RSV3 | OPCODE | MASK |
- FIN . 1, , 0, . .
- RSV1, RSV2, RSV3 .
- OPCODE 4 . : . . , UTF8, . 3 ping, pong, close. .
- 0 , —
- 1
- 2
- 8
- 9 Ping
- xA Pong
- MASK — . 0, , 1, . , , . , , .
- 7 , .
. 0 12
- <= 125, , , . ,
- = 126 2
- = 127 8
| 0, 2, 8 | 0, 4 |
|---|---|
, , . . — 4 , . , XOR. , , XOR.
, WebSocket .
, . WebSocket , . Ping. , . Ping, , . , Pong , Ping. ,
private MASK_LENGTH = 4; // .
private OPCODE = {
PING: 0x89, // Ping
SHORT_TEXT_MESSAGE: 0x81, // , 125
};
private DATA_LENGTH = {
MIDDLE: 128, // ,
SHORT: 125, //
LONG: 126, // , 2
VERY_LONG: 127, // , 8
};
Ping
private ping(message?: string) {
const payload = Buffer.from(message || '');
const meta = Buffer.alloc(2);
meta[0] = this.OPCODE.PING;
meta[1] = payload.length;
return Buffer.concat([meta, payload]);
}
, , . - Ping. , . , , . .
private CONTROL_MESSAGES = {
PING: Buffer.from([this.OPCODE.PING, 0x0]),
};
private connections: Set<stream.Duplex> = new Set();
, Ping 5 , .
setInterval(() => socket.write(this.CONTROL_MESSAGES.PING), heartbeatTimeout);
this.connections.add(socket);
. . , , . , , , , , .
private decryptMessage(message: Buffer) {
const length = message[1] ^ this.DATA_LENGTH.MIDDLE; // 1
if (length <= this.DATA_LENGTH.SHORT) {
return {
length,
mask: message.slice(2, 6), // 2
data: message.slice(6),
};
}
if (length === this.DATA_LENGTH.LONG) {
return {
length: message.slice(2, 4).readInt16BE(), // 3
mask: message.slice(4, 8),
data: message.slice(8),
};
}
if (length === this.DATA_LENGTH.VERY_LONG) {
return {
payloadLength: message.slice(2, 10).readBigInt64BE(), // 4
mask: message.slice(10, 14),
data: message.slice(14),
};
}
throw new Error('Wrong message format');
}
- . XOR , 128 , 10000000. , , , 1.
- 126,
- 127,
. ,
private unmasked(mask: Buffer, data: Buffer) {
return Buffer.from(data.map((byte, i) => byte ^ mask[i % this.MASK_LENGTH]));
}
XOR . 4 . .
public sendShortMessage(message: Buffer, socket: stream.Duplex) {
const meta = Buffer.alloc(2);
meta[0] = this.OPCODE.SHORT_TEXT_MESSAGE;
meta[1] = message.length;
socket.write(Buffer.concat([meta, message]));
}
. , .
socket.on('data', (data: Buffer) => {
if (data[0] === this.OPCODE.SHORT_TEXT_MESSAGE) { //
const meta = this.decryptMessage(data);
const message = this.unmasked(meta.mask, meta.data);
this.connections.forEach(socket => {
this.sendShortMessage(message, socket);
});
}
});
this.connections.forEach(socket => {
this.sendShortMessage(
Buffer.from(` . ${this.connections.size}`),
socket,
);
});
. .
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = ({ data }) => console.log(data);
socket.send('Hello world!');

当然,如果您的应用程序需要WebSocket,并且很可能需要它们,那么除非绝对必要,否则您不应该自己实现该协议。您始终可以从npm中的各种库中选择合适的解决方案。更好地重用已经编写和测试的代码。但是,了解其“幕后工作原理”将为您带来的不仅仅是仅仅使用别人的代码。上面的示例在github上可用