diff --git a/docker-compose.dcproj b/docker-compose.dcproj new file mode 100644 index 0000000..0d8feee --- /dev/null +++ b/docker-compose.dcproj @@ -0,0 +1,14 @@ + + + + 2.1 + Linux + + + + docker-compose.yml + + + + + diff --git a/src/FastGateway/Dockerfile b/src/FastGateway/Dockerfile index 968711e..5d1cbeb 100644 --- a/src/FastGateway/Dockerfile +++ b/src/FastGateway/Dockerfile @@ -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"] \ No newline at end of file diff --git a/src/FastGateway/FastGateway.csproj b/src/FastGateway/FastGateway.csproj index 8d7460c..fd88624 100644 --- a/src/FastGateway/FastGateway.csproj +++ b/src/FastGateway/FastGateway.csproj @@ -11,16 +11,17 @@ - - - - - - + + + + + + + - + @@ -33,13 +34,7 @@ - - - - - - .dockerignore - + diff --git a/src/FastGateway/Gateway/FastGatewayForwarderHttpClientFactory.cs b/src/FastGateway/Gateway/FastGatewayForwarderHttpClientFactory.cs new file mode 100644 index 0000000..564db38 --- /dev/null +++ b/src/FastGateway/Gateway/FastGatewayForwarderHttpClientFactory.cs @@ -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); + } + } +} diff --git a/src/FastGateway/Gateway/Gateway.cs b/src/FastGateway/Gateway/Gateway.cs index ccb0665..959a3fb 100644 --- a/src/FastGateway/Gateway/Gateway.cs +++ b/src/FastGateway/Gateway/Gateway.cs @@ -35,6 +35,25 @@ public static class Gateway /// private static readonly DefaultContentTypeProvider DefaultContentTypeProvider = new(); + /// + /// 证书缓存(按 SNI 名称及默认证书缓存 X509Certificate2 实例) + /// + private static readonly ConcurrentDictionary CertificateCache = new(); + + /// + /// 证书缓存失效(根据域名) + /// + /// SNI 域名 + 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 */ } + } + } + /// /// 是否存在80端口服务 /// @@ -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 } /// - /// 找到证书 + /// 找到证书(带缓存,避免每次握手都创建新实例) /// /// - /// + /// SNI 主机名 /// 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"); + } } /// @@ -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}}"; diff --git a/src/FastGateway/Properties/launchSettings.json b/src/FastGateway/Properties/launchSettings.json index 0831e78..d7f93d8 100644 --- a/src/FastGateway/Properties/launchSettings.json +++ b/src/FastGateway/Properties/launchSettings.json @@ -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", diff --git a/src/FastGateway/Services/CertService.cs b/src/FastGateway/Services/CertService.cs index 89bbd45..f4aefc9 100644 --- a/src/FastGateway/Services/CertService.cs +++ b/src/FastGateway/Services/CertService.cs @@ -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("证书"); diff --git a/src/FastGateway/Services/QpsService.cs b/src/FastGateway/Services/QpsService.cs index fe47f91..e9a6fbd 100644 --- a/src/FastGateway/Services/QpsService.cs +++ b/src/FastGateway/Services/QpsService.cs @@ -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 _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 _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 _cpuCounter = new(() => + private static readonly Lazy _cpuCounter = new(() => new PerformanceCounter("Processor", "% Processor Time", "_Total")); - private static readonly Lazy _memoryCounter = new(() => + private static readonly Lazy _memoryCounter = new(() => new PerformanceCounter("Memory", "Available MBytes")); - private static readonly Lazy _diskReadCounter = new(() => + private static readonly Lazy _diskReadCounter = new(() => new PerformanceCounter("PhysicalDisk", "Disk Read Bytes/sec", "_Total")); - private static readonly Lazy _diskWriteCounter = new(() => + private static readonly Lazy _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 { } } } \ No newline at end of file diff --git a/src/FastGateway/Tunnels/TunnelClientFactory.cs b/src/FastGateway/Tunnels/TunnelClientFactory.cs index 26e2cab..7b660e7 100644 --- a/src/FastGateway/Tunnels/TunnelClientFactory.cs +++ b/src/FastGateway/Tunnels/TunnelClientFactory.cs @@ -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; diff --git a/src/TunnelClient/TunnelClient.csproj b/src/TunnelClient/TunnelClient.csproj index c77b3bd..9f1cee6 100644 --- a/src/TunnelClient/TunnelClient.csproj +++ b/src/TunnelClient/TunnelClient.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/web/src/components/ui/bar-chart.tsx b/web/src/components/ui/bar-chart.tsx index bbf13a1..f01c847 100644 --- a/web/src/components/ui/bar-chart.tsx +++ b/web/src/components/ui/bar-chart.tsx @@ -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 (
- + {showGrid && ( ))} diff --git a/web/src/pages/dashboard/page.tsx b/web/src/pages/dashboard/page.tsx index a658dc6..c3dce43 100644 --- a/web/src/pages/dashboard/page.tsx +++ b/web/src/pages/dashboard/page.tsx @@ -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>([]); - const [qpsChart, setQpsChart] = useState>(() => - Array.from({ length: 30 }, () => ({ - time: '00:00', - QPS: 0, - })) - ); + const [diskChart, setDiskChart] = useState>(() => { + 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>(() => { + 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 = () => { {qpsBadgeLabel} - 最近 30s 滑动窗口 + 最近 3s 滑动窗口

{qps.toLocaleString()}

@@ -321,23 +354,28 @@ const Dashboard = () => { - - - - 成功率 + + + + 请求概况 - 请求成功/失败的比例 + 全量请求与今日增量 - -
-

{successRate.toFixed(1)}%

-

成功 {data.success.toLocaleString()} / 失败 {data.fail.toLocaleString()}

+ +
+ {requestStats.map(stat => ( +
+
+ +
+

{stat.label}

+

{stat.label === '成功率' ? `${stat.value}%` : stat.value.toLocaleString()}

+ {stat.change !== undefined && stat.label !== '成功率' && ( +

+{stat.change.toLocaleString()} 今日

+ )} +
+ ))}
- 90 ? 'green' : successRate > 70 ? 'yellow' : 'red'} - />
@@ -374,10 +412,10 @@ const Dashboard = () => { QPS 趋势 - 滚动的 30 次请求曲线 + 滚动的 50 次请求曲线 - { valueFormatter={(value: number) => `${value} req/s`} index="time" showXAxis={false} + showYAxis={false} className="h-full w-full" /> @@ -399,21 +438,17 @@ const Dashboard = () => { 上/下行速度 - {networkHistory.length ? ( - `${bytesToSize(value)}/s`} - index="time" - className="h-full w-full" - /> - ) : ( -
- 等待网络数据… -
- )} + `${bytesToSize(value)}/s`} + index="time" + showXAxis={false} + showYAxis={false} + className="h-full w-full" + />
@@ -471,47 +506,38 @@ const Dashboard = () => { 读取 / 写入 每秒 - {diskChart.length ? ( - bytesToSize(value)} - className="h-full" - /> - ) : ( -
- 等待磁盘指标… -
- )} + bytesToSize(value)} + showXAxis={false} + showYAxis={false} + barRadius={4} + className="h-full" + />
- - - - 请求概况 + + + + 成功率 - 全量请求与今日增量 + 请求成功/失败的比例 - -
- {requestStats.map(stat => ( -
-
- -
-

{stat.label}

-

{stat.label === '成功率' ? `${stat.value}%` : stat.value.toLocaleString()}

- {stat.change !== undefined && stat.label !== '成功率' && ( -

+{stat.change.toLocaleString()} 今日

- )} -
- ))} + +
+

{successRate.toFixed(1)}%

+

成功 {data.success.toLocaleString()} / 失败 {data.fail.toLocaleString()}

- + 90 ? 'green' : successRate > 70 ? 'yellow' : 'red'} + />
{latencyMetrics.map(metric => (
diff --git a/web/src/pages/server/features/ProxyList.tsx b/web/src/pages/server/features/ProxyList.tsx index 9a9cf52..c6f00b4 100644 --- a/web/src/pages/server/features/ProxyList.tsx +++ b/web/src/pages/server/features/ProxyList.tsx @@ -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(() => {

开始创建您的第一个代理服务器配置

-
- - -
) : (