本教程旨在为你提供一个完整的、可复制的、从零到一的实时聊天应用构建指南。我们将使用 Node.js 作为服务器端环境,并利用 WebSocket 协议实现客户端与服务器之间的双向实时通信。本教程不仅包含所有必要的代码,还将详细解释每个技术环节的“为什么”和“如何做”,帮助你建立起扎实的知识体系,从而能够举一反三,应对更复杂的项目。

目录


1. 技术栈与核心概念

本项目旨在利用 WebSocket 协议,解决传统 HTTP 协议在实时通信中的局限性。

  • HTTP 的局限性: HTTP 是一种无状态的单向协议,每次通信都由客户端发起请求,服务器响应。这种模式在需要服务器主动向客户端推送数据时(例如聊天消息),效率极低,通常需要通过轮询等方式来模拟,从而产生大量的性能开销和延迟。
  • WebSocket 的优势: WebSocket 建立在 TCP 协议之上,一旦握手成功,它会创建一个持久化的、全双工(双向)连接。这意味着服务器和客户端可以随时互相发送数据,无需像 HTTP 那样频繁地建立和关闭连接,大大减少了延迟和网络开销。

我们的项目将由以下技术栈构成:

  • 服务器端:
    • Node.js: 一个基于 V8 引擎的 JavaScript 运行时环境,非常适合构建高性能的网络应用。
    • ws 库: 一个轻量级、高性能的 WebSocket 服务器和客户端库。
  • 客户端:
    • HTML: 构建聊天界面的骨架。
    • CSS: 美化界面,使其更具吸引力。
    • JavaScript: 处理用户交互,并使用浏览器内置的 WebSocket 对象与服务器通信。

2. 环境准备与项目初始化

2.1 安装 Node.js

在开始之前,你需要在你的电脑上安装 Node.js。这是运行服务器端代码的必要环境。

  1. 访问 Node.js 官方网站
  2. 下载并安装最新稳定版本(LTS 版本)。
  3. 安装完成后,打开你的终端或命令提示符,输入以下命令验证安装是否成功:
node -v
npm -v

如果命令能够返回版本号,则说明 Node.js 和其包管理器 npm 已经成功安装。

2.2 项目初始化

我们将项目组织成一个清晰的结构,方便管理客户端和服务器端代码。

  1. 创建一个名为 websocket-chat-app 的新文件夹。
  2. 进入该文件夹,并在终端中运行以下命令来初始化 Node.js 项目:
npm init -y

这个命令会快速生成一个默认的 package.json 文件,用于记录项目的元数据和依赖。

  1. 创建以下文件和文件夹,使项目结构如下所示:
websocket-chat-app/
├── node_modules/         # npm 自动创建,用于存放依赖包
├── package.json          # npm 项目配置文件
├── server.js             # 服务器端核心代码
└── public/               # 客户端静态文件目录
    ├── index.html        # 聊天界面 HTML
    ├── style.css         # 聊天界面 CSS
    └── client.js         # 客户端 JavaScript 逻辑

3. 服务器端实现 (Node.js & ws)

3.1 安装依赖

websocket-chat-app 根目录下,打开终端,安装 ws 库:

npm install ws
3.2 编写服务器端代码 server.js

在项目根目录下创建 server.js 文件,并复制以下代码:

// server.js

// 引入所需的库:http 用于创建 HTTP 服务器,ws 用于创建 WebSocket 服务器
const http = require('http');
const WebSocket = require('ws');
const path = require('path');
const fs = require('fs');

// 存储所有已连接的客户端
const clients = new Set();

// 创建 HTTP 服务器,用于提供静态文件服务
const server = http.createServer((req, res) => {
    // 简单的路由,将所有请求映射到 public 文件夹下的文件
    const filePath = path.join(__dirname, 'public', req.url === '/' ? 'index.html' : req.url);

    // 读取并发送文件
    fs.readFile(filePath, (err, data) => {
        if (err) {
            res.writeHead(404, { 'Content-Type': 'text/plain' });
            res.end('404 Not Found');
            return;
        }

        // 设置正确的 MIME 类型
        let contentType = 'text/plain';
        if (filePath.endsWith('.html')) contentType = 'text/html';
        if (filePath.endsWith('.css')) contentType = 'text/css';
        if (filePath.endsWith('.js')) contentType = 'text/javascript';

        res.writeHead(200, { 'Content-Type': contentType });
        res.end(data);
    });
});

// 在 HTTP 服务器上创建 WebSocket 服务器
const wss = new WebSocket.Server({ server });

// 监听 WebSocket 连接事件
wss.on('connection', ws => {
    console.log('一个新的客户端已连接');
    
    // 将新连接的客户端添加到集合中,以便后续广播消息
    clients.add(ws);

    // 监听客户端发来的消息
    ws.on('message', message => {
        const receivedMessage = message.toString();
        console.log(`收到消息: ${receivedMessage}`);
        
        // 遍历所有客户端,将消息广播出去
        clients.forEach(client => {
            // 确保连接是开放状态,并且不是发送者自己
            if (client !== ws && client.readyState === WebSocket.OPEN) {
                client.send(receivedMessage);
            }
        });
    });

    // 监听连接关闭事件
    ws.on('close', () => {
        console.log('一个客户端已断开连接');
        // 从集合中移除断开的客户端
        clients.delete(ws);
    });

    // 监听错误事件
    ws.on('error', error => {
        console.error('WebSocket 发生错误:', error);
    });
});

// 启动服务器并监听端口
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
    console.log(`服务器正在 http://localhost:${PORT} 运行`);
});
3.3 核心逻辑解析
  • const wss = new WebSocket.Server({ server }); 我们将 WebSocket 服务器挂载到 HTTP 服务器上,这样它们可以共享同一个端口。
  • wss.on('connection', ws => { ... }); 这是 WebSocket 服务器的核心入口。当一个新客户端成功建立连接时,这个事件会被触发,ws 参数就是该客户端的实例。
  • clients.add(ws); 我们使用一个 Set 来管理所有在线用户,这是一种高效的数据结构,可以避免重复。
  • ws.on('message', message => { ... }); 当服务器从客户端接收到消息时触发。我们在这里遍历 clients 集合,将消息发送给所有其他客户端,从而实现消息的广播。
  • message.toString() 从 WebSocket 接收到的数据通常是 Buffer 类型,需要使用 .toString() 方法将其转换为可读的字符串。

4. 客户端实现 (HTML, CSS, JavaScript)

4.1 HTML 页面结构 index.html

public 文件夹下创建 index.html,这是用户将看到的界面。

<!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>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="chat-container">
        <h1 class="chat-title">Node.js WebSocket 聊天室</h1>
        <div class="chat-window" id="chat-window">
            </div>
        <form id="message-form" class="message-form">
            <input type="text" id="message-input" class="message-input" placeholder="输入消息..." autocomplete="off">
            <button type="submit" class="send-button">发送</button>
        </form>
    </div>

    <script src="client.js"></script>
</body>
</html>
4.2 CSS 样式美化 style.css

public 文件夹下创建 style.css,为界面添加样式。

/* public/style.css */
body {
    font-family: 'Segoe UI', Arial, sans-serif;
    background-color: #f4f7f9;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
}

.chat-container {
    width: 90%;
    max-width: 600px;
    background-color: #fff;
    border-radius: 12px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.chat-title {
    background-color: #4a90e2;
    color: #fff;
    padding: 18px;
    margin: 0;
    text-align: center;
    font-size: 22px;
}

.chat-window {
    flex-grow: 1;
    padding: 20px;
    overflow-y: auto;
    max-height: 400px;
    background-color: #fafafa;
}

.message {
    margin-bottom: 15px;
    padding: 12px 15px;
    border-radius: 10px;
    word-wrap: break-word;
    line-height: 1.5;
}

.message-mine {
    background-color: #eaf2ff;
    text-align: right;
    margin-left: 20%;
}

.message-other {
    background-color: #f0f0f0;
    text-align: left;
    margin-right: 20%;
}

.message-form {
    display: flex;
    padding: 15px;
    background-color: #fff;
    border-top: 1px solid #eee;
}

.message-input {
    flex-grow: 1;
    padding: 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    margin-right: 10px;
    font-size: 16px;
    outline: none;
    transition: border-color 0.3s;
}

.message-input:focus {
    border-color: #4a90e2;
}

.send-button {
    padding: 12px 25px;
    border: none;
    background-color: #4a90e2;
    color: white;
    border-radius: 6px;
    cursor: pointer;
    font-size: 16px;
    transition: background-color 0.3s;
}

.send-button:hover {
    background-color: #357abd;
}
4.3 JavaScript 客户端逻辑 client.js

public 文件夹下创建 client.js,它将负责所有客户端的交互和通信逻辑。

// public/client.js

// 获取 DOM 元素
const chatWindow = document.getElementById('chat-window');
const messageForm = document.getElementById('message-form');
const messageInput = document.getElementById('message-input');

// 1. 建立 WebSocket 连接
// 注意:如果服务器运行在不同地址,请修改此处
const ws = new WebSocket('ws://localhost:3000');

// 2. 监听连接成功事件
ws.addEventListener('open', () => {
    console.log('已成功连接到服务器!');
    displayMessage('系统通知:您已成功加入聊天室。');
});

// 3. 监听接收消息事件
ws.addEventListener('message', event => {
    // 收到消息后,将其添加到聊天窗口
    displayMessage(event.data, 'other');
});

// 4. 监听连接关闭事件
ws.addEventListener('close', () => {
    console.log('与服务器的连接已断开。');
    displayMessage('系统通知:您已断开连接。');
});

// 5. 监听错误事件
ws.addEventListener('error', error => {
    console.error('WebSocket 发生错误:', error);
    displayMessage('系统通知:连接发生错误。');
});

// 6. 监听表单提交事件,发送消息
messageForm.addEventListener('submit', event => {
    event.preventDefault(); // 阻止表单默认提交行为(页面刷新)

    const message = messageInput.value.trim();
    if (message !== '') {
        ws.send(message); // 使用 ws.send() 发送消息给服务器
        displayMessage(message, 'mine'); // 在本地立即显示自己的消息
        messageInput.value = ''; // 清空输入框
    }
});

// 辅助函数:创建并显示消息元素
function displayMessage(text, type = 'system') {
    const messageElement = document.createElement('div');
    messageElement.classList.add('message');
    
    // 根据消息类型添加不同的样式类
    if (type === 'mine') {
        messageElement.classList.add('message-mine');
        messageElement.textContent = `你: ${text}`;
    } else if (type === 'other') {
        messageElement.classList.add('message-other');
        messageElement.textContent = `匿名用户: ${text}`;
    } else { // system type
        messageElement.textContent = text;
        messageElement.style.color = '#777';
        messageElement.style.fontStyle = 'italic';
    }

    chatWindow.appendChild(messageElement);
    // 自动滚动到最新消息
    chatWindow.scrollTop = chatWindow.scrollHeight;
}

5. 运行与测试

5.1 启动服务器

确保你位于 websocket-chat-app 根目录下,在终端中执行以下命令:

node server.js

如果一切顺利,你将看到以下输出:

服务器正在 http://localhost:3000 运行
5.2 访问与测试
  1. 打开你的浏览器,访问 http://localhost:3000
  2. 在新的标签页中再次打开 http://localhost:3000
  3. 在任一聊天窗口中输入消息并发送。
  4. 观察消息是否能够即时同步到另一个窗口,无需任何页面刷新。

6. 功能扩展与最佳实践

6.1 用户名与消息格式化

当前的聊天是匿名的,所有人都显示为“匿名用户”。一个简单的改进是,在用户输入消息前,先弹出一个对话框让其输入用户名。

实现思路:

  • client.js 中,添加一个变量来存储用户名。
  • 在连接建立后,使用 prompt() 弹窗让用户输入用户名。
  • ws.send() 消息时,将用户名和消息封装成一个 JSON 对象,例如 JSON.stringify({ user: '张三', text: '你好' })
  • server.js 中,收到消息后,使用 JSON.parse() 解析消息,然后重新格式化并广播。
6.2 鲁棒性增强
  • 服务器端输入验证: 永远不要信任客户端发来的数据。在 server.js 中,对接收到的消息进行长度、内容等校验,以防止恶意输入或过长消息导致的服务器崩溃。
  • 断线重连:client.js 中,当 ws.addEventListener('close', ...) 被触发后,可以设置一个延时器,在一段时间后自动尝试重新连接,提高用户体验。
  • 错误处理: 添加更详细的错误日志,帮助你定位问题。在服务器端,使用 try...catch 块来处理可能发生的异常。
6.3 未来展望
  • 多聊天室功能: 通过为每个连接分配一个 roomId,服务器端可以维护一个房间-客户端的映射表,从而实现消息的定向广播,创建多个独立的聊天室。
  • 用户列表: 在服务器端维护一个在线用户列表,并在用户加入或离开时,向所有客户端广播更新后的列表。
  • 消息持久化: 使用数据库(如 SQLite, MongoDB)来存储聊天记录,这样新加入的用户就可以看到历史消息。
  • 表情和图片: 允许用户发送更多样化的内容,例如表情包或图片。这需要你在 WebSocket 的基础上定义更丰富的消息协议。

恭喜你,你已经完成了你的第一个实时聊天应用!现在,你可以基于这个坚实的基础,继续探索更复杂的功能,将其打造成一个真正强大且实用的工具。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐