摘要:实时聊天是很多产品的基础功能:客服系统、协作工具、游戏房间、直播弹幕等。本文手把手教你从零构建一个可水平扩展的实时聊天系统,使用技术栈:Node.js + Socket.IO + Redis(Pub/Sub / adapter)+ Nginx + Docker
实时聊天是很多产品的基础功能:客服系统、协作工具、游戏房间、直播弹幕等。本文手把手教你从零构建一个可水平扩展的实时聊天系统,使用技术栈:Node.js + Socket.IO + Redis(Pub/Sub / adapter)+ Nginx + Docker Compose,并说明如何在多实例场景下保证消息路由与性能。
目标:可在本地 5 分钟跑通,生产可扩展、支持多实例部署、支持房间/广播、包含 Docker Compose 配置与 Nginx 反向代理。
目录
环境与准备项目结构实现服务端(Socket.IO + Redis Adapter)实现简单前端演示页面(HTML + JS)Dockerfile 与 Docker Compose 配置(包含 Redis & nginx)运行与测试(单实例 & 多实例)水平扩展与注意点(负载、粘性会话、认证、限流)生产建议与总结1. 环境与准备
Node.js 版本:18+Docker & Docker Compose(用于容器化部署)Redis(作为消息中间件,用于跨实例广播)Nginx(反向代理 / TLS 终端)在本地测试先安装 Node.js、Docker、Docker Compose 即可。
2. 项目结构
realtime-chat/
├── server/
│ ├── package.json
│ ├── index.js
│ └── Dockerfile
├── web/
│ └── index.html
├── nginx/
│ └── nginx.conf
└── docker-compose.yml
3. 实现服务端(Socket.IO + Redis Adapter)
在 server/ 目录下创建 package.json:
{
"name": "realtime-chat-server",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"socket.io": "^4.8.1",
"ioredis": "^5.3.2",
"socket.io-redis": "^6.1.1" /* 或使用 @socket.io/redis-adapter */
}
}
注:socket.io-redis 旧适配器仍常见;Socket.IO 官方也提供 @socket.io/redis-adapter。下面示例用官方 @socket.io/redis-adapter 更好(示例使用 ioredis + @socket.io/redis-adapter)。
安装依赖(在 server/ 下):
npm install express socket.io ioredis @socket.io/redis-adapter
创建 index.js:
// server/index.js
const express = require('express');
const http = require('http');
const { createClient } = require('redis');
const { instrument } = require("@socket.io/admin-ui");
const { createAdapter } = require("@socket.io/redis-adapter");
const { Server } = require("socket.io");
const app = express;
const server = http.createServer(app);
// 提供静态页面,便于测试(或由 Nginx 提供)
app.use(express.static(__dirname + '/../web'));
const io = new Server(server, {
cors: {
origin: "*", // 本地测试允许任意源;生产请改为具体域名
methods: ["GET", "POST"]
},
pingInterval: 25000,
pingTimeout: 60000
});
// Redis 客户端(ioredis 或 redis)
const pubClient = createClient({ url: 'redis://redis:6379' });
const subClient = pubClient.duplicate;
(async => {
await pubClient.connect;
await subClient.connect;
io.adapter(createAdapter(pubClient, subClient));
// 可选:集成 socket.io admin UI(用于调试)
// instrument(io, { auth: false });
io.on('connection', (socket) => {
console.log(`Socket connected: ${socket.id}`);
// 用户加入房间(room)
socket.on('join', (room) => {
socket.join(room);
socket.to(room).emit('message', {
from: 'system',
text: `${socket.id} joined ${room}`,
ts: Date.now
});
});
// 用户发送消息到房间
socket.on('message', ({ room, text, user }) => {
const payload = { from: user || socket.id, text, ts: Date.now };
// 向房间广播(跨实例由 adapter 转发到 Redis)
io.to(room).emit('message', payload);
});
// 私聊示例
socket.on('private_message', ({ toSocketId, text }) => {
io.to(toSocketId).emit('private_message', { from: socket.id, text, ts: Date.now });
});
socket.on('disconnect', (reason) => {
console.log(`Socket disconnected: ${socket.id} (${reason})`);
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, => {
console.log(`Socket.IO server running on port ${PORT}`);
});
});
说明:
使用 Redis adapter,可在多实例之间广播事件与执行房间路由。redis://redis:6379 假定 Docker Compose 中 Redis 服务名为 redis。4. 实现简单前端演示页面(HTML + JS)
在 web/index.html 写一个最小示例用于测试:
实时聊天室 Demo
body { font-family: Arial, sans-serif; margin: 20px; }
#messages { height: 300px; overflow:auto; border:1px solid #ddd; padding:10px; }
#messages div { margin-bottom:8px; }
实时聊天室(Socket.IO + Redis)
显示名:
房间:
加入房间
发送
const socket = io; // 将从同源或 Nginx 代理连接
const messages = document.getElementById('messages');
const userEl = document.getElementById('user');
const roomEl = document.getElementById('room');
document.getElementById('join').onclick = => {
const room = roomEl.value.trim;
if (!room) return alert('请输入房间名');
socket.emit('join', room);
appendMessage('system', `已加入房间 ${room}`);
};
document.getElementById('send').onclick = => {
const text = document.getElementById('msg').value;
const room = roomEl.value.trim;
if (!text || !room) return;
socket.emit('message', { room, text, user: userEl.value });
document.getElementById('msg').value = '';
};
socket.on('message', (data) => {
appendMessage(data.from, data.text, data.ts);
});
socket.on('private_message', (data) => {
appendMessage(`私信-${data.from}`, data.text);
});
function appendMessage(from, text, ts) {
const div = document.createElement('div');
const time = ts ? new Date(ts).toLocaleTimeString : '';
div.textContent = `[${time}] ${from}: ${text}`;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
说明:简单明了,便于测试单房间广播、私聊。
5. Dockerfile 与 Docker Compose 配置(包含 Redis & Nginx)
server/Dockerfile
# server/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
nginx/nginx.conf
将 Nginx 配置为反向代理并做 WebSocket 转发:
# nginx/nginx.conf
events { }
http {
upstream backend {
server server1:3000;
server server2:3000;
# 生产推荐使用动态服务发现或 docker swarm/k8s
}
server {
listen 80;
server_name _;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /socket.io/ {
proxy_pass http://backend/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
}
}
docker-compose.yml(根目录)
version: '3.8'
services:
redis:
image: redis:7
container_name: redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
server1:
build:
context: ./server
container_name: server1
environment:
- PORT=3000
- NODE_ENV=production
depends_on:
- redis
networks:
- backend
server2:
build:
context: ./server
container_name: server2
environment:
- PORT=3000
- NODE_ENV=production
depends_on:
- redis
networks:
- backend
nginx:
image: nginx:stable-alpine
container_name: nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- server1
- server2
networks:
- backend
networks:
backend:
driver: bridge
说明:
我用 server1 和 server2 两个实例模拟水平扩展。Nginx upstream 中使用 server1 与 server2。在真实生产环境建议用服务发现或 Kubernetes Service。在项目根目录执行构建与启动:
docker compose build
docker compose up -d
访问浏览器:http://localhost/ (页面托管在 server 的静态目录)
打开两个或多个浏览器窗口,输入不同用户名并加入同一房间,互相发送消息验证:
如果在不同浏览器/不同机器上,消息应能相互收到(跨实例通过 Redis adapter 转发)。在 server logs 可看到连接与广播日志。测试多实例广播:
停止 server1:docker compose stop server1,发送消息仍应由 server2 接收并继续工作(无单点故障)。重启 server1:docker compose start server1。7. 水平扩展与注意点
7.1 粘性会话(Sticky Session)
WebSocket 连接是状态ful的,Nginx 轮询可能分配到不同后端,但一旦连接建立,该连接绑定到该后端实例。在使用负载均衡器(比如 Nginx)时无需粘性会话,因为 Socket.IO 初始握手后连接长期存在;但如果后端使用多进程/多个主机,消息跨实例依赖 Redis adapter,无需粘性。对于 HTTP 长轮询等不稳定连接,建议启用粘性会话或使用负载均衡器支持 WebSocket 的策略。7.2 认证与鉴权
在 socket.handshake 阶段验证用户 token。示例:客户端在连接时发送 auth token(或在 query string),服务端在 io.use 中验证:io.use(async (socket, next) => {
const token = socket.handshake.auth?.token;
// 验证 token...
if (valid) next;
else next(new Error("Authentication error"));
});
7.3 限流与防刷
使用 socket.on('message', ...) 时做频率限制(per-socket)。可在 Redis 中使用计数器或令牌桶算法,实现全局限流。7.4 消息可靠性
Socket.IO 确保消息传输,但若在跨实例场景需要持久化消息(离线消息),需写入数据库(如 MongoDB)并在用户上线时回放。7.5 性能优化
使用 engine.io 的压缩、使用 gzip/deflate。使用现代 Node.js worker threads 或 PM2 cluster 模式配合 Redis adapter。对高并发场景,将 Socket.IO upgrade 与静态资源分离(静态由 CDN 提供)。8. 生产建议与总结
生产部署建议
将 Nginx 放在最外层做 TLS 终端(Let’s Encrypt / CA)。使用 Kubernetes(Deployment + Service)部署 server instances,利用 @socket.io/redis-adapter 连接到 Redis Cluster。Redis 使用哨兵或集群模式保证高可用。使用监控(Prometheus + Grafana)采集连接数、消息吞吐、延迟等指标。在生产启用日志分级、错误追踪(Sentry)及指标告警。优点总结
Redis Pub/Sub adapter 使 Socket.IO 在多实例间协同成为可能,支持水平扩展。Docker Compose 快速上手,Kubernetes 则适合生产。方案适合聊天、游戏、协作、推送等实时应用。git clone realtime-chat
cd realtime-chat
# 在 server 目录先准备 package.json 并复制 index.js、web
docker compose build
docker compose up -d
# 查看日志
docker compose logs -f server1
停止并移除容器:
docker compose down
来源:硬核科技志