支持容器化及性能优化,改进证书与UI

增加对 Docker 的支持,优化了 `docker-compose` 和 `Dockerfile` 配置,支持多阶段构建。改进了 QPS 计算逻辑,采用无锁滑动窗口机制,提升性能并添加历史记录功能。增强了证书管理,支持缓存和失效逻辑,避免重复加载。更新了仪表盘 UI,替换折线图为柱状图,优化布局和交互。删除冗余代码,提升代码可读性和维护性。
This commit is contained in:
token
2025-11-15 01:34:24 +08:00
parent cc1586c833
commit 42ed5deb17
13 changed files with 412 additions and 294 deletions
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
<PropertyGroup Label="Globals">
<ProjectVersion>2.1</ProjectVersion>
<DockerTargetOS>Linux</DockerTargetOS>
</PropertyGroup>
<ItemGroup>
<None Include="docker-compose.override.yml">
<DependentUpon>docker-compose.yml</DependentUpon>
</None>
<None Include="docker-compose.yml" />
<None Include=".dockerignore" />
</ItemGroup>
</Project>
+11 -10
View File
@@ -1,4 +1,7 @@
FROM mcr.microsoft.com/dotnet/nightly/aspnet:10.0-preview AS base
# 请参阅 https://aka.ms/customizecontainer 以了解如何自定义调试容器,以及 Visual Studio 如何使用此 Dockerfile 生成映像以更快地进行调试。
# 此阶段用于在快速模式(默认为调试配置)下从 VS 运行时
FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS base
USER root
RUN ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone
RUN apt update \
@@ -11,30 +14,28 @@ RUN apt update \
&& apt-get purge -y --auto-remove wget && apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM node:alpine AS builder
WORKDIR /src
COPY web .
RUN npm i
RUN npm run build
FROM mcr.microsoft.com/dotnet/nightly/sdk:10.0-preview AS build
# 此阶段用于生成服务项目
FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Directory.Build.props", "."]
COPY ["src/FastGateway/FastGateway.csproj", "src/FastGateway/"]
COPY ["src/Core/Core.csproj", "src/Core/"]
RUN dotnet restore "src/FastGateway/FastGateway.csproj"
RUN dotnet restore "./src/FastGateway/FastGateway.csproj"
COPY . .
WORKDIR "/src/src/FastGateway"
RUN dotnet build "./FastGateway.csproj" -c $BUILD_CONFIGURATION -o /app/build
# 此阶段用于发布要复制到最终阶段的服务项目
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./FastGateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
# 此阶段在生产中使用,或在常规模式下从 VS 运行时使用(在不使用调试配置时为默认值)
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
COPY --from=builder /src/dist ./wwwroot
ENTRYPOINT ["dotnet", "FastGateway.dll"]
ENTRYPOINT ["dotnet", "FastGateway.dll"]
+9 -14
View File
@@ -11,16 +11,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0"/>
<PackageReference Include="Certes" Version="3.0.4"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0"/>
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="Certes" Version="3.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="10.0.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj"/>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
<ItemGroup>
@@ -33,13 +34,7 @@
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\"/>
</ItemGroup>
<ItemGroup>
<Content Include="..\..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>
@@ -0,0 +1,25 @@
using System.Diagnostics;
using System.Net;
using Yarp.ReverseProxy.Forwarder;
namespace FastGateway.Gateway
{
public class FastGatewayForwarderHttpClientFactory : IForwarderHttpClientFactory
{
public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context)
{
var handler = new SocketsHttpHandler
{
UseProxy = false,
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None | DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
UseCookies = false,
EnableMultipleHttp2Connections = true,
ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current),
ConnectTimeout = TimeSpan.FromSeconds(600),
};
return new HttpMessageInvoker(handler, disposeHandler: true);
}
}
}
+111 -87
View File
@@ -35,6 +35,25 @@ public static class Gateway
/// </summary>
private static readonly DefaultContentTypeProvider DefaultContentTypeProvider = new();
/// <summary>
/// 证书缓存(按 SNI 名称及默认证书缓存 X509Certificate2 实例)
/// </summary>
private static readonly ConcurrentDictionary<string, X509Certificate2> CertificateCache = new();
/// <summary>
/// 证书缓存失效(根据域名)
/// </summary>
/// <param name="domain">SNI 域名</param>
public static void InvalidateCertificate(string domain)
{
if (string.IsNullOrWhiteSpace(domain)) return;
var key = $"sni:{domain.Trim().ToLowerInvariant()}";
if (CertificateCache.TryRemove(key, out var cert))
{
try { cert.Dispose(); } catch { /* ignore */ }
}
}
/// <summary>
/// 是否存在80端口服务
/// </summary>
@@ -98,6 +117,9 @@ public static class Gateway
if (GatewayWebApplications.TryRemove(serverId, out var webApplication))
{
await webApplication.StopAsync();
await webApplication.DisposeAsync();
return true;
}
@@ -105,24 +127,50 @@ public static class Gateway
}
/// <summary>
/// 找到证书
/// 找到证书(带缓存,避免每次握手都创建新实例)
/// </summary>
/// <param name="context"></param>
/// <param name="name"></param>
/// <param name="name">SNI 主机名</param>
/// <returns></returns>
private static X509Certificate2 ServerCertificateSelector(ConnectionContext? context, string name)
{
// 规范化缓存键(可能为空)
var sni = string.IsNullOrWhiteSpace(name) ? "__default__" : name.Trim().ToLowerInvariant();
try
{
var cert = CertService.GetCert(name);
if (cert != null) return new X509Certificate2(cert.Certs.File, cert.Certs.Password);
// 优先按 SNI 查找业务证书
var certInfo = CertService.GetCert(name);
if (certInfo != null)
{
return CertificateCache.GetOrAdd($"sni:{sni}", _ =>
new X509Certificate2(
certInfo.Certs.File,
certInfo.Certs.Password,
X509KeyStorageFlags.EphemeralKeySet));
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
return new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "gateway.pfx"), "010426");
// 回退到默认证书
var defaultKey = "default";
try
{
return CertificateCache.GetOrAdd(defaultKey, _ =>
new X509Certificate2(
Path.Combine(AppContext.BaseDirectory, "gateway.pfx"),
"010426",
X509KeyStorageFlags.EphemeralKeySet));
}
catch (Exception ex)
{
Console.WriteLine(ex);
// 发生异常时,尽量最后再尝试无 flags 的构造,保持与旧实现兼容
return new X509Certificate2(Path.Combine(AppContext.BaseDirectory, "gateway.pfx"), "010426");
}
}
/// <summary>
@@ -179,11 +227,6 @@ public static class Gateway
options.AllowedOrigins.Add("*");
});
builder.Services.AddRequestTimeouts(options =>
{
});
builder.Services
.AddCors(options =>
{
@@ -208,14 +251,65 @@ public static class Gateway
builder.Services
.AddReverseProxy()
.LoadFromMemory(routes, clusters)
.AddGateway(server);
.AddTransforms(context =>
{
var prefix = context.Route.Match.Path?
.Replace("/{**catch-all}", "")
.Replace("{**catch-all}", "");
// 如果存在泛域名则需要保留原始Host
if (context.Route.Match.Hosts?.Any(x => x.Contains('*')) == true)
context.AddOriginalHost();
else if (server.CopyRequestHost) context.AddOriginalHost();
if (!string.IsNullOrEmpty(prefix)) context.AddPathRemovePrefix(prefix);
#region 静态站点
if (context.Route.Metadata!.TryGetValue(Root, out var root))
{
var tryFiles = context.Cluster!.Metadata!.Select(p => p.Key).ToArray();
context.AddRequestTransform(async transformContext =>
{
var response = transformContext.HttpContext.Response;
var path = Path.Combine(root, transformContext.Path.Value![1..]);
if (File.Exists(path))
{
DefaultContentTypeProvider.TryGetContentType(path, out var contentType);
response.Headers.ContentType = contentType;
await response.SendFileAsync(path);
return;
}
// 搜索 try_files
foreach (var tryFile in tryFiles)
{
var tryPath = Path.Combine(root, tryFile);
if (!File.Exists(tryPath)) continue;
DefaultContentTypeProvider.TryGetContentType(tryPath, out var contentType);
response.Headers.ContentType = contentType;
await response.SendFileAsync(tryPath);
return;
}
response.StatusCode = 404;
});
}
#endregion
}); // 删除 ConfigureHttpClient 避免与自定义工厂冲突
var app = builder.Build();
app.UseCors("AllowAll");
app.UseWebSockets();
app.UseRequestTimeouts();
if (server.StaticCompress)
app.UseResponseCompression();
@@ -292,86 +386,18 @@ public static class Gateway
app.Lifetime.ApplicationStopping.Register(() => { GatewayWebApplications.Remove(server.Id, out _); });
await app.RunAsync();
}catch (Exception e)
}
catch (Exception e)
{
Console.WriteLine("网关启动错误:"+e);
Console.WriteLine("网关启动错误:" + e);
throw;
}
}
finally
{
GatewayWebApplications.Remove(server.Id, out _);
}
}
private static IReverseProxyBuilder AddGateway(this IReverseProxyBuilder builder, Server server)
{
builder.AddTransforms(context =>
{
var prefix = context.Route.Match.Path?
.Replace("/{**catch-all}", "")
.Replace("{**catch-all}", "");
// 如果存在泛域名则需要保留原始Host
if (context.Route.Match.Hosts?.Any(x => x.Contains('*')) == true)
context.AddOriginalHost();
else if (server.CopyRequestHost) context.AddOriginalHost();
if (!string.IsNullOrEmpty(prefix)) context.AddPathRemovePrefix(prefix);
#region 静态站点
if (context.Route.Metadata!.TryGetValue(Root, out var root))
{
var tryFiles = context.Cluster!.Metadata!.Select(p => p.Key).ToArray();
context.AddRequestTransform(async transformContext =>
{
var response = transformContext.HttpContext.Response;
var path = Path.Combine(root, transformContext.Path.Value![1..]);
if (File.Exists(path))
{
DefaultContentTypeProvider.TryGetContentType(path, out var contentType);
response.Headers.ContentType = contentType;
await response.SendFileAsync(path);
return;
}
// 搜索 try_files
foreach (var tryFile in tryFiles)
{
var tryPath = Path.Combine(root, tryFile);
if (!File.Exists(tryPath)) continue;
DefaultContentTypeProvider.TryGetContentType(tryPath, out var contentType);
response.Headers.ContentType = contentType;
await response.SendFileAsync(tryPath);
return;
}
response.StatusCode = 404;
});
}
#endregion
});
builder.ConfigureHttpClient((client, socket) =>
{
socket.ConnectTimeout = TimeSpan.FromSeconds(server.Timeout);
socket.Expect100ContinueTimeout = TimeSpan.FromSeconds(server.Timeout);
socket.ResponseDrainTimeout = TimeSpan.FromSeconds(server.Timeout);
socket.ActivityHeadersPropagator = new ReverseProxyPropagator(DistributedContextPropagator.Current);
socket.AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.GZip | DecompressionMethods.Deflate;
socket.EnableMultipleHttp2Connections = true;
});
return builder;
}
private static WebApplication UseInitGatewayMiddleware(this WebApplication app)
{
app.Use(async (context, next) =>
@@ -402,9 +428,7 @@ public static class Gateway
foreach (var domainName in domainNames)
{
var path = domainName.Path ?? string.Empty;
if (string.IsNullOrWhiteSpace(path))
path = "/{**catch-all}";
else if (path == "/")
if (string.IsNullOrWhiteSpace(path) || path == "/")
path = "/{**catch-all}";
else
path = $"/{path.TrimStart('/')}/{{**catch-all}}";
@@ -27,6 +27,16 @@
"ASPNETCORE_URLS": "http://localhost:5202"
},
"distributionName": ""
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast",
"environmentVariables": {
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true,
"useSSL": false
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
+6
View File
@@ -91,6 +91,8 @@ public static class CertService
{
configService.UpdateCert(cert);
InitCert(configService.GetCerts().Where(x => !x.Expired).ToArray());
// 清除网关证书缓存以便新证书生效
Gateway.Gateway.InvalidateCertificate(cert.Domain);
}
return ResultDto.CreateSuccess();
@@ -219,6 +221,8 @@ public static class CertService
{
configService.DeleteCert(id);
CertWebApplications.TryRemove(certItem.Domain, out _);
// 删除证书后移除网关缓存
Gateway.Gateway.InvalidateCertificate(certItem.Domain);
}
}).WithDescription("删除证书").WithDisplayName("删除证书").WithTags("证书");
@@ -231,6 +235,8 @@ public static class CertService
cert.Id = id;
configService.UpdateCert(cert);
// 更新证书后移除网关缓存
Gateway.Gateway.InvalidateCertificate(cert.Domain);
}).WithDescription("更新证书").WithDisplayName("更新证书").WithTags("证书");
+91 -77
View File
@@ -8,18 +8,61 @@ namespace FastGateway.Services;
public sealed class QpsItem
{
private int _lastQps;
private int _lastQps; // last calculated 3s average QPS
private long _totalRequests;
private long _successRequests;
private long _failedRequests;
private readonly ConcurrentQueue<long> _responseTimes = new();
private readonly object _lockObject = new();
// 3s window tracking (lock-free)
private long _windowRequestCount; // requests accumulated in current window
private long _windowStartTicks = Stopwatch.GetTimestamp();
// 缓存最近 50 次 QPS 数据
private readonly ConcurrentQueue<QpsHistoryItem> _qpsHistory = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IncrementRequests()
{
Interlocked.Increment(ref _lastQps);
Interlocked.Increment(ref _totalRequests);
Interlocked.Increment(ref _windowRequestCount);
TryCalculateWindowQps();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void TryCalculateWindowQps()
{
// Read start ticks
var start = Volatile.Read(ref _windowStartTicks);
double elapsedSeconds = (Stopwatch.GetTimestamp() - start) / (double)Stopwatch.Frequency;
if (elapsedSeconds < 3.0) return;
// Re-evaluate with fresh timestamp
long nowTicks = Stopwatch.GetTimestamp();
start = Volatile.Read(ref _windowStartTicks);
elapsedSeconds = (nowTicks - start) / (double)Stopwatch.Frequency;
if (elapsedSeconds < 3.0) return;
// Attempt to rotate window atomically
if (Interlocked.CompareExchange(ref _windowStartTicks, nowTicks, start) == start)
{
long windowCount = Interlocked.Exchange(ref _windowRequestCount, 0);
var calculatedQps = elapsedSeconds > 0 ? (int)(windowCount / elapsedSeconds) : 0;
_lastQps = calculatedQps;
// 添加到历史记录
_qpsHistory.Enqueue(new QpsHistoryItem
{
Time = DateTime.Now.ToString("HH:mm:ss"),
Qps = calculatedQps
});
// 保持最近 50 条记录
while (_qpsHistory.Count > 50)
{
_qpsHistory.TryDequeue(out _);
}
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -38,23 +81,25 @@ public sealed class QpsItem
public void RecordResponseTime(long milliseconds)
{
_responseTimes.Enqueue(milliseconds);
// 保持队列大小不超过1000
// Keep queue size <= 1000
while (_responseTimes.Count > 1000)
{
_responseTimes.TryDequeue(out _);
}
}
// Manual trigger (e.g., timer) to ensure QPS updates even during idle periods
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CalculateQps()
{
Interlocked.Exchange(ref _lastQps, 0);
TryCalculateWindowQps();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetQps()
{
// Ensure we refresh if window elapsed without new requests
TryCalculateWindowQps();
return _lastQps;
}
@@ -72,7 +117,6 @@ public sealed class QpsItem
}
Array.Sort(times);
var avg = (long)times.Average();
var min = times[0];
var max = times[^1];
@@ -83,27 +127,37 @@ public sealed class QpsItem
return new ResponseTimeStats(avg, p95, p99, min, max);
}
public QpsHistoryItem[] GetQpsHistory()
{
return _qpsHistory.ToArray();
}
}
public record QpsHistoryItem
{
public string Time { get; init; } = string.Empty;
public int Qps { get; init; }
}
public record ResponseTimeStats(long Avg, long P95, long P99, long Min, long Max);
public static class SystemMonitorService
{
private static readonly Lazy<PerformanceCounter> _cpuCounter = new(() =>
private static readonly Lazy<PerformanceCounter> _cpuCounter = new(() =>
new PerformanceCounter("Processor", "% Processor Time", "_Total"));
private static readonly Lazy<PerformanceCounter> _memoryCounter = new(() =>
private static readonly Lazy<PerformanceCounter> _memoryCounter = new(() =>
new PerformanceCounter("Memory", "Available MBytes"));
private static readonly Lazy<PerformanceCounter> _diskReadCounter = new(() =>
private static readonly Lazy<PerformanceCounter> _diskReadCounter = new(() =>
new PerformanceCounter("PhysicalDisk", "Disk Read Bytes/sec", "_Total"));
private static readonly Lazy<PerformanceCounter> _diskWriteCounter = new(() =>
private static readonly Lazy<PerformanceCounter> _diskWriteCounter = new(() =>
new PerformanceCounter("PhysicalDisk", "Disk Write Bytes/sec", "_Total"));
private static bool _performanceCountersAvailable = true;
private static bool _diskCountersAvailable = true;
static SystemMonitorService()
{
// 初始化时读取一次以确保计数器准备就绪
try
{
if (_performanceCountersAvailable)
@@ -116,7 +170,7 @@ public static class SystemMonitorService
{
_performanceCountersAvailable = false;
}
try
{
if (_diskCountersAvailable)
@@ -139,7 +193,7 @@ public static class SystemMonitorService
float availableMemoryMB = 0;
long diskReadBytesPerSec = 0;
long diskWriteBytesPerSec = 0;
if (_performanceCountersAvailable)
{
try
@@ -152,7 +206,7 @@ public static class SystemMonitorService
_performanceCountersAvailable = false;
}
}
if (_diskCountersAvailable)
{
try
@@ -165,20 +219,18 @@ public static class SystemMonitorService
_diskCountersAvailable = false;
}
}
var totalMemoryBytes = GC.GetTotalMemory(false);
var workingSetBytes = Environment.WorkingSet;
// 如果性能计数器不可用,使用近似计算
if (!_performanceCountersAvailable)
{
// 使用进程工作集作为已用内存的粗略估计
var totalSystemMemoryBytes = workingSetBytes * 4; // 粗略估计系统总内存
var totalSystemMemoryBytes = workingSetBytes * 4;
var usedMemoryBytes = workingSetBytes;
var memoryUsagePercent = (double)usedMemoryBytes / totalSystemMemoryBytes * 100;
return new SystemStats(
CpuUsage: 0, // 无法获取CPU使用率
CpuUsage: 0,
MemoryUsagePercent: Math.Min(memoryUsagePercent, 100),
TotalMemoryBytes: totalSystemMemoryBytes,
UsedMemoryBytes: usedMemoryBytes,
@@ -190,8 +242,7 @@ public static class SystemMonitorService
DiskWriteBytesPerSec: diskWriteBytesPerSec
);
}
// 使用性能计数器的准确数据
var totalSystemMemoryBytesAccurate = (long)(availableMemoryMB * 1024 * 1024) + workingSetBytes;
var usedMemoryBytesAccurate = totalSystemMemoryBytesAccurate - (long)(availableMemoryMB * 1024 * 1024);
var memoryUsagePercentAccurate = (double)usedMemoryBytesAccurate / totalSystemMemoryBytesAccurate * 100;
@@ -211,10 +262,9 @@ public static class SystemMonitorService
}
catch
{
// 如果获取系统信息失败,返回基本信息
var workingSetBytes = Environment.WorkingSet;
var gcMemoryBytes = GC.GetTotalMemory(false);
return new SystemStats(
CpuUsage: 0,
MemoryUsagePercent: 0,
@@ -233,7 +283,7 @@ public static class SystemMonitorService
public record SystemStats(
float CpuUsage,
double MemoryUsagePercent,
double MemoryUsagePercent,
long TotalMemoryBytes,
long UsedMemoryBytes,
long AvailableMemoryBytes,
@@ -247,20 +297,17 @@ public record SystemStats(
public static class QpsService
{
private static readonly QpsItem QpsItem = new();
private static bool _enable;
private static readonly DateTime _startTime = DateTime.UtcNow;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void IncrementServiceRequests()
{
if (!_enable) return;
QpsItem.IncrementRequests();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RecordSuccessRequest(long responseTimeMs)
{
if (!_enable) return;
QpsItem.IncrementSuccessRequests();
QpsItem.RecordResponseTime(responseTimeMs);
}
@@ -268,7 +315,6 @@ public static class QpsService
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void RecordFailedRequest(long responseTimeMs = 0)
{
if (!_enable) return;
QpsItem.IncrementFailedRequests();
if (responseTimeMs > 0)
{
@@ -278,15 +324,12 @@ public static class QpsService
public static void CalculateServiceQps()
{
if (!_enable) return;
QpsItem.CalculateQps();
}
public static int GetServiceQps()
{
var qps = QpsItem.GetQps();
QpsItem.CalculateQps();
return qps;
return QpsItem.GetQps();
}
public static (long total, long success, long failed) GetRequestCounts()
@@ -299,18 +342,15 @@ public static class QpsService
return QpsItem.GetResponseTimeStats();
}
public static QpsHistoryItem[] GetQpsHistory()
{
return QpsItem.GetQpsHistory();
}
public static TimeSpan GetUptime()
{
return DateTime.UtcNow - _startTime;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnableQps(bool enable)
{
_enable = enable;
}
public static bool IsEnabled => _enable;
}
public static class ApiQpsService
@@ -333,18 +373,12 @@ public static class ApiQpsService
public static async Task GetAsync(HttpContext context)
{
QpsService.EnableQps(true);
try
{
// 获取所有网络接口
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
// 存储每个接口的初始值
long initialBytesSent = 0;
long initialBytesReceived = 0;
// 只考虑活动的和支持IPv4的网络接口
foreach (var ni in networkInterfaces)
{
if (ni.OperationalStatus != OperationalStatus.Up ||
@@ -355,19 +389,14 @@ public static class ApiQpsService
initialBytesSent += interfaceStats.BytesSent;
initialBytesReceived += interfaceStats.BytesReceived;
}
catch
{
// 忽略获取统计信息失败的接口
}
catch { }
}
await Task.Delay(1000);
// 存储每个接口1秒后的值
long bytesSentAfter1Sec = 0;
long bytesReceivedAfter1Sec = 0;
// 再次遍历网络接口
foreach (var ni in networkInterfaces)
{
if (ni.OperationalStatus != OperationalStatus.Up ||
@@ -378,31 +407,25 @@ public static class ApiQpsService
bytesSentAfter1Sec += interfaceStats.BytesSent;
bytesReceivedAfter1Sec += interfaceStats.BytesReceived;
}
catch
{
// 忽略获取统计信息失败的接口
}
catch { }
}
// 计算1秒内发送和接收的总字节
var upload = Math.Max(0, bytesSentAfter1Sec - initialBytesSent);
var download = Math.Max(0, bytesReceivedAfter1Sec - initialBytesReceived);
// 获取系统统计信息
var systemStats = SystemMonitorService.GetSystemStats();
var requestCounts = QpsService.GetRequestCounts();
var responseTimeStats = QpsService.GetResponseTimeStats();
var qpsHistory = QpsService.GetQpsHistory();
var uptime = QpsService.GetUptime();
var data = new
{
// 基础QPS数据
qps = QpsService.GetServiceQps(),
qps = QpsService.GetServiceQps(), // 3s average QPS
qpsHistory = qpsHistory.Select(h => new { time = h.Time, qps = h.Qps }).ToArray(),
now = DateTime.Now.ToString("HH:mm:ss"),
upload,
download,
// 请求统计
requests = new
{
total = requestCounts.total,
@@ -410,8 +433,6 @@ public static class ApiQpsService
failed = requestCounts.failed,
successRate = requestCounts.total > 0 ? Math.Round((double)requestCounts.success / requestCounts.total * 100, 2) : 0.0
},
// 响应时间统计
responseTime = new
{
avg = responseTimeStats.Avg,
@@ -420,8 +441,6 @@ public static class ApiQpsService
min = responseTimeStats.Min,
max = responseTimeStats.Max
},
// 系统资源
system = new
{
cpu = new
@@ -444,8 +463,6 @@ public static class ApiQpsService
writeBytesPerSec = systemStats.DiskWriteBytesPerSec
}
},
// 服务状态
service = new
{
isOnline = true,
@@ -463,9 +480,6 @@ public static class ApiQpsService
await context.Response.WriteAsJsonAsync(data);
}
finally
{
QpsService.EnableQps(false);
}
finally { }
}
}
+10 -3
View File
@@ -44,13 +44,20 @@ internal class TunnelClientFactory(
{
handler.SslOptions.RemoteCertificateValidationCallback =
(sender, certificate, chain, errors) => true;
handler.MaxConnectionsPerServer = 100;
handler.PooledConnectionLifetime = TimeSpan.FromMinutes(5);
handler.PooledConnectionIdleTimeout = TimeSpan.FromMinutes(1);
handler.KeepAlivePingDelay = TimeSpan.FromSeconds(30);
handler.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
handler.KeepAlivePingDelay = TimeSpan.FromMinutes(5);
handler.KeepAlivePingTimeout = TimeSpan.FromMinutes(5);
handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.Always;
// 新增:从 Gateway.ConfigureHttpClient 移植的参数
handler.ConnectTimeout = TimeSpan.FromMinutes(5);
handler.ResponseDrainTimeout = TimeSpan.FromMinutes(5);
handler.EnableMultipleHttp2Connections = true;
handler.EnableMultipleHttp3Connections = true;
base.ConfigureHandler(httpClientContext, handler);
var previous = handler.ConnectCallback ?? DefaultConnectCallback;
+2 -2
View File
@@ -23,8 +23,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0-preview.7.25380.108" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0"/>
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0" />
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
</ItemGroup>
<ItemGroup>
+12 -5
View File
@@ -15,6 +15,8 @@ interface BarChartProps {
showXAxis?: boolean
showYAxis?: boolean
barRadius?: number
barSize?: number
barGap?: number | string
}
const defaultColors = [
@@ -36,6 +38,8 @@ export function BarChart({
showXAxis = false,
showYAxis = true,
barRadius = 3,
barSize,
barGap = 4,
}: BarChartProps) {
if (!data || data.length === 0) {
return (
@@ -47,15 +51,17 @@ export function BarChart({
return (
<div className={cn("w-full", className)}>
<ChartContainer config={chartConfig} className="h-[200px]">
<ChartContainer config={chartConfig} className="h-full w-full">
<RechartsBarChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
top: 5,
right: 5,
left: showYAxis ? 0 : 5,
bottom: 5,
}}
barGap={barGap}
barCategoryGap="10%"
>
{showGrid && (
<CartesianGrid
@@ -98,6 +104,7 @@ export function BarChart({
fill={colors[index % colors.length]}
radius={[barRadius, barRadius, barRadius, barRadius]}
minPointSize={5}
maxBarSize={barSize}
/>
))}
</RechartsBarChart>
+111 -85
View File
@@ -2,7 +2,6 @@ import { useCallback, useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ProgressBar } from '@/components/ui/progress-bar';
import { LineChart } from '@/components/ui/line-chart';
import { BarChart } from '@/components/ui/bar-chart';
import { TooltipProvider } from '@/components/ui/tooltip';
import { bytesToSize } from '@/utils/byte';
@@ -30,7 +29,18 @@ const Dashboard = () => {
const [network, setNetwork] = useState({ upload: 0, download: 0 });
const [networkHistory, setNetworkHistory] = useState<
Array<{ time: string; upload: number; download: number; total: number }>
>([]);
>(() => {
const now = new Date();
return Array.from({ length: 50 }, (_, i) => {
const time = new Date(now.getTime() - (49 - i) * 3000);
return {
time: time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
upload: 0,
download: 0,
total: 0,
};
});
});
const [data, setData] = useState({
total: 0,
success: 0,
@@ -57,13 +67,27 @@ const Dashboard = () => {
cores: 0,
temperature: 0,
});
const [diskChart, setDiskChart] = useState<Array<{ date: string; read: number; write: number }>>([]);
const [qpsChart, setQpsChart] = useState<Array<{ time: string; QPS: number }>>(() =>
Array.from({ length: 30 }, () => ({
time: '00:00',
QPS: 0,
}))
);
const [diskChart, setDiskChart] = useState<Array<{ date: string; read: number; write: number }>>(() => {
const now = new Date();
return Array.from({ length: 25 }, (_, i) => {
const time = new Date(now.getTime() - (24 - i) * 3000);
return {
date: time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
read: 0,
write: 0,
};
});
});
const [qpsChart, setQpsChart] = useState<Array<{ time: string; QPS: number }>>(() => {
const now = new Date();
return Array.from({ length: 50 }, (_, i) => {
const time = new Date(now.getTime() - (49 - i) * 3000);
return {
time: time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
QPS: 0,
};
});
});
const [isOnline, setIsOnline] = useState(true);
const [lastUpdate, setLastUpdate] = useState(new Date());
const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1200);
@@ -96,15 +120,24 @@ const Dashboard = () => {
const currentTime = fetchedData.now;
const currentQps = fetchedData.qps;
setQpsChart(prev => {
const newData = [...prev];
newData.shift();
newData.push({
time: currentTime,
QPS: currentQps,
// 如果后端返回了历史数据(首次加载),使用历史数据初始化
if (fetchedData.qpsHistory && fetchedData.qpsHistory.length > 0) {
setQpsChart(fetchedData.qpsHistory.map((item: any) => ({
time: item.time,
QPS: item.qps
})));
} else {
// 后续更新,添加新数据点
setQpsChart(prev => {
const newData = [...prev];
newData.shift();
newData.push({
time: currentTime,
QPS: currentQps,
});
return newData;
});
return newData;
});
}
setQps(currentQps);
calculateQpsMetrics(currentQps);
@@ -124,7 +157,7 @@ const Dashboard = () => {
download: newNetwork.download,
total: newNetwork.upload + newNetwork.download,
},
].slice(-30);
].slice(-50);
return newData;
});
@@ -169,7 +202,7 @@ const Dashboard = () => {
read: fetchedData.system.disk.readBytesPerSec,
write: fetchedData.system.disk.writeBytesPerSec,
},
].slice(-80);
].slice(-25);
return newData;
});
}
@@ -286,7 +319,7 @@ const Dashboard = () => {
</span>
<Badge variant={qpsBadgeVariant}>{qpsBadgeLabel}</Badge>
</CardTitle>
<CardDescription className="text-[0.85rem] text-muted-foreground">最近 30s 滑动窗口</CardDescription>
<CardDescription className="text-[0.85rem] text-muted-foreground">最近 3s 滑动窗口</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<p className="text-3xl font-semibold text-yellow-600">{qps.toLocaleString()}</p>
@@ -321,23 +354,28 @@ const Dashboard = () => {
</Card>
<Card className="space-y-3 border border-border">
<CardHeader className="space-y-1">
<CardTitle className="flex items-center gap-2 text-sm font-medium">
<CheckCircle className="h-4 w-4 text-blue-500" />
成功率
<CardHeader>
<CardTitle className="text-lg font-semibold flex items-center gap-2">
<Server className="h-4 w-4 text-blue-500" />
请求概况
</CardTitle>
<CardDescription className="text-[0.85rem] text-muted-foreground">请求成功/失败的比例</CardDescription>
<CardDescription>全量请求与今日增量</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-baseline gap-2">
<p className="text-3xl font-semibold text-blue-600">{successRate.toFixed(1)}%</p>
<p className="text-xs text-muted-foreground">成功 {data.success.toLocaleString()} / 失败 {data.fail.toLocaleString()}</p>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
{requestStats.map(stat => (
<div key={stat.label} className="space-y-1 text-center">
<div className={`mx-auto flex h-8 w-8 items-center justify-center rounded-full bg-muted text-muted-foreground`}>
<stat.icon className="h-4 w-4" />
</div>
<p className="text-sm font-medium text-muted-foreground">{stat.label}</p>
<p className={`text-lg font-semibold ${stat.tone}`}>{stat.label === '成功率' ? `${stat.value}%` : stat.value.toLocaleString()}</p>
{stat.change !== undefined && stat.label !== '成功率' && (
<p className="text-xs text-muted-foreground">+{stat.change.toLocaleString()} 今日</p>
)}
</div>
))}
</div>
<ProgressBar
value={successRate}
className="h-3"
color={successRate > 90 ? 'green' : successRate > 70 ? 'yellow' : 'red'}
/>
</CardContent>
</Card>
@@ -374,10 +412,10 @@ const Dashboard = () => {
<Activity className="h-5 w-5 text-yellow-500" />
QPS 趋势
</CardTitle>
<CardDescription>滚动的 30 次请求曲线</CardDescription>
<CardDescription>滚动的 50 次请求曲线</CardDescription>
</CardHeader>
<CardContent className="h-[280px]">
<LineChart
<BarChart
key={`qps-chart-${windowWidth}`}
data={qpsChart}
categories={['QPS']}
@@ -385,6 +423,7 @@ const Dashboard = () => {
valueFormatter={(value: number) => `${value} req/s`}
index="time"
showXAxis={false}
showYAxis={false}
className="h-full w-full"
/>
</CardContent>
@@ -399,21 +438,17 @@ const Dashboard = () => {
<CardDescription>上/下行速度</CardDescription>
</CardHeader>
<CardContent className="h-[280px]">
{networkHistory.length ? (
<LineChart
key={`network-chart-${windowWidth}`}
data={networkHistory}
categories={['upload', 'download']}
colors={['blue', 'green']}
valueFormatter={(value: number) => `${bytesToSize(value)}/s`}
index="time"
className="h-full w-full"
/>
) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
等待网络数据…
</div>
)}
<BarChart
key={`network-chart-${windowWidth}`}
data={networkHistory}
categories={['upload', 'download']}
colors={['blue', 'green']}
valueFormatter={(value: number) => `${bytesToSize(value)}/s`}
index="time"
showXAxis={false}
showYAxis={false}
className="h-full w-full"
/>
</CardContent>
</Card>
</section>
@@ -471,47 +506,38 @@ const Dashboard = () => {
<CardDescription>读取 / 写入 每秒</CardDescription>
</CardHeader>
<CardContent className="h-[260px]">
{diskChart.length ? (
<BarChart
data={diskChart}
categories={['read', 'write']}
colors={['cyan', 'indigo']}
index="date"
valueFormatter={(value: number) => bytesToSize(value)}
className="h-full"
/>
) : (
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
等待磁盘指标…
</div>
)}
<BarChart
data={diskChart}
categories={['read', 'write']}
colors={['#10b981', '#f59e0b']}
index="date"
valueFormatter={(value: number) => bytesToSize(value)}
showXAxis={false}
showYAxis={false}
barRadius={4}
className="h-full"
/>
</CardContent>
</Card>
<Card className="space-y-3 border border-border">
<CardHeader>
<CardTitle className="text-lg font-semibold flex items-center gap-2">
<Server className="h-4 w-4 text-blue-500" />
请求概况
<CardHeader className="space-y-1">
<CardTitle className="flex items-center gap-2 text-sm font-medium">
<CheckCircle className="h-4 w-4 text-blue-500" />
成功率
</CardTitle>
<CardDescription>全量请求与今日增量</CardDescription>
<CardDescription className="text-[0.85rem] text-muted-foreground">请求成功/失败的比例</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
{requestStats.map(stat => (
<div key={stat.label} className="space-y-1 text-center">
<div className={`mx-auto flex h-8 w-8 items-center justify-center rounded-full bg-muted text-muted-foreground`}>
<stat.icon className="h-4 w-4" />
</div>
<p className="text-sm font-medium text-muted-foreground">{stat.label}</p>
<p className={`text-lg font-semibold ${stat.tone}`}>{stat.label === '成功率' ? `${stat.value}%` : stat.value.toLocaleString()}</p>
{stat.change !== undefined && stat.label !== '成功率' && (
<p className="text-xs text-muted-foreground">+{stat.change.toLocaleString()} 今日</p>
)}
</div>
))}
<CardContent className="space-y-3">
<div className="flex items-baseline gap-2">
<p className="text-3xl font-semibold text-blue-600">{successRate.toFixed(1)}%</p>
<p className="text-xs text-muted-foreground">成功 {data.success.toLocaleString()} / 失败 {data.fail.toLocaleString()}</p>
</div>
<ProgressBar
value={successRate}
className="h-3"
color={successRate > 90 ? 'green' : successRate > 70 ? 'yellow' : 'red'}
/>
<div className="grid gap-3 text-sm text-muted-foreground md:grid-cols-2">
{latencyMetrics.map(metric => (
<div key={metric.label} className="rounded-lg border border-border p-3">
@@ -14,7 +14,6 @@ import {
Square,
MoreVertical,
Trash2,
BookOpen
} from 'lucide-react';
import { useNavigate } from "react-router-dom";
import UpdateServer from "./UpdateServer";
@@ -219,16 +218,6 @@ const ProxyList = memo(() => {
<p className="text-sm text-muted-foreground mb-6 max-w-sm">
开始创建您的第一个代理服务器配置
</p>
<div className="flex gap-2">
<Button>
<ServerIcon className="mr-2 h-4 w-4" />
创建服务器
</Button>
<Button variant="outline">
<BookOpen className="mr-2 h-4 w-4" />
查看文档
</Button>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">