feat: Upgrade server with HyNB v2.4 modular architecture, minigames, admin GM web panel, companion summons, and car radio
This commit is contained in:
1 parent
74e2963a12
commit
3e5adf1dc5
1571 files changed
+336468
-6348
No files matched your search
@@ -5,6 +5,15 @@ namespace Ananta.SDK.Network;
|
||||
|
||||
public static class FrameCodec
|
||||
{
|
||||
// ⛔ donor(kpect 参考实现)在**正常收包路径**上留的每帧调试输出,四份副本里逐行相同。
|
||||
// 无条件执行时它占全部日志约 44%(实测一场 30 分钟 18715/42685 行),
|
||||
// 且每个入站帧都要做一次 hex 格式化 + 一次 stderr 同步写(自动 flush ⇒ 每帧一个系统调用)。
|
||||
// 2026-09-28 起默认关闭;排查 wire / 解密问题时设环境变量 DRMK_FRAME_DBG=1 再启动即可,不用重编译。
|
||||
// ⛑ 下边「帧长非法」分支里那个同名 [FRAME-DBG] 是真出错才打的(且打完就 throw),**保留**。
|
||||
// 它上面那句 rawHead 必须在 XOR 之前取(head 是就地解密的),所以不能延后到分支里 —— 留着。
|
||||
private static readonly bool FrameDebugEnabled =
|
||||
Environment.GetEnvironmentVariable("DRMK_FRAME_DBG") == "1";
|
||||
|
||||
// Public/plain overload retained for handshake/debug compatibility.
|
||||
public static Task<Frame?> ReadFrameAsync(NetworkStream stream, CancellationToken token)
|
||||
=> ReadFrameAsync(stream, null, token);
|
||||
@@ -31,9 +40,12 @@ public static class FrameCodec
|
||||
if (payloadLength != 0)
|
||||
{
|
||||
if (!await ReadExactAsync(stream, payload, token)) return null;
|
||||
var rawPayloadHead = Convert.ToHexString(payload.AsSpan(0, Math.Min(16, payload.Length)));
|
||||
cryptor?.XorInPlace(payload);
|
||||
Console.Error.WriteLine($"[FRAME-DBG] ok len={payloadLength} mode={head[4]} rawPayloadHead={rawPayloadHead} decHead={Convert.ToHexString(head)}");
|
||||
if (FrameDebugEnabled)
|
||||
{
|
||||
var rawPayloadHead = Convert.ToHexString(payload.AsSpan(0, Math.Min(16, payload.Length)));
|
||||
Console.Error.WriteLine($"[FRAME-DBG] ok len={payloadLength} mode={head[4]} rawPayloadHead={rawPayloadHead} decHead={Convert.ToHexString(head)}");
|
||||
}
|
||||
}
|
||||
|
||||
return new Frame(head[4], payload);
|
||||
|
||||
@@ -11,14 +11,28 @@ public sealed class TcpServer
|
||||
private readonly IPEndPoint _endpoint;
|
||||
private readonly RpcRouter _router;
|
||||
private readonly Func<TcpSession, Frame, CancellationToken, Task>? _customFrameHandler;
|
||||
private readonly Func<TcpSession, Task>? _onClosed;
|
||||
private readonly int _heartbeatMs;
|
||||
private readonly int _writeTimeoutMs;
|
||||
private readonly ServerLogger _log;
|
||||
|
||||
public TcpServer(string scope, string host, int port, RpcRouter router, Func<TcpSession, Frame, CancellationToken, Task>? customFrameHandler = null)
|
||||
/// <param name="onClosed">
|
||||
/// 连接收尾钩子(纯可选)。在 <c>session.Close()</c> **之前** await —— 因为 Close 会
|
||||
/// 停掉写侧之后的再发幀,届时已无法通知其它会话。
|
||||
/// </param>
|
||||
public TcpServer(string scope, string host, int port, RpcRouter router,
|
||||
Func<TcpSession, Frame, CancellationToken, Task>? customFrameHandler = null,
|
||||
Func<TcpSession, Task>? onClosed = null,
|
||||
int heartbeatMs = 0,
|
||||
int writeTimeoutMs = 15000)
|
||||
{
|
||||
_scope = scope;
|
||||
_endpoint = new IPEndPoint(IPAddress.Parse(host), port);
|
||||
_router = router;
|
||||
_customFrameHandler = customFrameHandler;
|
||||
_onClosed = onClosed;
|
||||
_heartbeatMs = heartbeatMs;
|
||||
_writeTimeoutMs = writeTimeoutMs;
|
||||
_log = new ServerLogger(scope);
|
||||
}
|
||||
|
||||
@@ -37,7 +51,7 @@ public sealed class TcpServer
|
||||
|
||||
private async Task HandleClientAsync(TcpClient client, CancellationToken token)
|
||||
{
|
||||
var session = new TcpSession(client, _scope);
|
||||
var session = new TcpSession(client, _scope, _heartbeatMs, _writeTimeoutMs);
|
||||
session.Log.Info($"+ {client.Client.RemoteEndPoint}");
|
||||
|
||||
try
|
||||
@@ -70,6 +84,12 @@ public sealed class TcpServer
|
||||
finally
|
||||
{
|
||||
session.Log.Info("-");
|
||||
// 收尾钩子必须在 Close 之前跑 —— Close 之后这条会话就再也发不出东西了。
|
||||
if (_onClosed is not null)
|
||||
{
|
||||
try { await _onClosed(session); }
|
||||
catch (Exception ex) { session.Log.Warn($"onClosed handler failed: {ex.Message}"); }
|
||||
}
|
||||
session.Close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,20 @@ namespace Ananta.SDK.Network;
|
||||
public sealed class TcpSession
|
||||
{
|
||||
private readonly SemaphoreSlim _sendLock = new(1, 1);
|
||||
private readonly CancellationTokenSource _closed = new();
|
||||
/// <summary>会话关闭时触发。逐字对齐捐赠端 <c>TcpSession.ClosedToken</c>(<c>Ananta.SDK/Network/TcpSession.cs:13</c>)。
|
||||
/// ⛑ 主线原本已有 <c>_closed</c> 且 <c>Close()</c> 会 Cancel,只是没公开 → 这里<b>只加属性,不改生命周期</b>。</summary>
|
||||
public CancellationToken ClosedToken => _closed.Token;
|
||||
private Task? _heartbeat;
|
||||
private int _isClosed;
|
||||
private UxChaCha8? _readCryptor;
|
||||
private UxChaCha8? _writeCryptor;
|
||||
|
||||
/// <summary>服务端 → 客户端传输层 keepalive 间隔(毫秒)。<b>0 = 关闭</b>(沿用旧行为)。</summary>
|
||||
public int HeartbeatMs { get; }
|
||||
/// <summary>单次加密写的上限(毫秒);超时会退役这条连接。<b>0 = 不设上限</b>(沿用旧行为)。</summary>
|
||||
public int WriteTimeoutMs { get; }
|
||||
|
||||
public string Id { get; } = Guid.NewGuid().ToString("N")[..6];
|
||||
public TcpClient Client { get; }
|
||||
public NetworkStream Stream { get; }
|
||||
@@ -18,11 +29,15 @@ public sealed class TcpSession
|
||||
public Dictionary<string, object> Items { get; } = new();
|
||||
public bool CryptReady => _readCryptor is not null && _writeCryptor is not null;
|
||||
|
||||
public TcpSession(TcpClient client, string scope)
|
||||
public TcpSession(TcpClient client, string scope, int heartbeatMs = 0, int writeTimeoutMs = 15000)
|
||||
{
|
||||
Client = client;
|
||||
Stream = client.GetStream();
|
||||
Log = new ServerLogger($"{scope}:{Id}");
|
||||
HeartbeatMs = heartbeatMs;
|
||||
WriteTimeoutMs = writeTimeoutMs;
|
||||
// Small real-time RPCs should not wait for Nagle/delayed-ACK batching.
|
||||
Client.NoDelay = true;
|
||||
}
|
||||
|
||||
public Task<Frame?> ReadFrameAsync(CancellationToken token)
|
||||
@@ -69,23 +84,36 @@ public sealed class TcpSession
|
||||
// UXRSA.Verify result branch is patched out to accept the local
|
||||
// unsigned handshake.
|
||||
|
||||
await _sendLock.WaitAsync(token);
|
||||
try
|
||||
{
|
||||
// The handshake reply itself is plaintext. Encryption becomes active
|
||||
// only for the next frame in each direction.
|
||||
await FrameCodec.WriteFrameAsync(Stream, 0x01, payload, cryptor: null, token: token);
|
||||
_readCryptor = new UxChaCha8(clientWriteKey, streamNonce);
|
||||
_writeCryptor = new UxChaCha8(clientReadKey, streamNonce);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
// The handshake reply itself is plaintext. Encryption becomes active
|
||||
// only for the next frame in each direction — which is why the keys are
|
||||
// published *after* this write instead of before it.
|
||||
await WriteRawAsync(0x01, payload, token);
|
||||
_readCryptor = new UxChaCha8(clientWriteKey, streamNonce);
|
||||
_writeCryptor = new UxChaCha8(clientReadKey, streamNonce);
|
||||
|
||||
Log.Info($"HS keys clientWrite={Convert.ToHexString(clientWriteKey)} clientRead={Convert.ToHexString(clientReadKey)} nonce={Convert.ToHexString(streamNonce)} aes={Convert.ToHexString(aesKey)}");
|
||||
|
||||
Log.Info("HS ok -> UXChaCha8 enabled");
|
||||
|
||||
if (HeartbeatMs > 0) _heartbeat ??= SendHeartbeatsAsync();
|
||||
}
|
||||
|
||||
private async Task SendHeartbeatsAsync()
|
||||
{
|
||||
// Keep an inbound liveness signal on every socket, including idle gates,
|
||||
// independently of the client's RPC and heartbeat scheduling.
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(HeartbeatMs));
|
||||
try
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(_closed.Token))
|
||||
await SendFrameAsync(0x03, BitConverter.GetBytes(Environment.TickCount64), _closed.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (_closed.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn($"heartbeat send stopped: {ex.Message}");
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendFrameAsync(byte mode, byte[] body, CancellationToken token)
|
||||
@@ -104,22 +132,51 @@ public sealed class TcpSession
|
||||
return WriteEncryptedFrameAsync(0x09, RpcPacketWriter.WriteReturn(methodId, invokeId, err, body), token);
|
||||
}
|
||||
|
||||
private async Task WriteEncryptedFrameAsync(byte mode, ReadOnlyMemory<byte> body, CancellationToken token)
|
||||
private Task WriteRawAsync(byte mode, ReadOnlyMemory<byte> body, CancellationToken token)
|
||||
=> WriteFrameAsync(mode, body, null, token);
|
||||
|
||||
private async Task WriteFrameAsync(byte mode, ReadOnlyMemory<byte> body, UxChaCha8? cryptor, CancellationToken token)
|
||||
{
|
||||
await _sendLock.WaitAsync(token);
|
||||
using var deadline = WriteTimeoutMs > 0
|
||||
? CancellationTokenSource.CreateLinkedTokenSource(token, _closed.Token)
|
||||
: null;
|
||||
if (deadline is not null) deadline.CancelAfter(WriteTimeoutMs);
|
||||
var linked = deadline?.Token ?? token;
|
||||
var acquired = false;
|
||||
try
|
||||
{
|
||||
await FrameCodec.WriteFrameAsync(Stream, mode, body, _writeCryptor, token);
|
||||
await _sendLock.WaitAsync(linked);
|
||||
acquired = true;
|
||||
await FrameCodec.WriteFrameAsync(Stream, mode, body, cryptor, linked);
|
||||
}
|
||||
catch (OperationCanceledException) when (!_closed.IsCancellationRequested && !token.IsCancellationRequested)
|
||||
{
|
||||
// A partially-written encrypted frame cannot be retried: the stream cipher already
|
||||
// advanced. Retire only this socket so everyone else keeps being served.
|
||||
Close();
|
||||
throw new IOException($"TCP send cancelled or exceeded the {(WriteTimeoutMs > 0 ? WriteTimeoutMs : 0)}-ms write deadline");
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or SocketException)
|
||||
{
|
||||
Close();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
if (acquired) _sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private Task WriteEncryptedFrameAsync(byte mode, ReadOnlyMemory<byte> body, CancellationToken token)
|
||||
=> WriteFrameAsync(mode, body, _writeCryptor, token);
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _isClosed, 1) != 0) return;
|
||||
_closed.Cancel();
|
||||
try { Client.Close(); } catch { }
|
||||
_sendLock.Dispose();
|
||||
// Pending async sends may still own or await this semaphore. Disposing it
|
||||
// here races their finally/Release and hides the actual socket closure.
|
||||
// It owns no wait handle; let it be collected with this session.
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user