从零搭建聊天系统Node.js + Socket.IO + Redis Pub/Sub + Docker Compose

B站影视 港台电影 2025-10-12 23:39 1

摘要:实时聊天是很多产品的基础功能:客服系统、协作工具、游戏房间、直播弹幕等。本文手把手教你从零构建一个可水平扩展的实时聊天系统,使用技术栈: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

来源:硬核科技志

相关推荐