mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-10 19:36:36 +08:00
修复ws连接问题 增加PROXY Protocol支持
This commit is contained in:
Binary file not shown.
@@ -1,52 +0,0 @@
|
|||||||
package com.admin.common.task;
|
|
||||||
|
|
||||||
import com.admin.common.utils.WebSocketServer;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* WebSocket资源清理定时任务
|
|
||||||
* 定期清理已完成的请求、无效的session锁等,防止内存泄漏
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
|
||||||
public class WebSocketCleanupTask {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 每5分钟执行一次轻量级清理
|
|
||||||
* 清理已完成的请求和无效的session锁
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedRate = 5 * 60 * 1000) // 5分钟
|
|
||||||
public void lightweightCleanup() {
|
|
||||||
try {
|
|
||||||
log.debug("开始执行WebSocket轻量级清理...");
|
|
||||||
|
|
||||||
// 清理已完成的请求
|
|
||||||
WebSocketServer.cleanupCompletedRequests();
|
|
||||||
|
|
||||||
// 清理无效的session锁
|
|
||||||
WebSocketServer.cleanupInvalidSessionLocks();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("WebSocket轻量级清理失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 每30分钟执行一次全面清理
|
|
||||||
* 包括所有类型的资源清理
|
|
||||||
*/
|
|
||||||
@Scheduled(fixedRate = 30 * 60 * 1000) // 30分钟
|
|
||||||
public void fullCleanup() {
|
|
||||||
try {
|
|
||||||
log.info("开始执行WebSocket全面清理...");
|
|
||||||
|
|
||||||
// 执行全面清理
|
|
||||||
WebSocketServer.performFullCleanup();
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("WebSocket全面清理失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
VITE_API_BASE=http://192.168.100.6:6365
|
VITE_API_BASE=http://127.0.0.1:6365
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface Forward {
|
|||||||
inPort: number;
|
inPort: number;
|
||||||
remoteAddr: string;
|
remoteAddr: string;
|
||||||
strategy: string;
|
strategy: string;
|
||||||
|
proxyProtocol: number; // 添加 proxy_protocol 字段
|
||||||
status: number;
|
status: number;
|
||||||
inFlow: number;
|
inFlow: number;
|
||||||
outFlow: number;
|
outFlow: number;
|
||||||
@@ -57,6 +58,7 @@ interface ForwardForm {
|
|||||||
inPort: number | null;
|
inPort: number | null;
|
||||||
remoteAddr: string;
|
remoteAddr: string;
|
||||||
strategy: string;
|
strategy: string;
|
||||||
|
proxyProtocol: boolean; // 在表单中使用 boolean 类型更方便
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddressItem {
|
interface AddressItem {
|
||||||
@@ -106,7 +108,8 @@ export default function ForwardPage() {
|
|||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
inPort: null,
|
inPort: null,
|
||||||
remoteAddr: '',
|
remoteAddr: '',
|
||||||
strategy: 'fifo'
|
strategy: 'fifo',
|
||||||
|
proxyProtocol: false // 新增 proxyProtocol 字段
|
||||||
});
|
});
|
||||||
|
|
||||||
// 表单验证错误
|
// 表单验证错误
|
||||||
@@ -202,7 +205,8 @@ export default function ForwardPage() {
|
|||||||
tunnelId: null,
|
tunnelId: null,
|
||||||
inPort: null,
|
inPort: null,
|
||||||
remoteAddr: '',
|
remoteAddr: '',
|
||||||
strategy: 'fifo'
|
strategy: 'fifo',
|
||||||
|
proxyProtocol: false
|
||||||
});
|
});
|
||||||
setSelectedTunnel(null);
|
setSelectedTunnel(null);
|
||||||
setErrors({});
|
setErrors({});
|
||||||
@@ -219,7 +223,8 @@ export default function ForwardPage() {
|
|||||||
tunnelId: forward.tunnelId,
|
tunnelId: forward.tunnelId,
|
||||||
inPort: forward.inPort,
|
inPort: forward.inPort,
|
||||||
remoteAddr: forward.remoteAddr.split(',').join('\n'),
|
remoteAddr: forward.remoteAddr.split(',').join('\n'),
|
||||||
strategy: forward.strategy || 'fifo'
|
strategy: forward.strategy || 'fifo',
|
||||||
|
proxyProtocol: forward.proxyProtocol === 1 // 编辑时需要将数字转换为 boolean
|
||||||
});
|
});
|
||||||
const tunnel = tunnels.find(t => t.id === forward.tunnelId);
|
const tunnel = tunnels.find(t => t.id === forward.tunnelId);
|
||||||
setSelectedTunnel(tunnel || null);
|
setSelectedTunnel(tunnel || null);
|
||||||
@@ -297,7 +302,8 @@ export default function ForwardPage() {
|
|||||||
tunnelId: form.tunnelId,
|
tunnelId: form.tunnelId,
|
||||||
inPort: form.inPort,
|
inPort: form.inPort,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
strategy: addressCount > 1 ? form.strategy : 'fifo',
|
||||||
|
proxyProtocol: form.proxyProtocol ? 1 : 0 // 提交时需要将 boolean 转换为数字
|
||||||
};
|
};
|
||||||
res = await updateForward(updateData);
|
res = await updateForward(updateData);
|
||||||
} else {
|
} else {
|
||||||
@@ -307,7 +313,8 @@ export default function ForwardPage() {
|
|||||||
tunnelId: form.tunnelId,
|
tunnelId: form.tunnelId,
|
||||||
inPort: form.inPort,
|
inPort: form.inPort,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
strategy: addressCount > 1 ? form.strategy : 'fifo',
|
||||||
|
proxyProtocol: form.proxyProtocol ? 1 : 0 // 创建时需要将 boolean 转换为数字
|
||||||
};
|
};
|
||||||
res = await createForward(createData);
|
res = await createForward(createData);
|
||||||
}
|
}
|
||||||
@@ -901,6 +908,19 @@ export default function ForwardPage() {
|
|||||||
<SelectItem key="rand" >随机模式 - 随机选择</SelectItem>
|
<SelectItem key="rand" >随机模式 - 随机选择</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-foreground">启用PROXY Protocol</span>
|
||||||
|
<span className="text-xs text-default-500">不知道干嘛的就别动</span>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
isSelected={form.proxyProtocol}
|
||||||
|
onValueChange={(checked) => setForm(prev => ({ ...prev, proxyProtocol: checked }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import {
|
|||||||
getNodeList,
|
getNodeList,
|
||||||
updateNode,
|
updateNode,
|
||||||
deleteNode,
|
deleteNode,
|
||||||
getNodeInstallCommand,
|
getNodeInstallCommand
|
||||||
checkNodeStatus
|
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
|
|
||||||
interface Node {
|
interface Node {
|
||||||
@@ -72,11 +71,6 @@ export default function NodePage() {
|
|||||||
const [installCommand, setInstallCommand] = useState('');
|
const [installCommand, setInstallCommand] = useState('');
|
||||||
const [currentNodeName, setCurrentNodeName] = useState('');
|
const [currentNodeName, setCurrentNodeName] = useState('');
|
||||||
|
|
||||||
// 状态检查相关状态
|
|
||||||
const [statusCheckLoading, setStatusCheckLoading] = useState(false);
|
|
||||||
const [statusCheckResult, setStatusCheckResult] = useState<any>(null);
|
|
||||||
const [statusCheckModal, setStatusCheckModal] = useState(false);
|
|
||||||
|
|
||||||
const websocketRef = useRef<WebSocket | null>(null);
|
const websocketRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTimerRef = useRef<NodeJS.Timeout | null>(null);
|
const reconnectTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const reconnectAttemptsRef = useRef(0);
|
const reconnectAttemptsRef = useRef(0);
|
||||||
@@ -452,55 +446,6 @@ export default function NodePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 状态检查处理函数
|
|
||||||
const handleCheckAllStatus = async () => {
|
|
||||||
setStatusCheckLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await checkNodeStatus();
|
|
||||||
if (res.code === 0) {
|
|
||||||
setStatusCheckResult(res.data);
|
|
||||||
setStatusCheckModal(true);
|
|
||||||
|
|
||||||
// 刷新节点列表以显示最新状态
|
|
||||||
await loadNodes();
|
|
||||||
|
|
||||||
const { totalNodes, connectedNodes, inconsistentNodes } = res.data;
|
|
||||||
if (inconsistentNodes > 0) {
|
|
||||||
toast.success(`状态检查完成!修复了 ${inconsistentNodes} 个节点的状态不一致问题`);
|
|
||||||
} else {
|
|
||||||
toast.success(`状态检查完成!${connectedNodes}/${totalNodes} 个节点在线`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(res.msg || '状态检查失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('网络错误,请重试');
|
|
||||||
} finally {
|
|
||||||
setStatusCheckLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckSingleNodeStatus = async (nodeId: number) => {
|
|
||||||
try {
|
|
||||||
const res = await checkNodeStatus(nodeId);
|
|
||||||
if (res.code === 0) {
|
|
||||||
// 刷新节点列表
|
|
||||||
await loadNodes();
|
|
||||||
|
|
||||||
const result = res.data.details[0];
|
|
||||||
if (result.fixed) {
|
|
||||||
toast.success(`节点状态已修复:${result.message}`);
|
|
||||||
} else {
|
|
||||||
toast.success(result.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(res.msg || '状态检查失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
toast.error('网络错误,请重试');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!validateForm()) return;
|
if (!validateForm()) return;
|
||||||
@@ -578,19 +523,6 @@ export default function NodePage() {
|
|||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h1 className="text-2xl font-bold text-foreground">节点管理</h1>
|
<h1 className="text-2xl font-bold text-foreground">节点管理</h1>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
variant="flat"
|
|
||||||
onPress={handleCheckAllStatus}
|
|
||||||
isLoading={statusCheckLoading}
|
|
||||||
startContent={
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
状态检查
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={handleAdd}
|
onPress={handleAdd}
|
||||||
@@ -773,17 +705,6 @@ export default function NodePage() {
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
|
||||||
<div className="flex gap-1.5">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="flat"
|
|
||||||
color="secondary"
|
|
||||||
onPress={() => handleCheckSingleNodeStatus(node.id)}
|
|
||||||
className="flex-1 min-h-8"
|
|
||||||
>
|
|
||||||
检查
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
@@ -943,106 +864,6 @@ export default function NodePage() {
|
|||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 状态检查结果模态框 */}
|
|
||||||
<Modal
|
|
||||||
isOpen={statusCheckModal}
|
|
||||||
onClose={() => setStatusCheckModal(false)}
|
|
||||||
size="3xl"
|
|
||||||
scrollBehavior="outside"
|
|
||||||
>
|
|
||||||
<ModalContent>
|
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
|
||||||
<span>节点状态检查结果</span>
|
|
||||||
{statusCheckResult && (
|
|
||||||
<div className="flex gap-4 text-sm font-normal">
|
|
||||||
<span className="text-default-600">
|
|
||||||
总节点: <span className="font-semibold">{statusCheckResult.totalNodes}</span>
|
|
||||||
</span>
|
|
||||||
<span className="text-success-600">
|
|
||||||
在线: <span className="font-semibold">{statusCheckResult.connectedNodes}</span>
|
|
||||||
</span>
|
|
||||||
<span className="text-warning-600">
|
|
||||||
修复: <span className="font-semibold">{statusCheckResult.inconsistentNodes}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody>
|
|
||||||
{statusCheckResult && statusCheckResult.details && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{statusCheckResult.details.map((detail: any) => (
|
|
||||||
<Card key={detail.nodeId} className="border border-divider">
|
|
||||||
<CardBody className="py-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-semibold text-foreground">
|
|
||||||
{detail.nodeName || `节点 ${detail.nodeId}`}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-default-500">
|
|
||||||
ID: {detail.nodeId}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Chip
|
|
||||||
size="sm"
|
|
||||||
color={detail.connected ? 'success' : 'danger'}
|
|
||||||
variant="flat"
|
|
||||||
>
|
|
||||||
{detail.connected ? '在线' : '离线'}
|
|
||||||
</Chip>
|
|
||||||
<Chip
|
|
||||||
size="sm"
|
|
||||||
color={detail.fixed ? 'warning' : 'default'}
|
|
||||||
variant={detail.fixed ? 'flat' : 'bordered'}
|
|
||||||
>
|
|
||||||
{detail.fixed ? '已修复' : '正常'}
|
|
||||||
</Chip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-sm">
|
|
||||||
<div className="flex justify-between items-center text-xs">
|
|
||||||
<span className="text-default-600">数据库状态:</span>
|
|
||||||
<span className={`font-mono ${detail.databaseStatus === 1 ? 'text-success-600' : 'text-danger-600'}`}>
|
|
||||||
{detail.databaseStatus === 1 ? '在线' : '离线'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center text-xs mt-1">
|
|
||||||
<span className="text-default-600">实际状态:</span>
|
|
||||||
<span className={`font-mono ${detail.actualStatus ? 'text-success-600' : 'text-danger-600'}`}>
|
|
||||||
{detail.actualStatus ? '连接中' : '未连接'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{detail.fixed && (
|
|
||||||
<div className="flex justify-between items-center text-xs mt-1">
|
|
||||||
<span className="text-default-600">最终状态:</span>
|
|
||||||
<span className={`font-mono ${detail.finalStatus === 1 ? 'text-success-600' : 'text-danger-600'}`}>
|
|
||||||
{detail.finalStatus === 1 ? '在线' : '离线'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="mt-2 p-2 bg-default-50 dark:bg-default-100 rounded text-xs">
|
|
||||||
{detail.message}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
variant="flat"
|
|
||||||
onPress={() => setStatusCheckModal(false)}
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user