一、技术实现方案
1.1 核心架构设计
客户端层
├── 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
// 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)
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
<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 指数退避算法实现
// 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管理器
// 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组件示例
<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 消息队列管理器
// 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
// 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组件示例
<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实现
// 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实现
// 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组件示例
<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: 如何保证消息不丢失?
问题场景: 网络不稳定时,消息可能在发送过程中丢失,或者发送成功但服务端未确认。
解决方案
- 消息队列缓存
- 发送前先入队,成功后再出队
- 离线时消息暂存,连接恢复后批量发送
- 队列大小限制100条,超出后丢弃最早消息
- ACK确认机制
- 每条消息分配唯一messageId
- 发送后等待服务端ACK响应
- 5秒超时未收到则重发,最多3次
- 幂等性设计
- 服务端通过messageId去重
- 避免重复发送导致的重复处理
代码实现
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连接会浪费资源,但标签页间需要同步消息。
解决方案
- Leader选举机制
- 使用BroadcastChannel进行标签页通信
- 每个标签页启动时广播自己的ID和时间戳
- ID最小的成为Leader,建立WebSocket连接
- 消息广播
- Leader收到服务端消息后,广播给所有标签页
- 非Leader标签页发送消息时,通过广播请求Leader代发
- Leader故障转移
- Leader关闭时广播下线消息
- 其他标签页重新选举Leader
实现要点
// 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: 智能重连算法
创新点
- 指数退避避免服务器压力
- 自适应调整重连间隔
- 连接质量评估机制
实现细节
getNextDelay() {
// 基础延迟 * 2^尝试次数,上限30秒
const delay = Math.min(
this.baseDelay * Math.pow(2, this.currentAttempt),
this.maxDelay
)
// 加入随机抖动,避免同时重连
return delay + Math.random() * 1000
}
亮点2: 消息优先级队列
创新点
- 普通消息和紧急消息分开处理
- 紧急消息优先发送
- 低优先级消息可丢弃
实现方案
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 测试服务器
// 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}`)
})
启动服务器
npm install ws
node websocket-server.js