Node.js + Socket.io 搭建局域网聊天室教程

作者:服务小站

时间:2026-11-14

浏览:271

本教程详细讲解如何基于 Node.js 与 Socket.io 搭建一个跨设备的局域网即时聊天室,包含环境准备、服务端与客户端完整代码实现、局域网 IP 与端口放行排查,以及常见连接故障的解决方案。

文章目录
在同一局域网(如家用 Wi-Fi 或办公室内网)下,多台设备之间往往需要一种免外网依赖、免服务器公网 IP 的轻量即时通讯方案。本文将基于 Node.js 与 Socket.io,从零搭建一个跨多终端(PC、手机、平板)的局域网即时聊天室,详细讲解底层握手机制、前后端完整代码、局域网 IP 监听要点以及常见连接故障的排查方法。
Node.js + Socket.io 搭建局域网聊天室教程
局域网实时即时通讯网络拓扑:一台 Node.js 宿主机向同网段各接入终端分发 WebSocket 广播

一、局域网即时通信的核心工作机制

传统的 HTTP 协议基于“请求-响应”单向模型,客户端若想获知其他设备发送的最新消息,只能通过高频的定时轮询(Polling)。这种方式不仅占用大量不必要的 CPU 与无线带宽,且存在固有的响应延迟。局域网通信的核心目标是低延迟、低资源占用与高吞吐量,因此全双工通信(Full-Duplex)是解决此场景的理想方案。

Socket.io 并非单纯的 WebSocket 封装,而是一个由 Engine.io 和 Socket.io 两层构成的健壮实时通讯框架。在客户端初次连接时,Socket.io 会先通过普通的 HTTP 长轮询(Long-Polling)与服务端完成会话协商,确认通信能力并分配 sid(Session ID);紧接着,如果客户端和网关支持,它会立即通过 HTTP 101 Switching Protocols 将传输层平滑升级为原生的 WebSocket 双向信道。

Node.js + Socket.io 搭建局域网聊天室教程
图 1:Socket.io 握手协商、协议平滑升级与双向长连接通信信道模型

一旦信道升级完成,双向传输帧便不再包含冗长的 HTTP 请求头。当局域网内的某台手机向 Node.js 节点发射一个 chat:message 自定义事件时,Node.js 可以在极短的毫秒内将其广播(Broadcast)至局域网连接池内的所有套接字,实现多屏即刻同步。

二、搭建准备与基础环境初始化

在开始编码前,请确保开发主机已安装了 Node.js 运行时(推荐 Node.js v16、v18 或更高 LTS 版本)。可以通过终端执行 node -v 与 npm -v 确认环境就绪。

整个实现无需复杂的打包工具(如 Webpack 或 Vite),仅需一个极简的 Express 静态服务器与 Socket.io 配合即可。搭建步骤可以归纳为规范的五步操作流程:

Node.js + Socket.io 搭建局域网聊天室教程
图 2:从工程初始化到跨设备验证的五步标准实施路径

首先,在本地创建一个空目录并完成 npm 项目初始化:

# 创建项目目录并进入
mkdir lan-chat-room
cd lan-chat-room

# 初始化 package.json 文件(全部采用默认值)
npm init -y

# 安装 Express 服务端框架及 Socket.io 核心库
npm install express socket.io

安装完成后,项目的文件结构规划如下:

lan-chat-room/
├── node_modules/         # 第三方依赖库
├── package.json          # 项目配置文件
├── server.js             # 服务端启动主文件
└── public/               # 静态资源目录(托管客户端前端界面)
    └── index.html        # 聊天室前端网页

三、服务端编写:HTTP 服务与套接字挂载

服务端的任务包括两项:一是使用 Express 托管 public 目录下的静态网页;二是将 Socket.io 挂载到原生的 Node.js HTTP 服务器实例上,并监听自定义事件进行消息广播。

局域网监听的核心关键:在调用 server.listen() 时,必须显式监听 0.0.0.0 或省略 host 参数(默认即 0.0.0.0)。千万不要写成 127.0.0.1 或 localhost。如果写成 127.0.0.1,操作系统只会将套接字绑定在回环网卡(Loopback Adapter)上,局域网内的其他设备向此主机的物理 IP 发起连接时,将被直接拒绝。

在项目根目录下新建 server.js 文件,写入以下完整代码:

const express = require('express');
const http = require('http');
const path = require('path');
const { Server } = require('socket.io');

const app = express();
// 创建底层的 HTTP 服务器实例,以便 Express 和 Socket.io 共享同一端口
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: "*", // 允许局域网跨来源访问(便于开发调试)
    methods: ["GET", "POST"]
  }
});

const PORT = 3000;
const HOST = '0.0.0.0'; // 关键:绑定所有网络接口,向局域网完全开放

// 托管 public 目录下的前端静态资源
app.use(express.static(path.join(__dirname, 'public')));

// 在线客户端计数
let onlineUsers = 0;

// 监听客户端 WebSocket 连接
io.on('connection', (socket) => {
  onlineUsers++;
  const clientIp = socket.handshake.address;
  console.log(`[连接建立] 客户端接入 ID: ${socket.id}, IP: ${clientIp}, 当前在线人数: ${onlineUsers}`);

  // 向所有客户端广播当前在线人数
  io.emit('system:online_count', onlineUsers);

  // 监听客户端发送的聊天消息
  socket.on('chat:message', (payload) => {
    // 服务端防御性校验:限制消息长度与基本类型
    if (!payload || typeof payload.text !== 'string' || payload.text.trim() === '') {
      return;
    }

    const sanitizedData = {
      id: Date.now() + '-' + Math.random().toString(36).substr(2, 5),
      username: (payload.username || '匿名用户').trim().substring(0, 16),
      text: payload.text.trim().substring(0, 500), // 单条消息最长限制 500 字符
      timestamp: new Date().toLocaleTimeString('zh-CN', { hour12: false })
    };

    // 核心:使用 io.emit 进行全员广播,包括发送者自身
    io.emit('chat:message', sanitizedData);
  });

  // 监听客户端断开连接
  socket.on('disconnect', (reason) => {
    onlineUsers = Math.max(0, onlineUsers - 1);
    console.log(`[连接断开] 客户端退出 ID: ${socket.id}, 原因: ${reason}, 剩余在线: ${onlineUsers}`);
    io.emit('system:online_count', onlineUsers);
  });
});

// 启动服务并监听局域网
server.listen(PORT, HOST, () => {
  console.log(`===============================================`);
  console.log(`局域网聊天室服务端已启动成功!`);
  console.log(`本机访问地址: http://127.0.0.1:${PORT}`);
  console.log(`局域网其他设备访问: http://<你的局域网IP>:${PORT}`);
  console.log(`===============================================`);
});

四、客户端实现:页面交互与消息监听

客户端需要提供简洁的聊天记录滚动区、昵称设置框、消息输入框与发送按钮。Socket.io 服务端启动后,会自动在其内置路由 /socket.io/socket.io.js 提供官方客户端 JS 脚本,因此前端无需通过外部 CDN,直接引用相对路径即可在无外网环境下正常工作。

Node.js + Socket.io 搭建局域网聊天室教程
图 3:客户端消息发射、服务端数据净化与局域网全员广播时序图

在根目录下创建 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, maximum-scale=1.0, user-scalable=no">
  <title>局域网实时聊天室</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
      background: #EDF2F7;
      display: flex;
      justify-content: center;
      height: 100vh;
      color: #2D3748;
    }
    .chat-app {
      width: 100%;
      max-width: 600px;
      height: 100vh;
      display: flex;
      flex-direction: column;
      background: #FFFFFF;
      box-shadow: 0 0 16px rgba(0,0,0,0.08);
    }
    .header {
      padding: 14px 18px;
      background: #52677D;
      color: #FFFFFF;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .header h2 { font-size: 16px; font-weight: 600; }
    .badge {
      font-size: 12px;
      background: rgba(255,255,255,0.2);
      padding: 3px 8px;
      border-radius: 12px;
    }
    .user-bar {
      padding: 8px 16px;
      background: #F7FAFC;
      border-bottom: 1px solid #E2E8F0;
      display: flex;
      align-items: center;
      gap: 8px;
      font-size: 13px;
    }
    .user-bar input {
      padding: 4px 8px;
      border: 1px solid #CBD5E0;
      border-radius: 4px;
      outline: none;
      font-size: 13px;
      width: 140px;
    }
    .messages {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    .msg-item {
      display: flex;
      flex-direction: column;
      max-width: 80%;
    }
    .msg-item.self {
      align-self: flex-end;
      align-items: flex-end;
    }
    .msg-item.other {
      align-self: flex-start;
      align-items: flex-start;
    }
    .msg-meta {
      font-size: 11px;
      color: #718096;
      margin-bottom: 3px;
    }
    .msg-bubble {
      padding: 8px 12px;
      border-radius: 8px;
      font-size: 14px;
      line-height: 1.5;
      word-break: break-word;
    }
    .msg-item.self .msg-bubble {
      background: #52677D;
      color: #FFFFFF;
      border-bottom-right-radius: 2px;
    }
    .msg-item.other .msg-bubble {
      background: #EDF2F7;
      color: #2D3748;
      border-bottom-left-radius: 2px;
    }
    .input-form {
      padding: 12px 16px;
      background: #FFFFFF;
      border-top: 1px solid #E2E8F0;
      display: flex;
      gap: 10px;
    }
    .input-form input {
      flex: 1;
      padding: 10px 14px;
      border: 1px solid #CBD5E0;
      border-radius: 6px;
      outline: none;
      font-size: 14px;
    }
    .input-form button {
      padding: 10px 20px;
      background: #A66C5B;
      color: #FFFFFF;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-weight: 600;
      font-size: 14px;
    }
    .input-form button:hover { background: #8E5848; }
  </style>
</head>
<body>
  <div class="chat-app">
    <div class="header">
      <h2>局域网实时聊天室</h2>
      <span class="badge" id="onlineCount">在线: 0 人</span>
    </div>

    <div class="user-bar">
      <label for="usernameInput">当前昵称:</label>
      <input type="text" id="usernameInput" value="用户-<script>document.write(Math.floor(Math.random()*1000))</script>">
    </div>

    <div class="messages" id="messageList"></div>

    <form class="input-form" id="chatForm">
      <input type="text" id="messageInput" placeholder="输入聊天内容..." autocomplete="off" required>
      <button type="submit">发送</button>
    </form>
  </div>

  <!-- 引入服务端自带的客户端 SDK,无需连公网 CDN -->
  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io(); // 默认自动连接托管此页面的 HTTP 宿主

    const messageList = document.getElementById('messageList');
    const chatForm = document.getElementById('chatForm');
    const messageInput = document.getElementById('messageInput');
    const usernameInput = document.getElementById('usernameInput');
    const onlineCount = document.getElementById('onlineCount');

    // 监听在线人数广播
    socket.on('system:online_count', (count) => {
      onlineCount.textContent = `在线: ${count} 人`;
    });

    // 监听全员聊天消息
    socket.on('chat:message', (data) => {
      const isSelf = data.username === usernameInput.value.trim();
      const item = document.createElement('div');
      item.className = `msg-item ${isSelf ? 'self' : 'other'}`;

      const meta = document.createElement('div');
      meta.className = 'msg-meta';
      meta.textContent = `${data.username} · ${data.timestamp}`;

      const bubble = document.createElement('div');
      bubble.className = 'msg-bubble';
      bubble.textContent = data.text; // 使用 textContent 规避 XSS 脚本注入

      item.appendChild(meta);
      item.appendChild(bubble);
      messageList.appendChild(item);

      // 滚动到底部
      messageList.scrollTop = messageList.scrollHeight;
    });

    // 提交消息事件
    chatForm.addEventListener('submit', (e) => {
      e.preventDefault();
      const text = messageInput.value.trim();
      if (!text) return;

      socket.emit('chat:message', {
        username: usernameInput.value.trim() || '匿名用户',
        text: text
      });

      messageInput.value = '';
      messageInput.focus();
    });
  </script>
</body>
</html>

五、局域网联调:获取宿主机 IP 与网络放行

服务端与客户端代码就绪后,启动服务端:

node server.js

为了让同一局域网下的其他手机或电脑能够访问该聊天室,你必须查询宿主机在局域网内部的分配 IP,并在系统安全软件中放行对应端口。

1. 查询局域网私有 IP

不同操作系统查询 IPv4 地址的常用命令如下:

  • Windows 系统:打开 CMD 或 PowerShell,输入 ipconfig,找到处于“已连接”状态的无线局域网适配器(WLAN),查看其 IPv4 地址(通常形如 192.168.1.100 或 192.168.0.x)。
  • macOS 系统:打开终端,输入 ifconfig | grep "inet " | grep -v 127.0.0.1,查看分配的内网 IP;或按住 Option 键点击右上角 Wi-Fi 图标,直接查看当前 IP。
  • Linux 系统:输入 ip addr show 或 hostname -I,第一个非 127.0.0.1 的 IP 即为局域网 IP。
便捷检测技巧:你也可以直接使用 Node.js 自带的 os 模块一条命令输出所有局域网 IPv4 地址:
node -e "console.log(Object.values(require('os').networkInterfaces()).flat().filter(d=>d.family==='IPv4'&&!d.internal).map(d=>d.address))"

2. 防火墙端口放行设置

现代操作系统为了安全,默认会拦截来自外部局域网主动发起的 TCP 入站连接。如果手机浏览器访问页面一直转圈并最终提示超时(ERR_CONNECTION_TIMED_OUT),绝大多数情况下是被宿主机的系统防火墙拦截。

  • Windows 防火墙:进入“控制面板” -> “系统和安全” -> “Windows Defender 防火墙” -> “高级设置” -> “入站规则” -> “新建规则”,选择“端口”,指定协议为 TCP、端口为 3000,选择“允许连接”,命名为 LAN-Chat-3000 并保存。
  • macOS 防火墙:在“系统设置” -> “网络” -> “防火墙”中,确认允许传入连接,或在弹出“是否允许 node 接受传入网络连接”时选择“允许”。
  • Linux 防火墙:如果开启了 UFW,执行 sudo ufw allow 3000/tcp 放行该端口。

六、局域网连接故障排查与核心要点总结

在跨设备调试局域网通信时,常会遇到无法访问或断线等异常。以下为典型故障的排查决策与解决方案总结:

Node.js + Socket.io 搭建局域网聊天室教程
图 4:局域网连接故障排查决策矩阵与架构核心要素总结
故障表现 根本原因分析 排查与修复方案
本机可以访问,手机提示无法连接或超时 ① 服务端绑定了 127.0.0.1;
② 防火墙拦截入站 TCP 3000;
③ 两台设备不在同一网段。
① 将 listen 的 host 改为 0.0.0.0;
② 在防火墙添加入站放行规则;
③ 检查手机 Wi-Fi,确保与电脑连接同一个 SSID。
同一 Wi-Fi 下互 ping 报“请求超时” 路由器开启了“AP 隔离”(Client Isolation)或“访客网络隔离”。 进入路由器管理后台(通常为 192.168.1.1),在无线高级设置中关闭“AP 隔离”;若在公司受限网络下,可用一台手机开启移动热点供两台设备接入。
页面能打开,但在线人数为 0、消息发不出 WebSocket 升级被网关代理拦截,或客户端引用外部 CDN 失败。 确保使用自带的 /socket.io/socket.io.js 路径;打开手机浏览器开发者工具检查 Console 是否存在升级 101 错误。

通过本文的讲解与完整代码示例,读者可以在不到十分钟内,在完全不依赖公网服务器与云服务的前提下,构建出一个高性能、双向实时的局域网即时通信系统。在此基础架构上,后续还可便捷利用 Socket.io 的 socket.join('roomName') 机制扩展出群聊分组、私聊频道,甚至基于局域网 WebRTC 进行音视频通话点对点信令交互。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章