聊天室全栈开发-保姆级教程(Node.js+Websocket+Redis+HTML+CSS)

发布于:2025-08-13 ⋅ 阅读:(12) ⋅ 点赞:(0)

前言

最近在学习websocket全双工通信,想要做一个联机小游戏,做游戏之前先做一个聊天室练练手。
跟着本篇博客,可以从0搭建一个属于你自己的聊天室。

准备阶段

什么人适合学习本篇文章?
答:前端开发者,有一定的HTML+CSS+JavaScript基础,会使用fetch进行网络请求

技术栈

  • 前端三大件:HTML+CSS+JavaScript
  • Redis数据库(本篇博客涉及到的是Redis基础中的基础,小白也可以轻松拿捏!)
  • Node.js+Express框架+pug模板引擎(本篇博客后端部分使用Node.js的Express框架完成,不了解的同学也不需要害怕,代码简单易懂,开箱即用,直接复制即可使用)

工具准备

1.编译器

首先你需要有一个写代码的编译器,这里推荐两个(是我自己用的最多的,简单方便的):

  1. vscode:老牌编译器,功能强大,有丰富的插件资源,免费
    官网直接下载: https://code.visualstudio.com/
  2. trea:AI编译器,新时代热门编译器,有国内和海外两个版本
    国内:https://www.trae.cn/ide/download
    优点:完全免费内置许多国内ai,也可自添模型
    缺点:没有国外ai,模型数量比国外版少很多
    国内版trea模型图|100x146
    海外:https://www.trae.ai/download
    优点:部分免费,可充值为pro版,内置许多国外国内ai,可自添模型
    缺点:国外热门ai如GPT,Claude模型等普通版要排队很久,pro版不用排队,但是每个月也有次数限制,一般够用
    国外版trea模型图|100

2.Node.js环境

两个方法:
1.使用nvm下载node版本
2.直接下载node

这里不管是新手还是老手都强烈建议使用nvm来下载node,nvm的优势在于可以同时管理多个node版本,并且在需要的时候随时切换不同版本

vnm(下载安装中文网叙述的很清楚,这里不在赘述,注意一点,安装nvm前需要把电脑已有的node卸载):
下载:https://nvm.uihtm.com/doc/download-nvm.html
安装:https://nvm.uihtm.com/doc/install.html

node(如果想要快速开始的话,可直接下载node版本,建议下载v20.x.x版本较稳定):
下载:https://nodejs.org/en/download
node下载

3.Redis数据库

redis是linux环境下开发的高并发性能好的键值类型数据库,要想在windows环境下使用,常用有三种方法:

  • 使用windows虚拟机模拟linux环境,运行redis
  • 使用Docker Desktop拉取镜像并在容器中运行
  • 使用windows版redis,虽说比不上linux版的redis但是日常练习和教学完全够用了

这里我们主要讲第三种方法,想要使用windows版的redis需要访问GitHub上的开源库:https://github.com/redis-windows/redis-windows/releases,可能需要翻墙这个看运气
在这里插入图片描述
确保自己的电脑是windows,64位的,前四个都可下载,带service 的要比不带service的多一个自动添加服务的功能,你可以理解为有一个开机自启动Redis的功能,因为我们不需要这个功能,所以我选择下载的是第四个
下载好之后解压到一个文件夹里面,解压后的文件大概长这样
在这里插入图片描述

想要使用redis的服务,首先就是要运行起服务端
打开黑窗口运行:redis-server.exe redis.conf出现下图的图案就算运行服务端成功了
两个注意点

  1. 运行命令的文件路径需要是你解压后的文件夹路径,例如我这里就是:C:\software\redis,运行时需要换成你自己的路径否则会报命令不存在的错误
  2. 运行完之后这个黑窗口不能关,关了就取消服务了,就会访问不到redis了

解决方法
要解决上面提到的找不到命令的问题,除了在正确的路径下运行命令外,还有一个很常用的方法,就是配置一下环境变量,这个很简单,这里就不讲了,如果有不懂的同学可以在评论区问我,到时候再解答

在这里插入图片描述

开始实践

如果你跟着步骤看到这里,你应该已经拥有了一个编译器(写代码的地方),Node.js环境(运行后端服务的地方),redis数据库(存储数据的地方)

有了这些之后可以正式开始写代码了!

1.搭建项目

找一个存放项目的文件夹,新建chat-room文件夹,用来存放项目

在这里插入图片描述

在trea里面打开chat-room文件夹(使用vscode打开也是一样的,不影响项目运行)

在这里插入图片描述

刚开始什么都没有,让我们先初始化一个package.json配置文件用来管理项目

使用npm工具来初始化package.json,这里我们使用npm init -y来快捷创建默认的配置文件

在这里插入图片描述

执行完命令后你的工作区应该长这个样子,并且内容如下

在这里插入图片描述

我们的聊天室前端部分pug语法渲染要和express框架结合,所以我们先下载express框架,引入pug语法,使用npm install express pug nodemon下载依赖(nodemon是我后来加的,下面图片没有,是可选的,推荐下载可以用来实现node服务运行的时候热重载

在这里插入图片描述

按照下图,创建项目的基础结构

在这里插入图片描述

现在我们逐个文件填充代码,接下来的文件可直接复制使用,如果有疑问可直接复制给ai讲解代码,学会运用ai是当下我们程序员要培养的基本素养了

app.js:node服务启动文件

// 引入 express 模块

const express = require("express");
const path = require("path");

// 创建应用实例
const app = express();

// 1. 引入静态资源
app.get(express.static(path.join(__dirname, "public")));
// 2. 模板引擎
app.set("view engine", "pug");
// 3. 视图目录
app.set("views", path.join(__dirname, "views"));

// 拦截/地址路由,渲染登录页面
app.use("/", (req, res) => {
  res.render("login", {
    title: "请登录",
  });
});

// 监听listen
app.listen(3000, () => {
  console.log("服务器启动成功");
});

layout.pug:布局文件

doctype html
html
  head
    meta(charset='utf-8')
    meta(name='viewport', content='width=device-width, initial-scale=1.0')
    title= title 
    link(rel='stylesheet', href='/css/style.css')
  body
    block content

login.pug:登录页面

extend layout 
block content 
  .container
    .login-container 
      h1= title
      form(method='post', action='/login' id='loginForm')
        input(type='text',name='username',placeholder='请输入用户名',required,autocomplete='off')
        button(type='submit') 进入聊天室
    #error-message 
  script(src='/js/login.js')

style.css:样式文件(完整版的)

:root {
  --primary-color: #4caf50;
  --shadow-color: rgba(0, 0, 0, 0.1);
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: #f5f5f5;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.container {
  width: 100%;
  max-width: 400px;
  padding: 20px;
}

.login-container {
  background: white;
  padding: 2rem;
  border-radius: 8px;
  box-shadow: 0 4px 6px var(--shadow-color);
}

h1 {
  color: var(--primary-color);
  text-align: center;
  margin-bottom: 1.5rem;
  font-size: 1.8rem;
}

form {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

input {
  padding: 12px;
  border: 2px solid #e0e0e0;
  border-radius: 4px;
  font-size: 16px;
  transition: border-color 0.3s;
}

input:focus {
  outline: none;
  border-color: var(--primary-color);
}

button {
  background: var(--primary-color);
  color: white;
  border: none;
  padding: 12px;
  border-radius: 4px;
  font-size: 16px;
  cursor: pointer;
  transition: opacity 0.3s;
}

button:hover {
  opacity: 0.9;
}

#error-message {
  display: none;
  padding: 10px;
  margin-top: 15px;
  border-radius: 4px;
  background-color: #fef2f2;
  border: 1px solid #fecaca;
  color: #ef4444;
  transition: opacity 0.3s ease;
}

#error-message.show {
  display: block;
  animation: fadeIn 0.3s ease;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

写完这四个文件并且package.json中配置好启动命令,就可以启动项目了,我这里用的nodemon启动的项目,没有的同学可以把start后面的命令改为node app.js,但是还是推荐先下载一下npm install nodemon使用nodemon启动项目

在这里插入图片描述
在这里插入图片描述

2.准备登录接口和静态聊天室

现在开始准备表单提交逻辑

public/js/login.js

// 获取表单元素,监听提交事件
document.getElementById("loginForm").addEventListener("submit", async (e) => {
  // 阻止表单默认提交
  e.preventDefault();

  const formData = new FormData(e.target);
  const username = formData.get("username");

  try {
    // 发送登录请求

    const response = await fetch("/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ username }),
    });

    const data = await response.json();

    if (!response.ok) {
      throw new Error(data.error || "登录失败");
    }

    // 登录成功,重定向到聊天页面
    if (data.success) {
      window.location.href = "/chat";
    }
  } catch (error) {
    showError(error.message);
  }
});

function showError(message) {
  // 显示错误提示
  const errorDiv = document.getElementById("error-message");
  errorDiv.textContent = message;
  errorDiv.classList.add("show");

  // 3秒后自动隐藏错误提示
  setTimeout(() => {
    errorDiv.classList.remove("show");
  }, 3000);
}

下载两个依赖npm install jsonwebtoken dotenv用来提高登录功能的健壮性,根目录下新建.env文件,更新根目录下的app.js添加登录接口

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// 引入 express 模块
const express = require("express");
const path = require("path");
const jwt = require("jsonwebtoken");
require("dotenv").config();

// 创建应用实例
const app = express();

// 1. 引入静态资源
app.use(express.static(path.join(__dirname, "public")));
// 2. 模板引擎
app.set("view engine", "pug");
// 3. 视图目录
app.set("views", path.join(__dirname, "views"));
// 4. 解析 JSON 请求体
app.use(express.json());

// 拦截/地址路由,渲染登录页面
app.get("/", (req, res) => {
  res.render("login", {
    title: "请登录",
  });
});

// 处理登录接口
app.post("/login", async (req, res) => {
  try {
    const { username } = req.body;
    if (!username) {
      return res.status(400).send("用户名不能为空");
    }

    // 生成 token
    const token = jwt.sign({ username }, process.env.JWT_SECRET, {
      expiresIn: "2h",
    });

    res.cookie("token", token, { httpOnly: true });
    res.json({
      success: true,
    });
  } catch (e) {
    console.error("登录失败:", e);
    res.status(500).send("服务器错误");
  }
});

// 监听listen
app.listen(3000, () => {
  console.log("服务器启动成功");
});

现在可以登录了,但是还需要准备chat页面和对应的鉴权逻辑

新建views/chat.pug

extend layout 
block content  
  .container 
    .chat-container
      #messages 
      form#form.chat-form
        input#input(type='text',placeholder='请输入消息...',autocomplete='off')
        button(type='submit') 发送
  script(src='/js/chat.js')

下载 npm install cookie-parser依赖
在这里插入图片描述
更新app.js

// 引入 express 模块
const express = require("express");
const path = require("path");
const jwt = require("jsonwebtoken");
require("dotenv").config();
const cookieParser = require("cookie-parser");

// 创建应用实例
const app = express();

// 1. 引入静态资源
app.use(express.static(path.join(__dirname, "public")));
// 2. 模板引擎
app.set("view engine", "pug");
// 3. 视图目录
app.set("views", path.join(__dirname, "views"));
// 4. 解析 JSON 请求体
app.use(express.json());
// 5. 解析 Cookie
app.use(cookieParser());

// 拦截/地址路由,渲染登录页面
app.get("/", (req, res) => {
  res.render("login", {
    title: "请登录",
  });
});

// 处理登录接口
app.post("/login", async (req, res) => {
  try {
    const { username } = req.body;
    if (!username) {
      return res.status(400).send("用户名不能为空");
    }

    // 生成 token
    const token = jwt.sign({ username }, process.env.JWT_SECRET, {
      expiresIn: "2h",
    });

    res.cookie("token", token, { httpOnly: true });
    res.json({
      success: true,
    });
  } catch (e) {
    console.error("登录失败:", e);
    res.status(500).send("服务器错误");
  }
});
// 处理聊天室路由
app.use("/chat", mustAuth, (_, res) =>
  res.render("chat", { title: "实时聊天室" })
);

/* ====== 中间件:JWT 校验 ====== */
function mustAuth(req, res, next) {
  const token = req.cookies.token;
  if (!token) return res.redirect("/");
  try {
    jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.redirect("/");
  }
}

// 监听listen
app.listen(3000, () => {
  console.log("服务器启动成功");
});

3.websocket实现实时通讯聊天室

现在登录功能和静态的聊天室页面已经准备好了,接下来准备设置websocket,实现实时通讯

设置websocket服务端:根目录下新建ws-server.js,并引入npm install ws依赖
新增ws-server.js

引入ws依赖

ws.server.js

const WebSocket = require("ws");
const jwt = require("jsonwebtoken");

// 定义用户状态常量
const USER_STATUS = {
  ONLINE: 1,
  OFFLINE: 2,
};

// 启动 WebSocket 服务
module.exports = (server) => {
  const wss = new WebSocket.Server({
    server,
    // 握手阶段拦截
    verifyClient: (info, cb) => {
      const cookies = info.req.headers.cookie || "";
      console.log("cookies", cookies);
      // 从 cookies 中提取 token
      const token = cookies.match(/token=([^;]+)/)?.[1];
      if (!token) return cb(false, 401, "Missing token");

      try {
        jwt.verify(token, process.env.JWT_SECRET);
        cb(true); // 放行
      } catch {
        cb(false, 401, "Invalid token");
      }
    },
  });

  // 监听客户端连接
  wss.on("connection", (ws, req) => {
    console.log("客户端连接成功");
    // 解析用户名
    const cookies = req.headers.cookie || "";
    const token = cookies.match(/token=([^;]+)/)?.[1];
    ws.username = "匿名用户";
    if (token) {
      try {
        const payload = jwt.verify(token, process.env.JWT_SECRET);
        ws.username = payload.username || ws.username;
      } catch (e) {
        // token无效,保持匿名
        console.error("Token 验证失败:", e);
      }
    }

    // 广播欢迎消息给所有客户端
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(
          JSON.stringify({
            type: "sys",
            text: `欢迎 ${ws.username} 进入聊天室`,
            number: wss.clients.size,
          })
        );
      }
    });

    // 广播
    ws.on("message", (data) => {
      console.log("收到消息:", data);
      const message = JSON.parse(data);

      // 广播给所有客户端
      wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN) {
          client.send(
            JSON.stringify({
              type: "chat",
              from: ws.username,
              text: message.text,
              time: new Date().toLocaleString(),
              number: wss.clients.size,
            })
          );
        }
      });
    });

    // 监听断开连接
    ws.on("close", async () => {
      console.log(`用户 ${ws.username} 断开连接`);

      try {
        if (ws.username && ws.username !== "匿名用户") {
          // 广播用户离开消息
          wss.clients.forEach((client) => {
            if (client !== ws && client.readyState === WebSocket.OPEN) {
              client.send(
                JSON.stringify({
                  type: "sys",
                  text: `${ws.username} 离开了聊天室`,
                  number: wss.clients.size, // 减去即将断开的连接
                })
              );
            }
          });
        }
      } catch (error) {
        console.error(`更新用户 ${ws.username} 状态失败:`, error);
      }
    });
  });
};

准备websocket客户端,public/js目录下新建chat.js

public/js/chat.js

const socket = new WebSocket(`ws://${location.host}`);
const messages = document.getElementById("messages");
const form = document.getElementById("form");
const input = document.getElementById("input");

// 监听消息,并展示
socket.onmessage = (event) => {
  const { type, from, text, time, number } = JSON.parse(event.data);
  const div = document.createElement("div");
  div.innerHTML =
    type === "sys"
      ? `<em>${text},当前在线人数: ${number}</em>`
      : `<strong>${from}</strong>: <small>${time}</small>:${text}`;
  messages.appendChild(div);
  messages.scrollTop = messages.scrollHeight; // 滚动到底部
};

// 发送消息
form.addEventListener("submit", (e) => {
  e.preventDefault();
  const message = input.value.trim();
  if (!message) return;

  try {
    socket.send(JSON.stringify({ text: message }));
    input.value = ""; // 清空输入框
  } catch (err) {
    console.error("发送消息失败:", err);
    alert("发送失败,请检查网络连接");
  }
});

更新app.js,建立websocket连接

// 引入 express 模块
const express = require("express");
const path = require("path");
const jwt = require("jsonwebtoken");
require("dotenv").config();
const cookieParser = require("cookie-parser");

// 创建应用实例
const app = express();

// 1. 引入静态资源
app.use(express.static(path.join(__dirname, "public")));
// 2. 模板引擎
app.set("view engine", "pug");
// 3. 视图目录
app.set("views", path.join(__dirname, "views"));
// 4. 解析 JSON 请求体
app.use(express.json());
// 5. 解析 Cookie
app.use(cookieParser());

// 拦截/地址路由,渲染登录页面
app.get("/", (req, res) => {
  res.render("login", {
    title: "请登录",
  });
});

// 处理登录接口
app.post("/login", async (req, res) => {
  try {
    const { username } = req.body;
    if (!username) {
      return res.status(400).send("用户名不能为空");
    }

    // 生成 token
    const token = jwt.sign({ username }, process.env.JWT_SECRET, {
      expiresIn: "2h",
    });

    res.cookie("token", token, { httpOnly: true });
    res.json({
      success: true,
    });
  } catch (e) {
    console.error("登录失败:", e);
    res.status(500).send("服务器错误");
  }
});

// 处理聊天页面路由
app.use("/chat", mustAuth, (_, res) =>
  res.render("chat", { title: "实时聊天室" })
);

/* ====== 中间件:JWT 校验 ====== */
function mustAuth(req, res, next) {
  const token = req.cookies.token;
  if (!token) return res.redirect("/");
  try {
    jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.redirect("/");
  }
}

app.use((err, req, res, next) => {
  // 错误处理中间件
  console.error(err);
});

// 监听listen
app.listen(3000, () => {
  console.log("服务器启动成功");
});

// 启动 WebSocket 服务
require("./ws-server")(server);

到这里我们的实时聊天室已经实现了,项目根目录下运行npm run start就可以把我们的聊天室在本地localhost:3000跑起来了

聊天室预览

4.项目引入Redis,禁止重复登录

虽然我们的聊天室已经完成了,但是基础好的同学就会发现,我们的聊天室对于登录的账号是没有限制的,所以就可能出现同一个账户重复登录的情况

重复登录情况
重复登录

现在我们要借用redis记录登录状态,进而防止重复登录

本地启动redis服务端

win+R打开运行窗口输入cmd打开黑窗口

打开黑窗口
在安装redis的目录下输入redis-server.exe redis.conf启动redis服务,启动完服务后,这个黑窗口不能关,一关,redis就断开连接了
启动redis

我们根目录下新建configs/redis.js,同时下载npm install ioredis用来在项目中配置连接redis

新建redis.js

configs/redis.js

// 引入 ioredis 模块
const Redis = require("ioredis");

// 连接 Redis 数据库

const redis = new Redis({
  host: process.env.REDIS_HOST || "127.0.0.1",
  port: process.env.REDIS_PORT || 6379,
  password: process.env.REDIS_PASSWORD || "123456",
});

// 监听 Redis 连接事件

redis.on("connect", () => {
  console.log("Redis 连接成功");
});
// 监听 Redis 错误事件

redis.on("error", (err) => {
  console.error("Redis 连接失败:", err);
});
// 导出 Redis 实例

module.exports = redis;

更新app.js

// 引入 express 模块
const express = require("express");
const path = require("path");
const jwt = require("jsonwebtoken");
require("dotenv").config();
const cookieParser = require("cookie-parser");
const redis = require("./configs/redis"); // 新引入redis实例

// 创建应用实例
const app = express();

// 1. 引入静态资源
app.use(express.static(path.join(__dirname, "public")));
// 2. 模板引擎
app.set("view engine", "pug");
// 3. 视图目录
app.set("views", path.join(__dirname, "views"));
// 4. 解析 JSON 请求体
app.use(express.json());
// 5. 解析 Cookie
app.use(cookieParser());

// 拦截/地址路由,渲染登录页面
app.get("/", (req, res) => {
  res.render("login", {
    title: "请登录",
  });
});

// 处理登录接口
app.post("/login", async (req, res) => {
  try {
    const { username } = req.body;
    if (!username) {
      return res.status(400).send("用户名不能为空");
    }

    // 将用户信息存储到 Redis
    const userKey = `user:${username}`;
    if ((await redis.hget(userKey, "status")) === "1") {
      return res.status(400).json({ error: "用户已登录,请勿重复登录" });
    }

    // 生成 token
    const token = jwt.sign({ username }, process.env.JWT_SECRET, {
      expiresIn: "2h",
    });

    // 如果用户已存在,更新状态
    await redis.hset(userKey, "status", 1);
    // 设置过期时间(2小时)
    await redis.expire(userKey, 7200);

    res.cookie("token", token, { httpOnly: true });
    res.json({
      success: true,
    });
  } catch (e) {
    console.error("登录失败:", e);
    res.status(500).send("服务器错误");
  }
});

// 处理聊天页面路由
app.use("/chat", mustAuth, (_, res) =>
  res.render("chat", { title: "实时聊天室" })
);

/* ====== 中间件:JWT 校验 ====== */
function mustAuth(req, res, next) {
  const token = req.cookies.token;
  if (!token) return res.redirect("/");
  try {
    jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.redirect("/");
  }
}

app.use((err, req, res, next) => {
  // 错误处理中间件
  console.error(err);
});

// 监听listen
const server = app.listen(3000, () => {
  console.log("服务器启动成功");
});

// 启动 WebSocket 服务
require("./ws-server")(server);

更新完app.js后,你再运行项目,就会发现,不能同时登录同一个账户了,但是还有一个问题,那就是即使我们退出一个账号,想再次登录的时候仍然登录不了!这是因为没有做用户离开聊天室时,对登录状态更改的逻辑!

阻止重复登录

更新根目录下的ws-server.js处理用户聊天室后,取消登录状态

ws-server.js

const WebSocket = require("ws");
const jwt = require("jsonwebtoken");
const redis = require("./configs/redis");

// 定义用户状态常量
const USER_STATUS = {
  ONLINE: 1,
  OFFLINE: 2,
};

// 启动 WebSocket 服务
module.exports = (server) => {
  const wss = new WebSocket.Server({
    server,
    // 握手阶段拦截
    verifyClient: (info, cb) => {
      const cookies = info.req.headers.cookie || "";
      console.log("cookies", cookies);
      // 从 cookies 中提取 token
      const token = cookies.match(/token=([^;]+)/)?.[1];
      if (!token) return cb(false, 401, "Missing token");

      try {
        jwt.verify(token, process.env.JWT_SECRET);
        cb(true); // 放行
      } catch {
        cb(false, 401, "Invalid token");
      }
    },
  });

  // 监听客户端连接
  wss.on("connection", (ws, req) => {
    console.log("客户端连接成功");
    // 解析用户名
    const cookies = req.headers.cookie || "";
    const token = cookies.match(/token=([^;]+)/)?.[1];
    ws.username = "匿名用户";
    if (token) {
      try {
        const payload = jwt.verify(token, process.env.JWT_SECRET);
        ws.username = payload.username || ws.username;

        // 设置用户在线状态
        const userKey = `user:${ws.username}`;
        redis
          .hset(userKey, "status", USER_STATUS.ONLINE)
          .catch((err) => console.error("设置用户在线状态失败:", err));
      } catch (e) {
        // token无效,保持匿名
        console.error("Token 验证失败:", e);
      }
    }

    // 广播欢迎消息给所有客户端
    wss.clients.forEach((client) => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(
          JSON.stringify({
            type: "sys",
            text: `欢迎 ${ws.username} 进入聊天室`,
            number: wss.clients.size,
          })
        );
      }
    });

    // 广播
    ws.on("message", (data) => {
      console.log("收到消息:", data);
      const message = JSON.parse(data);

      // 广播给所有客户端
      wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN) {
          client.send(
            JSON.stringify({
              type: "chat",
              from: ws.username,
              text: message.text,
              time: new Date().toLocaleString(),
              number: wss.clients.size,
            })
          );
        }
      });
    });

    // 监听断开连接
    ws.on("close", async () => {
      console.log(`用户 ${ws.username} 断开连接`);

      try {
        if (ws.username && ws.username !== "匿名用户") {
          const userKey = `user:${ws.username}`;
          // 更新用户状态为离线
          await redis.hset(userKey, "status", USER_STATUS.OFFLINE);
          console.log(`用户 ${ws.username} 状态已更新为离线`);

          // 广播用户离开消息
          wss.clients.forEach((client) => {
            if (client !== ws && client.readyState === WebSocket.OPEN) {
              client.send(
                JSON.stringify({
                  type: "sys",
                  text: `${ws.username} 离开了聊天室`,
                  number: wss.clients.size, // 减去即将断开的连接
                })
              );
            }
          });
        }
      } catch (error) {
        console.error(`更新用户 ${ws.username} 状态失败:`, error);
      }
    });
  });
};

未完待续

到此为止,我们已经拥有了一个有一定登录权限控制的实时聊天室了
但是我想我们的追求不应该止步于此,后续我会持续更新几个新功能
感兴趣的小伙伴可以适时再来查看这篇博客
  • 2025.8.17:完成UI样式更新
  • 2025.8.20:新增好友功能,实现单聊

有问题的同学欢迎在评论区讨论学习!!!

本篇博客涉及到的学习资源如下,想深入学习的同学可自行阅读:

WebSocket:https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
Express:https://express.js.cn/en/guide/routing.html
Redis:https://www.runoob.com/redis/redis-tutorial.html


网站公告

今日签到

点亮在社区的每一天
去签到