// ========================================================================================== // GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利 // GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights // 均受中华人民共和国及相关国际法律法规保护。 // are protected by the laws of the People's Republic of China and relevant international regulations. // 使用本项目须严格遵守相应法律法规及开源许可证之规定。 // Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses. // 本项目采用 Apache License 2.0 单协议分发, // This project is licensed solely under the Apache License 2.0, // 完整许可证文本请参见源代码根目录下的 LICENSE 文件。 // please refer to the LICENSE file in the root directory of the source code for the full license text. // 禁止利用本项目实施任何危害国家安全、破坏社会秩序、 // It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order, // 侵犯他人合法权益等法律法规所禁止的行为! // or infringe upon the legitimate rights and interests of others, as prohibited by laws and regulations! // 因基于本项目二次开发所产生的一切法律纠纷与责任, // Any legal disputes and liabilities arising from secondary development based on this project // 本项目组织与贡献者概不承担。 // shall be borne solely by the developer; the project organization and contributors assume no responsibility. // GitHub 仓库:https://github.com/GameFrameX // GitHub Repository: https://github.com/GameFrameX // Gitee 仓库:https://gitee.com/GameFrameX // Gitee Repository: https://gitee.com/GameFrameX // CNB 仓库:https://cnb.cool/GameFrameX // CNB Repository: https://cnb.cool/GameFrameX // 官方文档:https://gameframex.doc.alianblank.com/ // Official Documentation: https://gameframex.doc.alianblank.com/ // ========================================================================================== using System.Reflection; using System.Security.Cryptography.X509Certificates; using GameFrameX.Foundation.Logger; using GameFrameX.Foundation.Localization.Core; using GameFrameX.Network.HTTP; using GameFrameX.Utility; using GameFrameX.Foundation.Extensions; using GameFrameX.Foundation.Utility; using GameFrameX.Utility.Setting; using GameFrameX.StartUp.ServiceDefaults; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi; using Serilog; namespace GameFrameX.StartUp; /// /// 程序启动器基类 - 提供服务器的基础功能实现。 /// /// /// Application startup base class - provides server basic functionality implementation. /// This partial class specifically handles HTTP server startup and configuration functionality. /// public abstract partial class AppStartUpBase { /// /// 当前 HTTP 服务器宿主实例(启动路径保存引用,供停机路径停止与释放; 表示未启动或已停止)。 /// /// /// The current HTTP server host instance (kept by the startup path so the shutdown path can stop and dispose it; /// when the HTTP server was never started or has already stopped). /// Mirrors the socket host field _gameServer: assigned right after build and before start, cleared by /// so repeated shutdown calls stay idempotent. Instance-level (not static) on /// purpose — every role owns its own AppStartUpBase instance in a multi-role process. /// private WebApplication _webApplication; /// /// 启动 HTTP 服务器。 /// /// /// Start HTTP server. /// This method is responsible for configuring and starting the HTTP server, including Swagger documentation, health checks, exception handling, and other features. /// /// HTTP 处理器列表,用于处理不同的 HTTP 请求 / HTTP handler list for processing different HTTP requests /// HTTP 处理器工厂,根据命令标识符创建对应的处理器实例 / HTTP handler factory that creates corresponding handler instances based on command identifiers /// AOP 处理器列表,用于在 HTTP 请求处理前后执行额外的逻辑 / AOP handler list for executing additional logic before and after HTTP request processing /// 日志记录的最小级别,用于控制日志输出 / Minimum level for logging to control log output /// 表示异步操作的任务 / A task representing the asynchronous operation /// 当 HTTP URL 格式不正确时抛出 / Thrown when HTTP URL format is incorrect /// 当启用 HTTPS 但未实现时抛出 / Thrown when HTTPS is enabled but not implemented private async Task StartHttpServerAsync(List baseHandler, Func httpFactory, List aopHandlerTypes = null, LogLevel minimumLevelLogLevel = LogLevel.Debug) { // 前置校验:启用状态与 HTTP URL 格式 if (!EnsureHttpServerEnabled()) { return; } // Localization: StartUp.HttpServer.StartingServer - 启动 [HTTP] 服务器... LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.StartingServer)); if (!Setting.HttpRequireSign) { // Localization: StartUp.HttpServer.SignDisabledWarning - HTTP 服务器已启动,但 HttpRequireSign=false,签名校验默认关闭:对外暴露部署存在请求被截获重放的风险,建议生产环境开启签名校验并配置强密钥 LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SignDisabledWarning)); } if (!Setting.HttpPort.IsRange(5000, ushort.MaxValue - 1)) { // Localization: StartUp.HttpServer.PortOutOfRange - 启动 [HTTP] 服务器端口 [{0}] 超出范围 [{1}-{2}],HTTP服务无法启动,启动已忽略 LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOutOfRange, Setting.HttpPort, 5000, ushort.MaxValue - 1)); return; } // 检查端口是否可用:不可用则记录占用详情后返回 if (!NetHelper.PortIsAvailable(Setting.HttpPort)) { LogHttpPortOccupied(); return; } await RunHttpServerAsync(baseHandler, httpFactory, aopHandlerTypes, minimumLevelLogLevel); } /// /// 校验 HTTP 服务器是否启用及 URL 格式是否合法。 /// /// Validate whether the HTTP server is enabled and the URL format is valid. Extracted from to keep cognitive complexity under the Sonar S3776 threshold. /// 启用且 URL 合法返回 true;未启用返回 false;URL 不合法时抛出 / Returns true if enabled and URL is valid; returns false if disabled; throws for invalid URL /// 当 HTTP URL 格式不正确时抛出 / Thrown when HTTP URL format is incorrect private bool EnsureHttpServerEnabled() { // 检查是否启用HTTP服务 if (!Setting.IsEnableHttp) { // Localization: StartUp.HttpServer.ServiceDisabled - HTTP服务已禁用,启动已忽略 LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ServiceDisabled)); return false; } // 验证HTTP URL格式 if (!Setting.HttpUrl.StartsWith('/')) { // Localization: StartUp.HttpExceptions.AddressMustStartWithSlash - 地址必须以斜杠开头。地址:{0}。 throw new ArgumentException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.AddressMustStartWithSlash, Setting.HttpUrl), nameof(Setting.HttpUrl)); } if (!Setting.HttpUrl.EndsWith('/')) { // Localization: StartUp.HttpExceptions.AddressMustEndWithSlash - 地址必须以斜杠结尾。地址:{0}。 throw new ArgumentException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.AddressMustEndWithSlash, Setting.HttpUrl), nameof(Setting.HttpUrl)); } return true; } /// /// 记录 HTTP 端口被占用时的占用进程详情。 /// /// Log the occupying process details when the HTTP port is occupied. Extracted from to keep cognitive complexity under the Sonar S3776 threshold. private void LogHttpPortOccupied() { // Localization: StartUp.HttpServer.PortOccupied - HTTP服务器端口被占用。端口:{0}。 LogHelper.Error(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOccupied, Setting.HttpPort)); var occupiedProcesses = NetHelper.GetPortOccupyingProcesses(Setting.HttpPort); if (occupiedProcesses.Count > 0) { // Localization: StartUp.HttpServer.PortOccupiedDetails - HTTP端口[{0}]占用详情: {1} LogHelper.Error(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOccupiedDetails, Setting.HttpPort, string.Join(" | ", occupiedProcesses))); } else { // Localization: StartUp.HttpServer.PortOccupiedNoProcessDetails - HTTP端口[{0}]已被占用,但未能获取占用进程详情。 LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOccupiedNoProcessDetails, Setting.HttpPort)); } } /// /// 构建、配置并启动 HTTP 服务器。 /// /// Build, configure and start the HTTP server. Extracted from to keep cognitive complexity under the Sonar S3776 threshold. /// HTTP 处理器列表,用于处理不同的 HTTP 请求 / HTTP handler list for processing different HTTP requests /// HTTP 处理器工厂,根据命令标识符创建对应的处理器实例 / HTTP handler factory that creates corresponding handler instances based on command identifiers /// AOP 处理器列表,用于在 HTTP 请求处理前后执行额外的逻辑 / AOP handler list for executing additional logic before and after HTTP request processing /// 日志记录的最小级别,用于控制日志输出 / Minimum level for logging to control log output /// 表示异步操作的任务 / A task representing the asynchronous operation /// 当启用 HTTPS 但未实现时抛出 / Thrown when HTTPS is enabled but not implemented private async Task RunHttpServerAsync(List baseHandler, Func httpFactory, List aopHandlerTypes, LogLevel minimumLevelLogLevel) { var builder = WebApplication.CreateBuilder(); // 确定是否为开发环境 var development = Setting.HttpIsDevelopment || EnvironmentHelper.IsDevelopment(); var openApiInfo = GetOpenApiInfo(); var corsAllowedOrigins = ResolveHttpCorsAllowedOrigins(Setting.HttpCorsAllowedOrigins, development); ConfigureHttpBuilder(builder, baseHandler, development, openApiInfo, corsAllowedOrigins, minimumLevelLogLevel); var app = builder.Build(); // 宿主引用落字段(构建后、启动前,对齐 socket 宿主 _gameServer 模式):停机路径 StopHttpServerAsync 依赖它执行 StopAsync/DisposeAsync _webApplication = app; app.MapDefaultEndpoints(); ConfigureHttpApplication(app, development, openApiInfo, corsAllowedOrigins, baseHandler, httpFactory, aopHandlerTypes); await app.StartAsync(); // Localization: StartUp.HttpServer.StartupComplete - HTTP服务器启动完成 - 端口: {0} LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.StartupComplete, Setting.HttpPort)); } /// /// 停止并释放 HTTP 服务器宿主(停机时 Kestrel 端口等系统资源的确定性释放)。 /// /// /// Stops and disposes the HTTP server host so Kestrel listeners and other system resources are /// deterministically released during shutdown. Null-guarded no-op when the HTTP server was never /// started (disabled or skipped) — those processes keep byte-identical stop behavior. Called by /// StopServerAsync after the socket host (_gameServer) has stopped, i.e. after the /// role's actor and business services have already shut down (the Launcher role template stops the /// capability family before delegating to the base stop flow), so no new shutdown race is introduced. /// The stop has a hard 5-second budget; the reference is cleared afterwards so repeated shutdown /// calls stay idempotent. /// private async Task StopHttpServerAsync() { if (_webApplication == null) { return; } await _webApplication.StopAsync(TimeSpan.FromSeconds(5)); await _webApplication.DisposeAsync(); _webApplication = null; } /// /// 配置 HTTP 服务器的 侧服务(CORS、Swagger、Kestrel、日志、OpenTelemetry)。 /// /// Configure the builder-side services of the HTTP server (CORS, Swagger, Kestrel, logging, OpenTelemetry). Extracted from to keep cognitive complexity under the Sonar S3776 threshold. /// Web 应用程序构建器 / Web application builder /// HTTP 处理器列表 / HTTP handler list /// 是否为开发环境 / Whether it is development environment /// OpenAPI 信息 / OpenAPI information /// 允许的跨域来源 / Allowed CORS origins /// 日志记录的最小级别 / Minimum level for logging private void ConfigureHttpBuilder(WebApplicationBuilder builder, List baseHandler, bool development, OpenApiInfo openApiInfo, string[] corsAllowedOrigins, LogLevel minimumLevelLogLevel) { if (corsAllowedOrigins.Length > 0) { builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.WithOrigins(corsAllowedOrigins) .AllowAnyMethod() .AllowAnyHeader(); }); }); } // 在开发环境下配置Swagger if (development) { builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc(openApiInfo.Version, openApiInfo); options.SchemaFilter(); options.OperationFilter(baseHandler); options.CustomSchemaIds(type => type.Name); }); } // 配置Web主机 var httpsCertificate = Setting.HttpsPort > 0 ? LoadHttpsCertificate(Setting) : null; var hostBuilder = builder.WebHost.UseKestrel(options => { options.Limits.MaxRequestBodySize = Setting.HttpMaxRequestBodyBytes; options.ListenAnyIP(Setting.HttpPort); if (httpsCertificate != null) { // HTTPS 真监听:证书在 Kestrel 配置回调外预加载并 fail-fast; // 端口被占用时由 Kestrel bind 失败直接阻断启动,不再静默跳过(静默降级面移除)。 options.ListenAnyIP(Setting.HttpsPort, listenOptions => listenOptions.UseHttps(httpsCertificate)); } ConfigureMetricsPortListen(options); }); hostBuilder.ConfigureLogging(logging => { logging.ClearProviders(); logging.AddSerilog(Log.Logger); logging.SetMinimumLevel(minimumLevelLogLevel); }); builder.AddServiceDefaults(Setting.IsOpenTelemetry, Setting.IsOpenTelemetryMetrics, Setting.IsOpenTelemetryTracing); } /// /// 为 Kestrel 追加独立指标抓取端口(MetricsPort)监听。 /// /// /// 承载端口契约语义:对外宣称的独立指标抓取端口由主 HTTP 宿主 Kestrel 多绑实现(对齐 HttpsPort 条件监听先例), /// 不新增第二个 HTTP 服务器组件;仅 OTel 双开关开启且端口未被占用时追加监听,被占用时记 Warning 跳过附加监听、不阻断主服务。 /// Append the standalone metrics scraping port (MetricsPort) listener to the main HTTP host; /// skipped with a Warning when the port is occupied so the main service is never blocked. /// /// Kestrel 服务器选项 / Kestrel server options private void ConfigureMetricsPortListen(KestrelServerOptions options) { if (!Setting.IsOpenTelemetry || !Setting.IsOpenTelemetryMetrics || Setting.MetricsPort == 0) { return; } // 端口被占用:跳过附加监听,/metrics 端点因 RequireHost 端口限定随之失配,主服务继续运行 if (!NetHelper.PortIsAvailable(Setting.MetricsPort)) { // Localization: StartUp.MetricsPortInUse - 指标端口 [{0}] 被占用,无法启动独立指标服务器 LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.MetricsPortInUse, Setting.MetricsPort)); return; } options.ListenAnyIP(Setting.MetricsPort); } /// /// 加载 HTTPS 证书(PFX),供 监听使用;任一配置错误在 Kestrel 监听前 fail-fast。 /// /// /// Loads the HTTPS certificate (PFX) for the listener; any configuration error /// fails fast before Kestrel starts listening. /// 调用前提: 的 HttpsPort 大于 0。 /// /// 应用配置 / Application setting /// 已加载的证书 / The loaded certificate /// 证书路径为空 / The certificate path is empty /// 证书文件不存在 / The certificate file does not exist /// 证书加载失败(密码错误或文件损坏)/ The certificate failed to load (wrong password or corrupted file) internal static X509Certificate2 LoadHttpsCertificate(AppSetting setting) { if (setting.HttpsCertPath.IsNullOrWhiteSpace()) { // Localization: StartUp.HttpExceptions.HttpsCertPathMissing - 已配置 HTTPS 端口 [{0}],但 HttpsCertPath 未配置(PFX 文件路径),无法启用 HTTPS 监听 throw new InvalidOperationException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.HttpsCertPathMissing, setting.HttpsPort)); } if (!File.Exists(setting.HttpsCertPath)) { // Localization: StartUp.HttpExceptions.HttpsCertFileNotFound - HTTPS 证书文件不存在: {0} throw new InvalidOperationException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.HttpsCertFileNotFound, setting.HttpsCertPath)); } try { return X509CertificateLoader.LoadPkcs12FromFile(setting.HttpsCertPath, setting.HttpsCertPassword, X509KeyStorageFlags.DefaultKeySet, Pkcs12LoaderLimits.Defaults); } catch (Exception e) { // Localization: StartUp.HttpExceptions.HttpsCertLoadFailed - HTTPS 证书加载失败(密码错误或文件损坏): {0} throw new InvalidOperationException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.HttpsCertLoadFailed, setting.HttpsCertPath), e); } } /// /// 配置 HTTP 服务器的 请求管道(Swagger UI、CORS、异常处理、Prometheus、路由)。 /// /// Configure the request pipeline of the HTTP server (Swagger UI, CORS, exception handling, Prometheus, routing). Extracted from to keep cognitive complexity under the Sonar S3776 threshold. /// Web 应用程序实例 / Web application instance /// 是否为开发环境 / Whether it is development environment /// OpenAPI 信息 / OpenAPI information /// 允许的跨域来源 / Allowed CORS origins /// HTTP 处理器列表 / HTTP handler list /// HTTP 处理器工厂 / HTTP handler factory /// AOP 处理器列表 / AOP handler list private void ConfigureHttpApplication(WebApplication app, bool development, OpenApiInfo openApiInfo, string[] corsAllowedOrigins, List baseHandler, Func httpFactory, List aopHandlerTypes) { var ipList = NetHelper.GetLocalIpList(); // 开发环境下的Swagger UI配置 if (development) { // 添加 Swagger 中间件 app.UseSwagger(); app.UseSwaggerUI(options => { var swaggerEndpoint = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SwaggerEndpointFormat, openApiInfo.Version); options.SwaggerEndpoint(swaggerEndpoint, openApiInfo.Title); options.RoutePrefix = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SwaggerRoutePrefix); }); foreach (var ip in ipList) { // Localization: StartUp.HttpServer.SwaggerUiAccess - Swagger UI 可通过 http://{0}:{1}/swagger 访问 LogHelper.Debug(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SwaggerUiAccess, ip, Setting.HttpPort)); } } if (corsAllowedOrigins.Length > 0) { app.UseCors(); } // 配置全局异常处理 app.UseExceptionHandler(ExceptionHandler); ConfigureHttpPrometheusEndpoint(app, ipList); RegisterHandlerRoutes(app, baseHandler, httpFactory, aopHandlerTypes, development); } /// /// 配置 OpenTelemetry Prometheus 指标端点(内联形态或 MetricsPort 端口限定形态)。 /// /// Configure the OpenTelemetry Prometheus metrics endpoint (inline form or the MetricsPort host-restricted form). Extracted from to keep cognitive complexity under the Sonar S3776 threshold. /// Web 应用程序实例 / Web application instance /// 本机 IP 列表 / Local IP list private void ConfigureHttpPrometheusEndpoint(WebApplication app, List ipList) { // 内联形态:未配置独立指标端口时,/metrics 挂主 API 端口 if (Setting.IsOpenTelemetry && Setting.IsOpenTelemetryMetrics && Setting.MetricsPort == 0) { app.MapPrometheusScrapingEndpoint(); foreach (var ip in ipList) { // Localization: StartUp.PrometheusMetricsEndpointEnabledInline - Prometheus指标端点已内联启用。 LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.PrometheusMetricsEndpointEnabledInline, ip, Setting.HttpPort)); } } else if (Setting.IsOpenTelemetry && Setting.IsOpenTelemetryMetrics && Setting.MetricsPort > 0) { // 端口契约:独立指标抓取端口由主 HTTP 宿主 Kestrel 多绑承载(ConfigureMetricsPortListen 追加监听), // /metrics 以 RequireHost("*:MetricsPort") 限定仅指标端口应答,主 API 端口不暴露指标面。 // Port contract: /metrics only answers on MetricsPort (RequireHost host:port matching); the main API port does not expose it. app.MapPrometheusScrapingEndpoint().RequireHost($"*:{Setting.MetricsPort}"); foreach (var ip in ipList) { // Localization: StartUp.PrometheusMetricsEndpointEnabled - 独立普罗米修斯指标端点已启用: http://{0}:{1}/metrics LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.PrometheusMetricsEndpointEnabled, ip, Setting.MetricsPort)); } } } /// /// 注册全部 HTTP 处理器路由(驼峰与下划线两种格式)。 /// /// Register routes for all HTTP handlers (both camelCase and underscore forms). Extracted from to keep cognitive complexity under the Sonar S3776 threshold. /// Web 应用程序实例 / Web application instance /// HTTP 处理器列表 / HTTP handler list /// HTTP 处理器工厂 / HTTP handler factory /// AOP 处理器列表 / AOP handler list /// 是否为开发环境 / Whether it is development environment private static void RegisterHandlerRoutes(WebApplication app, List baseHandler, Func httpFactory, List aopHandlerTypes, bool development) { var registeredRouteKeys = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var handler in baseHandler) { var handlerType = handler.GetType(); var mappingAttribute = handlerType.GetCustomAttribute(); if (mappingAttribute == null) { continue; } // 注册驼峰格式路由 RegisterHandlerRoute(app, mappingAttribute, mappingAttribute.OriginalCmd, httpFactory, aopHandlerTypes, development, registeredRouteKeys); // 注册下划线格式路由(如果与驼峰格式不同) if (mappingAttribute.OriginalCmd != mappingAttribute.StandardCmd) { RegisterHandlerRoute(app, mappingAttribute, mappingAttribute.StandardCmd, httpFactory, aopHandlerTypes, development, registeredRouteKeys); } } } /// /// 获取或创建 Swagger 信息。 /// /// /// Get or create Swagger information. /// Used to configure basic information for Swagger documentation, including title, version, contact information, etc. /// /// 包含 API 文档详细信息的 OpenAPI 信息对象 / OpenAPI information object containing API documentation details private OpenApiInfo GetOpenApiInfo() { // 添加 Swagger 服务 var version = Assembly.GetExecutingAssembly().GetName().Version; if (version == null) { version = new Version(1, 0, 0); } var openApiInfo = new OpenApiInfo { Title = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiTitle), Version = $"v{version.Major}.{version.Minor}", TermsOfService = new Uri("https://gameframex.doc.alianblank.com"), Contact = new OpenApiContact { Url = new Uri("https://gameframex.doc.alianblank.com"), Name = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiContactName), Email = "wangfj11@foxmail.com", }, License = new OpenApiLicense { Name = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiLicenseName), Url = new Uri("https://github.com/GameFrameX/GameFrameX"), }, Description = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiDescription), }; return openApiInfo; } private static string[] ResolveHttpCorsAllowedOrigins(string allowedOrigins, bool development) { if (!string.IsNullOrWhiteSpace(allowedOrigins)) { return allowedOrigins.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); } if (!development) { return Array.Empty(); } return new[] { "http://localhost", "https://localhost", "http://127.0.0.1", "https://127.0.0.1", }; } /// /// 异常处理器,用于处理 HTTP 请求中的未处理异常。 /// /// /// Exception handler for processing unhandled exceptions in HTTP requests. /// Provides a global exception handling mechanism to ensure all unhandled exceptions are properly handled and returned to the client. /// /// 用于配置错误处理管道的应用程序构建器 / Application builder for configuring error handling pipeline private static void ExceptionHandler(IApplicationBuilder errorContext) { errorContext.Run(async context => { // 获取异常信息 var exceptionHandlerPathFeature = context.Features.Get(); // 记录详细错误日志 // Localization: StartUp.HttpServer.HttpRequestError - HTTP请求错误: {0} LogHelper.Error(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HttpRequestError, exceptionHandlerPathFeature?.Error.ToString() ?? "No exception available")); // 返回通用错误消息,避免泄露敏感信息 await context.Response.WriteAsync("An error occurred while processing your request."); }); } /// /// 获取 HTTP 方法的摘要描述。 /// /// /// Gets the summary description for the HTTP method. /// /// HTTP 方法类型 / HTTP method type /// 摘要描述 / Summary description private static string GetHttpMethodSummary(HttpMethodType httpMethod) { switch (httpMethod) { case HttpMethodType.GET: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGetRequest); case HttpMethodType.PUT: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandlePutRequest); case HttpMethodType.DELETE: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleDeleteRequest); default: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandlePostRequest); } } /// /// 获取 HTTP 方法的详细描述。 /// /// /// Gets the detailed description for the HTTP method. /// /// HTTP 方法类型 / HTTP method type /// 详细描述 / Detailed description private static string GetHttpMethodDescription(HttpMethodType httpMethod) { switch (httpMethod) { case HttpMethodType.GET: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientGetRequest); case HttpMethodType.PUT: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientPutRequest); case HttpMethodType.DELETE: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientDeleteRequest); default: return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientPostRequest); } } /// /// 注册单个 HTTP 处理器路由。 /// /// /// Registers a single HTTP handler route. /// Registers the route based on the HTTP method - using different Map methods according to HttpMethod. /// In development environment, configures API documentation. /// /// Web 应用程序实例 / Web application instance /// HTTP 消息映射特性 / HTTP message mapping attribute /// 命令标识符(用于构建 API 路径)/ Command identifier (used to build API path) /// HTTP 处理器工厂 / HTTP handler factory /// AOP 处理器列表 / AOP handler list /// 是否为开发环境 / Whether it is development environment /// 已注册路由键集合,用于去重 / The set of registered route keys, used for deduplication private static void RegisterHandlerRoute(WebApplication app, HttpMessageMappingAttribute mappingAttribute, string cmd, Func httpFactory, List aopHandlerTypes, bool development, ISet registeredRouteKeys) { var apiPath = $"{GlobalSettings.CurrentSetting.HttpUrl}{cmd}"; var httpMethod = mappingAttribute.HttpMethod; if (!TryAddRouteKey(httpMethod, apiPath, registeredRouteKeys)) { return; } IEndpointConventionBuilder route; switch (httpMethod) { case HttpMethodType.GET: route = app.MapGet(apiPath, async (HttpContext context) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); }); break; case HttpMethodType.PUT: route = app.MapPut(apiPath, async (HttpContext context, string _) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); }); break; case HttpMethodType.DELETE: route = app.MapDelete(apiPath, async (HttpContext context, string _) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); }); break; default: route = app.MapPost(apiPath, async (HttpContext context, string _) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); }); break; } // 开发环境下配置API文档 if (development) { route.WithSummary(GetHttpMethodSummary(httpMethod)); route.WithDescription(GetHttpMethodDescription(httpMethod)); } } /// /// 尝试添加路由唯一键,避免大小写差异导致重复注册。 /// /// HTTP方法 /// API路径 /// 已注册路由键集合 /// 添加成功返回 true,已存在返回 false private static bool TryAddRouteKey(HttpMethodType httpMethod, string apiPath, ISet registeredRouteKeys) { var routeKey = $"{httpMethod}:{apiPath}"; return registeredRouteKeys.Add(routeKey); } }