Node.js + Socket.io 搭建局域网聊天室教程
本教程详细讲解如何基于 Node.js 与 Socket.io 搭建一个跨设备的局域网即时聊天室,包含环境准备、服务端与客户端完整代码实现、局域网 IP 与端口放行排查,以及常见连接故障的解决方案。
一、局域网即时通信的核心工作机制
传统的 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 双向信道。
一旦信道升级完成,双向传输帧便不再包含冗长的 HTTP 请求头。当局域网内的某台手机向 Node.js 节点发射一个 chat:message 自定义事件时,Node.js 可以在极短的毫秒内将其广播(Broadcast)至局域网连接池内的所有套接字,实现多屏即刻同步。
二、搭建准备与基础环境初始化
在开始编码前,请确保开发主机已安装了 Node.js 运行时(推荐 Node.js v16、v18 或更高 LTS 版本)。可以通过终端执行 node -v 与 npm -v 确认环境就绪。
整个实现无需复杂的打包工具(如 Webpack 或 Vite),仅需一个极简的 Express 静态服务器与 Socket.io 配合即可。搭建步骤可以归纳为规范的五步操作流程:
首先,在本地创建一个空目录并完成 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,直接引用相对路径即可在无外网环境下正常工作。
在根目录下创建 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。
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放行该端口。
六、局域网连接故障排查与核心要点总结
在跨设备调试局域网通信时,常会遇到无法访问或断线等异常。以下为典型故障的排查决策与解决方案总结:
| 故障表现 | 根本原因分析 | 排查与修复方案 |
|---|---|---|
| 本机可以访问,手机提示无法连接或超时 | ① 服务端绑定了 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 进行音视频通话点对点信令交互。
怎么使用nvm-windows管理Node.js版本以避免安装冲突
2026-11-13 20:06:36
手机怎么访问电脑搭建的 JS 局域网聊天室
2026-11-13 17:06:40
llama.cpp后端修复如何按b12283与硬件条件判断适用范围
2026-11-13 16:59:42
国内网络环境下加速Node.js和npm下载的配置方法
2026-11-12 23:24:51
没有Node.js安卓版怎么办?通过VS Code Remote SSH在安卓上开发
2026-11-12 21:24:48








