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以内。

THE END