WebSocket实时通信架构:从连接到百万并发的实战指南

深入讲解WebSocket实时通信架构的设计与实现,涵盖连接管理、心跳机制、消息队列集成、水平扩展、负载均衡等核心主题,提供Node.js和Go的完整实现代码。

WebSocket vs 其他实时方案

实时通信方案对比:
┌─────────────────────────────────────────┐
│ WebSocket                                │
│ ✓ 全双工通信,低延迟                     │
│ ✓ 持久连接,减少握手开销                 │
│ ✓ 适合高频双向交互(聊天、游戏)         │
│ ✗ 需要维护连接状态                       │
│ ✗ 扩展性需要考虑连接亲和性               │
│                                         │
│ Server-Sent Events (SSE)                │
│ ✓ 基于HTTP,简单实现                     │
│ ✓ 自动重连,易于调试                     │
│ ✓ 适合单向推送(通知、股票行情)         │
│ ✗ 只能服务器到客户端                     │
│ ✗ 连接数受限(HTTP/1.1每域6个)          │
│                                         │
│ 长轮询(Long Polling)                   │
│ ✓ 兼容性好,无需特殊支持                 │
│ ✓ 实现简单                               │
│ ✗ 延迟较高                               │
│ ✗ 服务器资源消耗大                       │
│                                         │
│ 选择建议:                               │
│ - 双向高频交互 → WebSocket               │
│ - 单向实时推送 → SSE                     │
│ - 兼容性要求高 → 长轮询                  │
└─────────────────────────────────────────┘

SSE 服务端推送事件详解

在构建实时通信系统时,并非所有场景都需要 WebSocket 的全双工能力。对于纯服务端到客户端的单向数据流,Server-Sent Events(SSE) 是一种更轻量、更符合 REST 风格的技术选择。SSE 基于标准 HTTP 协议,天然支持自动重连(Auto-Reconnect)和断点续传(resume via Last-Event-ID),并且能够穿透大多数企业防火墙和代理服务器。与 WebSocket 相比,SSE 不需要额外的握手开销,也不需要处理复杂的二进制帧解析逻辑。对于股票行情推送、新闻实时通知、后台任务进度反馈等场景,SSE 往往是更稳妥的方案。当系统需要扩展而非 WebSocket 独占的推送方案时,也可以参考 https://plumephp.com/graphql-subscriptions-realtime-updates/ 中关于订阅机制的实现对比。

// sse-server.js — 基于 Express 的 SSE 服务端实现
const express = require('express');
const { v4: uuidv4 } = require('uuid');

const app = express();
const clients = new Map();

app.get('/events', (req, res) => {
  const lastEventId = req.headers['last-event-id'] || '0';
  const clientId = uuidv4();

  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  res.flushHeaders();

  clients.set(clientId, res);
  console.log(`SSE client connected: ${clientId}, lastEventId=${lastEventId}`);

  // 推送断点续传期间累积的离线消息
  const missedMessages = getMissedMessages(Number(lastEventId));
  missedMessages.forEach(msg => {
    res.write(`id: ${msg.id}\n`);
    res.write(`event: ${msg.event}\n`);
    res.write(`data: ${JSON.stringify(msg.data)}\n\n`);
  });

  req.on('close', () => {
    clients.delete(clientId);
    console.log(`SSE client disconnected: ${clientId}`);
  });
});

// 广播推送函数(支持按频道过滤)
function broadcast(event, data, channel = 'all') {
  const id = Date.now();
  const payload = `id: ${id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
  clients.forEach((res) => {
    res.write(payload);
  });
}

setInterval(() => {
  broadcast('heartbeat', { ts: Date.now() });
}, 30000);

app.listen(3001, () => {
  console.log('SSE server running on port 3001');
});

SSE 的 EventSource 客户端接口天然支持自动重连,无需手动实现心跳逻辑。通过设置 id 字段,浏览器在断线恢复时会自动带上 Last-Event-ID 请求头,服务端据此推送断线期间遗漏的消息,从而实现可靠的有序推送。

WebSocket安全威胁与防护

WebSocket 协议虽然功能强大,但也引入了新的攻击面。在生产环境中部署 WebSocket 服务时,必须防范 CSWSH(Cross-Site WebSocket Hijacking)、帧注入、资源耗尽攻击等风险。CSWSH 类似于 CSRF 攻击,攻击者诱导用户的浏览器发起 WebSocket 连接到目标站点,并携带用户的认证 Cookie,从而达到冒充用户的目的。由于 WebSocket 握手使用的是 HTTP Upgrade 请求,传统的 CSRF Token 防护机制在此阶段并不直接适用。因此,服务端必须在 Sec-WebSocket-Key 之外,额外校验 Origin 头部或要求握手阶段携带不可预测的 Token。同时,对于大规模部署,还需要在网关层实现速率限制(Rate Limiting)和消息大小检查,防止恶意客户端通过超大帧 or 高频消息耗尽服务器资源。关于现代 API 网关的防护策略,可以进一步了解 https://plumephp.com/api-gateway-ratelimiting-circuitbreaker-practice/ 中针对 WebSocket 流量限流的具体方案。

// security/middleware.go — WebSocket 安全防护中间件
package security

import (
	"net/http"
	"strings"
	"time"

	"github.com/gorilla/websocket"
	"golang.org/x/time/rate"
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		origin := r.Header.Get("Origin")
		allowed := []string{"https://app.example.com", "https://admin.example.com"}
		for _, o := range allowed {
			if strings.EqualFold(origin, o) {
				return true
			}
		}
		return false
	},
}

// RateLimiter 基于令牌桶的每连接限流器
type RateLimiter struct {
	limiter *rate.Limiter
}

func NewRateLimiter(rps rate.Limit, burst int) *RateLimiter {
	return &RateLimiter{limiter: rate.NewLimiter(rps, burst)}
}

func (rl *RateLimiter) Allow() bool {
	return rl.limiter.Allow()
}

// SecureUpgrade 在升级前执行多层安全检查
func SecureUpgrade(w http.ResponseWriter, r *http.Request, checkToken func(string) error) (*websocket.Conn, error) {
	// 1. 验证握手 Token(防御 CSWSH)
	token := r.URL.Query().Get("token")
	if token == "" || checkToken(token) != nil {
		http.Error(w, "Invalid handshake token", http.StatusForbidden)
		return nil, websocket.ErrBadHandshake
	}

	// 2. 帧大小限制已在 Upgrader 层面通过 CheckOrigin 和后续 ReadLimit 控制
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		return nil, err
	}

	// 3. 设置读取上限(防御超大帧攻击)
	conn.SetReadLimit(65536)
	conn.SetReadDeadline(time.Now().Add(120 * time.Second))

	return conn, nil
}

在生产环境中,建议将 WebSocket 服务置于 WAF 或 API 网关之后,利用网关的统一策略管理 Origin 白名单、连接数上限和首包大小限制,从而在边缘层过滤掉绝大多数恶意流量。

微服务架构中的WebSocket网关

在微服务架构下,WebSocket 连接管理如果分散在各个业务服务中,会导致重复建设、认证逻辑不统一、服务间消息路由混乱等问题。引入独立的 WebSocket 网关层 是解决这些痛点的关键举措。WebSocket 网关承担统一入口职责:负责长连接的保持与心跳管理,对客户端请求进行 JWT 认证与权限校验,并根据消息中的路由字段将业务消息转发到对应的后端服务(如聊天服务、通知服务、游戏房间服务等)。网关本身无状态,水平扩展时通过 Redis 等共享存储同步会话信息。在服务发现层面,网关可以与 Consul、etcd 或 ZooKeeper 集成,动态感知后端服务的健康状态,实现流量的智能调度。若对分布式协调组件感兴趣,可深入阅读 https://plumephp.com/zookeeper-coordination/ 中关于服务注册与选主机制的内容。

# websocket-gateway-config.yaml — 网关路由与认证配置
apiVersion: v1
kind: ConfigMap
metadata:
  name: websocket-gateway-config
data:
  gateway.yaml: |
    server:
      port: 8080
      maxConnectionsPerIP: 5000
      heartbeatInterval: 25s
      heartbeatTimeout: 60s

    auth:
      type: jwt
      secretEnv: JWT_SECRET
      tokenHeader: Sec-WebSocket-Protocol
      tokenQueryParam: token

    routing:
      rules:
        - prefix: "chat."
          service: chat-service
          namespace: realtime
          port: 8081
        - prefix: "notify."
          service: notification-service
          namespace: realtime
          port: 8082
        - prefix: "game."
          service: game-room-service
          namespace: realtime
          port: 8083

    discovery:
      type: consul
      address: consul-server.default.svc.cluster.local:8500
      healthCheckInterval: 10s

    metrics:
      enabled: true
      path: /metrics
      port: 9090

WebSocket 网关的核心设计原则是连接与业务分离。网关只负责维持长连接和轻量级消息路由,所有业务状态计算交给后端微服务处理。这种分层架构使得前端无需关心后端服务的部署拓扑,同时也方便对 WebSocket 流量进行统一监控和限流。

消息队列与WebSocket的集成

在高并发的实时系统中,WebSocket 服务器直接处理所有业务逻辑会导致单点瓶颈和耦合度过高。更优雅的架构是将 WebSocket 定位为最后一公里推送层,而将可靠的消息分发交由专业的消息队列(如 Kafka、NATS、RabbitMQ)承担。消息队列作为后端事件总线,负责削峰填谷、消息持久化以及跨服务的事件广播;WebSocket 服务器则消费队列中的事件,并将其推送给在线客户端。这种模式不仅解耦了消息生产者与消费者,还使得离线消息回放、多机房同步、灰度发布等高级能力变得易于实现。例如,当用户下单成功后,订单服务将事件发布到 Kafka 的 order.completed Topic,WebSocket 消费组拉取消息后,查询哪些用户当前在线并推送通知。关于事件驱动架构的更多设计模式,请参阅 https://plumephp.com/event-driven-architecture-domain-events/ 中领域事件与消息总线的章节。

// kafka-websocket-bridge.js — Kafka 消费者桥接 WebSocket 推送
const { Kafka } = require('kafkajs');
const { Server } = require('socket.io');

const kafka = new Kafka({
  clientId: 'websocket-bridge',
  brokers: ['kafka-1:9092', 'kafka-2:9092', 'kafka-3:9092']
});

const consumer = kafka.consumer({ groupId: 'websocket-push-group' });

async function startBridge(io) {
  await consumer.connect();

  // 订阅多个业务 Topic
  await consumer.subscribe({ topics: ['order.completed', 'notify.broadcast', 'chat.message'] });

  await consumer.run({
    eachMessage: async ({ topic, partition, message }) => {
      const event = JSON.parse(message.value.toString());
      const { userId, roomId, payload, type } = event;

      switch (topic) {
        case 'order.completed':
          // 推送给指定用户(个人通知)
          io.to(`user:${userId}`).emit('order:completed', payload);
          break;

        case 'notify.broadcast':
          // 全站广播
          io.emit('broadcast', payload);
          break;

        case 'chat.message':
          // 推送给房间所有成员
          if (roomId) {
            io.to(`room:${roomId}`).emit('message:new', payload);
          }
          break;
      }

      console.log(`[Bridge] Forwarded ${topic} to user=${userId} room=${roomId}`);
    }
  });
}

// 优雅关闭:先停止消费再断开连接
async function gracefulShutdown() {
  await consumer.disconnect();
  console.log('Kafka bridge shut down gracefully');
}

module.exports = { startBridge, gracefulShutdown };

使用消息队列作为中间层还有一个显著优势:水平扩展的灵活性。当 WebSocket 服务器集群需要扩容时,只需增加消费者实例即可自动参与消息分发;当需要缩容时,Kafka 的再平衡机制会将分区重新分配,保证消息不丢失。这种架构使得 WebSocket 层成为无状态的推送终端,任何节点的故障都不影响整体服务的可用性。

百万级并发性能调优

当 WebSocket 服务需要支撑百万级并发连接时,仅靠代码层面的优化远远不够,必须从操作系统内核、网络栈、进程模型多个维度进行系统性调优。在 Linux 环境下,文件描述符限制是首要瓶颈。每个 WebSocket 连接至少占用一个文件描述符,因此必须通过调整 ulimit -n 以及 /etc/security/limits.conf 确保进程可打开的文件数达到百万级别。其次,启用 SO_REUSEPORT 选项可以让多个监听进程绑定到同一个端口,由内核在传输层进行负载均衡,避免单 Listen 进程成为瓶颈。此外,还需要调整 TCP 内核参数,如调大 net.ipv4.tcp_max_syn_backlog、net.core.somaxconn 和 net.netfilter.nf_conntrack_max,以应对突发的大量新建连接。Go 语言的 goroutine 模型天然适合高并发场景,但仍需关注 goroutine 的内存占用,通过设置合理的读写缓冲区大小和复用对象池来降低 GC 压力。对于需要异构语言服务协同的场景,Kafka 流处理是不可或缺的扩音器,可参考 https://plumephp.com/kafka-connect/ 中关于高吞吐消息管道的调优经验。

// tuning/server.go — 百万级并发调优配置
package main

import (
	"net"
	"syscall"

	"github.com/gorilla/websocket"
)

// reusePortListen 使用 SO_REUSEPORT 支持多进程复用同一端口
func reusePortListen(network, address string) (net.Listener, error) {
	lc := net.ListenConfig{
		Control: func(network, address string, c syscall.RawConn) error {
			var err error
			c.Control(func(fd uintptr) {
				// 启用端口复用,允许多个进程监听同一地址
				err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
			})
			return err
		},
	}
	return lc.Listen(nil, network, address)
}

func main() {
	listener, err := reusePortListen("tcp", ":8080")
	if err != nil {
		panic(err)
	}

	upgrader := websocket.Upgrader{
		ReadBufferSize:  2048,
		WriteBufferSize: 2048,
		// 启用压缩以减少网络传输量(CPU 换带宽)
		EnableCompression: true,
	}

	// 每连接尽量复用缓冲区,减少内存分配
	upgrader.ReadBufferPool = &sync.Pool{
		New: func() interface{} {
			return make([]byte, 2048)
		},
	}

	// ... 注册 handler 并启动服务
}

除了代码和内核调优,连接数监控也是百万级并发运维的必备手段。建议通过 Prometheus 采集 websocket_connected_clients、go_goroutines、process_open_fds 等指标,并配合 Grafana 设置告警阈值。当连接数接近 ulimit 上限或内存占用超过预设阈值时,及时触发自动扩容,确保服务在流量洪峰下依然稳健运行。

WebSocket服务器实现

Node.js + Socket.IO

// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const redis = require('redis');
const jwt = require('jsonwebtoken');

const app = express();
const server = http.createServer(app);

// Socket.IO配置
const io = new Server(server, {
  cors: {
    origin: process.env.CLIENT_URL,
    methods: ['GET', 'POST']
  },
  pingTimeout: 60000,
  pingInterval: 25000
});

// Redis适配器(多进程支持)
const pubClient = redis.createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

io.adapter(createAdapter(pubClient, subClient));

// 连接认证中间件
io.use(async (socket, next) => {
  try {
    const token = socket.handshake.auth.token;
    if (!token) {
      return next(new Error('Authentication error'));
    }
    
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    socket.userId = decoded.userId;
    socket.username = decoded.username;
    
    next();
  } catch (err) {
    next(new Error('Authentication error'));
  }
});

// 连接管理
const userSockets = new Map(); // userId -> Set<socketId>

io.on('connection', (socket) => {
  console.log(`User ${socket.userId} connected: ${socket.id}`);
  
  // 记录用户的socket连接
  if (!userSockets.has(socket.userId)) {
    userSockets.set(socket.userId, new Set());
  }
  userSockets.get(socket.userId).add(socket.id);
  
  // 加入用户的个人房间
  socket.join(`user:${socket.userId}`);
  
  // 心跳检测
  socket.on('pong', () => {
    socket.isAlive = true;
  });
  
  // 加入聊天室
  socket.on('join:room', async (roomId) => {
    try {
      // 验证用户是否有权限加入
      const hasAccess = await checkRoomAccess(socket.userId, roomId);
      if (!hasAccess) {
        socket.emit('error', { message: 'Access denied' });
        return;
      }
      
      socket.join(`room:${roomId}`);
      socket.emit('room:joined', { roomId });
      
      // 通知房间内其他用户
      socket.to(`room:${roomId}`).emit('user:joined', {
        userId: socket.userId,
        username: socket.username
      });
    } catch (err) {
      socket.emit('error', { message: 'Failed to join room' });
    }
  });
  
  // 发送消息
  socket.on('message:send', async (data) => {
    try {
      const { roomId, content, type = 'text' } = data;
      
      // 创建消息记录
      const message = await createMessage({
        roomId,
        userId: socket.userId,
        content,
        type,
        timestamp: Date.now()
      });
      
      // 广播给房间内所有用户
      io.to(`room:${roomId}`).emit('message:new', {
        id: message.id,
        roomId,
        userId: socket.userId,
        username: socket.username,
        content,
        type,
        timestamp: message.timestamp
      });
      
      // 发送确认给发送者
      socket.emit('message:sent', { messageId: message.id });
    } catch (err) {
      socket.emit('error', { message: 'Failed to send message' });
    }
  });
  
  // 输入状态
  socket.on('typing:start', (roomId) => {
    socket.to(`room:${roomId}`).emit('user:typing', {
      userId: socket.userId,
      username: socket.username
    });
  });
  
  socket.on('typing:stop', (roomId) => {
    socket.to(`room:${roomId}`).emit('user:stop-typing', {
      userId: socket.userId
    });
  });
  
  // 断开连接
  socket.on('disconnect', (reason) => {
    console.log(`User ${socket.userId} disconnected: ${reason}`);
    
    // 清理用户socket记录
    const sockets = userSockets.get(socket.userId);
    if (sockets) {
      sockets.delete(socket.id);
      if (sockets.size === 0) {
        userSockets.delete(socket.userId);
      }
    }
    
    // 通知相关房间
    socket.rooms.forEach(room => {
      if (room.startsWith('room:')) {
        socket.to(room).emit('user:left', {
          userId: socket.userId,
          username: socket.username
        });
      }
    });
  });
});

// 心跳检测定时器
const heartbeatInterval = setInterval(() => {
  io.sockets.sockets.forEach((socket) => {
    if (socket.isAlive === false) {
      return socket.disconnect(true);
    }
    
    socket.isAlive = false;
    socket.emit('ping');
  });
}, 30000);

// 清理
server.on('close', () => {
  clearInterval(heartbeatInterval);
  pubClient.quit();
  subClient.quit();
});

server.listen(3000, () => {
  console.log('WebSocket server running on port 3000');
});

Go实现(高并发场景)

// main.go
package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "sync"
    "time"
    
    "github.com/gorilla/websocket"
    "github.com/redis/go-redis/v9"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        return true // 生产环境需要验证origin
    },
}

type Message struct {
    Type    string      `json:"type"`
    RoomID  string      `json:"roomId,omitempty"`
    Content interface{} `json:"content"`
    UserID  string      `json:"userId"`
    Time    int64       `json:"time"`
}

type Client struct {
    ID       string
    UserID   string
    Conn     *websocket.Conn
    Send     chan []byte
    Rooms    map[string]bool
    mu       sync.RWMutex
}

type Hub struct {
    Clients    map[string]*Client
    Rooms      map[string]map[string]*Client
    Register   chan *Client
    Unregister chan *Client
    Broadcast  chan *BroadcastMessage
    mu         sync.RWMutex
}

type BroadcastMessage struct {
    RoomID  string
    Message []byte
    Exclude string
}

func NewHub() *Hub {
    return &Hub{
        Clients:    make(map[string]*Client),
        Rooms:      make(map[string]map[string]*Client),
        Register:   make(chan *Client),
        Unregister: make(chan *Client),
        Broadcast:  make(chan *BroadcastMessage, 256),
    }
}

func (h *Hub) Run(ctx context.Context) {
    for {
        select {
        case client := <-h.Register:
            h.mu.Lock()
            h.Clients[client.ID] = client
            h.mu.Unlock()
            log.Printf("Client connected: %s (user: %s)", client.ID, client.UserID)
            
        case client := <-h.Unregister:
            h.mu.Lock()
            if _, ok := h.Clients[client.ID]; ok {
                delete(h.Clients, client.ID)
                close(client.Send)
                
                // 从所有房间移除
                client.mu.RLock()
                for roomID := range client.Rooms {
                    if room, ok := h.Rooms[roomID]; ok {
                        delete(room, client.ID)
                        if len(room) == 0 {
                            delete(h.Rooms, roomID)
                        }
                    }
                }
                client.mu.RUnlock()
            }
            h.mu.Unlock()
            log.Printf("Client disconnected: %s", client.ID)
            
        case msg := <-h.Broadcast:
            h.mu.RLock()
            if room, ok := h.Rooms[msg.RoomID]; ok {
                for clientID, client := range room {
                    if clientID == msg.Exclude {
                        continue
                    }
                    select {
                    case client.Send <- msg.Message:
                    default:
                        // 客户端发送队列满,断开连接
                        close(client.Send)
                        delete(room, clientID)
                    }
                }
            }
            h.mu.RUnlock()
            
        case <-ctx.Done():
            return
        }
    }
}

func (h *Hub) JoinRoom(clientID, roomID string) {
    h.mu.Lock()
    defer h.mu.Unlock()
    
    client, ok := h.Clients[clientID]
    if !ok {
        return
    }
    
    client.mu.Lock()
    client.Rooms[roomID] = true
    client.mu.Unlock()
    
    if _, ok := h.Rooms[roomID]; !ok {
        h.Rooms[roomID] = make(map[string]*Client)
    }
    h.Rooms[roomID][clientID] = client
}

func (h *Hub) LeaveRoom(clientID, roomID string) {
    h.mu.Lock()
    defer h.mu.Unlock()
    
    client, ok := h.Clients[clientID]
    if !ok {
        return
    }
    
    client.mu.Lock()
    delete(client.Rooms, roomID)
    client.mu.Unlock()
    
    if room, ok := h.Rooms[roomID]; ok {
        delete(room, clientID)
        if len(room) == 0 {
            delete(h.Rooms, roomID)
        }
    }
}

var hub = NewHub()
var redisClient *redis.Client

func init() {
    redisClient = redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
    // 验证JWT token
    token := r.URL.Query().Get("token")
    userID, err := validateToken(token)
    if err != nil {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("Upgrade error: %v", err)
        return
    }
    
    client := &Client{
        ID:     generateID(),
        UserID: userID,
        Conn:   conn,
        Send:   make(chan []byte, 256),
        Rooms:  make(map[string]bool),
    }
    
    hub.Register <- client
    
    // 读取消息
    go client.readPump()
    
    // 写入消息
    go client.writePump()
}

func (c *Client) readPump() {
    defer func() {
        hub.Unregister <- c
        c.Conn.Close()
    }()
    
    c.Conn.SetReadLimit(4096)
    c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
    c.Conn.SetPongHandler(func(string) error {
        c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second))
        return nil
    })
    
    for {
        _, message, err := c.Conn.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
                log.Printf("Read error: %v", err)
            }
            break
        }
        
        var msg Message
        if err := json.Unmarshal(message, &msg); err != nil {
            log.Printf("Unmarshal error: %v", err)
            continue
        }
        
        msg.UserID = c.UserID
        msg.Time = time.Now().UnixMilli()
        
        switch msg.Type {
        case "join":
            hub.JoinRoom(c.ID, msg.RoomID)
            c.Send <- createResponse("joined", msg.RoomID)
            
        case "leave":
            hub.LeaveRoom(c.ID, msg.RoomID)
            c.Send <- createResponse("left", msg.RoomID)
            
        case "message":
            msgBytes, _ := json.Marshal(msg)
            hub.Broadcast <- &BroadcastMessage{
                RoomID:  msg.RoomID,
                Message: msgBytes,
                Exclude: c.ID,
            }
            
            // 保存到数据库
            go saveMessage(msg)
        }
    }
}

func (c *Client) writePump() {
    ticker := time.NewTicker(30 * time.Second)
    defer func() {
        ticker.Stop()
        c.Conn.Close()
    }()
    
    for {
        select {
        case message, ok := <-c.Send:
            c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
            if !ok {
                c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }
            
            w, err := c.Conn.NextWriter(websocket.TextMessage)
            if err != nil {
                return
            }
            w.Write(message)
            
            if err := w.Close(); err != nil {
                return
            }
            
        case <-ticker.C:
            c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
            if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                return
            }
        }
    }
}

func main() {
    ctx := context.Background()
    go hub.Run(ctx)
    
    http.HandleFunc("/ws", handleWebSocket)
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

水平扩展方案

Redis Pub/Sub实现多服务器通信

// scaling/redis-adapter.js
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');

class RedisAdapter {
  constructor() {
    this.pubClient = createClient({ url: process.env.REDIS_URL });
    this.subClient = this.pubClient.duplicate();
    
    this.pubClient.on('error', err => console.error('Redis Pub Error:', err));
    this.subClient.on('error', err => console.error('Redis Sub Error:', err));
  }
  
  async init(io) {
    await Promise.all([
      this.pubClient.connect(),
      this.subClient.connect()
    ]);
    
    io.adapter(createAdapter(this.pubClient, this.subClient));
    
    console.log('Redis adapter initialized');
  }
  
  // 跨服务器发送消息
  async broadcastToRoom(room, event, data) {
    await this.pubClient.publish(
      `room:${room}`,
      JSON.stringify({ event, data })
    );
  }
  
  // 获取在线用户数(跨服务器)
  async getOnlineUsers() {
    const keys = await this.pubClient.keys('socket:user:*');
    return keys.length;
  }
  
  // 获取房间成员(跨服务器)
  async getRoomMembers(room) {
    const members = await this.pubClient.sMembers(`room:${room}:members`);
    return members;
  }
}

module.exports = RedisAdapter;

// docker-compose.yml
version: '3.8'
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - ws-server-1
      - ws-server-2
      - ws-server-3
  
  ws-server-1:
    build: .
    environment:
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - redis
  
  ws-server-2:
    build: .
    environment:
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - redis
  
  ws-server-3:
    build: .
    environment:
      - REDIS_URL=redis://redis:6379
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - redis

volumes:
  redis-data:

Nginx负载均衡配置

# nginx.conf
upstream websocket_backend {
    # 使用IP hash保证连接粘性
    ip_hash;
    
    server ws-server-1:3000 max_fails=3 fail_timeout=30s;
    server ws-server-2:3000 max_fails=3 fail_timeout=30s;
    server ws-server-3:3000 max_fails=3 fail_timeout=30s;
    
    keepalive 32;
}

server {
    listen 80;
    server_name ws.example.com;
    
    # WebSocket连接升级
    location /ws {
        proxy_pass http://websocket_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;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 超时配置
        proxy_connect_timeout 7d;
        proxy_send_timeout 7d;
        proxy_read_timeout 7d;
        
        # 缓冲配置
        proxy_buffering off;
    }
    
    # 健康检查
    location /health {
        access_log off;
        return 200 "healthy\n";
    }
}

消息持久化与离线消息

// services/messageService.js
const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

class MessageService {
  // 保存消息
  async saveMessage(message) {
    const query = `
      INSERT INTO messages (room_id, user_id, content, type, timestamp)
      VALUES ($1, $2, $3, $4, $5)
      RETURNING id
    `;
    
    const result = await pool.query(query, [
      message.roomId,
      message.userId,
      message.content,
      message.type,
      message.timestamp
    ]);
    
    return result.rows[0].id;
  }
  
  // 获取历史消息
  async getMessages(roomId, limit = 50, before = null) {
    let query = `
      SELECT m.*, u.username, u.avatar
      FROM messages m
      JOIN users u ON m.user_id = u.id
      WHERE m.room_id = $1
    `;
    
    const params = [roomId];
    
    if (before) {
      query += ' AND m.timestamp < $2';
      params.push(before);
    }
    
    query += ' ORDER BY m.timestamp DESC LIMIT $' + (params.length + 1);
    params.push(limit);
    
    const result = await pool.query(query, params);
    return result.rows.reverse();
  }
  
  // 获取离线消息
  async getOfflineMessages(userId, since) {
    const query = `
      SELECT m.*, r.name as room_name
      FROM messages m
      JOIN rooms r ON m.room_id = r.id
      JOIN room_members rm ON r.id = rm.room_id
      WHERE rm.user_id = $1
        AND m.timestamp > $2
        AND m.user_id != $1
      ORDER BY m.timestamp ASC
    `;
    
    const result = await pool.query(query, [userId, since]);
    return result.rows;
  }
  
  // 标记消息已读
  async markAsRead(userId, roomId, messageId) {
    const query = `
      INSERT INTO message_reads (user_id, room_id, message_id, read_at)
      VALUES ($1, $2, $3, NOW())
      ON CONFLICT (user_id, message_id) DO NOTHING
    `;
    
    await pool.query(query, [userId, roomId, messageId]);
  }
}

module.exports = new MessageService();

// server.js - 集成离线消息
io.on('connection', async (socket) => {
  // 发送离线消息
  const offlineMessages = await messageService.getOfflineMessages(
    socket.userId,
    socket.lastSeen || Date.now() - 7 * 24 * 60 * 60 * 1000 // 7天内
  );
  
  if (offlineMessages.length > 0) {
    socket.emit('messages:offline', { messages: offlineMessages });
  }
  
  // 更新最后在线时间
  socket.on('disconnect', async () => {
    await updateUserLastSeen(socket.userId, Date.now());
  });
});

性能优化与监控

// monitoring/metrics.js
const prometheus = require('prom-client');

// 定义指标
const connectedClients = new prometheus.Gauge({
  name: 'websocket_connected_clients',
  help: 'Number of connected WebSocket clients'
});

const messagesTotal = new prometheus.Counter({
  name: 'websocket_messages_total',
  help: 'Total number of WebSocket messages',
  labelNames: ['type']
});

const messageDuration = new prometheus.Histogram({
  name: 'websocket_message_duration_seconds',
  help: 'Message processing duration',
  labelNames: ['type'],
  buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1]
});

const roomMembers = new prometheus.Gauge({
  name: 'websocket_room_members',
  help: 'Number of members in each room',
  labelNames: ['room']
});

// 更新指标
io.on('connection', (socket) => {
  connectedClients.inc();
  
  socket.on('message:send', (data) => {
    const end = messageDuration.startTimer({ type: 'send' });
    messagesTotal.inc({ type: 'send' });
    
    // 处理消息...
    
    end();
  });
  
  socket.on('join:room', (roomId) => {
    roomMembers.inc({ room: roomId });
  });
  
  socket.on('leave:room', (roomId) => {
    roomMembers.dec({ room: roomId });
  });
  
  socket.on('disconnect', () => {
    connectedClients.dec();
  });
});

// 暴露metrics端点
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', prometheus.register.contentType);
  res.end(await prometheus.register.metrics());
});

// monitoring/health.js
app.get('/health', (req, res) => {
  const health = {
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    connections: io.sockets.sockets.size,
    memory: process.memoryUsage(),
    cpu: process.cpuUsage()
  };
  
  res.json(health);
});

// 连接限制
const MAX_CONNECTIONS_PER_USER = 5;

io.use((socket, next) => {
  const userConnections = Array.from(io.sockets.sockets.values())
    .filter(s => s.userId === socket.userId)
    .length;
  
  if (userConnections >= MAX_CONNECTIONS_PER_USER) {
    return next(new Error('Too many connections'));
  }
  
  next();
});

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「backend」更多文章

  1. 零信任安全架构:从边界防御到身份中心的安全范式
  2. 混沌工程实践:构建高可用系统的故障注入与弹性测试
  3. 流式数据处理:Kafka Streams与Flink实战指南