using System.Collections.Concurrent; using System.Net.WebSockets; using System.Threading.Channels; using Fleet.Core; using Fleet.Rpc; namespace Fleet.Server; /// /// 一个节点的 WebSocket 会话:挑战 → 签名认证 → hello → 状态推送 / 收件箱 / 产品 RPC。 /// 收到的消息按顺序处理,所以产品的处理函数不能等待“向同一个节点发起的请求”,否则会死锁。 /// internal sealed partial class NodeConnection : IDisposable { private static readonly TimeSpan s_authTimeout = TimeSpan.FromSeconds(15); private static readonly TimeSpan s_presenceRefresh = TimeSpan.FromSeconds(60); private static readonly TimeSpan s_statePushRetry = TimeSpan.FromSeconds(5); private static readonly TimeSpan s_closeTimeout = TimeSpan.FromSeconds(2); private readonly FleetLinkHost _host; private readonly WebSocket _socket; private readonly SemaphoreSlim _sendLock = new(1, 1); private readonly ConcurrentDictionary> _pending = new(); private readonly Channel _stateSignal = Channel.CreateBounded(new BoundedChannelOptions(1) { FullMode = BoundedChannelFullMode.DropWrite }); private readonly Dictionary _lastPushedVersion = new(StringComparer.Ordinal); private readonly CancellationTokenSource _lifetime = new(); private long _nextRequestId; private long _lastPresenceTicks; private NodeRecord? _node; private NodeSessionInfo? _session; private string? _closeReason; private bool _helloDone; private int _violations; public NodeConnection(FleetLinkHost host, WebSocket socket) { _host = host; _socket = socket; } public NodeId NodeId => _node!.Id; public NodeSessionInfo Session => _session!; public async Task RunAsync(CancellationToken ct) { using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct, _lifetime.Token); CancellationToken token = linked.Token; bool registered = false; Task? pusher = null; try { if (!await AuthenticateAsync(token)) { return; } NodeConnection? old = _host.Register(this); registered = true; if (old is not null) { await old.CloseAsync("replaced"); } string status = StatusName(_node!.Status); FleetLog.Connected(_host.Logger, _node.Id.Value, status); await UpdatePresenceAsync(online: true, token); await FireAsync(NodeEventKind.Online, token); pusher = Task.Run(() => StatePushLoopAsync(token), CancellationToken.None); await ReceiveLoopAsync(token); } catch (OperationCanceledException) { // 关闭原因由触发取消的一方设置 } catch (WebSocketException) { _closeReason = "connection lost"; } finally { await _lifetime.CancelAsync(); if (pusher is not null) { try { await pusher; } catch (OperationCanceledException) { // 正常结束 } } foreach (TaskCompletionSource waiter in _pending.Values) { waiter.TrySetCanceled(CancellationToken.None); } if (registered) { _host.Unregister(this); FleetLog.Disconnected(_host.Logger, _node!.Id.Value, _closeReason ?? "closed"); // 被新连接替换时,在线状态属于新连接。 if (!_host.IsOnline(_node.Id)) { await UpdatePresenceAsync(online: false, CancellationToken.None); await FireAsync(NodeEventKind.Offline, CancellationToken.None); } } if (_socket.State is WebSocketState.Open or WebSocketState.CloseReceived) { // 完整的关闭握手:发送 Close 并等待对端确认(有上限),这样对端能收到关闭原因 try { using var closeTimeout = new CancellationTokenSource(s_closeTimeout, _host.Time); await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, _closeReason ?? "closed", closeTimeout.Token); } catch (WebSocketException) { // 对端已经断开 } catch (OperationCanceledException) { // 对端没有在时限内确认:不再等待 } } } } public void Dispose() { _lifetime.Dispose(); _sendLock.Dispose(); } }