Initial commit: Client Doc docs Server Tools

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ud18010
2026-07-10 10:24:29 +08:00
co-authored by Cursor
commit 7e35d8da31
3374 changed files with 680813 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
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<IServer>()
.Features.Get<IServerAddressesFeature>().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;
}
}
}