using System.Net.WebSockets; using System.Text.Json; using Fleet.Core; using Fleet.Rpc; namespace Fleet.Server; /// 发给节点:关闭、吊销、发送一条消息、发起请求并等响应。 internal sealed partial class NodeConnection { /// /// 服务端发起的关闭:发送 Close 帧,等对端确认之后接收循环自然结束。 /// 注意不能直接取消接收的令牌——取消 ReceiveAsync 会中止整个 WebSocket,对端拿不到关闭原因;取消只作为超时兜底。 /// public async ValueTask CloseAsync(string reason) { _closeReason ??= reason; try { await _sendLock.WaitAsync(CancellationToken.None); } catch (ObjectDisposedException) { return; } try { if (_disposed) { return; } _lifetime.CancelAfter(s_closeTimeout); if (_socket.State == WebSocketState.Open) { await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, reason, CancellationToken.None); } } catch (WebSocketException) { // 对端已经断开 } finally { _sendLock.Release(); } } public async ValueTask RevokeAsync(string reason, CancellationToken ct) { await SendAsync(RpcMessage.Notification(FleetMethods.Revoked, new RevokedParams { Reason = reason }, FleetRpcContext.Default.RevokedParams), ct); await CloseAsync(reason); } public async ValueTask SendAsync(RpcMessage message, CancellationToken ct) { if (message.Method is not null) { // 服务端只能发自己有资格发起的方法,并且对端的角色要允许(响应没有方法名,不检查) string? peerRole = _node is null ? null : _host.RoleOf?.Invoke(_node); _host.Router.CheckSend(message.Method, message.Id is null ? MethodKind.Notification : MethodKind.Request, peerRole); } byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(message, FleetRpcContext.Default.RpcMessage); try { await _sendLock.WaitAsync(ct); } catch (ObjectDisposedException) { return false; } try { if (_disposed || _socket.State != WebSocketState.Open) { return false; } if (_cipher is { } cipher) { await _socket.SendAsync(cipher.Seal(0, bytes), WebSocketMessageType.Binary, endOfMessage: true, ct); } else { await _socket.SendAsync(bytes, WebSocketMessageType.Text, endOfMessage: true, ct); } return true; } catch (WebSocketException) { return false; } finally { _sendLock.Release(); } } public async ValueTask> RequestAsync(string method, JsonElement parameters, CancellationToken ct) { long id = Interlocked.Increment(ref _nextRequestId); var waiter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); _pending[id] = waiter; try { var request = new RpcMessage { Id = id, Method = method, Params = parameters }; if (!await SendAsync(request, ct)) { return Result.Fail(FleetErrors.NodeOffline, "node is not connected"); } using var timeout = new CancellationTokenSource(_host.Options.RequestTimeout, _host.Time); using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeout.Token); RpcMessage response = await waiter.Task.WaitAsync(linked.Token); return response.Error is not null ? Result.Fail(response.Error.ToFleetError()) : Result.Ok(response.Result ?? default); } catch (OperationCanceledException) when (!ct.IsCancellationRequested) { return Result.Fail(FleetErrors.Timeout, $"{method} timed out"); } finally { _pending.TryRemove(id, out _); } } }