using System; using System.Linq; using System.Net; using System.Net.WebSockets; using System.Threading; 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.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using XWorld.Framework.Protocol; using XWorld.Server.Host; using XWorld.Server.Auth; namespace XWorld.Server.Gateway { // 建 Kestrel + /ws 端点,把 socket 接入 ServerLoop。Start 返回实际监听地址(端口由系统分配)。 public sealed class GameServerHost { private WebApplication _app; private ServerLoop _loop; private CancellationTokenSource _cts; private Task _loopTask; private DevDiscoveryResponder _discovery; private AccountStore _accounts; public string WsBaseUrl { get; private set; } // 例如 ws://127.0.0.1:54321 public int DevDiscoveryPort => _discovery?.Port ?? 0; public async Task StartAsync(string gamesRoot, int tickIntervalMs, long reconnectWindowTicks = 100, long matchTimeoutTicks = 150, float? logicalDt = null, string publicKeyPem = null, int listenPort = 0, bool bindAllInterfaces = false, string devDiscoveryToken = null, string advertiseGatewayUrl = null, string advertiseResourceBaseUrl = null, int devDiscoveryPort = DevDiscovery.Port, byte[] authSecret = null, string authDbPath = null) { _loop = new ServerLoop(gamesRoot, new ConsoleLogger("[loop]"), reconnectWindowTicks, matchTimeoutTicks, publicKeyPem); _cts = new CancellationTokenSource(); float dt = logicalDt ?? tickIntervalMs / 1000f; var builder = WebApplication.CreateBuilder(); builder.Logging.ClearProviders(); builder.WebHost.ConfigureKestrel(o => o.Listen(bindAllInterfaces ? IPAddress.Any : IPAddress.Loopback, listenPort)); // 0 = 临时端口 TokenService tokens = null; if (authSecret != null) { tokens = new TokenService(authSecret); _accounts = new AccountStore(string.IsNullOrEmpty(authDbPath) ? "Data Source=accounts.db" : authDbPath); } var app = builder.Build(); app.UseWebSockets(); if (tokens != null) AuthEndpoints.Map(app, _accounts, tokens, TimeSpan.FromDays(7)); app.Use(async (ctx, next) => { if (ctx.Request.Path == "/ws" && ctx.WebSockets.IsWebSocketRequest) { int pid; if (tokens != null) { string token = ctx.Request.Query["token"]; if (!tokens.Verify(token, out pid)) { ctx.Response.StatusCode = 401; return; } } else if (!int.TryParse(ctx.Request.Query["pid"], out pid)) { ctx.Response.StatusCode = 400; return; } using var ws = await ctx.WebSockets.AcceptWebSocketAsync(); var conn = new Connection(pid, ws, _loop); _loop.Submit(new ConnectCommand { PlayerId = pid, Connection = conn }); try { await conn.RunAsync(ctx.RequestAborted); } catch (OperationCanceledException) { } // 关停/中止:正常收束 finally { if (ws.State == WebSocketState.Open) { try { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "server stopping", CancellationToken.None); } catch { /* 关闭握手尽力而为 */ } } _loop.Submit(new DisconnectCommand { PlayerId = pid, Connection = conn }); } } else { await next(); } }); await app.StartAsync(); _app = app; var addresses = app.Services.GetRequiredService() .Features.Get().Addresses; string http = addresses.FirstOrDefault() ?? throw new InvalidOperationException("Kestrel 已启动但未注册任何监听地址"); WsBaseUrl = http.Replace("http://", "ws://"); if (!string.IsNullOrEmpty(devDiscoveryToken)) { string adGw = advertiseGatewayUrl; if (string.IsNullOrEmpty(adGw)) { // 绑所有网卡时 WsBaseUrl 形如 ws://0.0.0.0:port,对内网其它设备无意义 —— // 用本机真实 LAN IP 广而告之(仿 C++ XBroadcastServer.GetLocalIPAddress)。 if (bindAllInterfaces) adGw = $"ws://{LanIp.Resolve()}:{new Uri(WsBaseUrl).Port}/ws"; else adGw = WsBaseUrl + "/ws"; } _discovery = new DevDiscoveryResponder( Environment.MachineName, adGw, advertiseResourceBaseUrl ?? "", devDiscoveryToken, devDiscoveryPort); _discovery.Start(); } _loopTask = _loop.RunAsync(tickIntervalMs, dt, _cts.Token); } public async Task StopAsync() { if (_discovery != null) { await _discovery.StopAsync(); _discovery = null; } _cts.Cancel(); try { await _loopTask; } catch { /* 忽略取消 */ } await _app.StopAsync(); await _app.DisposeAsync(); _accounts?.Dispose(); _accounts = null; } } }