fix: Web端Socket.IO连接配置修复

- 传输方式从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://,向后兼容旧地址
This commit is contained in:
wdvipa
2026-02-15 15:52:23 +08:00
parent 115b15c0fc
commit 74062e2b19
2 changed files with 67 additions and 46 deletions

View File

@@ -21,20 +21,19 @@ const ConnectDialog: React.FC<ConnectDialogProps> = ({
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<ConnectDialogProps> = ({
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<ConnectDialogProps> = ({
form={form}
layout="vertical"
initialValues={{
serverUrl: 'ws://localhost:3001'
serverUrl: 'http://localhost:3001'
}}
>
<Form.Item
label="服务器地址"
name="serverUrl"
rules={[
{ validator: validateWebSocketUrl }
{ validator: validateServerUrl }
]}
help="输入WebSocket服务器地址,例如: ws://localhost:3001"
help="输入服务器地址,例如: http://localhost:3001"
>
<Input
placeholder="ws://localhost:3001"
placeholder="http://localhost:3001"
size="large"
autoFocus
onPressEnter={handleConnect}

View File

@@ -159,29 +159,42 @@ const RemoteControlApp: React.FC = () => {
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('与服务器断开连接')
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'))
}
})