likes
comments
collection
share

NodeJS+TS手写websocket库(上)

作者站长头像
站长
· 阅读数 29

网络协议是一组规则和标准,用于定义计算机网络中的通信方式。这些规则确保不同设备之间能够有效地交换数据。网络协议可以分为多个层次,包括应用层、传输层、网络层和数据链路层等,每一层都有不同的协议来处理不同类型的通信任务。

网络协议的层次和示例

  • 应用层

    • HTTP(超文本传输协议):用于Web浏览器和服务器之间的通信。
    • HTTPS(HTTP安全协议):HTTP的加密版本,使用TLS/SSL。
    • FTP(文件传输协议):用于在网络上传输文件。
    • SMTP(简单邮件传输协议):用于发送电子邮件。
    • DNS(域名系统):将域名转换为IP地址。
    • SSH(安全外壳):用于安全地登录到远程计算机。
    • WebSocket:用于在客户端和服务器之间建立全双工的实时通信通道。
  • 传输层

    • TCP(传输控制协议):提供可靠的、面向连接的传输服务。
    • UDP(用户数据报协议):提供无连接的、不可靠的传输服务。
    • SCTP(流控制传输协议):提供多流的、可靠的传输服务。
  • 网络层

    • IP(互联网协议):用于在网络之间传输数据包。
    • ICMP(互联网控制消息协议):用于发送错误消息和操作信息。
  • 数据链路层

    • Ethernet(以太网):用于有线局域网(LAN)。
    • Wi-Fi(无线保真):用于无线局域网(WLAN)。

HTTP和WebSocket

HTTP(超文本传输协议)

HTTP(HyperText Transfer Protocol)是用于在万维网上传输超文本的应用层协议。它是Web浏览器和Web服务器之间通信的基础。HTTP协议定义了客户端(通常是浏览器)如何向服务器发送请求以及服务器如何响应这些请求。

HTTP的主要特点包括:

  1. 无状态:每个HTTP请求都是独立的,服务器不会保留前一个请求的信息。
  2. 请求-响应模型:客户端发送请求,服务器返回响应。
  3. 基于文本:HTTP消息是可读的文本格式,便于调试和理解。

HTTP有多个版本,目前常用的包括HTTP/1.1和HTTP/2,HTTP/3也在逐渐普及中。

WebSocket

WebSocket是一种全双工通信协议,旨在解决HTTP协议的局限性。它允许在客户端和服务器之间建立持久连接,并在该连接上进行双向数据传输。这使得WebSocket特别适合需要实时通信的应用,如在线聊天、实时游戏和股票行情推送等。

WebSocket的主要特点包括:

  1. 全双工通信:客户端和服务器可以同时发送和接收消息。
  2. 低延迟:由于连接是持久的,省去了每次通信都要建立和关闭连接的开销,减少了延迟。
  3. 基于事件:WebSocket使用事件驱动模型,客户端和服务器可以在任何时候发送数据。

WebSocket连接的建立过程如下:

  1. 握手阶段:客户端通过HTTP请求向服务器发起WebSocket握手请求。
  2. 协议升级:如果服务器同意升级协议,会返回一个101状态码,表示协议从HTTP升级为WebSocket。
  3. 数据传输:握手成功后,客户端和服务器之间就可以通过WebSocket连接进行数据传输。

HTTP 和 WebSocket 的优缺点对比

  • HTTP
    • 优点:简单、易于实现、广泛支持、适合请求-响应模型
    • 缺点:无状态、每次请求都需建立连接,开销较大
  • WebSocket
    • 优点:持久连接、低延迟、全双工通信
    • 缺点:实现较复杂、需要处理连接管理和安全问题

总结来说,HTTP适用于传统的请求-响应模型,而WebSocket则更适合需要实时、低延迟通信的应用场景。

WebSocket与HTTP的关系

  1. 连接建立

    • WebSocket连接的建立需要通过HTTP/HTTPS协议进行初始握手。客户端向服务器发送一个HTTP请求,这个请求包含一些特定的头部字段,表明客户端希望升级连接到WebSocket协议。
    • 服务器接收到请求后,如果同意升级,会返回一个101状态码的响应,表示协议切换成功。
  2. 连接升级

    • 在HTTP握手阶段,使用的头部字段包括UpgradeConnection,表明HTTP协议将被升级为WebSocket协议。
    • 一旦握手完成,连接就从HTTP切换到WebSocket,后续的通信不再使用HTTP,而是使用WebSocket协议。
  3. 后续通信

    • 握手完成后,WebSocket连接使用一个全双工的通信通道,允许客户端和服务器之间实时地双向传输数据。这与HTTP的请求-响应模型不同。

WebSocket握手示例

以下是一个WebSocket握手的示例:

客户端请求

GET /chat HTTP/1.1
Host: 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=

在这个握手过程中:

  • Upgrade头部字段表明客户端希望将协议升级到WebSocket。
  • Connection头部字段包含Upgrade,表示连接将被升级。
  • Sec-WebSocket-Key是一个随机生成的Base64编码字符串,用于安全验证。
  • Sec-WebSocket-Version表明WebSocket协议的版本。

服务器通过计算Sec-WebSocket-Key的哈希值并返回Sec-WebSocket-Accept头部字段来验证握手请求的有效性。

node中的HTTP服务和WebSocket服务

Node.js 自带了 http 模块,用于实现 HTTP 协议。我们可以使用这个模块来创建 HTTP 服务器和客户端,处理 HTTP 请求和响应。

以下是一个简单的示例,展示如何使用 Node.js 内置的 http 模块创建一个 HTTP 服务器:

const http = require('http');

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello, World!\n');
});

server.listen(3000, '127.0.0.1', () => {
  console.log('Server running at http://127.0.0.1:3000/');
});

和HTTP服务不同,Node.js 并没有自带 WebSocket 的实现。如果需要在 Node.js 中使用 WebSocket,需要使用第三方包。我最常用的 WebSocket 包是 ws以下是一个使用 ws 包创建 WebSocket 服务器的示例:

  1. 首先,需要安装 ws 包:
npm install ws
  1. 然后,可以使用以下代码创建一个简单的 WebSocket 服务器:
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('New client connected');

  ws.on('message', (message) => {
    console.log(`Received message => ${message}`);
  });

  ws.send('Hello! Message from server...');
});

console.log('WebSocket server is running on ws://localhost:8080');

这段代码创建了一个 WebSocket 服务器,监听在端口 8080。每当有客户端连接时,服务器会发送一条消息给客户端,并打印接收到的任何消息。

使用node自己实现一个websocket服务器

尽管 Node.js 没有内置的 WebSocket 模块,但仍然可以使用原生 Node.js 实现一个简单的 WebSocket 服务器。实现 WebSocket 协议需要处理一些特定的协议细节,如握手、数据帧的格式和掩码处理等。 以下是一个使用原生 Node.js 实现简单 WebSocket 服务器的示例:

  1. 创建一个 HTTP 服务器来接受 WebSocket 握手请求。
  2. 处理 WebSocket 握手。
  3. 处理 WebSocket 数据帧。

服务端代码

const http = require('http');
const crypto = require('crypto');

// 创建一个 HTTP 服务器
const server = http.createServer((req, res) => {
  res.writeHead(404);
  res.end();
});

// 监听 'upgrade' 事件来处理 WebSocket 握手请求
server.on('upgrade', (req, socket, head) => {
  const key = req.headers['sec-websocket-key'];
  const acceptKey = generateAcceptValue(key);
  const responseHeaders = [
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: websocket',
    'Connection: Upgrade',
    `Sec-WebSocket-Accept: ${acceptKey}`
  ];

  // 发送 WebSocket 握手响应
  socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');

  // 监听 'data' 事件来处理 WebSocket 消息
  socket.on('data', (buffer) => {
    const message = parseMessage(buffer);
    if (message) {
      console.log('Received message:', message);
      socket.write(constructReply(message));
    }
  });

  // 错误处理
  socket.on('error', (err) => {
    console.error('Socket error:', err);
  });

  // 连接关闭处理
  socket.on('close', () => {
    console.log('Client disconnected');
  });
});

// 监听端口 8080
server.listen(8080, () => {
  console.log('WebSocket server is running on ws://localhost:8080');
});

// 生成 Sec-WebSocket-Accept 值
function generateAcceptValue(key) {
  return crypto
    .createHash('sha1')
    .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary')
    .digest('base64');
}

// 解析 WebSocket 消息
function parseMessage(buffer) {
  const firstByte = buffer.readUInt8(0);
  const secondByte = buffer.readUInt8(1);

  // 检查是否是最终帧
  const isFinalFrame = Boolean((firstByte >>> 7) & 0x1);
  // 保留位(通常为0)
  const [reserved1, reserved2, reserved3] = [
    Boolean((firstByte >>> 6) & 0x1),
    Boolean((firstByte >>> 5) & 0x1),
    Boolean((firstByte >>> 4) & 0x1)
  ];
  // 操作码(0x1 表示文本帧)
  const opCode = firstByte & 0xf;
  // 检查是否有掩码
  const isMasked = Boolean((secondByte >>> 7) & 0x1);
  let payloadLength = secondByte & 0x7f;

  let currentOffset = 2;

  // 处理延长的 payload 长度
  if (payloadLength > 125) {
    if (payloadLength === 126) {
      payloadLength = buffer.readUInt16BE(currentOffset);
      currentOffset += 2;
    } else {
      payloadLength = buffer.readUInt32BE(currentOffset) * 2 ** 32 + buffer.readUInt32BE(currentOffset + 4);
      currentOffset += 8;
    }
  }

  // 读取掩码键
  const maskingKey = buffer.slice(currentOffset, currentOffset + 4);
  currentOffset += 4;

  // 读取并解码数据
  const data = buffer.slice(currentOffset, currentOffset + payloadLength).map((byte, idx) => byte ^ maskingKey[idx % 4]);

  return data.toString('utf8');
}

// 构建 WebSocket 回复
function constructReply(message) {
  const messageBuffer = Buffer.from(message);
  const messageLength = messageBuffer.length;
  const buffer = Buffer.alloc(2 + messageLength);

  // 设置 FIN 位和文本帧操作码
  buffer.writeUInt8(0b10000001, 0);
  // 设置 payload 长度
  buffer.writeUInt8(messageLength, 1);
  // 复制消息内容到缓冲区
  messageBuffer.copy(buffer, 2);

  return buffer;
}

代码解释:

  1. 创建 HTTP 服务器:使用 http.createServer 创建一个 HTTP 服务器,并监听 upgrade 事件以处理 WebSocket 握手请求。
  2. WebSocket 握手:在 upgrade 事件中,生成 Sec-WebSocket-Accept 头部,并返回 101 Switching Protocols 响应,完成 WebSocket 握手。
  3. 解析 WebSocket 消息:在 socket.on('data') 事件中,解析 WebSocket 数据帧,提取消息内容。
    • parseMessage 函数解析 WebSocket 数据帧,包括处理掩码和解码消息内容。
  4. 构建 WebSocket 回复:构建一个简单的 WebSocket 回复帧,并发送回客户端。
    • constructReply 函数构建一个 WebSocket 回复帧,设置 FIN 位和操作码,并将消息内容复制到缓冲区。
  5. 错误处理和连接关闭:在 socket.on('error')socket.on('close') 中处理错误和连接关闭事件。

客户端代码

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket 客户端</title>
  </head>
  <body>
    <h1>WebSocket 客户端</h1>
    <div>
      <label for="messageInput">消息:</label>
      <input type="text" id="messageInput" placeholder="输入你的消息" />
      <button onclick="sendMessage()">发送</button>
    </div>
    <div>
      <h2>消息记录:</h2>
      <ul id="messages"></ul>
    </div>

    <script>
      const ws = new WebSocket("ws://localhost:8080");

      ws.onopen = () => {
        console.log("已连接到 WebSocket 服务器");
      };

      ws.onmessage = (event) => {
        const messagesList = document.getElementById("messages");
        const newMessage = document.createElement("li");
        newMessage.textContent = `服务器: ${event.data}`;
        messagesList.appendChild(newMessage);
      };

      ws.onclose = () => {
        console.log("已断开与 WebSocket 服务器的连接");
      };

      ws.onerror = (error) => {
        console.error("WebSocket 错误:", error);
      };

      function sendMessage() {
        const input = document.getElementById("messageInput");
        const message = input.value;
        ws.send(message);

        const messagesList = document.getElementById("messages");
        const newMessage = document.createElement("li");
        newMessage.textContent = `客户端: ${message}`;
        messagesList.appendChild(newMessage);

        input.value = "";
      }
    </script>
  </body>
</html>

代码解释

  1. HTML结构
    • <head>部分包含了页面的元数据,如字符编码和视口设置。
    • <body>部分包含了页面的主要内容,包括一个标题、一个输入框、一个按钮和一个消息记录列表。
  2. WebSocket连接
    • 在 <script> 标签内,创建了一个新的 WebSocket 对象 ws,连接到 ws://localhost:8080
    • ws.onopen:当 WebSocket 连接成功时触发,输出连接成功的日志。
    • ws.onmessage:当收到服务器发送的消息时触发,将消息显示在消息记录列表中。
    • ws.onclose:当 WebSocket 连接关闭时触发,输出连接关闭的日志。
    • ws.onerror:当 WebSocket 发生错误时触发,输出错误信息。
  3. 发送消息
    • sendMessage 函数在点击“发送”按钮时调用。
    • 从输入框中获取消息内容,并通过 ws.send(message) 发送给服务器。
    • 将发送的消息添加到消息记录列表中,并清空输入框。

第一版成品图

NodeJS+TS手写websocket库(上)

参考资料和进一步阅读

精品调研

有同学把上面的代码运行起来的话就能发现,这个demo是一个十分十分简陋的产品。一个正常ws库该有的他一样也没有,那说回来一个完整的ws库都应该有什么呢?

我在MDN上看到了这个

NodeJS+TS手写websocket库(上) 下面,我们挑几个顺眼的对比一下

Git 仓库信息

仓库最近更新Star 数量实现语言
Socket.IO2024-06-1860.4kTs/Js
ws2024-06-1721.2kJavaScript
µWebSockets2024-05-2217kC++/Node.js
SocketCluster2024-03-286.1kJavaScript

表格数据时间:2024年06月19日12:16:59

我只用过ws和一点点Socket.IO

功能对比表

功能/库Socket.IOwsµWebSocketsSocketCluster
协议支持WebSocket, 长轮询WebSocketWebSocketWebSocket
自动重连
消息广播
命名空间支持
房间/频道支持
二进制数据传输
消息压缩
心跳机制
身份验证
负载均衡
集群支持
跨平台支持
社区支持
文档和教程丰富中等中等中等
性能中等

详细功能解释:

  • 协议支持:指库支持的通信协议类型。Socket.IO 除了 WebSocket 外,还支持长轮询等其他协议。
  • 自动重连:是否支持客户端自动重连机制。
  • 消息广播:是否支持将消息广播到所有连接的客户端。
  • 命名空间支持:是否支持将连接划分到不同的命名空间以隔离通信。
  • 房间/频道支持:是否支持将连接分配到不同的房间或频道进行分组通信。
  • 二进制数据传输:是否支持传输二进制数据。
  • 消息压缩:是否支持消息的压缩传输。
  • 心跳机制:是否支持定期发送心跳包以保持连接活跃。
  • 身份验证:是否支持在连接时进行身份验证。
  • 负载均衡:是否支持负载均衡机制。
  • 集群支持:是否支持在多台服务器上运行以实现高可用性。
  • 跨平台支持:是否支持在不同操作系统和平台上运行。
  • 社区支持:社区的活跃度和支持度。
  • 文档和教程:官方文档和教程的丰富程度。
  • 性能:库的性能表现。

v0.0.2功能设计与实现

在第一版的基础上,我们将实现以下功能:

  1. 频道管理:允许用户加入和离开不同的频道。
  2. 消息广播:在同一频道内广播消息。
  3. 在线状态:显示当前在线用户及其状态。

频道管理

频道管理的目的是将用户划分到不同的组中,以便在同一频道内进行消息广播。我们可以使用一个简单的对象来存储每个频道中的用户列表。

消息广播

在实现频道管理后,当用户发送消息时,只需将消息广播到同一频道内的所有用户即可。

在线状态

我们可以维护一个在线用户列表,当用户连接或断开连接时更新该列表,并将更新后的列表广播给所有用户。

服务端代码

以下是更新后的服务端代码:

const http = require("http");
const crypto = require("crypto");

// 存储所有客户端连接和频道信息
const clients = new Map();
const channels = new Map();

// 创建一个 HTTP 服务器
const server = http.createServer((req, res) => {
  res.writeHead(404);
  res.end();
});

// 监听 'upgrade' 事件来处理 WebSocket 握手请求
server.on("upgrade", (req, socket, head) => {
  const key = req.headers["sec-websocket-key"];
  const acceptKey = generateAcceptValue(key);
  const responseHeaders = [
    "HTTP/1.1 101 Switching Protocols",
    "Upgrade: websocket",
    "Connection: Upgrade",
    `Sec-WebSocket-Accept: ${acceptKey}`,
  ];

  // 发送 WebSocket 握手响应
  socket.write(responseHeaders.join("\r\n") + "\r\n\r\n");
  console.log("WebSocket 握手成功");

  // 为每个连接生成唯一的ID
  const clientId = crypto.randomBytes(16).toString("hex");
  clients.set(clientId, { socket, channels: new Set(), userId: null });
  console.log(`客户端连接成功: ${clientId}`);

  // 监听 'data' 事件来处理 WebSocket 消息
  socket.on("data", (buffer) => {
    try {
      const message = parseMessage(buffer);
      if (message.opCode === 0x8) {
        // 处理关闭帧
        handleDisconnect(clientId);
      } else {
        const parsedData = JSON.parse(message.data);
        console.log(
          `收到消息: ${JSON.stringify(parsedData)} 来自客户端: ${clientId}`
        );
        handleMessage(clientId, parsedData);
      }
    } catch (error) {
      console.error("解析消息时出错:", error);
    }
  });

  // 错误处理
  socket.on("error", (err) => {
    console.error("Socket 错误:", err);
  });

  // 连接关闭处理
  socket.on("close", () => {
    handleDisconnect(clientId);
  });
});

// 监听端口 8080
server.listen(8080, () => {
  console.log("WebSocket 服务器运行在 ws://localhost:8080");
});

// 生成 Sec-WebSocket-Accept 值
function generateAcceptValue(key) {
  return crypto
    .createHash("sha1")
    .update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", "binary")
    .digest("base64");
}

// 解析 WebSocket 消息
function parseMessage(buffer) {
  const firstByte = buffer.readUInt8(0);
  const secondByte = buffer.readUInt8(1);

  const isFinalFrame = Boolean((firstByte >>> 7) & 0x1);
  const opCode = firstByte & 0xf;
  const isMasked = Boolean((secondByte >>> 7) & 0x1);
  let payloadLength = secondByte & 0x7f;

  let currentOffset = 2;

  if (payloadLength > 125) {
    if (payloadLength === 126) {
      payloadLength = buffer.readUInt16BE(currentOffset);
      currentOffset += 2;
    } else {
      payloadLength =
        buffer.readUInt32BE(currentOffset) * 2 ** 32 +
        buffer.readUInt32BE(currentOffset + 4);
      currentOffset += 8;
    }
  }

  const maskingKey = buffer.slice(currentOffset, currentOffset + 4);
  currentOffset += 4;

  const data = buffer
    .slice(currentOffset, currentOffset + payloadLength)
    .map((byte, idx) => byte ^ maskingKey[idx % 4]);

  return {
    isFinalFrame,
    opCode,
    isMasked,
    payloadLength,
    data: data.toString("utf8"),
  };
}

// 构建 WebSocket 回复
function constructReply(message) {
  const messageBuffer = Buffer.from(JSON.stringify(message));
  const messageLength = messageBuffer.length;
  const buffer = Buffer.alloc(2 + messageLength);

  buffer.writeUInt8(0b10000001, 0);
  buffer.writeUInt8(messageLength, 1);
  messageBuffer.copy(buffer, 2);

  return buffer;
}

// 处理 WebSocket 消息
function handleMessage(clientId, message) {
  const client = clients.get(clientId);

  switch (message.type) {
    case "join":
      client.userId = message.userId; // 保存用户ID
      console.log(`用户 ${client.userId} 请求加入频道 ${message.channel}`);
      joinChannel(clientId, message.channel);
      break;
    case "leave":
      console.log(`用户 ${client.userId} 请求离开频道 ${message.channel}`);
      leaveChannel(clientId, message.channel);
      break;
    case "broadcast":
      console.log(
        `用户 ${client.userId} 在频道 ${message.channel} 广播消息: ${message.content}`
      );
      broadcastMessage(clientId, message.channel, message.content);
      break;
    case "status":
      console.log(`用户 ${client.userId} 请求在线状态`);
      sendStatus(clientId);
      break;
    default:
      console.error("未知消息类型:", message.type);
  }
}

// 加入频道
function joinChannel(clientId, channel) {
  const client = clients.get(clientId);
  if (!channels.has(channel)) {
    channels.set(channel, new Set());
  }
  channels.get(channel).add(clientId);
  client.channels.add(channel);

  const message = {
    type: "system",
    content: `用户 ${client.userId} 加入频道 ${channel}`,
  };
  console.log(`用户 ${client.userId} 加入频道 ${channel}`);
  broadcastToChannel(channel, message);
}

// 离开频道
function leaveChannel(clientId, channel) {
  const client = clients.get(clientId);
  if (channels.has(channel)) {
    channels.get(channel).delete(clientId);
    if (channels.get(channel).size === 0) {
      channels.delete(channel);
    }
  }
  client.channels.delete(channel);

  const message = {
    type: "system",
    content: `用户 ${client.userId} 离开频道 ${channel}`,
  };
  console.log(`用户 ${client.userId} 离开频道 ${channel}`);
  broadcastToChannel(channel, message);
}

// 广播消息到频道
function broadcastToChannel(channel, message) {
  if (channels.has(channel)) {
    const messageBuffer = constructReply(message);
    for (const id of channels.get(channel)) {
      const recipient = clients.get(id);
      if (recipient) {
        recipient.socket.write(messageBuffer);
      }
    }
    console.log(`消息广播到频道 ${channel}: ${JSON.stringify(message)}`);
  }
}

// 广播消息
function broadcastMessage(clientId, channel, content) {
  const client = clients.get(clientId);
  if (channel && channels.has(channel)) {
    const message = {
      type: "broadcast",
      channel,
      userId: client.userId,
      content,
    };
    broadcastToChannel(channel, message);
  } else {
    console.error("广播消息失败: 频道不存在或未提供频道名称");
  }
}

// 发送在线状态
function sendStatus(clientId) {
  const client = clients.get(clientId);
  const onlineClients = Array.from(clients.keys());
  const message = { type: "status", online: onlineClients };
  client.socket.write(constructReply(message));
  console.log(
    `发送在线状态给客户端 ${clientId}: ${JSON.stringify(onlineClients)}`
  );
}

// 处理断开连接
function handleDisconnect(clientId) {
  const client = clients.get(clientId);
  if (client) {
    for (const channel of client.channels) {
      leaveChannel(clientId, channel);
    }
    clients.delete(clientId);
    console.log(`客户端断开连接: ${clientId}`);
  }
}

NodeJS+TS手写websocket库(上)

客户端代码

以下是更新后的客户端代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WebSocket客户端</title>
    <script>
      document.addEventListener("DOMContentLoaded", () => {
        // 创建WebSocket连接到本地服务器
        const socket = new WebSocket("ws://localhost:8080");

        // 设置默认房间号为1001
        document.getElementById("room").value = "1001";

        // 生成随机用户ID
        function generateUserId() {
          return "user_" + Math.random().toString(36).substr(2, 9);
        }

        // 加入房间按钮点击事件
        document.getElementById("joinRoom").addEventListener("click", () => {
          let room = document.getElementById("room").value; // 获取房间号
          let userId = document.getElementById("userId").value; // 获取用户ID
          // 如果用户ID未填写,则生成随机用户ID
          if (!userId) {
            userId = generateUserId();
            document.getElementById("userId").value = userId; // 显示生成的用户ID
          }
          // 发送加入房间的消息到服务器
          socket.send(JSON.stringify({ type: "join", channel: room, userId }));
        });

        // 发送消息按钮点击事件
        document.getElementById("sendMessage").addEventListener("click", () => {
          const message = document.getElementById("message").value; // 获取消息内容
          const room = document.getElementById("room").value; // 获取房间号
          let userId = document.getElementById("userId").value; // 获取用户ID
          // 如果用户ID未填写,则生成随机用户ID
          if (!userId) {
            userId = generateUserId();
            document.getElementById("userId").value = userId; // 显示生成的用户ID
          }
          // 发送消息到服务器
          socket.send(
            JSON.stringify({
              type: "broadcast",
              channel: room,
              userId,
              content: message,
            })
          );
          // 清空消息输入框
          document.getElementById("message").value = "";
        });

        // 接收到服务器消息时的处理
        socket.onmessage = function (event) {
          const msg = JSON.parse(event.data);
          let displayMessage = "";
          if (msg.type === "broadcast") {
            displayMessage = `用户 ${msg.userId} 在频道 ${msg.channel} 中说: ${msg.content}`;
          } else if (msg.type === "system") {
            displayMessage = `系统消息: ${msg.content}`;
          }
          // 将接收到的消息显示在页面上
          document.getElementById("serverMessages").textContent +=
            displayMessage + "\n";
        };
      });
    </script>
    <style type="text/css">
      main {
        display: flex;
        justify-content: space-between;
        width: 100vw;
        height: 100vh;
      }
      .message {
        flex: 0 0 75vw;
        height: 100vh;
        position: relative;
      }
      .message-main {
        flex: 0 0 75vw;
        height: 70vh;
        min-height: 100px;
      }
      .send-message {
        flex: 0 0 75vw;
        height: 300px;
        position: absolute;
        bottom: 0;
      }
      .send-message input {
        width: calc(75vw - 100px);
      }
      .login {
        flex: 0 0 20vw;
        height: 100vh;
      }
    </style>
  </head>
  <body>
    <h1>WebSocket客户端</h1>
    <main>
      <div class="message">
        <div
          class="message-main"
          id="serverMessages"
          style="white-space: pre"
        ></div>
        <div class="send-message">
          <input type="text" id="message" />
          <button id="sendMessage">发送消息</button>
        </div>
      </div>
      <div class="login">
        <p>
          <label for="userId">用户ID:</label>
          <input type="text" id="userId" placeholder="留空将自动生成" />
        </p>
        <p>
          <label for="room">房间号:</label>
          <input type="text" id="room" />
        </p>
        <p><button id="joinRoom">加入房间</button></p>
      </div>
    </main>
  </body>
</html>

NodeJS+TS手写websocket库(上)

代码解释

服务端代码

  1. 创建 HTTP 服务器:使用 http.createServer 创建一个 HTTP 服务器,并监听 upgrade 事件以处理 WebSocket 握手请求。

  2. WebSocket 握手:在 upgrade 事件中,生成 Sec-WebSocket-Accept 头部,并返回 101 Switching Protocols 响应,完成 WebSocket 握手。

  3. 生成唯一客户端 ID:为每个连接生成一个唯一的客户端 ID,并将其与连接的 socket 以及频道信息一起存储在 clients Map 中。

  4. 处理 WebSocket 消息:在 socket.on('data') 事件中,解析 WebSocket 数据帧,提取消息内容,并根据消息类型进行处理。

    • handleMessage 函数根据消息类型(如 join、leave、broadcast、status)调用相应的处理函数。
  5. 加入频道joinChannel 函数将客户端加入指定的频道,并广播系统消息通知其他用户。

  6. 离开频道leaveChannel 函数将客户端从指定频道移除,并广播系统消息通知其他用户。

  7. 广播消息broadcastMessage 函数将消息广播到指定频道内的所有用户。

  8. 发送在线状态sendStatus 函数将当前在线用户列表发送给请求的客户端。

  9. 处理断开连接handleDisconnect 函数在客户端断开连接时,将其从所有频道中移除,并删除客户端记录。

客户端代码

  1. HTML 结构:页面包含一个标题、一个消息输入框、一个发送按钮、一个消息记录区域、用户 ID 输入框、房间号输入框和加入房间按钮。

  2. WebSocket 连接:在页面加载完成后,创建一个 WebSocket 连接到服务器。

  3. 生成随机用户 IDgenerateUserId 函数生成一个随机用户 ID。

  4. 加入房间:点击“加入房间”按钮时,获取房间号和用户 ID(如果未填写则生成随机用户 ID),并发送加入房间的消息到服务器。

  5. 发送消息:点击“发送消息”按钮时,获取消息内容和房间号,发送消息到服务器,并清空消息输入框。

  6. 接收服务器消息:在 socket.onmessage 事件中,处理服务器发送的消息,根据消息类型(如 broadcast、system)显示在页面上。

  7. 消息显示:将接收到的消息显示在消息记录区域中。

运行方法

  1. 启动服务端:将服务端代码保存为 server.js 文件,并在终端运行以下命令启动服务器:

    node server.js
    
  2. 启动客户端:将客户端代码保存为 index.html 文件,使用浏览器打开该文件。

  3. 测试功能

    • 在浏览器中打开 index.html 文件,输入用户 ID(可选)和房间号,然后点击“加入房间”按钮。
    • 在消息输入框中输入消息,点击“发送消息”按钮,消息将发送到服务器,并广播到同一房间内的所有用户。
    • 在多个浏览器窗口或标签页中重复上述步骤,可以测试消息广播和在线状态功能。

总结

通过上述代码示例,我们实现了一个简单的基于 WebSocket 的聊天室应用(V0.0.2),包括频道管理、消息广播和在线状态显示等功能。这个示例展示了如何使用 Node.js 和原生 WebSocket 协议实现实时通信,并为进一步扩展和优化提供了基础。

后面把Fastify+ws的im基础服务做完,我们将从业务出发继续实现后续的功能

转载自:https://juejin.cn/post/7381787510073278473
评论
请登录