返回笔记首页

WebSocket 架构设计

主题配置

一、技术实现方案

1.1 核心架构设计

plain
客户端层
  ├── WebSocket Manager (连接管理器)
  ├── Message Queue (消息队列)
  ├── Reconnect Strategy (重连策略)
  └── Heartbeat Monitor (心跳监控)

服务端层
  ├── Connection Pool (连接池)
  ├── Message Router (消息路由)
  └── State Manager (状态管理)

1.2 技术选型

  • 客户端: Vue 3 + Native WebSocket API
  • 消息格式: JSON
  • 心跳间隔: 30秒
  • 重连策略: 指数退避算法
  • 消息确认: ACK机制

二、连接管理与心跳

2.1 代码实现

websocket-manager.js

javascript
// WebSocket 管理器
export class WebSocketManager {
  constructor(url, options = {}) {
    this.url = url
    this.ws = null
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5
    this.reconnectInterval = options.reconnectInterval || 1000
    this.heartbeatInterval = options.heartbeatInterval || 30000
    this.heartbeatTimer = null
    this.reconnectTimer = null
    this.isManualClose = false

    // 回调函数
    this.onOpen = options.onOpen || (() => {})
    this.onMessage = options.onMessage || (() => {})
    this.onError = options.onError || (() => {})
    this.onClose = options.onClose || (() => {})
  }

  // 建立连接
  connect() {
    try {
      this.ws = new WebSocket(this.url)
      this.setupEventListeners()
    } catch (error) {
      console.error('WebSocket连接失败:', error)
      this.handleReconnect()
    }
  }

  // 设置事件监听
  setupEventListeners() {
    this.ws.onopen = () => {
      console.log('WebSocket连接成功')
      this.reconnectAttempts = 0
      this.startHeartbeat()
      this.onOpen()
    }

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data)

      // 处理心跳响应
      if (data.type === 'pong') {
        console.log('收到心跳响应')
        return
      }

      this.onMessage(data)
    }

    this.ws.onerror = (error) => {
      console.error('WebSocket错误:', error)
      this.onError(error)
    }

    this.ws.onclose = () => {
      console.log('WebSocket连接关闭')
      this.stopHeartbeat()
      this.onClose()

      if (!this.isManualClose) {
        this.handleReconnect()
      }
    }
  }

  // 发送消息
  send(data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
      return true
    }
    console.warn('WebSocket未连接,消息发送失败')
    return false
  }

  // 启动心跳
  startHeartbeat() {
    this.stopHeartbeat()
    this.heartbeatTimer = setInterval(() => {
      this.send({ type: 'ping', timestamp: Date.now() })
    }, this.heartbeatInterval)
  }

  // 停止心跳
  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  // 处理重连
  handleReconnect() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('达到最大重连次数,停止重连')
      return
    }

    const delay = Math.min(
      this.reconnectInterval * Math.pow(2, this.reconnectAttempts),
      30000
    )

    console.log(`${delay}ms后尝试第${this.reconnectAttempts + 1}次重连`)

    this.reconnectTimer = setTimeout(() => {
      this.reconnectAttempts++
      this.connect()
    }, delay)
  }

  // 手动关闭连接
  close() {
    this.isManualClose = true
    this.stopHeartbeat()

    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
    }

    if (this.ws) {
      this.ws.close()
    }
  }
}
useWebSocket.js (Vue 3 Composition API)
javascript
import { ref, onMounted, onUnmounted } from 'vue'
import { WebSocketManager } from './websocket-manager.js'

export function useWebSocket(url, options = {}) {
  const isConnected = ref(false)
  const connectionStatus = ref('disconnected') // disconnected, connecting, connected
  const lastMessage = ref(null)
  const error = ref(null)

  let wsManager = null

  const connect = () => {
    connectionStatus.value = 'connecting'

    wsManager = new WebSocketManager(url, {
      ...options,
      onOpen: () => {
        isConnected.value = true
        connectionStatus.value = 'connected'
        error.value = null
        options.onOpen?.()
      },
      onMessage: (data) => {
        lastMessage.value = data
        options.onMessage?.(data)
      },
      onError: (err) => {
        error.value = err
        options.onError?.(err)
      },
      onClose: () => {
        isConnected.value = false
        connectionStatus.value = 'disconnected'
        options.onClose?.()
      }
    })

    wsManager.connect()
  }

  const send = (data) => {
    return wsManager?.send(data)
  }

  const close = () => {
    wsManager?.close()
  }

  onMounted(() => {
    if (options.autoConnect !== false) {
      connect()
    }
  })

  onUnmounted(() => {
    close()
  })

  return {
    isConnected,
    connectionStatus,
    lastMessage,
    error,
    connect,
    send,
    close
  }
}

2.2 使用示例

WebSocketDemo.vue

vue
<script setup>
import { ref } from 'vue'
import { useWebSocket } from './useWebSocket.js'

const messages = ref([])
const inputMessage = ref('')

const { isConnected, connectionStatus, send } = useWebSocket(
  'ws://localhost:8080/ws',
  {
    onMessage: (data) => {
      messages.value.push({
        id: Date.now(),
        content: data.content,
        time: new Date().toLocaleTimeString(),
        type: 'received'
      })
    },
    heartbeatInterval: 30000,
    maxReconnectAttempts: 5
  }
)

const sendMessage = () => {
  if (!inputMessage.value.trim()) return

  const message = {
    type: 'message',
    content: inputMessage.value,
    timestamp: Date.now()
  }

  if (send(message)) {
    messages.value.push({
      id: Date.now(),
      content: inputMessage.value,
      time: new Date().toLocaleTimeString(),
      type: 'sent'
    })
    inputMessage.value = ''
  }
}

const getStatusColor = () => {
  const colors = {
    connected: '#67c23a',
    connecting: '#e6a23c',
    disconnected: '#f56c6c'
  }
  return colors[connectionStatus.value] || '#909399'
}

const getStatusText = () => {
  const texts = {
    connected: '已连接',
    connecting: '连接中',
    disconnected: '已断开'
  }
  return texts[connectionStatus.value] || '未知'
}
</script>

<template>
  <div class="websocket-demo">
    <div class="header">
      <h2>WebSocket 连接管理演示</h2>
      <div class="status">
        <span class="status-dot" :style="{ backgroundColor: getStatusColor() }"></span>
        <span class="status-text">{{ getStatusText() }}</span>
      </div>
    </div>

    <div class="message-container">
      <div class="message-list">
        <div
          v-for="msg in messages"
          :key="msg.id"
          :class="['message-item', msg.type]"
        >
          <div class="message-content">{{ msg.content }}</div>
          <div class="message-time">{{ msg.time }}</div>
        </div>
      </div>

      <div class="input-area">
        <input
          v-model="inputMessage"
          type="text"
          placeholder="输入消息..."
          @keyup.enter="sendMessage"
          :disabled="!isConnected"
        />
        <button @click="sendMessage" :disabled="!isConnected">
          发送
        </button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.websocket-demo {
  max-width: 800px;
  margin: 20px auto;
  border: 1px solid #ddd;
  border-radius: 8px;
  overflow: hidden;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 16px 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.header h2 {
  margin: 0;
  font-size: 18px;
}

.status {
  display: flex;
  align-items: center;
  gap: 8px;
}

.status-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  animation: pulse 2s infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

.status-text {
  font-size: 14px;
}

.message-container {
  height: 500px;
  display: flex;
  flex-direction: column;
}

.message-list {
  flex: 1;
  padding: 20px;
  overflow-y: auto;
  background: #f5f7fa;
}

.message-item {
  margin-bottom: 16px;
  display: flex;
  flex-direction: column;
}

.message-item.sent {
  align-items: flex-end;
}

.message-item.received {
  align-items: flex-start;
}

.message-content {
  max-width: 60%;
  padding: 10px 14px;
  border-radius: 8px;
  word-wrap: break-word;
}

.message-item.sent .message-content {
  background: #409eff;
  color: white;
}

.message-item.received .message-content {
  background: white;
  color: #333;
  border: 1px solid #e4e7ed;
}

.message-time {
  font-size: 12px;
  color: #909399;
  margin-top: 4px;
}

.input-area {
  display: flex;
  padding: 16px;
  background: white;
  border-top: 1px solid #e4e7ed;
}

.input-area input {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
  outline: none;
}

.input-area input:focus {
  border-color: #409eff;
}

.input-area input:disabled {
  background: #f5f7fa;
  cursor: not-allowed;
}

.input-area button {
  margin-left: 12px;
  padding: 10px 24px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.input-area button:hover:not(:disabled) {
  background: #66b1ff;
}

.input-area button:disabled {
  background: #c0c4cc;
  cursor: not-allowed;
}
</style>

三、断线重连机制

3.1 指数退避算法实现

javascript
// reconnect-strategy.js
export class ReconnectStrategy {
  constructor(options = {}) {
    this.baseDelay = options.baseDelay || 1000
    this.maxDelay = options.maxDelay || 30000
    this.maxAttempts = options.maxAttempts || 5
    this.currentAttempt = 0
  }

  // 计算下次重连延迟(指数退避)
  getNextDelay() {
    if (this.currentAttempt >= this.maxAttempts) {
      return null
    }

    const delay = Math.min(
      this.baseDelay * Math.pow(2, this.currentAttempt),
      this.maxDelay
    )

    this.currentAttempt++
    return delay
  }

  // 重置计数
  reset() {
    this.currentAttempt = 0
  }

  // 是否可以继续重连
  canReconnect() {
    return this.currentAttempt < this.maxAttempts
  }

  // 获取当前尝试次数
  getAttemptCount() {
    return this.currentAttempt
  }
}

3.2 增强版WebSocket管理器

javascript
// enhanced-websocket.js
import { ReconnectStrategy } from './reconnect-strategy.js'

export class EnhancedWebSocket {
  constructor(url, options = {}) {
    this.url = url
    this.ws = null
    this.reconnectStrategy = new ReconnectStrategy(options)
    this.reconnectTimer = null
    this.isManualClose = false

    // 连接状态
    this.connectionState = 'disconnected' // disconnected, connecting, connected, reconnecting

    // 事件回调
    this.eventHandlers = {
      open: [],
      message: [],
      error: [],
      close: [],
      reconnect: []
    }
  }

  // 添加事件监听
  on(event, handler) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].push(handler)
    }
  }

  // 触发事件
  emit(event, data) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].forEach(handler => handler(data))
    }
  }

  // 连接
  connect() {
    if (this.connectionState === 'connected' || this.connectionState === 'connecting') {
      return
    }

    this.connectionState = 'connecting'

    try {
      this.ws = new WebSocket(this.url)
      this.setupEventListeners()
    } catch (error) {
      console.error('WebSocket连接失败:', error)
      this.handleReconnect()
    }
  }

  // 设置事件监听
  setupEventListeners() {
    this.ws.onopen = () => {
      this.connectionState = 'connected'
      this.reconnectStrategy.reset()
      this.emit('open', { timestamp: Date.now() })
    }

    this.ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)
        this.emit('message', data)
      } catch (error) {
        console.error('消息解析失败:', error)
      }
    }

    this.ws.onerror = (error) => {
      this.emit('error', error)
    }

    this.ws.onclose = (event) => {
      this.connectionState = 'disconnected'
      this.emit('close', {
        code: event.code,
        reason: event.reason,
        wasClean: event.wasClean
      })

      if (!this.isManualClose) {
        this.handleReconnect()
      }
    }
  }

  // 处理重连
  handleReconnect() {
    const delay = this.reconnectStrategy.getNextDelay()

    if (delay === null) {
      console.error('达到最大重连次数')
      return
    }

    this.connectionState = 'reconnecting'

    this.emit('reconnect', {
      attempt: this.reconnectStrategy.getAttemptCount(),
      delay: delay
    })

    console.log(`将在${delay}ms后进行第${this.reconnectStrategy.getAttemptCount()}次重连`)

    this.reconnectTimer = setTimeout(() => {
      this.connect()
    }, delay)
  }

  // 发送消息
  send(data) {
    if (this.connectionState === 'connected' && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
      return true
    }
    return false
  }

  // 关闭连接
  close() {
    this.isManualClose = true

    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
    }

    if (this.ws) {
      this.ws.close()
    }

    this.connectionState = 'disconnected'
  }

  // 获取连接状态
  getState() {
    return this.connectionState
  }
}

3.3 Vue组件示例

vue
<script setup>
import { ref, computed } from 'vue'
import { EnhancedWebSocket } from './enhanced-websocket.js'

const ws = ref(null)
const connectionState = ref('disconnected')
const reconnectAttempts = ref(0)
const reconnectDelay = ref(0)
const messages = ref([])

const stateConfig = computed(() => {
  const configs = {
    disconnected: { text: '已断开', color: '#f56c6c', icon: '✕' },
    connecting: { text: '连接中...', color: '#e6a23c', icon: '↻' },
    connected: { text: '已连接', color: '#67c23a', icon: '✓' },
    reconnecting: { text: `重连中(${reconnectAttempts.value})`, color: '#409eff', icon: '↻' }
  }
  return configs[connectionState.value] || configs.disconnected
})

const initWebSocket = () => {
  ws.value = new EnhancedWebSocket('ws://localhost:8080/ws', {
    baseDelay: 1000,
    maxDelay: 30000,
    maxAttempts: 5
  })

  ws.value.on('open', () => {
    connectionState.value = 'connected'
    reconnectAttempts.value = 0
    addLog('连接成功', 'success')
  })

  ws.value.on('message', (data) => {
    addLog(`收到消息: ${JSON.stringify(data)}`, 'info')
  })

  ws.value.on('error', (error) => {
    addLog(`连接错误: ${error.message}`, 'error')
  })

  ws.value.on('close', (data) => {
    connectionState.value = 'disconnected'
    addLog(`连接关闭 [${data.code}]: ${data.reason}`, 'warning')
  })

  ws.value.on('reconnect', (data) => {
    connectionState.value = 'reconnecting'
    reconnectAttempts.value = data.attempt
    reconnectDelay.value = data.delay
    addLog(`第${data.attempt}次重连,延迟${data.delay}ms`, 'info')
  })

  ws.value.connect()
}

const addLog = (message, type = 'info') => {
  messages.value.unshift({
    id: Date.now(),
    message,
    type,
    time: new Date().toLocaleTimeString()
  })

  if (messages.value.length > 50) {
    messages.value = messages.value.slice(0, 50)
  }
}

const handleConnect = () => {
  if (!ws.value) {
    initWebSocket()
  } else {
    ws.value.connect()
  }
}

const handleDisconnect = () => {
  ws.value?.close()
  connectionState.value = 'disconnected'
  addLog('手动断开连接', 'warning')
}

const sendTestMessage = () => {
  const success = ws.value?.send({
    type: 'test',
    content: 'Hello WebSocket',
    timestamp: Date.now()
  })

  if (success) {
    addLog('测试消息已发送', 'success')
  } else {
    addLog('消息发送失败:未连接', 'error')
  }
}

// 初始化
initWebSocket()
</script>

<template>
  <div class="reconnect-demo">
    <div class="header">
      <h2>断线重连机制演示</h2>
      <div class="status-badge" :style="{ background: stateConfig.color }">
        <span class="icon">{{ stateConfig.icon }}</span>
        {{ stateConfig.text }}
      </div>
    </div>

    <div class="control-panel">
      <button @click="handleConnect" :disabled="connectionState === 'connected'">
        连接
      </button>
      <button @click="handleDisconnect" :disabled="connectionState === 'disconnected'">
        断开
      </button>
      <button @click="sendTestMessage" :disabled="connectionState !== 'connected'">
        发送测试消息
      </button>
    </div>

    <div class="info-panel">
      <div class="info-item">
        <label>连接状态:</label>
        <span>{{ stateConfig.text }}</span>
      </div>
      <div class="info-item">
        <label>重连次数:</label>
        <span>{{ reconnectAttempts }}/5</span>
      </div>
      <div class="info-item" v-if="reconnectDelay > 0">
        <label>重连延迟:</label>
        <span>{{ reconnectDelay }}ms</span>
      </div>
    </div>

    <div class="log-panel">
      <h3>连接日志</h3>
      <div class="log-list">
        <div
          v-for="log in messages"
          :key="log.id"
          :class="['log-item', log.type]"
        >
          <span class="log-time">{{ log.time }}</span>
          <span class="log-message">{{ log.message }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.reconnect-demo {
  max-width: 900px;
  margin: 20px auto;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.header h2 {
  margin: 0;
  font-size: 20px;
}

.status-badge {
  padding: 6px 16px;
  border-radius: 20px;
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.icon {
  font-size: 16px;
  animation: rotate 2s linear infinite;
}

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.control-panel {
  padding: 20px;
  display: flex;
  gap: 12px;
  border-bottom: 1px solid #e4e7ed;
}

.control-panel button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  background: #409eff;
  color: white;
  cursor: pointer;
  font-size: 14px;
  transition: all 0.3s;
}

.control-panel button:hover:not(:disabled) {
  background: #66b1ff;
  transform: translateY(-2px);
}

.control-panel button:disabled {
  background: #c0c4cc;
  cursor: not-allowed;
  transform: none;
}

.info-panel {
  padding: 20px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
  background: #f5f7fa;
}

.info-item {
  display: flex;
  align-items: center;
  gap: 8px;
}

.info-item label {
  font-weight: 600;
  color: #606266;
}

.info-item span {
  color: #303133;
}

.log-panel {
  padding: 20px;
}

.log-panel h3 {
  margin: 0 0 16px 0;
  font-size: 16px;
  color: #303133;
}

.log-list {
  height: 400px;
  overflow-y: auto;
  background: #f5f7fa;
  border-radius: 4px;
  padding: 12px;
}

.log-item {
  padding: 8px 12px;
  margin-bottom: 8px;
  border-radius: 4px;
  font-size: 13px;
  display: flex;
  gap: 12px;
  border-left: 3px solid;
}

.log-item.success {
  background: #f0f9ff;
  border-color: #67c23a;
}

.log-item.error {
  background: #fef0f0;
  border-color: #f56c6c;
}

.log-item.warning {
  background: #fdf6ec;
  border-color: #e6a23c;
}

.log-item.info {
  background: #f4f4f5;
  border-color: #909399;
}

.log-time {
  color: #909399;
  flex-shrink: 0;
}

.log-message {
  color: #303133;
  flex: 1;
}
</style>

四、消息队列与确认

4.1 消息队列管理器

javascript
// message-queue.js
export class MessageQueue {
  constructor(options = {}) {
    this.queue = []
    this.pendingMessages = new Map() // 等待确认的消息
    this.maxQueueSize = options.maxQueueSize || 100
    this.timeout = options.timeout || 5000
    this.retryTimes = options.retryTimes || 3
  }

  // 添加消息到队列
  enqueue(message) {
    if (this.queue.length >= this.maxQueueSize) {
      console.warn('消息队列已满,丢弃最早的消息')
      this.queue.shift()
    }

    const messageWithId = {
      ...message,
      messageId: this.generateMessageId(),
      timestamp: Date.now(),
      retryCount: 0
    }

    this.queue.push(messageWithId)
    return messageWithId.messageId
  }

  // 获取队列中的消息
  dequeue() {
    return this.queue.shift()
  }

  // 批量获取消息
  dequeueBatch(count) {
    const messages = this.queue.splice(0, count)
    return messages
  }

  // 发送消息并等待确认
  async sendWithAck(message, sendFn) {
    const messageId = message.messageId || this.generateMessageId()
    message.messageId = messageId

    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        this.pendingMessages.delete(messageId)

        if (message.retryCount < this.retryTimes) {
          message.retryCount++
          console.log(`消息${messageId}超时,重试第${message.retryCount}次`)
          this.sendWithAck(message, sendFn).then(resolve).catch(reject)
        } else {
          reject(new Error(`消息${messageId}发送失败:超过重试次数`))
        }
      }, this.timeout)

      this.pendingMessages.set(messageId, {
        message,
        resolve,
        reject,
        timeoutId,
        timestamp: Date.now()
      })

      sendFn(message)
    })
  }

  // 确认消息已收到
  ack(messageId) {
    const pending = this.pendingMessages.get(messageId)

    if (pending) {
      clearTimeout(pending.timeoutId)
      pending.resolve(messageId)
      this.pendingMessages.delete(messageId)
      return true
    }

    return false
  }

  // 拒绝消息
  nack(messageId, reason) {
    const pending = this.pendingMessages.get(messageId)

    if (pending) {
      clearTimeout(pending.timeoutId)
      pending.reject(new Error(reason))
      this.pendingMessages.delete(messageId)
      return true
    }

    return false
  }

  // 生成消息ID
  generateMessageId() {
    return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
  }

  // 获取队列长度
  size() {
    return this.queue.length
  }

  // 获取待确认消息数量
  pendingSize() {
    return this.pendingMessages.size
  }

  // 清空队列
  clear() {
    this.queue = []
    this.pendingMessages.forEach(pending => {
      clearTimeout(pending.timeoutId)
      pending.reject(new Error('队列已清空'))
    })
    this.pendingMessages.clear()
  }
}

4.2 带消息队列的WebSocket

javascript
// websocket-with-queue.js
import { MessageQueue } from './message-queue.js'

export class WebSocketWithQueue {
  constructor(url, options = {}) {
    this.url = url
    this.ws = null
    this.messageQueue = new MessageQueue(options)
    this.isConnected = false
    this.eventHandlers = {
      open: [],
      message: [],
      error: [],
      close: []
    }
  }

  // 连接
  connect() {
    this.ws = new WebSocket(this.url)

    this.ws.onopen = () => {
      this.isConnected = true
      this.emit('open')
      this.processQueue()
    }

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data)

      // 处理ACK消息
      if (data.type === 'ack') {
        this.messageQueue.ack(data.messageId)
        return
      }

      // 处理NACK消息
      if (data.type === 'nack') {
        this.messageQueue.nack(data.messageId, data.reason)
        return
      }

      // 发送ACK确认
      this.sendAck(data.messageId)

      this.emit('message', data)
    }

    this.ws.onclose = () => {
      this.isConnected = false
      this.emit('close')
    }

    this.ws.onerror = (error) => {
      this.emit('error', error)
    }
  }

  // 发送消息(带队列)
  send(message) {
    const messageId = this.messageQueue.enqueue(message)

    if (this.isConnected) {
      this.processQueue()
    }

    return messageId
  }

  // 发送消息并等待确认
  async sendWithAck(message) {
    if (!this.isConnected) {
      throw new Error('WebSocket未连接')
    }

    return await this.messageQueue.sendWithAck(message, (msg) => {
      this.ws.send(JSON.stringify(msg))
    })
  }

  // 处理队列中的消息
  processQueue() {
    while (this.messageQueue.size() > 0 && this.isConnected) {
      const message = this.messageQueue.dequeue()
      this.ws.send(JSON.stringify(message))
    }
  }

  // 发送ACK确认
  sendAck(messageId) {
    if (this.isConnected && messageId) {
      this.ws.send(JSON.stringify({
        type: 'ack',
        messageId: messageId,
        timestamp: Date.now()
      }))
    }
  }

  // 添加事件监听
  on(event, handler) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].push(handler)
    }
  }

  // 触发事件
  emit(event, data) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].forEach(handler => handler(data))
    }
  }

  // 关闭连接
  close() {
    this.ws?.close()
    this.messageQueue.clear()
  }

  // 获取队列统计
  getQueueStats() {
    return {
      queueSize: this.messageQueue.size(),
      pendingSize: this.messageQueue.pendingSize()
    }
  }
}

4.3 Vue组件示例

vue
<script setup>
import { ref, computed } from 'vue'
import { WebSocketWithQueue } from './websocket-with-queue.js'

const ws = ref(null)
const isConnected = ref(false)
const queueSize = ref(0)
const pendingSize = ref(0)
const messageInput = ref('')
const messageLog = ref([])
const stats = ref({
  sent: 0,
  received: 0,
  acked: 0,
  failed: 0
})

const initWebSocket = () => {
  ws.value = new WebSocketWithQueue('ws://localhost:8080/ws', {
    maxQueueSize: 100,
    timeout: 5000,
    retryTimes: 3
  })

  ws.value.on('open', () => {
    isConnected.value = true
    addLog('WebSocket连接成功', 'success')
  })

  ws.value.on('message', (data) => {
    stats.value.received++
    addLog(`收到消息: ${data.content}`, 'info')
  })

  ws.value.on('close', () => {
    isConnected.value = false
    addLog('WebSocket连接关闭', 'warning')
  })

  ws.value.connect()
  startStatsUpdate()
}

const startStatsUpdate = () => {
  setInterval(() => {
    if (ws.value) {
      const queueStats = ws.value.getQueueStats()
      queueSize.value = queueStats.queueSize
      pendingSize.value = queueStats.pendingSize
    }
  }, 500)
}

const sendMessage = () => {
  if (!messageInput.value.trim()) return

  const message = {
    type: 'chat',
    content: messageInput.value,
    timestamp: Date.now()
  }

  ws.value.send(message)
  stats.value.sent++
  addLog(`发送消息: ${messageInput.value}`, 'sent')
  messageInput.value = ''
}

const sendWithAck = async () => {
  if (!messageInput.value.trim()) return

  const message = {
    type: 'chat',
    content: messageInput.value,
    timestamp: Date.now()
  }

  try {
    const messageId = await ws.value.sendWithAck(message)
    stats.value.sent++
    stats.value.acked++
    addLog(`消息已确认: ${messageInput.value} [${messageId}]`, 'success')
  } catch (error) {
    stats.value.failed++
    addLog(`消息发送失败: ${error.message}`, 'error')
  }

  messageInput.value = ''
}

const sendBatch = () => {
  for (let i = 1; i <= 10; i++) {
    ws.value.send({
      type: 'batch',
      content: `批量消息 ${i}`,
      timestamp: Date.now()
    })
  }
  stats.value.sent += 10
  addLog('已发送10条批量消息', 'info')
}

const addLog = (message, type = 'info') => {
  messageLog.value.unshift({
    id: Date.now() + Math.random(),
    message,
    type,
    time: new Date().toLocaleTimeString()
  })

  if (messageLog.value.length > 100) {
    messageLog.value = messageLog.value.slice(0, 100)
  }
}

const clearLogs = () => {
  messageLog.value = []
}

initWebSocket()
</script>

<template>
  <div class="queue-demo">
    <div class="header">
      <h2>消息队列与确认机制</h2>
      <div class="connection-status" :class="{ connected: isConnected }">
        {{ isConnected ? '已连接' : '未连接' }}
      </div>
    </div>

    <div class="stats-panel">
      <div class="stat-card">
        <div class="stat-value">{{ stats.sent }}</div>
        <div class="stat-label">已发送</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ stats.received }}</div>
        <div class="stat-label">已接收</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ stats.acked }}</div>
        <div class="stat-label">已确认</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ stats.failed }}</div>
        <div class="stat-label">失败</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ queueSize }}</div>
        <div class="stat-label">队列长度</div>
      </div>
      <div class="stat-card">
        <div class="stat-value">{{ pendingSize }}</div>
        <div class="stat-label">待确认</div>
      </div>
    </div>

    <div class="control-panel">
      <input
        v-model="messageInput"
        type="text"
        placeholder="输入消息内容..."
        @keyup.enter="sendMessage"
        :disabled="!isConnected"
      />
      <button @click="sendMessage" :disabled="!isConnected">
        发送
      </button>
      <button @click="sendWithAck" :disabled="!isConnected">
        发送(带确认)
      </button>
      <button @click="sendBatch" :disabled="!isConnected">
        批量发送(10条)
      </button>
      <button @click="clearLogs" class="clear-btn">
        清空日志
      </button>
    </div>

    <div class="log-panel">
      <h3>消息日志</h3>
      <div class="log-list">
        <div
          v-for="log in messageLog"
          :key="log.id"
          :class="['log-item', log.type]"
        >
          <span class="log-time">{{ log.time }}</span>
          <span class="log-message">{{ log.message }}</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.queue-demo {
  max-width: 1200px;
  margin: 20px auto;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 8px 8px 0 0;
}

.header h2 {
  margin: 0;
  font-size: 20px;
}

.connection-status {
  padding: 6px 16px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.2);
  font-size: 14px;
}

.connection-status.connected {
  background: #67c23a;
}

.stats-panel {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 16px;
  padding: 20px;
  background: #f5f7fa;
}

.stat-card {
  background: white;
  padding: 16px;
  border-radius: 8px;
  text-align: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.stat-value {
  font-size: 32px;
  font-weight: bold;
  color: #409eff;
  margin-bottom: 8px;
}

.stat-label {
  font-size: 14px;
  color: #909399;
}

.control-panel {
  padding: 20px;
  display: flex;
  gap: 12px;
  border-bottom: 1px solid #e4e7ed;
}

.control-panel input {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
  outline: none;
}

.control-panel input:focus {
  border-color: #409eff;
}

.control-panel button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  background: #409eff;
  color: white;
  cursor: pointer;
  font-size: 14px;
  white-space: nowrap;
}

.control-panel button:hover:not(:disabled) {
  background: #66b1ff;
}

.control-panel button:disabled {
  background: #c0c4cc;
  cursor: not-allowed;
}

.control-panel .clear-btn {
  background: #909399;
}

.control-panel .clear-btn:hover {
  background: #a6a9ad;
}

.log-panel {
  padding: 20px;
}

.log-panel h3 {
  margin: 0 0 16px 0;
  font-size: 16px;
  color: #303133;
}

.log-list {
  height: 400px;
  overflow-y: auto;
  background: #f5f7fa;
  border-radius: 4px;
  padding: 12px;
}

.log-item {
  padding: 8px 12px;
  margin-bottom: 8px;
  border-radius: 4px;
  font-size: 13px;
  display: flex;
  gap: 12px;
  border-left: 3px solid;
}

.log-item.success {
  background: #f0f9ff;
  border-color: #67c23a;
  color: #67c23a;
}

.log-item.error {
  background: #fef0f0;
  border-color: #f56c6c;
  color: #f56c6c;
}

.log-item.warning {
  background: #fdf6ec;
  border-color: #e6a23c;
  color: #e6a23c;
}

.log-item.info {
  background: #f4f4f5;
  border-color: #909399;
  color: #606266;
}

.log-item.sent {
  background: #ecf5ff;
  border-color: #409eff;
  color: #409eff;
}

.log-time {
  flex-shrink: 0;
  opacity: 0.8;
}

.log-message {
  flex: 1;
}
</style>

五、多标签页通信

5.1 SharedWorker实现

javascript
// shared-websocket-worker.js
let ws = null
let ports = []
let connectionState = 'disconnected'

self.onconnect = function(e) {
  const port = e.ports[0]
  ports.push(port)

  port.onmessage = function(event) {
    const { type, data } = event.data

    switch (type) {
      case 'connect':
        connectWebSocket(data.url)
        break
      case 'send':
        sendMessage(data)
        break
      case 'close':
        closeConnection()
        break
      case 'getState':
        port.postMessage({
          type: 'state',
          state: connectionState
        })
        break
    }
  }

  // 如果已连接,通知新标签页
  if (connectionState === 'connected') {
    port.postMessage({
      type: 'connected'
    })
  }

  port.start()
}

function connectWebSocket(url) {
  if (ws && connectionState === 'connected') {
    return
  }

  ws = new WebSocket(url)

  ws.onopen = () => {
    connectionState = 'connected'
    broadcastToAll({
      type: 'connected',
      timestamp: Date.now()
    })
  }

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data)
    broadcastToAll({
      type: 'message',
      data: data
    })
  }

  ws.onerror = (error) => {
    broadcastToAll({
      type: 'error',
      error: error.message
    })
  }

  ws.onclose = () => {
    connectionState = 'disconnected'
    broadcastToAll({
      type: 'disconnected'
    })
  }
}

function sendMessage(data) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(data))
  }
}

function closeConnection() {
  if (ws) {
    ws.close()
    ws = null
  }
}

function broadcastToAll(message) {
  ports.forEach(port => {
    try {
      port.postMessage(message)
    } catch (error) {
      console.error('发送消息到端口失败:', error)
    }
  })
}

5.2 BroadcastChannel实现

javascript
// broadcast-websocket.js
export class BroadcastWebSocket {
  constructor(url, channelName = 'websocket-channel') {
    this.url = url
    this.ws = null
    this.channel = new BroadcastChannel(channelName)
    this.isLeader = false
    this.leaderId = null
    this.tabId = `tab_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`

    this.eventHandlers = {
      open: [],
      message: [],
      error: [],
      close: []
    }

    this.setupChannel()
    this.electLeader()
  }

  // 设置广播频道
  setupChannel() {
    this.channel.onmessage = (event) => {
      const { type, data, senderId } = event.data

      // 忽略自己发送的消息
      if (senderId === this.tabId) {
        return
      }

      switch (type) {
        case 'leader-election':
          this.handleLeaderElection(data)
          break
        case 'websocket-message':
          this.emit('message', data)
          break
        case 'websocket-connected':
          this.emit('open', data)
          break
        case 'websocket-disconnected':
          this.emit('close', data)
          break
        case 'websocket-error':
          this.emit('error', data)
          break
      }
    }
  }

  // 选举Leader标签页
  electLeader() {
    // 广播选举消息
    this.broadcast('leader-election', {
      tabId: this.tabId,
      timestamp: Date.now()
    })

    // 1秒后检查是否成为Leader
    setTimeout(() => {
      if (!this.leaderId || this.leaderId === this.tabId) {
        this.becomeLeader()
      }
    }, 1000)
  }

  // 处理Leader选举
  handleLeaderElection(data) {
    if (!this.leaderId || data.timestamp < this.leaderTimestamp) {
      this.leaderId = data.tabId
      this.leaderTimestamp = data.timestamp
      this.isLeader = false
    }
  }

  // 成为Leader
  becomeLeader() {
    this.isLeader = true
    this.leaderId = this.tabId
    console.log(`标签页 ${this.tabId} 成为Leader`)
    this.connect()
  }

  // 连接WebSocket(仅Leader执行)
  connect() {
    if (!this.isLeader) {
      console.log('非Leader标签页,不建立WebSocket连接')
      return
    }

    this.ws = new WebSocket(this.url)

    this.ws.onopen = () => {
      console.log('Leader WebSocket连接成功')
      this.broadcast('websocket-connected', {
        timestamp: Date.now()
      })
      this.emit('open')
    }

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      this.broadcast('websocket-message', data)
      this.emit('message', data)
    }

    this.ws.onerror = (error) => {
      this.broadcast('websocket-error', {
        message: error.message
      })
      this.emit('error', error)
    }

    this.ws.onclose = () => {
      console.log('Leader WebSocket连接关闭')
      this.broadcast('websocket-disconnected', {
        timestamp: Date.now()
      })
      this.emit('close')
    }
  }

  // 发送消息
  send(data) {
    if (this.isLeader && this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data))
      return true
    } else if (!this.isLeader) {
      // 非Leader标签页通过广播请求Leader发送
      this.broadcast('send-request', data)
      return true
    }
    return false
  }

  // 广播消息到其他标签页
  broadcast(type, data) {
    this.channel.postMessage({
      type,
      data,
      senderId: this.tabId
    })
  }

  // 添加事件监听
  on(event, handler) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].push(handler)
    }
  }

  // 触发事件
  emit(event, data) {
    if (this.eventHandlers[event]) {
      this.eventHandlers[event].forEach(handler => handler(data))
    }
  }

  // 关闭连接
  close() {
    if (this.isLeader && this.ws) {
      this.ws.close()
    }
    this.channel.close()
  }

  // 获取标签页信息
  getTabInfo() {
    return {
      tabId: this.tabId,
      isLeader: this.isLeader,
      leaderId: this.leaderId
    }
  }
}

5.3 Vue组件示例

vue
<script setup>
import { ref, onMounted } from 'vue'
import { BroadcastWebSocket } from './broadcast-websocket.js'

const ws = ref(null)
const isConnected = ref(false)
const tabInfo = ref({})
const messages = ref([])
const inputMessage = ref('')
const activeTabsCount = ref(1)

const initWebSocket = () => {
  ws.value = new BroadcastWebSocket('ws://localhost:8080/ws')

  ws.value.on('open', () => {
    isConnected.value = true
    tabInfo.value = ws.value.getTabInfo()
    addMessage('WebSocket已连接', 'system')
  })

  ws.value.on('message', (data) => {
    addMessage(data.content || JSON.stringify(data), 'received')
  })

  ws.value.on('close', () => {
    isConnected.value = false
    addMessage('WebSocket已断开', 'system')
  })

  ws.value.on('error', (error) => {
    addMessage(`连接错误: ${error.message}`, 'error')
  })

  // 更新标签页信息
  setInterval(() => {
    if (ws.value) {
      tabInfo.value = ws.value.getTabInfo()
    }
  }, 1000)
}

const sendMessage = () => {
  if (!inputMessage.value.trim()) return

  const message = {
    type: 'chat',
    content: inputMessage.value,
    timestamp: Date.now()
  }

  if (ws.value.send(message)) {
    addMessage(inputMessage.value, 'sent')
    inputMessage.value = ''
  }
}

const addMessage = (content, type = 'info') => {
  messages.value.unshift({
    id: Date.now() + Math.random(),
    content,
    type,
    time: new Date().toLocaleTimeString(),
    tabId: tabInfo.value.tabId?.substr(0, 8)
  })

  if (messages.value.length > 50) {
    messages.value = messages.value.slice(0, 50)
  }
}

const openNewTab = () => {
  window.open(window.location.href, '_blank')
  activeTabsCount.value++
}

onMounted(() => {
  initWebSocket()

  // 监听其他标签页
  window.addEventListener('storage', (e) => {
    if (e.key === 'tab_count') {
      activeTabsCount.value = parseInt(e.newValue) || 1
    }
  })
})
</script>

<template>
  <div class="broadcast-demo">
    <div class="header">
      <h2>多标签页通信演示</h2>
      <div class="tab-badge" :class="{ leader: tabInfo.isLeader }">
        {{ tabInfo.isLeader ? 'Leader标签页' : '从属标签页' }}
      </div>
    </div>

    <div class="info-panel">
      <div class="info-group">
        <div class="info-item">
          <label>标签页ID:</label>
          <span class="mono">{{ tabInfo.tabId?.substr(0, 12) }}...</span>
        </div>
        <div class="info-item">
          <label>Leader ID:</label>
          <span class="mono">{{ tabInfo.leaderId?.substr(0, 12) }}...</span>
        </div>
        <div class="info-item">
          <label>连接状态:</label>
          <span :class="{ connected: isConnected }">
            {{ isConnected ? '已连接' : '未连接' }}
          </span>
        </div>
      </div>

      <button @click="openNewTab" class="new-tab-btn">
        打开新标签页测试
      </button>
    </div>

    <div class="message-panel">
      <h3>消息列表 (所有标签页同步)</h3>
      <div class="message-list">
        <div
          v-for="msg in messages"
          :key="msg.id"
          :class="['message-item', msg.type]"
        >
          <div class="message-header">
            <span class="message-time">{{ msg.time }}</span>
            <span class="message-tab" v-if="msg.tabId">Tab: {{ msg.tabId }}</span>
          </div>
          <div class="message-content">{{ msg.content }}</div>
        </div>
      </div>
    </div>

    <div class="input-panel">
      <input
        v-model="inputMessage"
        type="text"
        placeholder="输入消息(所有标签页会收到)..."
        @keyup.enter="sendMessage"
        :disabled="!isConnected"
      />
      <button @click="sendMessage" :disabled="!isConnected">
        发送到所有标签页
      </button>
    </div>

    <div class="tips">
      <h4>使用说明:</h4>
      <ul>
        <li>打开多个标签页,只有Leader标签页会建立WebSocket连接</li>
        <li>所有标签页通过BroadcastChannel共享消息</li>
        <li>在任一标签页发送消息,其他标签页都会收到</li>
        <li>关闭Leader标签页后,会自动选举新的Leader</li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.broadcast-demo {
  max-width: 1000px;
  margin: 20px auto;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 8px 8px 0 0;
}

.header h2 {
  margin: 0;
  font-size: 20px;
}

.tab-badge {
  padding: 6px 16px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.2);
  font-size: 14px;
}

.tab-badge.leader {
  background: #67c23a;
  font-weight: 600;
}

.info-panel {
  padding: 20px;
  background: #f5f7fa;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.info-group {
  display: flex;
  gap: 24px;
}

.info-item {
  display: flex;
  align-items: center;
  gap: 8px;
}

.info-item label {
  font-weight: 600;
  color: #606266;
}

.info-item span {
  color: #303133;
}

.info-item span.connected {
  color: #67c23a;
  font-weight: 600;
}

.mono {
  font-family: 'Courier New', monospace;
  font-size: 12px;
  background: white;
  padding: 2px 8px;
  border-radius: 4px;
}

.new-tab-btn {
  padding: 10px 20px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.new-tab-btn:hover {
  background: #66b1ff;
}

.message-panel {
  padding: 20px;
}

.message-panel h3 {
  margin: 0 0 16px 0;
  font-size: 16px;
  color: #303133;
}

.message-list {
  height: 350px;
  overflow-y: auto;
  background: #f5f7fa;
  border-radius: 4px;
  padding: 12px;
}

.message-item {
  padding: 12px;
  margin-bottom: 12px;
  border-radius: 6px;
  background: white;
  border-left: 3px solid;
}

.message-item.sent {
  border-color: #409eff;
}

.message-item.received {
  border-color: #67c23a;
}

.message-item.system {
  border-color: #909399;
}

.message-item.error {
  border-color: #f56c6c;
}

.message-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 8px;
  font-size: 12px;
  color: #909399;
}

.message-tab {
  font-family: 'Courier New', monospace;
  background: #f5f7fa;
  padding: 2px 6px;
  border-radius: 3px;
}

.message-content {
  color: #303133;
  font-size: 14px;
}

.input-panel {
  padding: 20px;
  display: flex;
  gap: 12px;
  border-top: 1px solid #e4e7ed;
}

.input-panel input {
  flex: 1;
  padding: 10px 14px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
  outline: none;
}

.input-panel input:focus {
  border-color: #409eff;
}

.input-panel button {
  padding: 10px 24px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  white-space: nowrap;
}

.input-panel button:hover:not(:disabled) {
  background: #66b1ff;
}

.input-panel button:disabled {
  background: #c0c4cc;
  cursor: not-allowed;
}

.tips {
  padding: 20px;
  background: #ecf5ff;
  border-top: 1px solid #d9ecff;
  border-radius: 0 0 8px 8px;
}

.tips h4 {
  margin: 0 0 12px 0;
  color: #409eff;
  font-size: 14px;
}

.tips ul {
  margin: 0;
  padding-left: 20px;
}

.tips li {
  color: #606266;
  font-size: 13px;
  line-height: 1.8;
}
</style>

六、简历描述模板

项目经验描述

WebSocket实时通信系统 (2023.06 - 2023.09)

负责公司客服系统和实时数据看板的WebSocket通信架构设计与开发,支持千级并发连接。

核心职责
  • 设计并实现了完整的WebSocket连接管理系统,包括心跳检测、断线重连和连接状态管理
  • 开发了基于指数退避算法的智能重连机制,将连接成功率提升至99.5%
  • 实现了消息队列和ACK确认机制,保证消息可靠传输,丢包率低于0.1%
  • 使用BroadcastChannel实现多标签页通信,降低了80%的重复连接
技术实现
  • 封装WebSocketManager类处理连接生命周期,支持自动重连和心跳保活
  • 实现MessageQueue管理待发送消息,支持离线缓存和重发机制
  • 通过指数退避算法优化重连策略,避免服务器压力过大
  • 使用BroadcastChannel实现标签页间通信,采用Leader选举机制确保单一连接
项目成果
  • WebSocket连接稳定性从85%提升到99.5%
  • 消息传输成功率达到99.9%以上
  • 支持单页面最高1000条消息的队列缓存
  • 多标签页场景下节省80%的服务器连接资源

七、SOP标准回答

面试问题1: 介绍一下你做的WebSocket项目

标准回答

"我在上家公司负责开发了一套完整的WebSocket实时通信系统,主要用于客服系统和数据看板。

在架构设计上,我将整个系统分为几个核心模块。首先是连接管理器,负责WebSocket的建立、维护和关闭。我实现了30秒一次的心跳机制,通过ping-pong消息确保连接有效性。

针对网络不稳定的情况,我设计了一套智能重连机制。使用指数退避算法,第一次断线等1秒重连,之后依次是2秒、4秒、8秒,最多重试5次。这样既能快速恢复连接,又不会给服务器造成压力。

消息可靠性方面,我实现了一个消息队列系统。每条消息都有唯一ID和ACK确认机制。如果5秒内没收到确认,消息会自动重发,最多重试3次。同时维护了一个待确认消息的Map,确保消息不会丢失。

比较有挑战的是多标签页通信。我用BroadcastChannel实现了标签页间的消息同步,通过Leader选举机制,确保只有一个标签页建立WebSocket连接,其他标签页通过广播获取消息,这样节省了80%的连接资源。

最终效果是连接稳定性达到99.5%,消息成功率99.9%以上,并且支持离线缓存1000条消息。"

面试问题2: WebSocket断线重连怎么实现的?

标准回答

"我的重连机制分为三个层次:

第一层是被动重连。监听WebSocket的onclose事件,一旦触发就启动重连流程。但要区分是用户主动关闭还是异常断开,通过isManualClose标志位判断。

第二层是重连策略。我采用指数退避算法,基础延迟1秒,每次失败后延迟翻倍,上限30秒。公式是: delay = min(baseDelay * 2^attempts, maxDelay)。这样既能快速恢复,又能避免服务器压力。最多重连5次,超过就提示用户网络异常。

第三层是状态管理。我维护了一个reconnectAttempts计数器和connectionState状态机。状态包括disconnected、connecting、connected、reconnecting四种。每次成功连接后重置计数器。

实现时遇到一个坑是,如果页面来回切换,可能会创建多个重连定时器。我的解决方案是在启动新重连前,先clearTimeout清理旧的定时器。

另外在重连期间,用户发送的消息会暂存在队列中,连接成功后批量发送,保证消息不丢失。"


八、难点与亮点分析

难点1: 如何保证消息不丢失?

问题场景: 网络不稳定时,消息可能在发送过程中丢失,或者发送成功但服务端未确认。

解决方案

  1. 消息队列缓存
    • 发送前先入队,成功后再出队
    • 离线时消息暂存,连接恢复后批量发送
    • 队列大小限制100条,超出后丢弃最早消息
  2. ACK确认机制
    • 每条消息分配唯一messageId
    • 发送后等待服务端ACK响应
    • 5秒超时未收到则重发,最多3次
  3. 幂等性设计
    • 服务端通过messageId去重
    • 避免重复发送导致的重复处理
代码实现
javascript
async sendWithAck(message) {
  const messageId = generateId()
  message.messageId = messageId

  return new Promise((resolve, reject) => {
    const timeoutId = setTimeout(() => {
      if (retryCount < 3) {
        retryCount++
        this.sendWithAck(message).then(resolve).catch(reject)
      } else {
        reject(new Error('发送失败'))
      }
    }, 5000)

    pendingMessages.set(messageId, { resolve, reject, timeoutId })
    ws.send(JSON.stringify(message))
  })
}

难点2: 多标签页如何共享WebSocket连接?

问题场景: 用户打开多个标签页,每个都建立WebSocket连接会浪费资源,但标签页间需要同步消息。

解决方案

  1. Leader选举机制
    • 使用BroadcastChannel进行标签页通信
    • 每个标签页启动时广播自己的ID和时间戳
    • ID最小的成为Leader,建立WebSocket连接
  2. 消息广播
    • Leader收到服务端消息后,广播给所有标签页
    • 非Leader标签页发送消息时,通过广播请求Leader代发
  3. Leader故障转移
    • Leader关闭时广播下线消息
    • 其他标签页重新选举Leader
实现要点
javascript
// Leader选举
electLeader() {
  this.broadcast('leader-election', {
    tabId: this.tabId,
    timestamp: Date.now()
  })

  setTimeout(() => {
    if (!this.leaderId || this.leaderId === this.tabId) {
      this.becomeLeader()
    }
  }, 1000)
}

// 消息广播
broadcastMessage(data) {
  this.channel.postMessage({
    type: 'websocket-message',
    data: data,
    senderId: this.tabId
  })
}

亮点1: 智能重连算法

创新点

  • 指数退避避免服务器压力
  • 自适应调整重连间隔
  • 连接质量评估机制
实现细节
javascript
getNextDelay() {
  // 基础延迟 * 2^尝试次数,上限30秒
  const delay = Math.min(
    this.baseDelay * Math.pow(2, this.currentAttempt),
    this.maxDelay
  )

  // 加入随机抖动,避免同时重连
  return delay + Math.random() * 1000
}

亮点2: 消息优先级队列

创新点

  • 普通消息和紧急消息分开处理
  • 紧急消息优先发送
  • 低优先级消息可丢弃
实现方案
javascript
class PriorityMessageQueue {
  constructor() {
    this.highPriority = []
    this.normalPriority = []
  }

  enqueue(message, priority = 'normal') {
    if (priority === 'high') {
      this.highPriority.push(message)
    } else {
      this.normalPriority.push(message)
    }
  }

  dequeue() {
    return this.highPriority.shift() || this.normalPriority.shift()
  }
}

九、Node.js 测试服务器

javascript
// websocket-server.js
const WebSocket = require('ws')
const http = require('http')

const server = http.createServer()
const wss = new WebSocket.Server({ server })

const clients = new Map()

wss.on('connection', (ws, req) => {
  const clientId = `client_${Date.now()}`
  clients.set(clientId, ws)

  console.log(`客户端连接: ${clientId}, 当前连接数: ${clients.size}`)

  ws.on('message', (message) => {
    try {
      const data = JSON.parse(message)
      console.log('收到消息:', data)

      // 处理心跳
      if (data.type === 'ping') {
        ws.send(JSON.stringify({
          type: 'pong',
          timestamp: Date.now()
        }))
        return
      }

      // 发送ACK确认
      if (data.messageId) {
        ws.send(JSON.stringify({
          type: 'ack',
          messageId: data.messageId,
          timestamp: Date.now()
        }))
      }

      // 广播消息给所有客户端
      clients.forEach((client, id) => {
        if (client.readyState === WebSocket.OPEN && id !== clientId) {
          client.send(JSON.stringify({
            ...data,
            from: clientId,
            timestamp: Date.now()
          }))
        }
      })

    } catch (error) {
      console.error('消息处理错误:', error)
    }
  })

  ws.on('close', () => {
    clients.delete(clientId)
    console.log(`客户端断开: ${clientId}, 当前连接数: ${clients.size}`)
  })

  ws.on('error', (error) => {
    console.error('WebSocket错误:', error)
  })

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

const PORT = 8080
server.listen(PORT, () => {
  console.log(`WebSocket服务器运行在 ws://localhost:${PORT}`)
})

启动服务器

bash
npm install ws
node websocket-server.js