mirror of
https://github.com/weiaiweiai/NezhaAgentHTTPBridge.git
synced 2025-12-18 01:16:55 +08:00
Add files via upload
This commit is contained in:
531
UI.html
Normal file
531
UI.html
Normal file
@@ -0,0 +1,531 @@
|
||||
<div id="wp-co-nd-host"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
/* 核心配置项 */
|
||||
const CFG = {
|
||||
api: 'https://127.0.0.1/WeatherForecast/lastws',//请求数据地址
|
||||
poll: 3000,
|
||||
offlineSec: 30,
|
||||
debug: true
|
||||
};
|
||||
|
||||
/* 元素ID定义 */
|
||||
const IDS = {
|
||||
container: 'wp-co-nd-host',
|
||||
wrapper: 'wp-co-nd-wrapper',
|
||||
grid: 'wp-co-nd-grid',
|
||||
status: 'wp-co-nd-status',
|
||||
error: 'wp-co-nd-error',
|
||||
warning: 'wp-co-nd-warning'
|
||||
};
|
||||
|
||||
/* 状态管理变量 */
|
||||
let servers = [];
|
||||
let isLoading = false;
|
||||
|
||||
/* ===================== 工具函数 ===================== */
|
||||
function getSafe(obj, path, defaultValue = null, logMissing = true) {
|
||||
const result = path.split('.').reduce((acc, part) => {
|
||||
if (acc === null || acc === undefined) return defaultValue;
|
||||
return acc[part] !== undefined ? acc[part] : defaultValue;
|
||||
}, obj);
|
||||
if (result === defaultValue && logMissing && CFG.debug) {
|
||||
console.log(`[服务器监控] 缺失字段: ${path}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatPercent(used, total) {
|
||||
if (total <= 0) return "0.0%";
|
||||
const pct = Math.min(100, Math.max(0, (used / total) * 100));
|
||||
return pct.toFixed(1) + "%";
|
||||
}
|
||||
function formatBytesToGB(bytes) {
|
||||
if (bytes === null || isNaN(bytes)) return "未知";
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
return gb >= 1 ? `${gb.toFixed(2)}GB` : `${(gb * 1024).toFixed(1)}MB`;
|
||||
}
|
||||
function formatSpeed(bytes) {
|
||||
if (bytes === null || isNaN(bytes)) return "未知";
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(2)}MB/s`;
|
||||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)}KB/s`;
|
||||
return `${bytes}B/s`;
|
||||
}
|
||||
function formatTraffic(bytes) {
|
||||
if (bytes === null || isNaN(bytes)) return "未知";
|
||||
const gb = bytes / (1024 * 1024 * 1024);
|
||||
return gb >= 1 ? `${gb.toFixed(2)}GB` : `${(gb * 1024).toFixed(1)}MB`;
|
||||
}
|
||||
function formatUptime(seconds) {
|
||||
if (seconds === null || isNaN(seconds)) return "未知";
|
||||
if (seconds < 60) return `${seconds}秒`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`;
|
||||
if (seconds < 86400) return `${(seconds / 3600).toFixed(1)}小时`;
|
||||
return `${(seconds / 86400).toFixed(1)}天`;
|
||||
}
|
||||
function formatCountryCode(code) {
|
||||
if (!code) return "未知地区";
|
||||
const regionMap = { 'hk': '中国香港', 'cn': '中国大陆' };
|
||||
return regionMap[code] || code;
|
||||
}
|
||||
function formatCpuInfo(cpuArr) {
|
||||
return cpuArr && cpuArr.length > 0 ? cpuArr.join(' | ') : "未知CPU信息";
|
||||
}
|
||||
function getStatusColor(cpu) {
|
||||
if (cpu === null || isNaN(cpu)) return "#a0aec0";
|
||||
if (cpu > 85) return "#e53e3e";
|
||||
if (cpu > 70) return "#ed8936";
|
||||
if (cpu > 40) return "#ecc94b";
|
||||
return "#48bb78";
|
||||
}
|
||||
function isServerOnline(server) {
|
||||
const lastActive = getSafe(server, 'last_active');
|
||||
if (!lastActive) return false;
|
||||
const lastActiveTime = new Date(lastActive).getTime();
|
||||
return (Date.now() - lastActiveTime) < CFG.offlineSec * 1000;
|
||||
}
|
||||
|
||||
/* ===================== 样式注入 ===================== */
|
||||
function injectStyles() {
|
||||
const oldStyle = document.getElementById('wp-co-nd-styles');
|
||||
if (oldStyle) oldStyle.remove();
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'wp-co-nd-styles';
|
||||
style.textContent = `
|
||||
#${IDS.wrapper} {
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
padding: 15px !important;
|
||||
font-family: "Microsoft YaHei", Arial, sans-serif !important;
|
||||
max-width: 1400px !important;
|
||||
margin: 0 auto !important;
|
||||
}
|
||||
|
||||
#${IDS.grid} {
|
||||
display: grid !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) !important;
|
||||
gap: 18px !important;
|
||||
margin-top: 18px !important;
|
||||
}
|
||||
|
||||
.wp-co-server-card {
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
border-radius: 10px !important;
|
||||
padding: 18px !important;
|
||||
box-shadow: 0 3px 8px rgba(0,0,0,0.06) !important;
|
||||
transition: box-shadow 0.2s ease !important;
|
||||
}
|
||||
.wp-co-server-card:hover {
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.08) !important;
|
||||
}
|
||||
.wp-co-server-card.offline {
|
||||
opacity: 0.72 !important;
|
||||
filter: grayscale(0.8) !important;
|
||||
}
|
||||
|
||||
.wp-co-status-indicator {
|
||||
display: inline-block !important;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
border-radius: 50% !important;
|
||||
margin-right: 8px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.wp-co-server-header {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.wp-co-server-name {
|
||||
font-weight: 600 !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.wp-co-server-region {
|
||||
font-size: 12px !important;
|
||||
padding: 3px 8px !important;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.wp-co-cpu-container {
|
||||
margin-bottom: 15px !important;
|
||||
}
|
||||
|
||||
.wp-co-cpu-label {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
font-size: 13px !important;
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
|
||||
.wp-co-cpu-bar-wrap {
|
||||
height: 6px !important;
|
||||
border-radius: 3px !important;
|
||||
overflow: hidden !important;
|
||||
border: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.wp-co-cpu-bar {
|
||||
height: 100% !important;
|
||||
transition: width 1s ease !important;
|
||||
}
|
||||
|
||||
.wp-co-section-title {
|
||||
font-size: 14px !important;
|
||||
font-weight: 500 !important;
|
||||
margin: 12px 0 6px 0 !important;
|
||||
padding-left: 4px !important;
|
||||
border-left: 2px solid #4299e1 !important;
|
||||
}
|
||||
|
||||
.wp-co-info-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr 1fr !important;
|
||||
gap: 8px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.wp-co-info-item {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.wp-co-info-label {
|
||||
font-size: 12px !important;
|
||||
margin-bottom: 2px !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
.wp-co-footer-stats {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
margin-top: 12px !important;
|
||||
font-size: 13px !important;
|
||||
flex-wrap: wrap !important;
|
||||
}
|
||||
|
||||
#${IDS.status} {
|
||||
font-size: 13px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 4px !important;
|
||||
}
|
||||
|
||||
#${IDS.error}, #${IDS.warning} {
|
||||
margin-top: 15px !important;
|
||||
padding: 10px 12px !important;
|
||||
border-radius: 6px !important;
|
||||
font-size: 13px !important;
|
||||
display: none !important;
|
||||
align-items: center !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
#${IDS.error} {
|
||||
border: 1px solid #ffe3e3 !important;
|
||||
}
|
||||
#${IDS.warning} {
|
||||
border: 1px solid #ffeaa7 !important;
|
||||
}
|
||||
|
||||
.wp-co-header {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.wp-co-page-title {
|
||||
font-size: 18px !important;
|
||||
font-weight: 600 !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.wp-co-unknown-data {
|
||||
font-style: italic !important;
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/* ===================== 结构创建 ===================== */
|
||||
function createBaseStructure() {
|
||||
const container = document.getElementById(IDS.container);
|
||||
if (!container) {
|
||||
console.error("[服务器监控] 容器元素不存在");
|
||||
return false;
|
||||
}
|
||||
|
||||
container.innerHTML = `
|
||||
<div id="${IDS.wrapper}">
|
||||
<div class="wp-co-header">
|
||||
<div class="wp-co-page-title">
|
||||
<i class="fa fa-server"></i>
|
||||
服务器状态监控
|
||||
</div>
|
||||
<div id="${IDS.status}">
|
||||
<i class="fa fa-refresh fa-spin"></i>
|
||||
加载中...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="${IDS.grid}">
|
||||
<div style="grid-column: 1 / -1; text-align: center; padding: 40px 0; font-size: 14px;">
|
||||
<i class="fa fa-spinner fa-spin" style="font-size: 20px; margin-bottom: 8px;"></i>
|
||||
<p>正在获取服务器数据...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="${IDS.error}">
|
||||
<i class="fa fa-exclamation-circle"></i>
|
||||
<span>获取数据失败,请稍后重试</span>
|
||||
</div>
|
||||
|
||||
<div id="${IDS.warning}">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<span>部分数据加载异常,详情请查看控制台</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ===================== 提示控制函数 ===================== */
|
||||
function showError(show) {
|
||||
const errorEl = document.getElementById(IDS.error);
|
||||
if (errorEl) errorEl.style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
function showWarning(show) {
|
||||
const warningEl = document.getElementById(IDS.warning);
|
||||
if (warningEl) warningEl.style.display = show ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
/* ===================== 数据渲染 ===================== */
|
||||
function renderServers() {
|
||||
const grid = document.getElementById(IDS.grid);
|
||||
const statusEl = document.getElementById(IDS.status);
|
||||
if (!grid || !statusEl) return;
|
||||
|
||||
const now = new Date();
|
||||
statusEl.innerHTML = `
|
||||
<i class="fa fa-clock-o"></i>
|
||||
最后更新: ${now.toLocaleTimeString()}
|
||||
`;
|
||||
|
||||
let hasMissingData = false;
|
||||
|
||||
if (servers.length === 0) {
|
||||
showError(true);
|
||||
showWarning(false);
|
||||
grid.innerHTML = `
|
||||
<div style="grid-column: 1 / -1; text-align: center; padding: 40px 0; font-size: 14px;">
|
||||
<i class="fa fa-folder-open-o" style="font-size: 20px; margin-bottom: 8px;"></i>
|
||||
<p>暂无服务器数据</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
showError(false);
|
||||
if (CFG.debug) console.log(`[服务器监控] 渲染 ${servers.length} 台服务器数据`);
|
||||
|
||||
grid.innerHTML = servers.map((server, index) => {
|
||||
const serverName = getSafe(server, 'name', `未命名服务器 #${index + 1}`);
|
||||
if (CFG.debug) console.log(`[服务器监控] 处理服务器: ${serverName}`);
|
||||
|
||||
const online = isServerOnline(server);
|
||||
const cpuUsed = getSafe(server, 'state.cpu', 0);
|
||||
const cpuUsage = parseFloat(formatPercent(cpuUsed, 100));
|
||||
const memUsed = getSafe(server, 'state.mem_used', 0);
|
||||
const memTotal = getSafe(server, 'host.mem_total', 0);
|
||||
const memUsage = formatPercent(memUsed, memTotal);
|
||||
const diskUsed = getSafe(server, 'state.disk_used', 0);
|
||||
const diskTotalVal = getSafe(server, 'host.disk_total', 0);
|
||||
const diskUsage = formatPercent(diskUsed, diskTotalVal);
|
||||
const statusColor = getStatusColor(cpuUsage);
|
||||
|
||||
const os = getSafe(server, 'host.platform', "未知系统");
|
||||
const cpu = formatCpuInfo(getSafe(server, 'host.cpu', []));
|
||||
const arch = getSafe(server, 'host.arch', "未知架构");
|
||||
const region = formatCountryCode(getSafe(server, 'country_code'));
|
||||
const uptime = formatUptime(getSafe(server, 'state.uptime'));
|
||||
|
||||
const netIn = formatSpeed(getSafe(server, 'state.net_in_speed'));
|
||||
const netOut = formatSpeed(getSafe(server, 'state.net_out_speed'));
|
||||
const trafficIn = formatTraffic(getSafe(server, 'state.net_in_transfer'));
|
||||
const trafficOut = formatTraffic(getSafe(server, 'state.net_out_transfer'));
|
||||
const tcpConn = getSafe(server, 'state.tcp_conn_count', "未知");
|
||||
const processCount = getSafe(server, 'state.process_count', "未知");
|
||||
|
||||
const serverHasMissingData = [
|
||||
os === "未知系统",
|
||||
cpu === "未知CPU信息",
|
||||
arch === "未知架构",
|
||||
tcpConn === "未知",
|
||||
uptime === "未知",
|
||||
processCount === "未知"
|
||||
].some(Boolean);
|
||||
|
||||
if (serverHasMissingData) {
|
||||
hasMissingData = true;
|
||||
if (CFG.debug) console.log(`[服务器监控] 服务器 "${serverName}" 存在缺失数据`);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="wp-co-server-card ${online ? '' : 'offline'}">
|
||||
<div class="wp-co-server-header">
|
||||
<div class="wp-co-server-name">
|
||||
<span class="wp-co-status-indicator" style="background-color: ${statusColor};"></span>
|
||||
${serverName}
|
||||
</div>
|
||||
<div class="wp-co-server-region">${region}</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-co-cpu-container">
|
||||
<div class="wp-co-cpu-label">
|
||||
<span>CPU 使用率</span>
|
||||
<span style="font-weight: 500;">${cpuUsage.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div class="wp-co-cpu-bar-wrap">
|
||||
<div class="wp-co-cpu-bar" style="width: ${cpuUsage}%; background-color: ${statusColor};"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-co-section-title">系统配置</div>
|
||||
<div class="wp-co-info-grid">
|
||||
<div class="wp-co-info-item">
|
||||
<div class="wp-co-info-label">操作系统</div>
|
||||
<div>${os === "未知系统" ? `<span class="wp-co-unknown-data">${os}</span>` : os}</div>
|
||||
</div>
|
||||
<div class="wp-co-info-item">
|
||||
<div class="wp-co-info-label">CPU信息</div>
|
||||
<div>${cpu === "未知CPU信息" ? `<span class="wp-co-unknown-data">${cpu}</span>` : cpu}</div>
|
||||
</div>
|
||||
<div class="wp-co-info-item">
|
||||
<div class="wp-co-info-label">架构</div>
|
||||
<div>${arch === "未知架构" ? `<span class="wp-co-unknown-data">${arch}</span>` : arch}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-co-section-title">实时性能</div>
|
||||
<div class="wp-co-info-grid">
|
||||
<div class="wp-co-info-item">
|
||||
<div class="wp-co-info-label">内存使用率</div>
|
||||
<div>${memUsage}</div>
|
||||
</div>
|
||||
<div class="wp-co-info-item">
|
||||
<div class="wp-co-info-label">磁盘使用率</div>
|
||||
<div>${diskUsage}</div>
|
||||
</div>
|
||||
<div class="wp-co-info-item">
|
||||
<div class="wp-co-info-label">网络上传</div>
|
||||
<div>${netOut} (${trafficOut})</div>
|
||||
</div>
|
||||
<div class="wp-co-info-item">
|
||||
<div class="wp-co-info-label">网络下载</div>
|
||||
<div>${netIn} (${trafficIn})</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wp-co-footer-stats">
|
||||
<div>TCP连接: ${tcpConn === "未知" ? `<span class="wp-co-unknown-data">${tcpConn}</span>` : tcpConn}</div>
|
||||
<div>进程数: ${processCount === "未知" ? `<span class="wp-co-unknown-data">${processCount}</span>` : processCount} | 运行: ${uptime === "未知" ? `<span class="wp-co-unknown-data">${uptime}</span>` : uptime}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
showWarning(hasMissingData);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== 数据请求 ===================== */
|
||||
async function fetchServers() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
|
||||
const statusEl = document.getElementById(IDS.status);
|
||||
let hadError = false;
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = `
|
||||
<i class="fa fa-refresh fa-spin"></i>
|
||||
加载中...
|
||||
`;
|
||||
}
|
||||
|
||||
if (CFG.debug) console.log(`[服务器监控] 开始请求数据: ${CFG.api}`);
|
||||
showError(false);
|
||||
showWarning(false);
|
||||
|
||||
try {
|
||||
const response = await fetch(CFG.api, { cache: 'no-store' });
|
||||
|
||||
if (!response.ok) {
|
||||
hadError = true;
|
||||
console.error(`[服务器监控] 请求失败: HTTP状态码 ${response.status}`);
|
||||
if (servers.length === 0) throw new Error(`HTTP状态错误: ${response.status}`);
|
||||
} else {
|
||||
if (CFG.debug) console.log(`[服务器监控] 请求成功: ${response.status}`);
|
||||
const rawData = await response.text();
|
||||
let parsedData = null;
|
||||
try {
|
||||
parsedData = JSON.parse(rawData);
|
||||
if (CFG.debug) console.log(`[服务器监控] 数据解析成功`);
|
||||
} catch (e) {
|
||||
hadError = true;
|
||||
console.error(`[服务器监控] 数据解析失败:`, e);
|
||||
if (servers.length === 0) throw new Error("数据解析失败");
|
||||
}
|
||||
|
||||
if (parsedData) {
|
||||
let newServers = [];
|
||||
if (parsedData?.state && parsedData?.host) {
|
||||
newServers = [parsedData];
|
||||
} else if (parsedData?.servers) {
|
||||
newServers = parsedData.servers;
|
||||
} else if (parsedData?.list || parsedData?.data) {
|
||||
newServers = parsedData.list || parsedData.data;
|
||||
}
|
||||
|
||||
if (newServers.length > 0) {
|
||||
servers = newServers;
|
||||
hadError = false;
|
||||
if (CFG.debug) console.log(`[服务器监控] 获取到 ${newServers.length} 台服务器数据`);
|
||||
} else {
|
||||
hadError = true;
|
||||
console.log(`[服务器监控] 未获取到有效服务器数据`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[服务器监控] 数据请求错误:`, error);
|
||||
hadError = true;
|
||||
if (servers.length === 0) servers = [];
|
||||
} finally {
|
||||
renderServers();
|
||||
isLoading = false;
|
||||
if (CFG.debug) console.log(`[服务器监控] 数据处理完成`);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== 初始化 ===================== */
|
||||
function init() {
|
||||
injectStyles();
|
||||
if (!createBaseStructure()) {
|
||||
setTimeout(init, 1000);
|
||||
return;
|
||||
}
|
||||
fetchServers();
|
||||
setInterval(fetchServers, CFG.poll);
|
||||
}
|
||||
|
||||
init();
|
||||
})();
|
||||
</script>
|
||||
30
WebApplication4.sln
Normal file
30
WebApplication4.sln
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36414.22
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication4", "WebApplication4\WebApplication4.csproj", "{4414834D-B663-48A6-B4ED-97E81AD8D3FF}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "解决方案项", "解决方案项", "{9FA3D6BD-1EC1-3BA5-80CB-CE02773A58D5}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
UI.html = UI.html
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4414834D-B663-48A6-B4ED-97E81AD8D3FF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E0942CCF-C02D-48D4-8861-7C9F9814D520}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
43
WebApplication4/Controllers/WeatherForecastController.cs
Normal file
43
WebApplication4/Controllers/WeatherForecastController.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using WebApplication4.Services;
|
||||
|
||||
namespace WebApplication4.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
private readonly WebSocketMessageStore _wsStore;
|
||||
|
||||
public WeatherForecastController(
|
||||
ILogger<WeatherForecastController> logger,
|
||||
WebSocketMessageStore wsStore)
|
||||
{
|
||||
_logger = logger;
|
||||
_wsStore = wsStore;
|
||||
}
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB> WebSocket <20><>Ϣ
|
||||
[HttpGet("lastws")]
|
||||
public IActionResult GetLastWebSocketMessage()
|
||||
{
|
||||
var (message, ts) = _wsStore.Get();
|
||||
if (message == null)
|
||||
{
|
||||
return NotFound(new
|
||||
{
|
||||
success = false,
|
||||
message = "<22><>δ<EFBFBD>յ<EFBFBD><D5B5>κ<EFBFBD> WebSocket <20><>Ϣ<EFBFBD><CFA2>"
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
WebApplication4/Program.cs
Normal file
34
WebApplication4/Program.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using WebApplication4.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
builder.Services.AddControllers();
|
||||
|
||||
// ȫ<><C8AB><EFBFBD>ſ<EFBFBD> CORS<52><53><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ս<EFBFBD><D5BD><EFBFBD>
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", policy =>
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod());
|
||||
});
|
||||
|
||||
// WebSocket <20><><EFBFBD><EFBFBD>
|
||||
builder.Services.AddSingleton<WebSocketMessageStore>();
|
||||
builder.Services.AddHostedService<WebSocketClientBackgroundService>();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
// builder.Services.AddSwaggerGen(); // <20><><EFBFBD><EFBFBD><EFBFBD>ٴ<EFBFBD><D9B4><EFBFBD>
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// <20><>Ҫ<EFBFBD><D2AA><EFBFBD><EFBFBD> MapControllers ֮ǰ
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD><D0BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA><EFBFBD>ɷ<EFBFBD><C9B7>ڴ˴<DAB4><CBB4><EFBFBD>
|
||||
// app.UseHttpsRedirection();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
31
WebApplication4/Properties/launchSettings.json
Normal file
31
WebApplication4/Properties/launchSettings.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:28450",
|
||||
"sslPort": 0
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"applicationUrl": "http://localhost:5020",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "weatherforecast",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
WebApplication4/Services/WebSocketClientBackgroundService.cs
Normal file
109
WebApplication4/Services/WebSocketClientBackgroundService.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace WebApplication4.Services
|
||||
{
|
||||
public class WebSocketClientBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<WebSocketClientBackgroundService> _logger;
|
||||
private readonly WebSocketMessageStore _store;
|
||||
// Ŀ<><C4BF> WebSocket <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ
|
||||
private readonly Uri _uri = new("ws://127.0.0.1:8008/api/v1/ws/server");
|
||||
|
||||
// <20>ɵ<EFBFBD><C9B5><EFBFBD><EFBFBD><EFBFBD>
|
||||
private readonly TimeSpan _reconnectDelay = TimeSpan.FromSeconds(5);
|
||||
private const int ReceiveBufferSize = 8 * 1024;
|
||||
|
||||
public WebSocketClientBackgroundService(
|
||||
ILogger<WebSocketClientBackgroundService> logger,
|
||||
WebSocketMessageStore store)
|
||||
{
|
||||
_logger = logger;
|
||||
_store = store;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("WebSocket <20><>̨<EFBFBD><CCA8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, Ŀ<><C4BF>: {Url}", _uri);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
using var client = new ClientWebSocket();
|
||||
try
|
||||
{
|
||||
client.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
|
||||
_logger.LogInformation("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> WebSocket...");
|
||||
await client.ConnectAsync(_uri, stoppingToken);
|
||||
_logger.LogInformation("WebSocket <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {State}", client.State);
|
||||
|
||||
await ReceiveLoopAsync(client, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogInformation("ֹͣ<CDA3><D6B9><EFBFBD><EFBFBD><EFBFBD>յ<EFBFBD><D5B5><EFBFBD><EFBFBD><EFBFBD>ֹ WebSocket ѭ<><D1AD><EFBFBD><EFBFBD>");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "WebSocket <20><><EFBFBD>ӻ<EFBFBD><D3BB><EFBFBD><EFBFBD>շ<EFBFBD><D5B7><EFBFBD><EFBFBD>쳣<EFBFBD><ECB3A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD> {Delay}s <20><><EFBFBD><EFBFBD><EFBFBD>ԡ<EFBFBD>",
|
||||
_reconnectDelay.TotalSeconds);
|
||||
}
|
||||
|
||||
if (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_reconnectDelay, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("WebSocket <20><>̨<EFBFBD><CCA8><EFBFBD><EFBFBD><EFBFBD>ѽ<EFBFBD><D1BD><EFBFBD><EFBFBD><EFBFBD>");
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(ClientWebSocket client, CancellationToken ct)
|
||||
{
|
||||
var buffer = new byte[ReceiveBufferSize];
|
||||
|
||||
while (!ct.IsCancellationRequested && client.State == WebSocketState.Open)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
WebSocketReceiveResult? result;
|
||||
|
||||
do
|
||||
{
|
||||
var segment = new ArraySegment<byte>(buffer);
|
||||
result = await client.ReceiveAsync(segment, ct);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
_logger.LogWarning("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD>: {Desc}", result.CloseStatusDescription);
|
||||
await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", ct);
|
||||
return;
|
||||
}
|
||||
|
||||
ms.Write(buffer, 0, result.Count);
|
||||
}
|
||||
while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
var text = Encoding.UTF8.GetString(ms.ToArray());
|
||||
_store.Set(text);
|
||||
_logger.LogDebug("<22>յ<EFBFBD><D5B5>ı<EFBFBD><C4B1><EFBFBD>Ϣ<EFBFBD><CFA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Len}", text.Length);
|
||||
}
|
||||
else if (result.MessageType == WebSocketMessageType.Binary)
|
||||
{
|
||||
_logger.LogDebug("<22><><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><CFA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Len}", ms.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
WebApplication4/Services/WebSocketMessageStore.cs
Normal file
30
WebApplication4/Services/WebSocketMessageStore.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace WebApplication4.Services
|
||||
{
|
||||
public class WebSocketMessageStore
|
||||
{
|
||||
private string? _lastMessage;
|
||||
private DateTime? _receivedAtUtc;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public void Set(string message)
|
||||
{
|
||||
if (message == null) return;
|
||||
lock (_lock)
|
||||
{
|
||||
_lastMessage = message;
|
||||
_receivedAtUtc = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
public (string? Message, DateTime? ReceivedAtUtc) Get()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return (_lastMessage, _receivedAtUtc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
WebApplication4/WeatherForecast.cs
Normal file
13
WebApplication4/WeatherForecast.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace WebApplication4
|
||||
{
|
||||
public class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
|
||||
public int TemperatureC { get; set; }
|
||||
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
|
||||
public string? Summary { get; set; }
|
||||
}
|
||||
}
|
||||
9
WebApplication4/WebApplication4.csproj
Normal file
9
WebApplication4/WebApplication4.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
8
WebApplication4/appsettings.Development.json
Normal file
8
WebApplication4/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
WebApplication4/appsettings.json
Normal file
9
WebApplication4/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
Reference in New Issue
Block a user