From 74062e2b19d0267f4a157e8fd672d4456ae97b2b Mon Sep 17 00:00:00 2001 From: wdvipa Date: Sun, 15 Feb 2026 15:52:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20Web=E7=AB=AFSocket.IO=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 传输方式从websocket-only改为polling+websocket双传输,增加连接可靠性 - 连接超时从20秒增加到60秒,匹配后端配置 - 重连次数从20次改为Infinity,永不放弃重连 - connect_error不再每次都设置error状态,避免阻断Socket.IO内置重连 - disconnect事件区分断开原因,transport error时保持connecting状态等待自动重连 - 新建连接前先断开旧socket,防止连接泄漏 - ConnectDialog默认地址从ws://改为http://,符合Socket.IO v4规范 - URL验证器兼容http/https/ws/wss四种协议 - connectToServer自动将ws://转换为http://,向后兼容旧地址 --- src/components/Connection/ConnectDialog.tsx | 32 ++++---- src/components/RemoteControlApp.tsx | 81 +++++++++++++-------- 2 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/components/Connection/ConnectDialog.tsx b/src/components/Connection/ConnectDialog.tsx index 4d5cf81..06f4b47 100644 --- a/src/components/Connection/ConnectDialog.tsx +++ b/src/components/Connection/ConnectDialog.tsx @@ -21,20 +21,19 @@ const ConnectDialog: React.FC = ({ const [loading, setLoading] = useState(false) const { status: connectionStatus, serverUrl } = useSelector((state: RootState) => state.connection) - // 预设服务器地址选项 + // Preset server addresses const presetServers = [ - 'ws://localhost:3001', - 'ws://127.0.0.1:3001', - 'ws://192.168.1.100:3001', // 示例局域网地址 + 'http://localhost:3001', + 'http://127.0.0.1:3001', + 'http://192.168.1.100:3001', ] useEffect(() => { if (visible) { - // 如果已有服务器地址,使用它作为默认值 if (serverUrl) { form.setFieldValue('serverUrl', serverUrl) } else { - form.setFieldValue('serverUrl', 'ws://localhost:3001') + form.setFieldValue('serverUrl', 'http://localhost:3001') } } }, [visible, serverUrl, form]) @@ -61,20 +60,23 @@ const ConnectDialog: React.FC = ({ form.setFieldValue('serverUrl', url) } - const validateWebSocketUrl = (_: any, value: string) => { + const validateServerUrl = (_: any, value: string) => { if (!value) { return Promise.reject(new Error('请输入服务器地址')) } - if (!value.startsWith('ws://') && !value.startsWith('wss://')) { - return Promise.reject(new Error('地址必须以 ws:// 或 wss:// 开头')) + if (!value.startsWith('http://') && !value.startsWith('https://') && + !value.startsWith('ws://') && !value.startsWith('wss://')) { + return Promise.reject(new Error('地址必须以 http:// 或 https:// 开头')) } try { - new URL(value) + // Normalize ws:// -> http:// for URL validation + const normalized = value.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://') + new URL(normalized) return Promise.resolve() } catch { - return Promise.reject(new Error('请输入有效的WebSocket地址')) + return Promise.reject(new Error('请输入有效的服务器地址')) } } @@ -103,19 +105,19 @@ const ConnectDialog: React.FC = ({ form={form} layout="vertical" initialValues={{ - serverUrl: 'ws://localhost:3001' + serverUrl: 'http://localhost:3001' }} > { return } + // Disconnect existing socket before creating a new one + if (webSocket) { + console.log('[Socket] Disconnecting existing socket before reconnect') + webSocket.disconnect() + dispatch(setWebSocket(null)) + } + dispatch(setConnectionStatus('connecting')) dispatch(setServerUrl(url)) + // Normalize ws:// -> http:// for Socket.IO v4 client + let normalizedUrl = url + if (normalizedUrl.startsWith('ws://')) { + normalizedUrl = normalizedUrl.replace(/^ws:\/\//, 'http://') + } else if (normalizedUrl.startsWith('wss://')) { + normalizedUrl = normalizedUrl.replace(/^wss:\/\//, 'https://') + } + // Socket.IO v4 client config - const socket = io(url, { - // Use websocket transport for low-latency streaming - transports: ['websocket'], + const socket = io(normalizedUrl, { + // Transport: polling first then upgrade to websocket for reliability + transports: ['polling', 'websocket'], + upgrade: true, // Reconnection config reconnection: true, - reconnectionDelay: 1000, // 1秒重连延迟 - reconnectionDelayMax: 5000, // 最大5秒重连延迟 - reconnectionAttempts: 20, // 最多重连20次 + reconnectionDelay: 1000, + reconnectionDelayMax: 10000, + reconnectionAttempts: Infinity, - // Timeout config (match server) - timeout: 20000, // 连接超时 + // Timeout config (match server side) + timeout: 60000, // Force new connection forceNew: true, - - // Other config autoConnect: true, - upgrade: false, // 已经是websocket,无需升级 // Auth: carry JWT token auth: { @@ -208,35 +221,41 @@ const RemoteControlApp: React.FC = () => { }) - socket.on('disconnect', () => { - console.log('与服务器断开连接') - dispatch(setConnectionStatus('disconnected')) - dispatch(addNotification({ - type: 'warning', - title: '连接断开', - message: '与服务器的连接已断开', - })) + socket.on('disconnect', (reason) => { + console.log('[Socket] disconnect, reason:', reason) + + if (reason === 'io server disconnect' || reason === 'io client disconnect') { + // Server or client explicitly disconnected, no auto-reconnect + dispatch(setConnectionStatus('disconnected')) + dispatch(addNotification({ + type: 'warning', + title: '连接断开', + message: '与服务器的连接已断开', + })) + } else { + // Transport error / ping timeout etc. - Socket.IO will auto-reconnect + dispatch(setConnectionStatus('connecting')) + console.log('[Socket] Will auto-reconnect...') + } }) socket.on('connect_error', (error) => { - console.error('连接错误:', error) - dispatch(setConnectionStatus('error')) + console.error('[Socket] connect_error:', error?.message || error) - if (autoConnectAttempted) { - // 自动连接失败,显示连接对话框让用户手动连接 - dispatch(addNotification({ - type: 'warning', - title: '自动连接失败', - message: '无法自动连接到本地服务器,请手动输入服务器地址', - })) - setConnectDialogVisible(true) - } else { + // Only show dialog and set error state when reconnection is exhausted + // Socket.IO will auto-reconnect; don't interfere by setting 'error' status on every attempt + if (!socket.active) { + // socket.active === false means reconnection gave up + dispatch(setConnectionStatus('error')) dispatch(addNotification({ type: 'error', title: '连接失败', - message: `无法连接到服务器: ${error.message}`, + message: `无法连接到服务器: ${error?.message || '未知错误'}`, })) setConnectDialogVisible(true) + } else { + // Still reconnecting, keep 'connecting' status + dispatch(setConnectionStatus('connecting')) } })