Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user