WebSocket 与 SSE 的区别,实际项目中应该怎么使用
WebSocket 与 SSE 的区别,实际项目中应该怎么使用
文章目录
- WebSocket 与 SSE 的区别,实际项目中应该怎么使用
- 1. WebSocket 与 SSE 核心区别
- 1.1 协议特性对比
- 1.2 技术细节对比
- 2. SSE 在 Vue 3 中的使用
- 2.1 基础 SSE 封装
- 2.2 实时通知系统 (SSE)
- 3. WebSocket 在 Vue 3 中的使用(对比示例)
- 3.1 基础 WebSocket 封装
- 3.2 实时聊天室 (WebSocket)
- 4. 性能对比和选择指南
- 4.1 性能特征对比
- 4.2 选择指南
- 5. 混合使用方案
- 5.1 WebSocket + SSE 混合方案
- 6. 总结
- WebSocket 适用场景:
- SSE 适用场景:
- 混合方案优势:
1. WebSocket 与 SSE 核心区别
1.1 协议特性对比
特性 | WebSocket | SSE (Server-Sent Events) |
---|---|---|
通信方向 | 全双工(双向) | 半双工(服务器→客户端) |
协议 | 独立的 WebSocket 协议 | 基于 HTTP |
连接方式 | 持久连接 | 持久 HTTP 连接 |
数据格式 | 二进制、文本 | 仅文本(UTF-8) |
自动重连 | 需要手动实现 | 浏览器自动处理 |
CORS | 支持跨域 | 同源策略限制 |
浏览器支持 | 广泛支持 | 广泛支持(除IE) |
适用场景 | 实时游戏、聊天、协作编辑 | 实时通知、股票行情、新闻推送 |
1.2 技术细节对比
// 协议头对比
const websocketHeaders = {// WebSocket 升级头'Upgrade': 'websocket','Connection': 'Upgrade'
}const sseHeaders = {// SSE 标准头'Content-Type': 'text/event-stream','Cache-Control': 'no-cache','Connection': 'keep-alive'
}
2. SSE 在 Vue 3 中的使用
2.1 基础 SSE 封装
// composables/useSSE.ts
import { ref, onUnmounted, onMounted } from 'vue'interface SSEOptions {url: stringonMessage?: (data: any) => voidonOpen?: () => voidonError?: (error: Event) => voidautoConnect?: booleanwithCredentials?: boolean
}export function useSSE(options: SSEOptions) {const {url,onMessage,onOpen,onError,autoConnect = true,withCredentials = false} = optionsconst eventSource = ref<EventSource | null>(null)const isConnected = ref(false)const lastMessage = ref<any>(null)const error = ref<Event | null>(null)const connect = () => {try {eventSource.value = new EventSource(url)eventSource.value.onopen = (event) => {isConnected.value = trueerror.value = nullconsole.log('SSE 连接成功')onOpen?.()}// 监听默认消息eventSource.value.onmessage = (event) => {try {const data = JSON.parse(event.data)lastMessage.value = dataonMessage?.(data)} catch (e) {lastMessage.value = event.dataonMessage?.(event.data)}}// 监听特定事件类型eventSource.value.addEventListener('custom-event', (event) => {const data = JSON.parse((event as MessageEvent).data)console.log('自定义事件:', data)})eventSource.value.onerror = (event) => {error.value = eventconsole.error('SSE 错误:', event)onError?.(event)// SSE 会自动重连,但我们可以在这里处理错误状态isConnected.value = false}} catch (err) {console.error('SSE 连接失败:', err)}}const disconnect = () => {if (eventSource.value) {eventSource.value.close()eventSource.value = null}isConnected.value = false}// 添加自定义事件监听器const addEventListener = (type: string, listener: (event: MessageEvent) => void) => {if (eventSource.value) {eventSource.value.addEventListener(type, listener)}}// 移除事件监听器const removeEventListener = (type: string, listener: (event: MessageEvent) => void) => {if (eventSource.value) {eventSource.value.removeEventListener(type, listener)}}onMounted(() => {if (autoConnect) {connect()}})onUnmounted(() => {disconnect()})return {eventSource,isConnected,lastMessage,error,connect,disconnect,addEventListener,removeEventListener}
}
2.2 实时通知系统 (SSE)
<!-- NotificationsSSE.vue -->
<template><div class="notifications-sse"><div class="header"><h2>实时通知系统 (SSE)</h2><div class="status" :class="{ connected: isConnected }">{{ isConnected ? '推送连接正常' : '连接断开' }}</div></div><div class="controls"><button @click="markAllAsRead" :disabled="notifications.length === 0">全部标记已读</button><button @click="clearAll" :disabled="notifications.length === 0">清空所有</button></div><div class="notifications-list"><divv-for="notification in sortedNotifications":key="notification.id":class="['notification', notification.type, { unread: !notification.read }]"@click="markAsRead(notification.id)"><div class="notification-icon"><span v-if="notification.type === 'info'">ℹ️</span><span v-else-if="notification.type === 'success'">✅</span><span v-else-if="notification.type === 'warning'">⚠️</span><span v-else-if="notification.type === 'error'">❌</span></div><div class="notification-content"><div class="title">{{ notification.title }}</div><div class="message">{{ notification.message }}</div><div class="time">{{ formatTime(notification.timestamp) }}</div></div><div class="notification-actions"><button @click.stop="removeNotification(notification.id)">×</button></div></div></div><div class="stats"><span>总通知: {{ notifications.length }}</span><span>未读: {{ unreadCount }}</span><span>最新: {{ lastMessage ? formatTime(lastMessage.timestamp) : '无' }}</span></div></div>
</template><script setup lang="ts">
import { ref, computed } from 'vue'
import { useSSE } from '@/composables/useSSE'interface Notification {id: stringtype: 'info' | 'success' | 'warning' | 'error'title: stringmessage: stringtimestamp: numberread: boolean
}const notifications = ref<Notification[]>([])const { isConnected, lastMessage, addEventListener } = useSSE({url: 'http://localhost:3000/api/notifications',onMessage: (data) => {if (data.type === 'notification') {const newNotification: Notification = {id: generateId(),type: data.notificationType || 'info',title: data.title,message: data.message,timestamp: data.timestamp || Date.now(),read: false}notifications.value.unshift(newNotification)// 限制通知数量if (notifications.value.length > 50) {notifications.value = notifications.value.slice(0, 50)}}}
})// 添加自定义事件监听
onMounted(() => {addEventListener('system-alert', (event) => {const data = JSON.parse(event.data)console.log('系统告警:', data)})
})const sortedNotifications = computed(() => {return [...notifications.value].sort((a, b) => b.timestamp - a.timestamp)
})const unreadCount = computed(() => {return notifications.value.filter(n => !n.read).length
})const generateId = () => {return Date.now().toString(36) + Math.random().toString(36).substr(2)
}const markAsRead = (id: string) => {const notification = notifications.value.find(n => n.id === id)if (notification) {notification.read = true}
}const markAllAsRead = () => {notifications.value.forEach(notification => {notification.read = true})
}const removeNotification = (id: string) => {const index = notifications.value.findIndex(n => n.id === id)if (index > -1) {notifications.value.splice(index, 1)}
}const clearAll = () => {notifications.value = []
}const formatTime = (timestamp: number) => {return new Date(timestamp).toLocaleTimeString()
}
</script><style scoped>
.notifications-sse {max-width: 500px;margin: 0 auto;border: 1px solid #e0e0e0;border-radius: 8px;overflow: hidden;
}.header {background: #f5f5f5;padding: 15px;display: flex;justify-content: space-between;align-items: center;border-bottom: 1px solid #e0e0e0;
}.status {padding: 4px 8px;border-radius: 4px;font-size: 12px;background: #dc3545;color: white;
}.status.connected {background: #28a745;
}.controls {padding: 10px 15px;background: #f8f9fa;border-bottom: 1px solid #e0e0e0;display: flex;gap: 10px;
}.controls button {padding: 5px 10px;border: 1px solid #ddd;border-radius: 4px;background: white;cursor: pointer;
}.controls button:disabled {opacity: 0.5;cursor: not-allowed;
}.notifications-list {max-height: 400px;overflow-y: auto;
}.notification {display: flex;padding: 12px 15px;border-bottom: 1px solid #f0f0f0;cursor: pointer;transition: background-color 0.2s;
}.notification:hover {background: #f8f9fa;
}.notification.unread {background: #f0f8ff;border-left: 4px solid #007bff;
}.notification.info {border-left-color: #17a2b8;
}.notification.success {border-left-color: #28a745;
}.notification.warning {border-left-color: #ffc107;
}.notification.error {border-left-color: #dc3545;
}.notification-icon {margin-right: 10px;font-size: 18px;
}.notification-content {flex: 1;
}.notification-content .title {font-weight: bold;margin-bottom: 4px;
}.notification-content .message {color: #666;font-size: 14px;margin-bottom: 4px;
}.notification-content .time {color: #999;font-size: 12px;
}.notification-actions button {background: none;border: none;font-size: 16px;cursor: pointer;color: #999;
}.notification-actions button:hover {color: #dc3545;
}.stats {padding: 10px 15px;background: #f8f9fa;display: flex;justify-content: space-between;font-size: 12px;color: #666;border-top: 1px solid #e0e0e0;
}
</style>
3. WebSocket 在 Vue 3 中的使用(对比示例)
3.1 基础 WebSocket 封装
// composables/useWebSocketAdvanced.ts
import { ref, onUnmounted, onMounted, watch, computed, reactive } from 'vue'// 类型定义
export interface WebSocketMessage<T = any> {type: stringdata?: Ttimestamp: numberid?: string
}export interface WebSocketStatus {isConnected: booleanisConnecting: booleanisReconnecting: booleanlastActivity: numberconnectionTime: number | null
}export interface WebSocketAdvancedOptions<T = any> {url: string | (() => string)// 事件处理器onMessage?: (message: WebSocketMessage<T>, event: MessageEvent) => voidonOpen?: (event: Event) => voidonClose?: (event: CloseEvent) => voidonError?: (event: Event) => voidonReconnect?: (attempt: number) => void// 配置选项autoConnect?: booleanreconnect?: booleanreconnectAttempts?: numberreconnectInterval?: numbermaxReconnectInterval?: number// 心跳heartbeat?: booleanheartbeatInterval?: numberheartbeatTimeout?: number// 消息管理messageQueue?: booleanmaxQueueSize?: number// 协议和头信息protocols?: string | string[]// 调试debug?: boolean
}export interface WebSocketAdvancedReturn<T = any> {// 状态status: WebSocketStatus// 数据lastMessage: Ref<WebSocketMessage<T> | null>messageHistory: Ref<WebSocketMessage<T>[]>error: Ref<Event | null>// 方法connect: () => voiddisconnect: (code?: number, reason?: string) => voidsend: <D = any>(type: string, data?: D, id?: string) => booleansendRaw: (data: any) => boolean// 统计stats: {messagesSent: numbermessagesReceived: numberconnectionAttempts: numberreconnectionCount: number}// 控制reconnectCount: Ref<number>clearMessageHistory: () => voidgetMessageById: (id: string) => WebSocketMessage<T> | undefined
}export function useWebSocket <T = any>(options: WebSocketAdvancedOptions<T>
): WebSocketAdvancedReturn<T> {const {url,onMessage,onOpen,onClose,onError,onReconnect,autoConnect = true,reconnect = true,reconnectAttempts = 5,reconnectInterval = 3000,maxReconnectInterval = 30000,heartbeat = true,heartbeatInterval = 25000,heartbeatTimeout = 5000,messageQueue = true,maxQueueSize = 100,protocols = [],debug = false} = optionsconst socket = ref<WebSocket | null>(null)const lastMessage = ref<WebSocketMessage<T> | null>(null)const messageHistory = ref<WebSocketMessage<T>[]>([])const error = ref<Event | null>(null)const reconnectCount = ref(0)// 状态管理const status = reactive<WebSocketStatus>({isConnected: false,isConnecting: false,isReconnecting: false,lastActivity: 0,connectionTime: null})// 统计信息const stats = reactive({messagesSent: 0,messagesReceived: 0,connectionAttempts: 0,reconnectionCount: 0})// 定时器和队列let reconnectTimer: NodeJS.Timeout | null = nulllet heartbeatTimer: NodeJS.Timeout | null = nulllet heartbeatTimeoutTimer: NodeJS.Timeout | null = nullconst messageQueue: any[] = []// 工具函数const log = (...args: any[]) => {if (debug) {console.log('[WebSocket]', ...args)}}const getUrl = (): string => {return typeof url === 'function' ? url() : url}const generateMessageId = (): string => {return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`}const processMessageQueue = () => {while (messageQueue.length > 0 && status.isConnected) {const message = messageQueue.shift()if (message && sendRaw(message)) {log('发送队列中的消息:', message)}}}const addToMessageHistory = (message: WebSocketMessage<T>) => {messageHistory.value.unshift(message)if (messageHistory.value.length > maxQueueSize) {messageHistory.value = messageHistory.value.slice(0, maxQueueSize)}}const startHeartbeat = () => {if (!heartbeat) returnconst sendHeartbeat = () => {if (status.isConnected && socket.value) {const heartbeatMsg: WebSocketMessage = {type: 'heartbeat',timestamp: Date.now(),id: generateMessageId()}if (sendRaw(heartbeatMsg)) {log('发送心跳')// 设置心跳超时检测if (heartbeatTimeoutTimer) {clearTimeout(heartbeatTimeoutTimer)}heartbeatTimeoutTimer = setTimeout(() => {log('心跳超时,断开连接')disconnect(1000, 'Heartbeat timeout')}, heartbeatTimeout)}}}heartbeatTimer = setInterval(sendHeartbeat, heartbeatInterval)}const stopHeartbeat = () => {if (heartbeatTimer) {clearInterval(heartbeatTimer)heartbeatTimer = null}if (heartbeatTimeoutTimer) {clearTimeout(heartbeatTimeoutTimer)heartbeatTimeoutTimer = null}}const resetHeartbeat = () => {if (heartbeat) {stopHeartbeat()startHeartbeat()}}const calculateReconnectDelay = (attempt: number): number => {const delay = reconnectInterval * Math.pow(1.5, attempt - 1)return Math.min(delay, maxReconnectInterval)}// 主要方法const connect = () => {// 清理之前的连接disconnect(1000, 'Reconnecting')status.isConnecting = truestats.connectionAttempts++try {const wsUrl = getUrl()socket.value = new WebSocket(wsUrl, protocols)socket.value.onopen = (event) => {status.isConnected = truestatus.isConnecting = falsestatus.isReconnecting = falsestatus.lastActivity = Date.now()status.connectionTime = Date.now()error.value = nullreconnectCount.value = 0log('连接成功:', wsUrl)onOpen?.(event)// 启动心跳检测startHeartbeat()// 处理消息队列if (messageQueue) {processMessageQueue()}}socket.value.onmessage = (event) => {try {status.lastActivity = Date.now()// 重置心跳resetHeartbeat()let data: anytry {data = JSON.parse(event.data)} catch {data = { type: 'raw', data: event.data }}const message: WebSocketMessage<T> = {type: data.type || 'unknown',data: data.data !== undefined ? data.data : data,timestamp: data.timestamp || Date.now(),id: data.id || generateMessageId()}lastMessage.value = messageaddToMessageHistory(message)stats.messagesReceived++log('收到消息:', message)onMessage?.(message, event)// 处理心跳响应if (message.type === 'heartbeat') {log('收到心跳响应')}} catch (e) {console.error('消息处理错误:', e)}}socket.value.onclose = (event) => {status.isConnected = falsestatus.isConnecting = falsestatus.connectionTime = nulllog('连接关闭:', event.code, event.reason)onClose?.(event)// 清理心跳stopHeartbeat()// 自动重连if (reconnect && reconnectCount.value < reconnectAttempts) {status.isReconnecting = truereconnectCount.value++stats.reconnectionCount++const delay = calculateReconnectDelay(reconnectCount.value)log(`尝试重连 (${reconnectCount.value}/${reconnectAttempts}),延迟 ${delay}ms...`)onReconnect?.(reconnectCount.value)reconnectTimer = setTimeout(() => {connect()}, delay)}}socket.value.onerror = (event) => {status.isConnecting = falseerror.value = eventlog('连接错误:', event)onError?.(event)}} catch (err) {status.isConnecting = falseconsole.error('WebSocket 连接失败:', err)error.value = err as Event}}const disconnect = (code?: number, reason?: string) => {// 清理定时器if (reconnectTimer) {clearTimeout(reconnectTimer)reconnectTimer = null}stopHeartbeat()// 关闭连接if (socket.value) {socket.value.close(code || 1000, reason)socket.value = null}status.isConnected = falsestatus.isConnecting = falsestatus.isReconnecting = falsereconnectCount.value = 0}const send = <D = any>(type: string, data?: D, id?: string): boolean => {const message: WebSocketMessage<D> = {type,data,timestamp: Date.now(),id: id || generateMessageId()}return sendRaw(message)}const sendRaw = (data: any): boolean => {if (socket.value && status.isConnected) {try {const message = typeof data === 'string' ? data : JSON.stringify(data)socket.value.send(message)stats.messagesSent++log('发送消息:', data)return true} catch (err) {console.error('发送消息失败:', err)return false}} else if (messageQueue) {// 将消息加入队列messageQueue.push(data)log('连接未就绪,消息加入队列,当前队列长度:', messageQueue.length)return true} else {log('WebSocket 未连接,无法发送消息')return false}}const clearMessageHistory = () => {messageHistory.value = []}const getMessageById = (id: string): WebSocketMessage<T> | undefined => {return messageHistory.value.find(msg => msg.id === id)}// 监听 URL 变化if (typeof url === 'function') {watch(url, (newUrl, oldUrl) => {if (newUrl !== oldUrl && status.isConnected) {log('URL 发生变化,重新连接...')connect()}})}// 生命周期onMounted(() => {if (autoConnect) {connect()}})onUnmounted(() => {disconnect()})return {status,lastMessage,messageHistory,error,connect,disconnect,send,sendRaw,stats,reconnectCount,clearMessageHistory,getMessageById}
}
3.2 实时聊天室 (WebSocket)
<!-- ChatRoomWebSocket.vue -->
<template><div class="chat-room-websocket"><div class="chat-header"><h2>实时聊天室 (WebSocket)</h2><div class="connection-info"><span :class="['status', isConnected ? 'connected' : 'disconnected']">{{ isConnected ? '已连接' : '连接中...' }}</span><span>在线用户: {{ onlineUsers }}</span></div></div><div class="users-online"><h4>在线用户列表</h4><div class="user-list"><spanv-for="user in userList":key="user.id"class="user-tag":style="{ color: user.color }">{{ user.username }}</span></div></div><div class="messages-container" ref="messagesContainer"><divv-for="message in messages":key="message.id":class="['message', message.type, { own: message.userId === currentUserId }]"><div class="message-header"><spanclass="username":style="{ color: getUserColor(message.userId) }">{{ message.username }}</span><span class="timestamp">{{ formatTime(message.timestamp) }}</span></div><div class="message-content">{{ message.content }}</div><div v-if="message.type === 'image'" class="message-image"><img :src="message.imageUrl" :alt="message.content" /></div></div></div><div class="input-area"><div class="input-controls"><inputv-model="inputMessage"@keyup.enter="sendMessage"placeholder="输入消息...":disabled="!isConnected"/><button @click="sendMessage" :disabled="!isConnected || !inputMessage.trim()">发送</button><button @click="sendTypingEvent" class="typing-indicator">正在输入...</button></div><div v-if="typingUsers.length > 0" class="typing-users">{{ typingUsers.join(', ') }} 正在输入...</div></div></div>
</template><script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'interface ChatUser {id: stringusername: stringcolor: string
}interface ChatMessage {id: stringtype: 'text' | 'image' | 'system'userId: stringusername: stringcontent: stringimageUrl?: stringtimestamp: number
}const currentUserId = ref(`user_${Math.random().toString(36).substr(2, 9)}`)
const currentUsername = ref(`用户${Math.floor(Math.random() * 1000)}`)
const userColor = ref(`hsl(${Math.random() * 360}, 70%, 60%)`)const inputMessage = ref('')
const messages = ref<ChatMessage[]>([])
const userList = ref<ChatUser[]>([])
const onlineUsers = ref(0)
const typingUsers = ref<string[]>([])
const messagesContainer = ref<HTMLElement>()let typingTimer: NodeJS.Timeout | null = nullconst { isConnected, send } = useWebSocket({url: 'ws://localhost:3001/chat',onMessage: (data) => {switch (data.type) {case 'message':messages.value.push({id: data.id,type: data.messageType || 'text',userId: data.userId,username: data.username,content: data.content,imageUrl: data.imageUrl,timestamp: data.timestamp})breakcase 'userList':userList.value = data.usersonlineUsers.value = data.users.lengthbreakcase 'userJoined':messages.value.push({id: data.id,type: 'system',userId: 'system',username: '系统',content: `${data.username} 加入了聊天室`,timestamp: data.timestamp})breakcase 'userLeft':messages.value.push({id: data.id,type: 'system',userId: 'system',username: '系统',content: `${data.username} 离开了聊天室`,timestamp: data.timestamp})breakcase 'typing':if (data.typing) {if (!typingUsers.value.includes(data.username)) {typingUsers.value.push(data.username)}} else {typingUsers.value = typingUsers.value.filter(user => user !== data.username)}break}// 滚动到底部nextTick(() => {if (messagesContainer.value) {messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight}})},onOpen: () => {// 发送用户加入消息send({type: 'join',userId: currentUserId.value,username: currentUsername.value,color: userColor.value})}
})const sendMessage = () => {if (!inputMessage.value.trim()) returnsend({type: 'message',userId: currentUserId.value,username: currentUsername.value,content: inputMessage.value.trim(),timestamp: Date.now()})// 清除输入状态send({type: 'typing',userId: currentUserId.value,username: currentUsername.value,typing: false})inputMessage.value = ''if (typingTimer) {clearTimeout(typingTimer)typingTimer = null}
}const sendTypingEvent = () => {send({type: 'typing',userId: currentUserId.value,username: currentUsername.value,typing: true})// 3秒后自动清除输入状态if (typingTimer) {clearTimeout(typingTimer)}typingTimer = setTimeout(() => {send({type: 'typing',userId: currentUserId.value,username: currentUsername.value,typing: false})}, 3000)
}const getUserColor = (userId: string) => {const user = userList.value.find(u => u.id === userId)return user?.color || '#666'
}const formatTime = (timestamp: number) => {return new Date(timestamp).toLocaleTimeString()
}// 模拟初始消息
onMounted(() => {messages.value.push({id: '1',type: 'system',userId: 'system',username: '系统',content: '欢迎进入聊天室!',timestamp: Date.now()})
})
</script><style scoped>
.chat-room-websocket {max-width: 800px;margin: 0 auto;border: 1px solid #ddd;border-radius: 8px;overflow: hidden;
}.chat-header {background: #007bff;color: white;padding: 15px;display: flex;justify-content: space-between;align-items: center;
}.connection-info {display: flex;gap: 15px;align-items: center;
}.status {padding: 4px 8px;border-radius: 4px;font-size: 12px;
}.status.connected {background: #28a745;
}.status.disconnected {background: #dc3545;
}.users-online {background: #f8f9fa;padding: 10px 15px;border-bottom: 1px solid #e9ecef;
}.users-online h4 {margin: 0 0 8px 0;font-size: 14px;
}.user-list {display: flex;flex-wrap: wrap;gap: 8px;
}.user-tag {padding: 2px 8px;background: white;border: 1px solid #dee2e6;border-radius: 12px;font-size: 12px;
}.messages-container {height: 400px;overflow-y: auto;padding: 15px;background: #f8f9fa;
}.message {margin-bottom: 15px;padding: 10px;border-radius: 8px;max-width: 70%;
}.message.own {margin-left: auto;background: #007bff;color: white;
}.message:not(.own) {background: white;border: 1px solid #dee2e6;
}.message.system {max-width: 100%;text-align: center;background: #fff3cd;border-color: #ffeaa7;color: #856404;
}.message-header {display: flex;justify-content: space-between;margin-bottom: 5px;font-size: 12px;
}.message.own .message-header {color: rgba(255, 255, 255, 0.8);
}.message-content {word-break: break-word;
}.message-image img {max-width: 200px;max-height: 200px;border-radius: 4px;margin-top: 5px;
}.input-area {padding: 15px;background: white;border-top: 1px solid #dee2e6;
}.input-controls {display: flex;gap: 10px;margin-bottom: 8px;
}.input-controls input {flex: 1;padding: 10px;border: 1px solid #ddd;border-radius: 4px;
}.input-controls button {padding: 10px 15px;background: #007bff;color: white;border: none;border-radius: 4px;cursor: pointer;
}.input-controls button:disabled {background: #6c757d;cursor: not-allowed;
}.typing-indicator {background: #6c757d !important;
}.typing-users {font-size: 12px;color: #6c757d;font-style: italic;
}
</style>
4. 性能对比和选择指南
4.1 性能特征对比
// 性能测试示例
interface PerformanceMetrics {latency: numberbandwidth: numberconnectionOverhead: numberbatteryImpact: number
}const websocketMetrics: PerformanceMetrics = {latency: 10, // ms (很低)bandwidth: 100, // 高 (支持二进制)connectionOverhead: 2, // KB (初始握手后很小)batteryImpact: 70 // 较高 (持久连接)
}const sseMetrics: PerformanceMetrics = {latency: 50, // ms (较低)bandwidth: 80, // 中等 (仅文本)connectionOverhead: 0.5, // KB (HTTP头)batteryImpact: 40 // 较低 (HTTP/2多路复用)
}
4.2 选择指南
// 技术选型决策函数
function chooseRealtimeTechnology(requirements: {bidirectional: booleanbinaryData: booleanscale: 'small' | 'medium' | 'large'mobileSupport: booleanexistingHTTP: boolean
}): 'websocket' | 'sse' | 'polling' {if (requirements.bidirectional) {return 'websocket'}if (requirements.binaryData) {return 'websocket'}if (requirements.existingHTTP && !requirements.bidirectional) {return 'sse'}if (requirements.mobileSupport && !requirements.bidirectional) {return 'sse'}return 'websocket' // 默认选择
}// 使用示例
const chatAppRequirements = {bidirectional: true, // 需要双向通信binaryData: false, // 不需要二进制数据scale: 'large', // 大规模应用mobileSupport: true, // 需要移动端支持existingHTTP: true // 已有 HTTP 基础设施
}const chosenTech = chooseRealtimeTechnology(chatAppRequirements)
console.log(`推荐技术: ${chosenTech}`) // 输出: websocket
5. 混合使用方案
5.1 WebSocket + SSE 混合方案
<!-- HybridRealtimeSystem.vue -->
<template><div class="hybrid-system"><div class="connection-status"><div class="websocket-status" :class="{ connected: wsConnected }">WebSocket: {{ wsConnected ? '已连接' : '断开' }}</div><div class="sse-status" :class="{ connected: sseConnected }">SSE: {{ sseConnected ? '已连接' : '断开' }}</div></div><!-- WebSocket 负责的实时交互 --><div class="interactive-section"><h3>实时协作 (WebSocket)</h3><button @click="sendAction" :disabled="!wsConnected">发送协作动作</button><div class="actions-log"><divv-for="action in recentActions":key="action.id"class="action-item">{{ action.user }}: {{ action.type }}</div></div></div><!-- SSE 负责的数据推送 --><div class="data-stream-section"><h3>数据流 (SSE)</h3><div class="metrics"><div class="metric"><label>CPU:</label><span>{{ metrics.cpu }}%</span></div><div class="metric"><label>内存:</label><span>{{ metrics.memory }}%</span></div><div class="metric"><label>网络:</label><span>{{ metrics.network }}MB/s</span></div></div></div><!-- 通知中心 --><div class="notifications-section"><h3>通知中心</h3><div class="notifications"><divv-for="notification in notifications":key="notification.id":class="['notification', notification.priority]">{{ notification.message }}</div></div></div></div>
</template><script setup lang="ts">
import { ref, reactive } from 'vue'
import { useWebSocket } from '@/composables/useWebSocket'
import { useSSE } from '@/composables/useSSE'// WebSocket 用于双向通信
const recentActions = ref<any[]>([])
const { isConnected: wsConnected, send: wsSend } = useWebSocket({url: 'ws://localhost:3001/collaboration',onMessage: (data) => {if (data.type === 'action') {recentActions.value.unshift(data)if (recentActions.value.length > 10) {recentActions.value = recentActions.value.slice(0, 10)}}}
})// SSE 用于服务器推送
const metrics = reactive({cpu: 0,memory: 0,network: 0
})const notifications = ref<any[]>([])const { isConnected: sseConnected } = useSSE({url: 'http://localhost:3000/api/stream',onMessage: (data) => {if (data.type === 'metrics') {metrics.cpu = data.cpumetrics.memory = data.memorymetrics.network = data.network} else if (data.type === 'notification') {notifications.value.unshift({id: Date.now(),message: data.message,priority: data.priority || 'info'})if (notifications.value.length > 5) {notifications.value = notifications.value.slice(0, 5)}}}
})const sendAction = () => {wsSend({type: 'userAction',action: 'button_click',timestamp: Date.now(),user: 'current_user'})
}
</script><style scoped>
.hybrid-system {max-width: 1000px;margin: 0 auto;padding: 20px;
}.connection-status {display: flex;gap: 15px;margin-bottom: 20px;
}.websocket-status, .sse-status {padding: 8px 16px;border-radius: 4px;background: #dc3545;color: white;
}.websocket-status.connected {background: #28a745;
}.sse-status.connected {background: #17a2b8;
}.interactive-section,
.data-stream-section,
.notifications-section {background: white;border: 1px solid #ddd;border-radius: 8px;padding: 20px;margin-bottom: 20px;
}.actions-log {height: 150px;overflow-y: auto;border: 1px solid #eee;padding: 10px;margin-top: 10px;
}.action-item {padding: 5px;border-bottom: 1px solid #f0f0f0;
}.metrics {display: flex;gap: 20px;
}.metric {display: flex;flex-direction: column;align-items: center;
}.metric label {font-size: 12px;color: #666;margin-bottom: 5px;
}.metric span {font-size: 18px;font-weight: bold;
}.notifications {max-height: 200px;overflow-y: auto;
}.notification {padding: 10px;margin: 5px 0;border-radius: 4px;border-left: 4px solid #6c757d;
}.notification.info {border-left-color: #17a2b8;background: #d1ecf1;
}.notification.warning {border-left-color: #ffc107;background: #fff3cd;
}.notification.error {border-left-color: #dc3545;background: #f8d7da;
}
</style>
6. 总结
WebSocket 适用场景:
- 实时聊天应用(双向通信)
- 多人在线游戏(低延迟要求)
- 实时协作编辑(频繁的双向数据交换)
- 视频会议应用(二进制数据传输)
SSE 适用场景:
- 实时通知系统(服务器向客户端推送)
- 股票行情推送(单向数据流)
- 新闻资讯推送(简单的文本数据)
- 系统监控仪表板(指标数据流)
混合方案优势:
- 性能优化:根据不同需求使用合适的技术
- 资源分配:SSE 处理推送,WebSocket 处理交互
- 容错能力:一种技术失败时另一种可以继续工作
- 成本控制:SSE 在服务器资源消耗上通常更低
根据具体的应用需求和场景特点,选择合适的实时通信技术或组合方案。