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:
@@ -21,20 +21,19 @@ const ConnectDialog: React.FC<ConnectDialogProps> = ({
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const { status: connectionStatus, serverUrl } = useSelector((state: RootState) => state.connection)
|
const { status: connectionStatus, serverUrl } = useSelector((state: RootState) => state.connection)
|
||||||
|
|
||||||
// 预设服务器地址选项
|
// Preset server addresses
|
||||||
const presetServers = [
|
const presetServers = [
|
||||||
'ws://localhost:3001',
|
'http://localhost:3001',
|
||||||
'ws://127.0.0.1:3001',
|
'http://127.0.0.1:3001',
|
||||||
'ws://192.168.1.100:3001', // 示例局域网地址
|
'http://192.168.1.100:3001',
|
||||||
]
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
// 如果已有服务器地址,使用它作为默认值
|
|
||||||
if (serverUrl) {
|
if (serverUrl) {
|
||||||
form.setFieldValue('serverUrl', serverUrl)
|
form.setFieldValue('serverUrl', serverUrl)
|
||||||
} else {
|
} else {
|
||||||
form.setFieldValue('serverUrl', 'ws://localhost:3001')
|
form.setFieldValue('serverUrl', 'http://localhost:3001')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [visible, serverUrl, form])
|
}, [visible, serverUrl, form])
|
||||||
@@ -61,20 +60,23 @@ const ConnectDialog: React.FC<ConnectDialogProps> = ({
|
|||||||
form.setFieldValue('serverUrl', url)
|
form.setFieldValue('serverUrl', url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateWebSocketUrl = (_: any, value: string) => {
|
const validateServerUrl = (_: any, value: string) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return Promise.reject(new Error('请输入服务器地址'))
|
return Promise.reject(new Error('请输入服务器地址'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!value.startsWith('ws://') && !value.startsWith('wss://')) {
|
if (!value.startsWith('http://') && !value.startsWith('https://') &&
|
||||||
return Promise.reject(new Error('地址必须以 ws:// 或 wss:// 开头'))
|
!value.startsWith('ws://') && !value.startsWith('wss://')) {
|
||||||
|
return Promise.reject(new Error('地址必须以 http:// 或 https:// 开头'))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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()
|
return Promise.resolve()
|
||||||
} catch {
|
} catch {
|
||||||
return Promise.reject(new Error('请输入有效的WebSocket地址'))
|
return Promise.reject(new Error('请输入有效的服务器地址'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,19 +105,19 @@ const ConnectDialog: React.FC<ConnectDialogProps> = ({
|
|||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={{
|
||||||
serverUrl: 'ws://localhost:3001'
|
serverUrl: 'http://localhost:3001'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="服务器地址"
|
label="服务器地址"
|
||||||
name="serverUrl"
|
name="serverUrl"
|
||||||
rules={[
|
rules={[
|
||||||
{ validator: validateWebSocketUrl }
|
{ validator: validateServerUrl }
|
||||||
]}
|
]}
|
||||||
help="输入WebSocket服务器地址,例如: ws://localhost:3001"
|
help="输入服务器地址,例如: http://localhost:3001"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
placeholder="ws://localhost:3001"
|
placeholder="http://localhost:3001"
|
||||||
size="large"
|
size="large"
|
||||||
autoFocus
|
autoFocus
|
||||||
onPressEnter={handleConnect}
|
onPressEnter={handleConnect}
|
||||||
|
|||||||
@@ -159,29 +159,42 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
return
|
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(setConnectionStatus('connecting'))
|
||||||
dispatch(setServerUrl(url))
|
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
|
// Socket.IO v4 client config
|
||||||
const socket = io(url, {
|
const socket = io(normalizedUrl, {
|
||||||
// Use websocket transport for low-latency streaming
|
// Transport: polling first then upgrade to websocket for reliability
|
||||||
transports: ['websocket'],
|
transports: ['polling', 'websocket'],
|
||||||
|
upgrade: true,
|
||||||
|
|
||||||
// Reconnection config
|
// Reconnection config
|
||||||
reconnection: true,
|
reconnection: true,
|
||||||
reconnectionDelay: 1000, // 1秒重连延迟
|
reconnectionDelay: 1000,
|
||||||
reconnectionDelayMax: 5000, // 最大5秒重连延迟
|
reconnectionDelayMax: 10000,
|
||||||
reconnectionAttempts: 20, // 最多重连20次
|
reconnectionAttempts: Infinity,
|
||||||
|
|
||||||
// Timeout config (match server)
|
// Timeout config (match server side)
|
||||||
timeout: 20000, // 连接超时
|
timeout: 60000,
|
||||||
|
|
||||||
// Force new connection
|
// Force new connection
|
||||||
forceNew: true,
|
forceNew: true,
|
||||||
|
|
||||||
// Other config
|
|
||||||
autoConnect: true,
|
autoConnect: true,
|
||||||
upgrade: false, // 已经是websocket,无需升级
|
|
||||||
|
|
||||||
// Auth: carry JWT token
|
// Auth: carry JWT token
|
||||||
auth: {
|
auth: {
|
||||||
@@ -208,35 +221,41 @@ const RemoteControlApp: React.FC = () => {
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', (reason) => {
|
||||||
console.log('与服务器断开连接')
|
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(setConnectionStatus('disconnected'))
|
||||||
dispatch(addNotification({
|
dispatch(addNotification({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
title: '连接断开',
|
title: '连接断开',
|
||||||
message: '与服务器的连接已断开',
|
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) => {
|
socket.on('connect_error', (error) => {
|
||||||
console.error('连接错误:', error)
|
console.error('[Socket] connect_error:', error?.message || error)
|
||||||
dispatch(setConnectionStatus('error'))
|
|
||||||
|
|
||||||
if (autoConnectAttempted) {
|
// 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
|
||||||
dispatch(addNotification({
|
if (!socket.active) {
|
||||||
type: 'warning',
|
// socket.active === false means reconnection gave up
|
||||||
title: '自动连接失败',
|
dispatch(setConnectionStatus('error'))
|
||||||
message: '无法自动连接到本地服务器,请手动输入服务器地址',
|
|
||||||
}))
|
|
||||||
setConnectDialogVisible(true)
|
|
||||||
} else {
|
|
||||||
dispatch(addNotification({
|
dispatch(addNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: '连接失败',
|
title: '连接失败',
|
||||||
message: `无法连接到服务器: ${error.message}`,
|
message: `无法连接到服务器: ${error?.message || '未知错误'}`,
|
||||||
}))
|
}))
|
||||||
setConnectDialogVisible(true)
|
setConnectDialogVisible(true)
|
||||||
|
} else {
|
||||||
|
// Still reconnecting, keep 'connecting' status
|
||||||
|
dispatch(setConnectionStatus('connecting'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user