跳轉到內容

WebSocket 實時通信與長連接架構實戰 2026 | 實時應用開發完全指南

WebSocket 實時通信與長連接架構實戰

實時通信是現代 Web 應用的核心能力。從聊天室、實時協作到在線遊戲和金融行情,WebSocket 都是不可替代的技術。本文將系統講解 WebSocket 協議原理、服務端實現、客戶端封裝、集群擴展及生產環境最佳實踐。


一、WebSocket 協議原理

1.1 與 HTTP 的對比

特性HTTPWebSocket
通信模式請求-響應全雙工
連接生命週期短連接(HTTP/1.1 Keep-Alive 有限)長連接
服務端推送不支持(SSE 可單向推送)原生支持
頭部開銷每次請求攜帶完整頭僅首次握手有頭
協議http:// / https://ws:// / wss://
數據格式文本為主文本 + 二進制

1.2 握手過程

客戶端 → 服務端(HTTP Upgrade 請求)

GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服務端 → 客戶端(101 Switching Protocols)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手完成後,TCP 連接升級為 WebSocket 連接
雙方可以隨時發送數據幀(Frame)

1.3 數據幀結構

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

Opcode:
  0x0: 延續幀
  0x1: 文本幀
  0x2: 二進制幀
  0x8: 關閉幀
  0x9: Ping 幀
  0xA: Pong 幀

二、Node.js WebSocket 服務端

2.1 使用 ws 庫

typescript
import { WebSocketServer, WebSocket } from 'ws'
import http from 'http'

const server = http.createServer()
const wss = new WebSocketServer({ server, path: '/ws' })

// 連接管理
const clients = new Map<string, WebSocket>()

wss.on('connection', (ws, req) => {
  const clientId = generateId()
  clients.set(clientId, ws)

  console.log(`Client connected: ${clientId}, total: ${clients.size}`)

  // 發送歡迎消息
  ws.send(JSON.stringify({
    type: 'welcome',
    clientId,
    timestamp: Date.now()
  }))

  // 接收消息
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data.toString())
      handleMessage(clientId, message)
    } catch (err) {
      console.error('Invalid message:', err)
    }
  })

  // 關閉連接
  ws.on('close', () => {
    clients.delete(clientId)
    console.log(`Client disconnected: ${clientId}, total: ${clients.size}`)
  })

  // 錯誤處理
  ws.on('error', (err) => {
    console.error(`WebSocket error [${clientId}]:`, err)
  })
})

function handleMessage(senderId: string, message: any) {
  switch (message.type) {
    case 'chat':
      // 廣播聊天消息
      broadcast({
        type: 'chat',
        from: senderId,
        content: message.content,
        timestamp: Date.now()
      })
      break

    case 'private':
      // 私聊
      const target = clients.get(message.to)
      if (target && target.readyState === WebSocket.OPEN) {
        target.send(JSON.stringify({
          type: 'private',
          from: senderId,
          content: message.content,
          timestamp: Date.now()
        }))
      }
      break
  }
}

function broadcast(message: object) {
  const data = JSON.stringify(message)
  clients.forEach((ws) => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(data)
    }
  })
}

server.listen(3000, () => {
  console.log('WebSocket server running on ws://localhost:3000/ws')
})

2.2 心跳保活機制

typescript
// 心跳檢測:定期發送 Ping,清理無響應的連接
class HeartbeatManager {
  private pingInterval: NodeJS.Timeout
  private clients = new Map<WebSocket, boolean>()

  constructor(private wss: WebSocketServer, private interval = 30000) {
    this.start()
  }

  private start() {
    this.pingInterval = setInterval(() => {
      this.wss.clients.forEach((ws) => {
        if (ws.readyState === WebSocket.OPEN) {
          // 檢查上次 Pong 是否收到
          if (this.clients.get(ws) === false) {
            ws.terminate()
            return
          }

          // 標記為未響應,發送 Ping
          this.clients.set(ws, false)
          ws.ping()
        }
      })
    }, this.interval)

    // 監聽 Pong 響應
    this.wss.on('connection', (ws) => {
      this.clients.set(ws, true)

      ws.on('pong', () => {
        this.clients.set(ws, true)
      })

      ws.on('close', () => {
        this.clients.delete(ws)
      })
    })
  }

  stop() {
    clearInterval(this.pingInterval)
  }
}

// 使用
const heartbeat = new HeartbeatManager(wss, 30000)

三、Socket.IO 框架

3.1 服務端

typescript
import { Server } from 'socket.io'
import { createServer } from 'http'

const httpServer = createServer()
const io = new Server(httpServer, {
  cors: {
    origin: ['https://example.com', 'http://localhost:5173'],
    credentials: true
  },
  // 適配器配置(集群模式)
  adapter: undefined, // 後面用 Redis Adapter
})

// 命名空間(Namespace):隔離不同業務
const chatNs = io.of('/chat')
const adminNs = io.of('/admin')

// 中間件:認證
chatNs.use((socket, next) => {
  const token = socket.handshake.auth.token
  if (!token) {
    return next(new Error('Authentication required'))
  }

  try {
    const user = verifyToken(token)
    socket.data.user = user
    next()
  } catch {
    next(new Error('Invalid token'))
  }
})

// 房間(Room):群組通信
chatNs.on('connection', (socket) => {
  const user = socket.data.user

  console.log(`User connected: ${user.name}`)

  // 加入房間
  socket.on('join-room', (roomId: string) => {
    socket.join(roomId)
    socket.to(roomId).emit('user-joined', {
      userId: user.id,
      name: user.name,
      timestamp: Date.now()
    })
  })

  // 離開房間
  socket.on('leave-room', (roomId: string) => {
    socket.leave(roomId)
    socket.to(roomId).emit('user-left', {
      userId: user.id,
      timestamp: Date.now()
    })
  })

  // 發送消息到房間
  socket.on('send-message', (data: { roomId: string; content: string }) => {
    chatNs.to(data.roomId).emit('new-message', {
      from: user.id,
      name: user.name,
      content: data.content,
      timestamp: Date.now()
    })
  })

  // 輸入狀態
  socket.on('typing', (roomId: string) => {
    socket.to(roomId).emit('user-typing', { userId: user.id })
  })

  socket.on('stop-typing', (roomId: string) => {
    socket.to(roomId).emit('user-stop-typing', { userId: user.id })
  })

  // 斷開連接
  socket.on('disconnect', (reason) => {
    console.log(`User disconnected: ${user.name}, reason: ${reason}`)
  })
})

httpServer.listen(3000, () => {
  console.log('Socket.IO server running on port 3000')
})

3.2 客戶端

typescript
import { io, Socket } from 'socket.io-client'

class ChatClient {
  private socket: Socket
  private reconnectAttempts = 0

  constructor(url: string, token: string) {
    this.socket = io(url, {
      auth: { token },
      transports: ['websocket'],  // 僅使用 WebSocket
      reconnection: true,
      reconnectionAttempts: 10,
      reconnectionDelay: 1000,
      reconnectionDelayMax: 5000,
    })

    this.setupListeners()
  }

  private setupListeners() {
    this.socket.on('connect', () => {
      console.log('Connected:', this.socket.id)
      this.reconnectAttempts = 0
    })

    this.socket.on('disconnect', (reason) => {
      console.log('Disconnected:', reason)
    })

    this.socket.on('connect_error', (err) => {
      console.error('Connection error:', err.message)
      this.reconnectAttempts++
    })

    // 重連成功
    this.socket.io.on('reconnect', (attempt) => {
      console.log(`Reconnected after ${attempt} attempts`)
    })
  }

  joinRoom(roomId: string) {
    this.socket.emit('join-room', roomId)
  }

  sendMessage(roomId: string, content: string) {
    this.socket.emit('send-message', { roomId, content })
  }

  onMessage(callback: (data: any) => void) {
    this.socket.on('new-message', callback)
  }

  onUserJoined(callback: (data: any) => void) {
    this.socket.on('user-joined', callback)
  }

  onUserLeft(callback: (data: any) => void) {
    this.socket.on('user-left', callback)
  }

  disconnect() {
    this.socket.disconnect()
  }
}

// 使用
const client = new ChatClient('https://api.example.com/chat', jwtToken)
client.joinRoom('room-123')
client.sendMessage('room-123', 'Hello World!')
client.onMessage((data) => {
  console.log(`${data.name}: ${data.content}`)
})

四、Vue 3 集成

4.1 Composable 封裝

typescript
// composables/useWebSocket.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useWebSocket(url: string) {
  const isConnected = ref(false)
  const messages = ref<any[]>([])
  const error = ref<string | null>(null)

  let ws: WebSocket | null = null
  let reconnectTimer: NodeJS.Timeout | null = null
  let reconnectAttempts = 0
  const MAX_RECONNECT = 5

  function connect() {
    ws = new WebSocket(url)

    ws.onopen = () => {
      isConnected.value = true
      error.value = null
      reconnectAttempts = 0
      console.log('WebSocket connected')
    }

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      messages.value.push(data)
    }

    ws.onerror = (err) => {
      error.value = 'WebSocket error'
      console.error('WebSocket error:', err)
    }

    ws.onclose = (event) => {
      isConnected.value = false
      console.log('WebSocket closed:', event.code, event.reason)

      // 自動重連
      if (reconnectAttempts < MAX_RECONNECT) {
        reconnectAttempts++
        const delay = Math.min(1000 * 2 ** reconnectAttempts, 30000)
        console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`)

        reconnectTimer = setTimeout(() => connect(), delay)
      }
    }
  }

  function send(data: any) {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify(data))
    } else {
      console.warn('WebSocket is not connected')
    }
  }

  function disconnect() {
    if (reconnectTimer) clearTimeout(reconnectTimer)
    if (ws) {
      ws.close()
      ws = null
    }
  }

  onMounted(connect)
  onUnmounted(disconnect)

  return { isConnected, messages, error, send, disconnect }
}

4.2 在組件中使用

vue
<script setup lang="ts">
import { useWebSocket } from '@/composables/useWebSocket'

const { isConnected, messages, send } = useWebSocket('ws://localhost:3000/ws')

const input = ref('')

function handleSend() {
  if (!input.value.trim()) return
  send({ type: 'chat', content: input.value })
  input.value = ''
}
</script>

<template>
  <div>
    <div class="status">
      <span :class="isConnected ? 'online' : 'offline'">
        {{ isConnected ? '在線' : '離線' }}
      </span>
    </div>

    <div class="messages">
      <div v-for="(msg, i) in messages" :key="i" class="message">
        <span class="from">{{ msg.from || 'System' }}:</span>
        <span class="content">{{ msg.content }}</span>
      </div>
    </div>

    <input
      v-model="input"
      @keyup.enter="handleSend"
      placeholder="輸入消息..."
    />
    <button @click="handleSend" :disabled="!isConnected">發送</button>
  </div>
</template>

五、集群擴展

5.1 Redis Pub/Sub 適配器

typescript
import { Server } from 'socket.io'
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient } from 'redis'

const io = new Server()

// 創建 Redis 客戶端
const pubClient = createClient({ url: 'redis://localhost:6379' })
const subClient = pubClient.duplicate()

await Promise.all([pubClient.connect(), subClient.connect()])

// 使用 Redis 適配器
io.adapter(createAdapter(pubClient, subClient))

// 現在多個 Socket.IO 實例可以共享事件
// Server A 上的客戶端發消息 → Server B 上的客戶端也能收到

5.2 多實例部署

                    ┌─────────────┐
                    │   Nginx     │
                    │  負載均衡    │
                    └──────┬──────┘
              ┌────────────┼────────────┐
              ↓            ↓            ↓
        ┌──────────┐ ┌──────────┐ ┌──────────┐
        │ Server A │ │ Server B │ │ Server C │
        │ Socket.IO│ │ Socket.IO│ │ Socket.IO│
        └────┬─────┘ └────┬─────┘ └────┬─────┘
             │            │            │
             └────────────┼────────────┘

                    ┌──────────┐
                    │  Redis   │
                    │ Pub/Sub  │
                    └──────────┘
nginx
# Nginx WebSocket 負載均衡
upstream websocket {
  ip_hash;  # 確保同一客戶端連接到同一服務器
  server 10.0.0.1:3000;
  server 10.0.0.2:3000;
  server 10.0.0.3:3000;
}

server {
  listen 443 ssl;
  server_name ws.example.com;

  location /socket.io/ {
    proxy_pass http://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 86400s;
    proxy_send_timeout 86400s;
  }
}

5.3 Redis 消息廣播

typescript
// 不使用 Socket.IO Adapter,直接用 Redis Pub/Sub
import { createClient } from 'redis'
import { WebSocketServer } from 'ws'

const redis = createClient({ url: 'redis://localhost:6379' })
await redis.connect()

const wss = new WebSocketServer({ port: 3000 })

// 訂閱頻道
await redis.subscribe('broadcast', (message) => {
  // 收到 Redis 消息後,廣播給本實例的所有客戶端
  wss.clients.forEach((ws) => {
    if (ws.readyState === ws.OPEN) {
      ws.send(message)
    }
  })
})

// 發佈消息到所有實例
function globalBroadcast(message: object) {
  redis.publish('broadcast', JSON.stringify(message))
}

六、實時應用場景

6.1 實時通知系統

typescript
class NotificationService {
  private userSockets = new Map<string, Set<WebSocket>>()

  // 用戶上線
  onUserConnect(userId: string, ws: WebSocket) {
    if (!this.userSockets.has(userId)) {
      this.userSockets.set(userId, new Set())
    }
    this.userSockets.get(userId)!.add(ws)
  }

  // 用戶下線
  onUserDisconnect(userId: string, ws: WebSocket) {
    this.userSockets.get(userId)?.delete(ws)
    if (this.userSockets.get(userId)?.size === 0) {
      this.userSockets.delete(userId)
    }
  }

  // 發送通知給指定用戶
  sendToUser(userId: string, notification: any) {
    const sockets = this.userSockets.get(userId)
    if (!sockets) return false // 用戶不在線

    const data = JSON.stringify({
      type: 'notification',
      data: notification
    })

    sockets.forEach(ws => {
      if (ws.readyState === ws.OPEN) {
        ws.send(data)
      }
    })

    return true
  }

  // 檢查用戶是否在線
  isUserOnline(userId: string): boolean {
    return this.userSockets.has(userId)
  }
}

6.2 實時協作(OT 算法基礎)

typescript
class CollaborativeDoc {
  private content = ''
  private version = 0
  private clients = new Map<WebSocket, { userId: string; cursor: number }>()

  // 應用操作
  applyOperation(ws: WebSocket, op: Operation) {
    // 基於 version 檢查
    if (op.baseVersion !== this.version) {
      // 需要變換(OT)
      op = this.transform(op, op.baseVersion)
    }

    // 應用操作
    if (op.type === 'insert') {
      this.content = this.content.slice(0, op.position) +
        op.text + this.content.slice(op.position)
    } else if (op.type === 'delete') {
      this.content = this.content.slice(0, op.position) +
        this.content.slice(op.position + op.length)
    }

    this.version++

    // 廣播給其他客戶端
    const data = JSON.stringify({
      type: 'operation',
      operation: op,
      version: this.version
    })

    this.clients.forEach((_, client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data)
      }
    })
  }

  transform(op: Operation, baseVersion: number): Operation {
    // 簡化的 OT 變換邏輯
    // 實際中需要更復雜的變換算法
    return op
  }
}

七、安全與性能

7.1 認證與授權

typescript
// 方式 1:URL 參數(不推薦,會出現在日誌中)
// ws://example.com/ws?token=xxx

// 方式 2:Sec-WebSocket-Protocol 頭
const ws = new WebSocket('wss://example.com/ws', ['bearer', jwtToken])

// 方式 3:Cookie(推薦)
// 瀏覽器會自動攜帶同源 Cookie
// 前端無需額外配置

// 服務端驗證
wss.on('connection', (ws, req) => {
  const cookie = req.headers.cookie
  const token = parseCookie(cookie, 'session')

  if (!token) {
    ws.close(4001, 'Unauthorized')
    return
  }

  try {
    const user = verifyToken(token)
    ws.data = { user }
  } catch {
    ws.close(4001, 'Invalid token')
  }
})

7.2 消息限流

typescript
class RateLimiter {
  private buckets = new Map<string, { count: number; resetTime: number }>()

  check(clientId: string, limit = 50, windowMs = 1000): boolean {
    const now = Date.now()
    const bucket = this.buckets.get(clientId)

    if (!bucket || now > bucket.resetTime) {
      this.buckets.set(clientId, { count: 1, resetTime: now + windowMs })
      return true
    }

    if (bucket.count >= limit) {
      return false
    }

    bucket.count++
    return true
  }
}

const limiter = new RateLimiter()

ws.on('message', (data) => {
  if (!limiter.check(clientId)) {
    ws.close(4003, 'Rate limit exceeded')
    return
  }

  // 處理消息
})

7.3 消息大小限制

typescript
import { WebSocketServer } from 'ws'

const wss = new WebSocketServer({
  port: 3000,
  maxPayload: 1024 * 1024,  // 最大 1MB
})

ws.on('message', (data, isBinary) => {
  if (data.length > 100 * 1024) {
    ws.close(4002, 'Message too large')
    return
  }
  // 處理消息
})

八、監控與運維

8.1 連接統計

typescript
class ConnectionStats {
  private stats = {
    totalConnections: 0,
    currentConnections: 0,
    messagesReceived: 0,
    messagesSent: 0,
    errors: 0,
  }

  onConnect() {
    this.stats.totalConnections++
    this.stats.currentConnections++
  }

  onDisconnect() {
    this.stats.currentConnections--
  }

  onMessageReceived() {
    this.stats.messagesReceived++
  }

  onMessageSent() {
    this.stats.messagesSent++
  }

  getStats() {
    return { ...this.stats, timestamp: Date.now() }
  }
}

// 定期上報到 Prometheus
setInterval(() => {
  const stats = connectionStats.getStats()
  // metrics.gauge('ws_current_connections', stats.currentConnections)
  // metrics.counter('ws_messages_total', stats.messagesReceived)
}, 10000)

8.2 優雅關閉

typescript
async function gracefulShutdown() {
  console.log('Shutting down WebSocket server...')

  // 1. 停止接受新連接
  wss.close()

  // 2. 通知所有客戶端
  wss.clients.forEach((ws) => {
    ws.send(JSON.stringify({
      type: 'server-shutdown',
      message: 'Server is shutting down. Please reconnect.',
      retryAfter: 5000
    }))
  })

  // 3. 等待消息發送完成
  await new Promise(resolve => setTimeout(resolve, 2000))

  // 4. 關閉所有連接
  wss.clients.forEach((ws) => {
    ws.close(1001, 'Server shutting down')
  })

  // 5. 關閉 HTTP 服務器
  server.close()
}

process.on('SIGTERM', gracefulShutdown)
process.on('SIGINT', gracefulShutdown)

九、總結

  • ✅ WebSocket 協議原理(握手、數據幀、與 HTTP 對比)
  • ✅ Node.js ws 庫實現(連接管理、消息處理、廣播)
  • ✅ 心跳保活機制(Ping/Pong、無響應清理)
  • ✅ Socket.IO 框架(命名空間、房間、中間件認證)
  • ✅ 客戶端封裝(自動重連、指數退避、Vue 3 Composable)
  • ✅ 集群擴展(Redis Pub/Sub Adapter、多實例部署、Nginx 負載均衡)
  • ✅ 實時應用場景(通知系統、實時協作)
  • ✅ 安全防護(認證授權、消息限流、大小限制)
  • ✅ 監控運維(連接統計、優雅關閉)

WebSocket 是構建實時應用的核心技術,掌握從單機到集群的完整方案是全棧工程師的必備能力。


相關閱讀:

最後更新於: