WebSocket实时应用部署指南:Node.js + Socket.io在香港VPS实现在线聊天与实时推送
传统HTTP是请求-响应模式,服务器无法主动推送数据给客户端。WebSocket建立持久连接后,服务器和客户端可以双向实时通信,是在线客服、实时通知、多人协作、游戏等场景的必选技术。
一、WebSocket vs 传统轮询
| 方案 | 原理 | 延迟 | 服务器压力 | 适用场景 |
|---|---|---|---|---|
| 短轮询 | 每N秒发一次HTTP请求 | N秒 | 极高(大量无效请求) | 不推荐 |
| 长轮询 | 请求挂起直到有数据 | 约1s | 高(连接数多) | 低频通知 |
| SSE | 服务器单向推送流 | <100ms | 中等 | 新闻推送、进度更新 |
| WebSocket | 全双工持久连接 | <50ms | 低(一次握手) | 聊天、游戏、协作 |
二、服务端:Node.js + Socket.io
# 初始化项目
mkdir /srv/realtime-app && cd /srv/realtime-app
npm init -y
npm install express socket.io @socket.io/redis-adapter ioredis<code">// server.js
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('ioredis');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: {
origin: ['https://yourdomain.com'],
methods: ['GET', 'POST'],
credentials: true,
},
transports: ['websocket', 'polling'], // 优先WebSocket,降级到polling
pingTimeout: 60000,
pingInterval: 25000,
});
// Redis适配器(多节点部署时同步事件)
const pubClient = createClient({ host: '127.0.0.1', port: 6379, password: '你的Redis密码' });
const subClient = pubClient.duplicate();
io.adapter(createAdapter(pubClient, subClient));
// ==================== 中间件:身份认证 ====================
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
if (!token) return next(new Error('未提供认证Token'));
// 验证Token(这里简化,实际应验证JWT)
try {
const userId = verifyToken(token);
socket.userId = userId;
next();
} catch (err) {
next(new Error('Token无效'));
}
});
// ==================== 连接处理 ====================
io.on('connection', (socket) => {
console.log(`用户 ${socket.userId} 已连接,Socket ID: ${socket.id}`);
// 用户加入个人频道(用于定向推送)
socket.join(`user:${socket.userId}`);
// ---- 聊天室功能 ----
socket.on('join_room', (roomId) => {
socket.join(`room:${roomId}`);
socket.to(`room:${roomId}`).emit('user_joined', {
userId: socket.userId,
timestamp: Date.now(),
});
console.log(`用户 ${socket.userId} 加入房间 ${roomId}`);
});
socket.on('send_message', (data) => {
const { roomId, content } = data;
if (!content?.trim()) return;
const message = {
id: Date.now().toString(),
userId: socket.userId,
content: content.trim().substring(0, 1000), // 限制长度
timestamp: Date.now(),
};
// 广播给房间内所有人(包括发送者)
io.to(`room:${roomId}`).emit('new_message', message);
// 持久化存储消息(实际应存入数据库)
saveMessage(roomId, message);
});
socket.on('leave_room', (roomId) => {
socket.leave(`room:${roomId}`);
socket.to(`room:${roomId}`).emit('user_left', { userId: socket.userId });
});
// ---- 打字状态指示器 ----
let typingTimer;
socket.on('typing_start', ({ roomId }) => {
socket.to(`room:${roomId}`).emit('user_typing', { userId: socket.userId });
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
socket.to(`room:${roomId}`).emit('user_stop_typing', { userId: socket.userId });
}, 3000);
});
// ---- 断开连接 ----
socket.on('disconnect', (reason) => {
console.log(`用户 ${socket.userId} 断开连接:${reason}`);
});
});
// ==================== 服务端主动推送 API ====================
app.use(express.json());
// 向指定用户推送通知(供其他服务调用)
app.post('/internal/push', (req, res) => {
const { userId, event, data } = req.body;
if (!userId || !event) return res.status(400).json({ error: '参数缺失' });
io.to(`user:${userId}`).emit(event, data);
res.json({ success: true });
});
// 向指定房间广播(用于系统通知)
app.post('/internal/broadcast', (req, res) => {
const { roomId, event, data } = req.body;
io.to(`room:${roomId}`).emit(event, data);
res.json({ success: true });
});
// 获取在线用户数
app.get('/internal/online-count', async (req, res) => {
const sockets = await io.fetchSockets();
res.json({ count: sockets.length });
});
httpServer.listen(3001, '127.0.0.1', () => {
console.log('WebSocket服务器运行在 ws://127.0.0.1:3001');
});
// 辅助函数(实际实现需要数据库操作)
function verifyToken(token) { return 'user_123'; }
function saveMessage(roomId, message) { /* 存入MySQL/MongoDB */ }三、Nginx WebSocket代理配置
<code">server {
listen 443 ssl http2;
server_name ws.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/ws.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ws.yourdomain.com/privkey.pem;
location /socket.io/ {
proxy_pass http://127.0.0.1:3001;
# WebSocket关键配置
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;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket超时(保持长连接)
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_connect_timeout 10s;
}
# 提供静态聊天室页面
location / {
root /var/www/chat-app;
try_files $uri $uri/ /index.html;
}
}四、客户端实现
<code">
五、PM2进程管理
<code"># ecosystem.config.js
module.exports = {
apps: [{
name: 'websocket-server',
script: './server.js',
instances: 2, // 多实例(配合Redis适配器)
exec_mode: 'cluster',
max_memory_restart: '500M',
env_production: {
NODE_ENV: 'production',
PORT: 3001,
},
error_file: '/var/log/pm2/ws-error.log',
out_file: '/var/log/pm2/ws-out.log',
}],
};
pm2 start ecosystem.config.js --env production
pm2 save && pm2 startup六、连接数与性能参考
| 服务器配置 | 可支撑并发WebSocket连接数 | 适用场景 |
|---|---|---|
| 2核4G | 约5000~10000 | 小型聊天室、实时通知 |
| 4核8G | 约20000~50000 | 中型应用、客服系统 |
| 8核16G(多实例) | 100000+ | 大型实时应用 |
七、总结
Node.js + Socket.io是构建WebSocket实时应用的黄金组合,Redis适配器让多实例部署下的实时推送无缝工作。IDC.Net的香港VPSCN2 GIA低延迟特性对WebSocket应用尤为重要——用户发消息到服务器再广播给其他用户,往返延迟决定了实时交互的流畅度,香港节点可将大陆用户的往返延迟控制在30ms以内。
版权声明:
作者:后浪云
链接:https://idc.net/help/442824/
文章版权归作者所有,未经允许请勿转载。
THE END
