如何用Node手写WebSocket协议

发布时间:2023-02-17 10:13:30 作者:iii
来源:亿速云 阅读:132

如何用Node手写WebSocket协议

目录

  1. 引言
  2. WebSocket协议概述
  3. Node.js基础
  4. WebSocket握手
  5. WebSocket帧格式
  6. 服务器">实现WebSocket服务器
  7. 处理WebSocket消息
  8. WebSocket客户端实现
  9. WebSocket协议扩展
  10. WebSocket安全性
  11. WebSocket性能优化
  12. WebSocket应用场景
  13. 总结

引言

WebSocket是一种在单个TCP连接上进行全双工通信的协议。它允许服务器和客户端之间进行实时、双向的数据传输,非常适合需要低延迟和高频率通信的应用场景。本文将详细介绍如何使用Node.js手写WebSocket协议,从基础概念到具体实现,逐步深入。

WebSocket协议概述

WebSocket协议是HTML5的一部分,旨在解决HTTP协议在实时通信中的局限性。与HTTP不同,WebSocket在建立连接后,客户端和服务器可以随时发送数据,而不需要频繁地建立和关闭连接。

WebSocket的特点

WebSocket协议的工作流程

  1. 握手阶段:客户端通过HTTP协议发起WebSocket握手请求,服务器响应并确认握手。
  2. 数据传输阶段:握手成功后,客户端和服务器通过WebSocket协议进行数据传输。
  3. 关闭连接:客户端或服务器可以主动关闭连接。

Node.js基础

在开始实现WebSocket协议之前,我们需要了解一些Node.js的基础知识。Node.js是一个基于Chrome V8引擎的JavaScript运行时,它允许我们在服务器端运行JavaScript代码。

Node.js的核心模块

创建一个简单的HTTP服务器

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握手是WebSocket协议的第一步,客户端通过HTTP协议发起握手请求,服务器响应并确认握手。

客户端握手请求

客户端发送的握手请求是一个标准的HTTP请求,包含以下关键字段:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务器握手响应

服务器收到握手请求后,需要生成一个响应,包含以下关键字段:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

生成Sec-WebSocket-Accept

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帧格式

WebSocket协议使用帧(Frame)来传输数据。每个帧包含一个帧头和帧体,帧头定义了帧的类型、长度等信息。

帧头结构

WebSocket帧头由2到14个字节组成,具体结构如下:

  1. FIN(1位):表示这是消息的最后一个帧。
  2. RSV1, RSV2, RSV3(各1位):保留位,必须为0。
  3. Opcode(4位):定义帧的类型。
    • 0x0:继续帧
    • 0x1:文本帧
    • 0x2:二进制帧
    • 0x8:关闭帧
    • 0x9:Ping帧
    • 0xA:Pong帧
  4. Mask(1位):表示帧体是否被掩码。
  5. Payload length(7位、7+16位或7+64位):表示帧体的长度。
  6. Masking-key(4字节):如果Mask为1,则包含4字节的掩码键。
  7. Payload data:帧体数据。

解析WebSocket帧

在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帧

我们还需要能够构建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握手和帧格式,接下来我们将实现一个完整的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服务器

将上述代码保存为websocket_server.js,然后在终端中运行:

node websocket_server.js

服务器将在3000端口监听WebSocket连接。你可以使用任何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帧用于保持连接活跃。服务器收到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服务器,我们还可以实现一个简单的WebSocket客户端,用于测试服务器。

使用Node.js实现WebSocket客户端

我们可以使用Node.js的net模块来实现一个WebSocket客户端。

”`javascript const net = require(‘net’); const crypto = require(‘crypto’);

const client = new net.S

推荐阅读:
  1. Kubernetes node的防火墙问题导致pod ip无法访问该怎么办
  2. 搭建node服务(三):使用TypeScript

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

node websocket

上一篇:怎么使用Echarts绘制街道及镇级地图

下一篇:JavaScript判断数据类型的方式有哪些

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》