您好,登录后才能下订单哦!
WebSocket是一种在单个TCP连接上进行全双工通信的协议。它允许服务器和客户端之间进行实时、双向的数据传输,非常适合需要低延迟和高频率通信的应用场景。本文将详细介绍如何使用Node.js手写WebSocket协议,从基础概念到具体实现,逐步深入。
WebSocket协议是HTML5的一部分,旨在解决HTTP协议在实时通信中的局限性。与HTTP不同,WebSocket在建立连接后,客户端和服务器可以随时发送数据,而不需要频繁地建立和关闭连接。
在开始实现WebSocket协议之前,我们需要了解一些Node.js的基础知识。Node.js是一个基于Chrome V8引擎的JavaScript运行时,它允许我们在服务器端运行JavaScript代码。
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
server.listen(3000, () => {
console.log('Server is listening on port 3000');
});
这个简单的HTTP服务器监听3000端口,并在收到请求时返回“Hello, World!”。
WebSocket握手是WebSocket协议的第一步,客户端通过HTTP协议发起握手请求,服务器响应并确认握手。
客户端发送的握手请求是一个标准的HTTP请求,包含以下关键字段:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务器收到握手请求后,需要生成一个响应,包含以下关键字段:
Sec-WebSocket-Key
生成的验证字符串。HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Accept
是通过将客户端发送的Sec-WebSocket-Key
与固定的GUID字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接,然后进行SHA-1哈希计算,最后进行Base64编码得到的。
const crypto = require('crypto');
function generateAcceptValue(secWebSocketKey) {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const hash = crypto.createHash('sha1').update(secWebSocketKey + GUID).digest('base64');
return hash;
}
在Node.js中,我们可以通过监听HTTP服务器的upgrade
事件来处理WebSocket握手请求。
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
server.on('upgrade', (req, socket, head) => {
const secWebSocketKey = req.headers['sec-websocket-key'];
const acceptValue = generateAcceptValue(secWebSocketKey);
const responseHeaders = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptValue}`,
'\r\n'
].join('\r\n');
socket.write(responseHeaders);
socket.pipe(socket); // Echo back the received data
});
server.listen(3000, () => {
console.log('Server is listening on port 3000');
});
WebSocket协议使用帧(Frame)来传输数据。每个帧包含一个帧头和帧体,帧头定义了帧的类型、长度等信息。
WebSocket帧头由2到14个字节组成,具体结构如下:
在Node.js中,我们可以通过读取TCP流中的数据来解析WebSocket帧。
function parseWebSocketFrame(buffer) {
const byte1 = buffer.readUInt8(0);
const byte2 = buffer.readUInt8(1);
const fin = (byte1 & 0x80) !== 0;
const opcode = byte1 & 0x0F;
const masked = (byte2 & 0x80) !== 0;
let payloadLength = byte2 & 0x7F;
let offset = 2;
if (payloadLength === 126) {
payloadLength = buffer.readUInt16BE(offset);
offset += 2;
} else if (payloadLength === 127) {
payloadLength = buffer.readBigUInt64BE(offset);
offset += 8;
}
let maskingKey;
if (masked) {
maskingKey = buffer.slice(offset, offset + 4);
offset += 4;
}
const payloadData = buffer.slice(offset, offset + payloadLength);
if (masked) {
for (let i = 0; i < payloadData.length; i++) {
payloadData[i] ^= maskingKey[i % 4];
}
}
return {
fin,
opcode,
masked,
payloadLength,
maskingKey,
payloadData
};
}
我们还需要能够构建WebSocket帧,以便向客户端发送数据。
function buildWebSocketFrame(payload, opcode = 0x1, fin = true, masked = false) {
const payloadLength = payload.length;
let frameBuffer;
if (payloadLength <= 125) {
frameBuffer = Buffer.alloc(2 + (masked ? 4 : 0) + payloadLength);
frameBuffer.writeUInt8((fin ? 0x80 : 0x00) | opcode, 0);
frameBuffer.writeUInt8((masked ? 0x80 : 0x00) | payloadLength, 1);
} else if (payloadLength <= 65535) {
frameBuffer = Buffer.alloc(4 + (masked ? 4 : 0) + payloadLength);
frameBuffer.writeUInt8((fin ? 0x80 : 0x00) | opcode, 0);
frameBuffer.writeUInt8((masked ? 0x80 : 0x00) | 126, 1);
frameBuffer.writeUInt16BE(payloadLength, 2);
} else {
frameBuffer = Buffer.alloc(10 + (masked ? 4 : 0) + payloadLength);
frameBuffer.writeUInt8((fin ? 0x80 : 0x00) | opcode, 0);
frameBuffer.writeUInt8((masked ? 0x80 : 0x00) | 127, 1);
frameBuffer.writeBigUInt64BE(BigInt(payloadLength), 2);
}
if (masked) {
const maskingKey = crypto.randomBytes(4);
maskingKey.copy(frameBuffer, frameBuffer.length - payloadLength - 4);
for (let i = 0; i < payloadLength; i++) {
frameBuffer[frameBuffer.length - payloadLength + i] ^= maskingKey[i % 4];
}
} else {
payload.copy(frameBuffer, frameBuffer.length - payloadLength);
}
return frameBuffer;
}
现在我们已经了解了WebSocket握手和帧格式,接下来我们将实现一个完整的WebSocket服务器。
我们将基于Node.js的http
模块创建一个WebSocket服务器,并处理握手和帧的解析与构建。
const http = require('http');
const crypto = require('crypto');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
server.on('upgrade', (req, socket, head) => {
const secWebSocketKey = req.headers['sec-websocket-key'];
const acceptValue = generateAcceptValue(secWebSocketKey);
const responseHeaders = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptValue}`,
'\r\n'
].join('\r\n');
socket.write(responseHeaders);
socket.on('data', (data) => {
const frame = parseWebSocketFrame(data);
if (frame.opcode === 0x1) { // Text frame
const message = frame.payloadData.toString('utf8');
console.log('Received message:', message);
const responseFrame = buildWebSocketFrame(Buffer.from('Hello, Client!'));
socket.write(responseFrame);
} else if (frame.opcode === 0x8) { // Close frame
console.log('Connection closed by client');
socket.end();
}
});
socket.on('close', () => {
console.log('Connection closed');
});
});
server.listen(3000, () => {
console.log('Server is listening on port 3000');
});
function generateAcceptValue(secWebSocketKey) {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const hash = crypto.createHash('sha1').update(secWebSocketKey + GUID).digest('base64');
return hash;
}
function parseWebSocketFrame(buffer) {
const byte1 = buffer.readUInt8(0);
const byte2 = buffer.readUInt8(1);
const fin = (byte1 & 0x80) !== 0;
const opcode = byte1 & 0x0F;
const masked = (byte2 & 0x80) !== 0;
let payloadLength = byte2 & 0x7F;
let offset = 2;
if (payloadLength === 126) {
payloadLength = buffer.readUInt16BE(offset);
offset += 2;
} else if (payloadLength === 127) {
payloadLength = buffer.readBigUInt64BE(offset);
offset += 8;
}
let maskingKey;
if (masked) {
maskingKey = buffer.slice(offset, offset + 4);
offset += 4;
}
const payloadData = buffer.slice(offset, offset + payloadLength);
if (masked) {
for (let i = 0; i < payloadData.length; i++) {
payloadData[i] ^= maskingKey[i % 4];
}
}
return {
fin,
opcode,
masked,
payloadLength,
maskingKey,
payloadData
};
}
function buildWebSocketFrame(payload, opcode = 0x1, fin = true, masked = false) {
const payloadLength = payload.length;
let frameBuffer;
if (payloadLength <= 125) {
frameBuffer = Buffer.alloc(2 + (masked ? 4 : 0) + payloadLength);
frameBuffer.writeUInt8((fin ? 0x80 : 0x00) | opcode, 0);
frameBuffer.writeUInt8((masked ? 0x80 : 0x00) | payloadLength, 1);
} else if (payloadLength <= 65535) {
frameBuffer = Buffer.alloc(4 + (masked ? 4 : 0) + payloadLength);
frameBuffer.writeUInt8((fin ? 0x80 : 0x00) | opcode, 0);
frameBuffer.writeUInt8((masked ? 0x80 : 0x00) | 126, 1);
frameBuffer.writeUInt16BE(payloadLength, 2);
} else {
frameBuffer = Buffer.alloc(10 + (masked ? 4 : 0) + payloadLength);
frameBuffer.writeUInt8((fin ? 0x80 : 0x00) | opcode, 0);
frameBuffer.writeUInt8((masked ? 0x80 : 0x00) | 127, 1);
frameBuffer.writeBigUInt64BE(BigInt(payloadLength), 2);
}
if (masked) {
const maskingKey = crypto.randomBytes(4);
maskingKey.copy(frameBuffer, frameBuffer.length - payloadLength - 4);
for (let i = 0; i < payloadLength; i++) {
frameBuffer[frameBuffer.length - payloadLength + i] ^= maskingKey[i % 4];
}
} else {
payload.copy(frameBuffer, frameBuffer.length - payloadLength);
}
return frameBuffer;
}
将上述代码保存为websocket_server.js
,然后在终端中运行:
node websocket_server.js
服务器将在3000端口监听WebSocket连接。你可以使用任何WebSocket客户端(如浏览器中的WebSocket
对象)连接到该服务器,并发送消息。
在WebSocket服务器中,我们需要处理不同类型的WebSocket消息,包括文本消息、二进制消息、Ping/Pong帧以及关闭帧。
文本消息是最常见的WebSocket消息类型,通常用于传输JSON数据或纯文本。
socket.on('data', (data) => {
const frame = parseWebSocketFrame(data);
if (frame.opcode === 0x1) { // Text frame
const message = frame.payloadData.toString('utf8');
console.log('Received message:', message);
const responseFrame = buildWebSocketFrame(Buffer.from('Hello, Client!'));
socket.write(responseFrame);
}
});
二进制消息用于传输二进制数据,如图片、音频等。
socket.on('data', (data) => {
const frame = parseWebSocketFrame(data);
if (frame.opcode === 0x2) { // Binary frame
console.log('Received binary data:', frame.payloadData);
const responseFrame = buildWebSocketFrame(frame.payloadData, 0x2);
socket.write(responseFrame);
}
});
Ping/Pong帧用于保持连接活跃。服务器收到Ping帧后,应返回一个Pong帧。
socket.on('data', (data) => {
const frame = parseWebSocketFrame(data);
if (frame.opcode === 0x9) { // Ping frame
console.log('Received Ping frame');
const pongFrame = buildWebSocketFrame(frame.payloadData, 0xA);
socket.write(pongFrame);
}
});
关闭帧用于关闭WebSocket连接。服务器收到关闭帧后,应关闭连接。
socket.on('data', (data) => {
const frame = parseWebSocketFrame(data);
if (frame.opcode === 0x8) { // Close frame
console.log('Connection closed by client');
socket.end();
}
});
除了实现WebSocket服务器,我们还可以实现一个简单的WebSocket客户端,用于测试服务器。
我们可以使用Node.js的net
模块来实现一个WebSocket客户端。
”`javascript const net = require(‘net’); const crypto = require(‘crypto’);
const client = new net.S
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。