perf(component): ⚡ 优化webSocket的重连性能和网络检测方法

This commit is contained in:
Dawn
2025-06-02 03:01:29 +08:00
parent 2d0d42e35d
commit 54f32eec5f
8 changed files with 163 additions and 365 deletions
-1
View File
@@ -1,4 +1,3 @@
pub mod common_cmd;
pub mod init;
pub mod network_monitor;
pub mod tray;
-216
View File
@@ -1,216 +0,0 @@
use serde::Serialize;
use std::net::IpAddr;
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use surge_ping::{Client, Config, PingIdentifier, PingSequence};
use tauri::{AppHandle, Emitter, Manager, Runtime};
use tokio::net::lookup_host;
// 网络状态枚举
#[derive(Debug, Clone, Copy, Serialize, PartialEq)]
pub enum NetworkStrength {
Strong, // 强网络
Weak, // 弱网络
Offline, // 离线
}
// 网络状态结构体
#[derive(Debug, Clone, Serialize)]
pub struct NetworkStatus {
pub is_online: bool,
pub strength: NetworkStrength,
pub latency_ms: u64, // 延迟时间(毫秒)
pub last_check: u64, // 上次检查时间戳
}
impl Default for NetworkStatus {
fn default() -> Self {
NetworkStatus {
is_online: true, // 默认假设在线
strength: NetworkStrength::Strong,
latency_ms: 0,
last_check: 0,
}
}
}
// 全局网络状态
lazy_static::lazy_static! {
static ref NETWORK_STATUS: Arc<Mutex<NetworkStatus>> = Arc::new(Mutex::new(NetworkStatus::default()));
}
// 将域名解析为IP地址
async fn resolve_host(host: &str) -> Option<IpAddr> {
// 删除可能的协议前缀
let host = host.replace("https://", "").replace("http://", "");
// 删除可能的路径
let host = host.split('/').next().unwrap_or(&host);
// 使用标准端口进行DNS解析
match lookup_host(format!("{host}:80")).await {
Ok(mut addrs) => addrs.next().map(|addr| addr.ip()),
Err(_) => None,
}
}
// 对IP地址进行ping测试
async fn ping_ip(ip: IpAddr, count: u8, timeout: Duration) -> Option<u64> {
// 注意:Config::builder()没有timeout方法,我们在pinger中设置timeout
let config = Config::default();
let client = match Client::new(&config) {
Ok(client) => client,
Err(_) => return None,
};
let mut total_rtt = 0u64;
let mut success_count = 0u8;
for i in 0..count {
let payload = vec![0; 56]; // 标准ping数据包大小
let ping_id = PingIdentifier(rand::random());
// 将u8转换为u16
let seq_num = PingSequence(i as u16);
// 创建pinger(这是一个Future,需要await)
// pinger直接返回Pinger对象,不是Result
let mut pinger = client.pinger(ip, ping_id).await;
// 设置超时(timeout方法返回&mut Pinger,已经修改了原来的pinger)
pinger.timeout(timeout);
// 执行ping
match pinger.ping(seq_num, &payload).await {
Ok((_, rtt)) => {
total_rtt += rtt.as_millis() as u64;
success_count += 1;
}
Err(_) => continue,
}
}
if success_count > 0 {
Some(total_rtt / success_count as u64)
} else {
None
}
}
// 异步检查网络状态
async fn check_network_status() -> NetworkStatus {
// 测试多个网站以提高准确性
let test_hosts = vec!["www.baidu.com", "www.qq.com", "www.aliyun.com"];
// 200ms的超时时间用于判断弱网
let weak_threshold = 200;
let ping_timeout = Duration::from_secs(1); // 单次ping超时时间
let ping_count = 5; // 每个网站ping 5次
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// 并发测试所有网站
let mut futures = Vec::new();
for host in test_hosts {
let host = host.to_string();
futures.push(tokio::spawn(async move {
// 先解析域名
if let Some(ip) = resolve_host(&host).await {
// 如果解析成功,进行ping测试
ping_ip(ip, ping_count, ping_timeout).await
} else {
None
}
}));
}
// 收集测试结果
let mut successful_tests = 0;
let mut total_latency = 0u64;
for future in futures {
if let Ok(result) = future.await {
if let Some(latency) = result {
successful_tests += 1;
total_latency += latency;
}
}
}
// 根据测试结果判断网络状态
if successful_tests == 0 {
// 所有测试都失败,认为离线
NetworkStatus {
is_online: false,
strength: NetworkStrength::Offline,
latency_ms: 0,
last_check: now,
}
} else {
// 至少有一个测试成功,计算平均延迟
let avg_latency = total_latency / successful_tests as u64;
let strength = if avg_latency > weak_threshold {
NetworkStrength::Weak
} else {
NetworkStrength::Strong
};
NetworkStatus {
is_online: true,
strength,
latency_ms: avg_latency,
last_check: now,
}
}
}
// 更新网络状态并通知前端
async fn update_network_status<R: Runtime>(app_handle: AppHandle<R>) {
let status = check_network_status().await;
// 获取旧状态进行比较
let mut global_status = NETWORK_STATUS.lock().unwrap();
let old_status = global_status.clone();
// 更新全局状态
*global_status = status.clone();
// 如果状态有变化,通知前端
if old_status.is_online != status.is_online || old_status.strength != status.strength {
// 沿用记忆中的错误处理方式,不抛出错误,而是尝试发送并忽略可能的错误
if let Some(window) = app_handle.get_webview_window("home") {
let _ = window.emit("network-status-changed", &status);
}
}
}
// 启动网络监控服务
pub fn start_network_monitor<R: Runtime>(app_handle: AppHandle<R>) {
let app_handle_clone = app_handle.clone();
// 立即进行一次检查
tauri::async_runtime::spawn(async move {
update_network_status(app_handle).await;
});
// 创建后台任务定期检查
tauri::async_runtime::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await; // 每5秒检查一次
update_network_status(app_handle_clone.clone()).await;
}
});
}
// 获取当前网络状态
#[tauri::command]
pub fn get_network_status() -> NetworkStatus {
NETWORK_STATUS.lock().unwrap().clone()
}
// 手动触发网络状态检查
#[tauri::command]
pub async fn check_network<R: Runtime>(app_handle: AppHandle<R>) -> NetworkStatus {
update_network_status(app_handle.clone()).await;
NETWORK_STATUS.lock().unwrap().clone()
}
-6
View File
@@ -12,8 +12,6 @@ use desktops::common_cmd;
#[cfg(desktop)]
use desktops::init;
#[cfg(desktop)]
use desktops::network_monitor::{check_network, get_network_status, start_network_monitor};
#[cfg(desktop)]
use desktops::tray;
#[cfg(desktop)]
use init::CustomInit;
@@ -49,8 +47,6 @@ fn setup_desktop() {
.init_window_event()
.setup(move |app| {
tray::create_tray(app.handle())?;
// 启动网络监测服务
start_network_monitor(app.handle().clone());
Ok(())
})
.invoke_handler(tauri::generate_handler![
@@ -61,8 +57,6 @@ fn setup_desktop() {
set_badge_count,
test_api_proxy,
test_ws_proxy,
get_network_status,
check_network,
#[cfg(target_os = "macos")]
hide_title_bar_buttons
])
+2 -16
View File
@@ -11,18 +11,6 @@
当前网络不可用,请检查你的网络设置
</n-flex>
<!-- 弱网提示 -->
<n-flex
v-if="networkStatus.isOnline.value && networkStatus.networkStrength.value === NetworkStrength.Weak"
align="center"
justify="center"
class="z-999 absolute w-full h-40px rounded-4px text-(12px [--warning-text]) bg-[--warning-bg]">
<svg class="size-16px">
<use href="#cloudWarning"></use>
</svg>
当前网络信号较弱,可能影响聊天体验 (延迟: {{ networkStatus.latencyMs.value }}ms)
</n-flex>
<!-- 置顶公告提示 -->
<div
v-if="isGroup && topAnnouncement"
@@ -469,6 +457,7 @@ import { useGlobalStore } from '@/stores/global'
import { useDebounceFn } from '@vueuse/core'
import { useCachedStore } from '@/stores/cached'
import apis from '@/services/apis'
import { useNetworkStatus } from '@/hooks/useNetworkStatus'
const appWindow = WebviewWindow.getCurrent()
const { addListener } = useTauriListener()
@@ -481,6 +470,7 @@ const userStore = useUserStore()
const groupStore = useGroupStore()
const globalStore = useGlobalStore()
const cachedStore = useCachedStore()
const networkStatus = useNetworkStatus()
// 记录当前滚动位置相关信息
const isAutoScrolling = ref(false)
@@ -531,10 +521,6 @@ const hoverBubble = ref<{
})
/** 记录右键菜单时选中的气泡的元素(用于处理mac右键会选中文本的问题) */
const recordEL = ref()
/** 网络连接是否正常 */
// 使用自定义网络状态钩子替代vueuse的useNetwork
import { useNetworkStatus, NetworkStrength } from '@/hooks/useNetworkStatus'
const networkStatus = useNetworkStatus()
const isMac = computed(() => type() === 'macos')
// 公告展示时需要减去的高度
const announcementHeight = computed(() => (isGroup.value && topAnnouncement.value ? 300 : 260))
+23 -76
View File
@@ -1,95 +1,42 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { listen } from '@tauri-apps/api/event'
import { invoke } from '@tauri-apps/api/core'
// 网络状态类型
export enum NetworkStrength {
Strong = 'Strong',
Weak = 'Weak',
Offline = 'Offline'
}
// 网络状态接口
export interface NetworkStatus {
is_online: boolean
strength: NetworkStrength
latency_ms: number
last_check: number
}
/**
* 网络状态监测钩子
* 提供网络是否在线、网络强度、延迟等信息
*/
export function useNetworkStatus() {
// 网络状态
const isOnline = ref(true)
// 网络强度
const networkStrength = ref<NetworkStrength>(NetworkStrength.Strong)
// 网络延迟(毫秒)
const latencyMs = ref(0)
// 上次检查时间
const lastCheck = ref(0)
// 网络是否为弱网
const isWeakNetwork = ref(false)
export const useNetworkStatus = () => {
// 网络状态 - 基于浏览器navigator.onLine
const isOnline = ref(navigator.onLine)
// 存储监听器清理函数
let unlisten: (() => void) | null = null
// 初始化网络状态
const initNetworkStatus = async () => {
try {
const status = await invoke<NetworkStatus>('get_network_status')
updateNetworkStatus(status)
} catch (error) {
console.error('获取网络状态失败:', error)
}
// 监听浏览器网络状态变化
const handleOnline = () => {
isOnline.value = true
}
// 手动检查网络状态
const checkNetwork = async () => {
try {
const status = await invoke<NetworkStatus>('check_network')
updateNetworkStatus(status)
return status
} catch (error) {
console.error('检查网络状态失败:', error)
return null
}
const handleOffline = () => {
isOnline.value = false
}
// 更新网络状态
const updateNetworkStatus = (status: NetworkStatus) => {
isOnline.value = status.is_online
networkStrength.value = status.strength
latencyMs.value = status.latency_ms
lastCheck.value = status.last_check
isWeakNetwork.value = status.strength === NetworkStrength.Weak
// 初始化网络状态监听
const initNetworkListener = () => {
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
}
onMounted(async () => {
// 初始化网络状态
await initNetworkStatus()
// 清理网络状态监听
const cleanupNetworkListener = () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
// 监听网络状态变化事件
unlisten = await listen<NetworkStatus>('network-status-changed', (event) => {
updateNetworkStatus(event.payload)
})
})
// 自动初始化监听器
initNetworkListener()
// 在组件卸载时清理监听器
onUnmounted(() => {
// 清理监听器
if (unlisten) {
unlisten()
}
cleanupNetworkListener()
})
return {
isOnline,
networkStrength,
latencyMs,
lastCheck,
isWeakNetwork,
checkNetwork
initNetworkListener,
cleanupNetworkListener
}
}
+94 -22
View File
@@ -31,14 +31,48 @@ const timerWorker: Worker = new Worker(new URL('../workers/timer.worker.ts', imp
// 添加一个标识是否是主窗口的变量
let isMainWindow = false
// LRU缓存实现
class LRUCache<K, V> {
private maxSize: number
private cache = new Map<K, V>()
constructor(maxSize: number = 1000) {
this.maxSize = maxSize
}
set(key: K, value: V) {
if (this.cache.has(key)) {
this.cache.delete(key)
} else if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
if (firstKey) {
this.cache.delete(firstKey)
}
}
this.cache.set(key, value)
}
has(key: K): boolean {
return this.cache.has(key)
}
clear() {
this.cache.clear()
}
get size(): number {
return this.cache.size
}
}
class WS {
// 添加消息队列大小限制
readonly #MAX_QUEUE_SIZE = 100
readonly #MAX_QUEUE_SIZE = 50 // 减少队列大小
#tasks: WsReqMsgContentType[] = []
// 重连🔐
#connectReady = false
// 使用LRU缓存替代简单的Set
#processedMsgCache = new Map<number, number>()
#processedMsgCache = new LRUCache<number, number>(1000) // 使用LRU缓存
#tauriListener: ReturnType<typeof useTauriListener> | null = null
@@ -112,13 +146,13 @@ class WS {
})
)
// 如果从不可见变为可见状态,并且网络重连工具已初始化,检查是否需要刷新数据
// 优化的可见性恢复检查
if (isVisible && this.#networkReconnect?.isOnline?.value) {
// 检查最后一次通信时间,如果太久没有通信,刷新数据
const now = Date.now()
const lastPongTime = this.#connectionHealth.lastPongTime
if (lastPongTime && now - lastPongTime > 60000) {
// 如果超过1分钟没有心跳
const heartbeatTimeout = 90000 // 增加到90秒,减少误触发
if (lastPongTime && now - lastPongTime > heartbeatTimeout) {
console.log('[Network] 应用从后台恢复且长时间无心跳,刷新数据')
this.#networkReconnect?.refreshAllData()
}
@@ -384,10 +418,19 @@ class WS {
if (this.#connectReady) {
this.#send(params)
} else {
// 队列限制
// 优化的队列管理
if (this.#tasks.length >= this.#MAX_QUEUE_SIZE) {
console.warn('消息队列已满,正在丢弃最旧的消息')
this.#tasks.shift()
// 优先丢弃非关键消息
const nonCriticalIndex = this.#tasks.findIndex(
(task) => typeof task === 'object' && task.type !== 1 && task.type !== 2
)
if (nonCriticalIndex !== -1) {
this.#tasks.splice(nonCriticalIndex, 1)
console.warn('消息队列已满,丢弃非关键消息')
} else {
this.#tasks.shift()
console.warn('消息队列已满,丢弃最旧消息')
}
}
this.#tasks.push(params)
}
@@ -597,21 +640,50 @@ class WS {
}
destroy() {
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
worker.terminate()
// 同时终止timer worker相关的心跳
timerWorker.postMessage({
type: 'stopPeriodicHeartbeat'
})
this.#tasks = []
this.#processedMsgCache.clear()
this.#connectReady = false
// 清理 Tauri 事件监听器
this.#tauriListener?.cleanup()
try {
// 优化的资源清理顺序
worker.postMessage(JSON.stringify({ type: 'clearReconnectTimer' }))
worker.postMessage(JSON.stringify({ type: 'stopHeartbeat' }))
// 清理所有watch
this.#unwatchFunctions.forEach((unwatch) => unwatch())
this.#unwatchFunctions = []
// 同时终止timer worker相关的心跳
timerWorker.postMessage({
type: 'stopPeriodicHeartbeat'
})
// 清理内存
this.#tasks.length = 0 // 更高效的数组清空
this.#processedMsgCache.clear()
this.#connectReady = false
// 重置连接健康状态
this.#connectionHealth = {
isHealthy: true,
lastPongTime: null,
timeSinceLastPong: null
}
// 清理 Tauri 事件监听器
this.#tauriListener?.cleanup()
this.#tauriListener = null
// 清理所有watch
this.#unwatchFunctions.forEach((unwatch) => {
try {
unwatch()
} catch (error) {
console.warn('清理watch函数时出错:', error)
}
})
this.#unwatchFunctions.length = 0
// 最后终止workers
setTimeout(() => {
worker.terminate()
timerWorker.terminate()
}, 100) // 给一点时间让消息处理完成
} catch (error) {
console.error('销毁WebSocket时出错:', error)
}
}
}
+15 -11
View File
@@ -24,17 +24,20 @@ const checkAllTimersCompleted = () => {
}
}
// 添加调试信息打印函数
// 优化的调试信息打印函数
const logDebugInfo = (msgId: number, remainingTime: number) => {
// 只有开启日志功能时才打印
if (enableLogging) {
// 只有开启日志功能时才打印,且只在关键时间点打印
if (enableLogging && (remainingTime <= 5000 || remainingTime % 10000 < 1000)) {
console.log(`[Worker Debug] 消息ID: ${msgId}, 剩余时间: ${(remainingTime / 1000).toFixed(1)}秒`)
self.postMessage({
type: 'debug',
msgId,
remainingTime,
timestamp: Date.now()
})
// 减少向主线程发送调试消息的频率
if (remainingTime <= 3000) {
self.postMessage({
type: 'debug',
msgId,
remainingTime,
timestamp: Date.now()
})
}
}
}
@@ -103,7 +106,8 @@ self.onmessage = (e) => {
// 立即打印一次初始状态
logDebugInfo(msgId, duration)
// 创建定时打印的间隔器,每1000ms打印一次
// 优化的调试间隔器,减少打印频率
const debugInterval = duration > 10000 ? 5000 : 1000 // 长任务每5秒打印,短任务每秒打印
const debugId = setInterval(() => {
const elapsed = Date.now() - startTime
const remaining = duration - elapsed
@@ -112,7 +116,7 @@ self.onmessage = (e) => {
} else {
clearInterval(debugId)
}
}, 1000) // 每秒打印一次
}, debugInterval)
const timerId = setTimeout(() => {
clearInterval(debugId)
+29 -17
View File
@@ -129,27 +129,35 @@ const sendSingleHeartbeat = () => {
return
}
// 检测连接健康状态
// 优化的连接健康检测机制
if (lastPongTime !== null) {
const timeSinceLastPong = lastPingSent - lastPongTime
const isConnectionHealthy = timeSinceLastPong < HEARTBEAT_INTERVAL * 2
const healthThreshold = HEARTBEAT_INTERVAL * 2.5 // 增加容错时间
const isConnectionHealthy = timeSinceLastPong < healthThreshold
// 如果连接不健康,通知主线程
if (!isConnectionHealthy) {
consecutiveHeartbeatFailures++
logHeartbeat('连接响应缓慢', {
consecutiveFailures: consecutiveHeartbeatFailures,
timeSinceLastPong
})
postMsg({
type: WorkerMsgEnum.ERROR,
value: {
msg: '连接响应较慢,可能存在网络问题',
timeSinceLastPong,
consecutiveFailures: consecutiveHeartbeatFailures
}
})
// 只在关键阈值时记录日志,减少日志开销
if (consecutiveHeartbeatFailures === 1 || consecutiveHeartbeatFailures % 3 === 0) {
logHeartbeat('连接响应缓慢', {
consecutiveFailures: consecutiveHeartbeatFailures,
timeSinceLastPong
})
}
// 延迟错误通知,避免频繁触发
if (consecutiveHeartbeatFailures >= 2) {
postMsg({
type: WorkerMsgEnum.ERROR,
value: {
msg: '连接响应较慢,可能存在网络问题',
timeSinceLastPong,
consecutiveFailures: consecutiveHeartbeatFailures
}
})
}
// 连续失败次数过多,尝试重连
if (consecutiveHeartbeatFailures >= MAX_HEARTBEAT_FAILURES) {
@@ -244,11 +252,15 @@ const tryReconnect = () => {
}
}
// 优化的智能退避算法
const getBackoffDelay = (retryCount: number) => {
const baseDelay = 1000 // 基础延迟1秒
const maxDelay = 30000 // 最大延迟30秒
const delay = Math.min(baseDelay * Math.pow(2, retryCount), maxDelay)
return delay + Math.random() * 1000 // 添加随机抖动
const maxDelay = 15000 // 减少最大延迟到15秒
const multiplier = Math.min(1.5, 2 - retryCount * 0.1)
const delay = Math.min(baseDelay * Math.pow(multiplier, retryCount), maxDelay)
// 减少随机抖动范围
return delay + Math.random() * 500
}
const onCloseHandler = () => {