using System; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; namespace XWorld.Server.Gateway { // 独立内网 CDN:Kestrel 静态文件服务,HTTP 根 = root(发布产物 client/ 目录)。 // 仿 GameServerHost 的 Start/Stop 形态;缺失文件返回 404,无目录浏览。 public sealed class StaticFileServer { private readonly string _root; private readonly int _port; private readonly bool _bindAll; private WebApplication _app; public string HttpBaseUrl { get; private set; } // 例如 http://127.0.0.1:15081 public StaticFileServer(string root, int port = 15081, bool bindAllInterfaces = false) { if (string.IsNullOrWhiteSpace(root)) throw new ArgumentException("root 不能为空", nameof(root)); _root = Path.GetFullPath(root); _port = port; _bindAll = bindAllInterfaces; } public async Task StartAsync() { Directory.CreateDirectory(_root); // 根目录不存在则建空目录(GET 全 404,而非启动崩溃) var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); builder.WebHost.ConfigureKestrel(o => o.Listen(_bindAll ? System.Net.IPAddress.Any : System.Net.IPAddress.Loopback, _port)); // 0 = 临时端口 var app = builder.Build(); var provider = new PhysicalFileProvider(_root); var opts = new StaticFileOptions { FileProvider = provider, ServeUnknownFileTypes = true, // .bytes/.unity3d/.sig 等无标准 MIME 也要发 DefaultContentType = "application/octet-stream", }; app.UseStaticFiles(opts); // 仅静态文件,不开目录浏览;未命中落到下面的 404 await app.StartAsync(); _app = app; string http = app.Services.GetRequiredService() .Features.Get().Addresses.FirstOrDefault() ?? throw new InvalidOperationException("Kestrel 已启动但未注册任何监听地址"); HttpBaseUrl = http; } public async Task StopAsync() { if (_app == null) return; await _app.StopAsync(); await _app.DisposeAsync(); _app = null; } } }