chore: initial project import
This commit is contained in:
commit
26620707a0
347 files changed
+56172
No files matched your search
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="$(MSBuildThisFileDirectory)../../../BannedSymbols.Node.txt" />
|
||||
<ProjectReference Include="..\Fleet.Core\Fleet.Core.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Fleet.Rpc;
|
||||
|
||||
/// <summary>框架保留的方法名。<c>fleet.*</c> 只属于框架,产品使用自己的前缀。</summary>
|
||||
public static class FleetMethods
|
||||
{
|
||||
public const string Challenge = "fleet.challenge";
|
||||
public const string Auth = "fleet.auth";
|
||||
public const string Hello = "fleet.hello";
|
||||
public const string Heartbeat = "fleet.heartbeat";
|
||||
public const string StateSync = "fleet.state.sync";
|
||||
public const string OutboxAck = "fleet.outbox.ack";
|
||||
public const string Revoked = "fleet.revoked";
|
||||
}
|
||||
|
||||
/// <summary>二进制通道:0–15 保留给框架,产品从 16 开始。</summary>
|
||||
public static class FleetChannels
|
||||
{
|
||||
public const byte FirstProductChannel = 16;
|
||||
}
|
||||
|
||||
public sealed class ChallengeParams
|
||||
{
|
||||
[JsonPropertyName("nonce")]
|
||||
public string Nonce { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("expires_at_ms")]
|
||||
public long ExpiresAtMs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AuthParams
|
||||
{
|
||||
[JsonPropertyName("node_id")]
|
||||
public string NodeId { get; set; } = "";
|
||||
|
||||
/// <summary>对 <c>fleet|v1|auth|{node_id}|{nonce}</c> 的签名(base64)。</summary>
|
||||
[JsonPropertyName("signature")]
|
||||
public string Signature { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class AuthResult
|
||||
{
|
||||
/// <summary>pending | active</summary>
|
||||
[JsonPropertyName("status")]
|
||||
public string Status { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class HelloParams
|
||||
{
|
||||
[JsonPropertyName("node_version")]
|
||||
public string NodeVersion { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("capabilities")]
|
||||
public List<string> Capabilities { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("now_utc_ms")]
|
||||
public long NowUtcMs { get; set; }
|
||||
|
||||
/// <summary>产品自定义的元数据(例如 Monitor 的 tls_spki、probe_ports、local_limits 摘要)。</summary>
|
||||
[JsonPropertyName("meta")]
|
||||
public JsonElement? Meta { get; set; }
|
||||
}
|
||||
|
||||
public sealed class HelloResult
|
||||
{
|
||||
[JsonPropertyName("server_time_ms")]
|
||||
public long ServerTimeMs { get; set; }
|
||||
|
||||
/// <summary>节点时钟 − 服务端时钟(毫秒,未扣除网络往返)。</summary>
|
||||
[JsonPropertyName("clock_offset_ms")]
|
||||
public long ClockOffsetMs { get; set; }
|
||||
}
|
||||
|
||||
public sealed class HeartbeatParams
|
||||
{
|
||||
[JsonPropertyName("extra")]
|
||||
public JsonElement? Extra { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StateSyncParams
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public long Version { get; set; }
|
||||
|
||||
[JsonPropertyName("doc")]
|
||||
public JsonElement Doc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class StateSyncResult
|
||||
{
|
||||
[JsonPropertyName("applied_version")]
|
||||
public long AppliedVersion { get; set; }
|
||||
|
||||
[JsonPropertyName("rejected")]
|
||||
public List<RejectedItem> Rejected { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class RejectedItem
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
/// <summary>unsupported | budget_exceeded | policy_denied | …</summary>
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class OutboxAckParams
|
||||
{
|
||||
[JsonPropertyName("ids")]
|
||||
public List<string> Ids { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("duplicates")]
|
||||
public List<string> Duplicates { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class RevokedParams
|
||||
{
|
||||
[JsonPropertyName("reason")]
|
||||
public string Reason { get; set; } = "";
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(RpcMessage))]
|
||||
[JsonSerializable(typeof(ChallengeParams))]
|
||||
[JsonSerializable(typeof(AuthParams))]
|
||||
[JsonSerializable(typeof(AuthResult))]
|
||||
[JsonSerializable(typeof(HelloParams))]
|
||||
[JsonSerializable(typeof(HelloResult))]
|
||||
[JsonSerializable(typeof(HeartbeatParams))]
|
||||
[JsonSerializable(typeof(StateSyncParams))]
|
||||
[JsonSerializable(typeof(StateSyncResult))]
|
||||
[JsonSerializable(typeof(OutboxAckParams))]
|
||||
[JsonSerializable(typeof(RevokedParams))]
|
||||
public sealed partial class FleetRpcContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,35 @@
|
||||
namespace Fleet.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// 发件箱二进制帧:<c>u8 channel | 16 字节 msg_id (UUIDv7) | 正文</c>。
|
||||
/// 服务端按 msg_id 去重,持久化之后才回复 <c>fleet.outbox.ack</c>。
|
||||
/// </summary>
|
||||
public static class OutboxFrame
|
||||
{
|
||||
public const int HeaderBytes = 1 + 16;
|
||||
|
||||
public static bool TryParse(ReadOnlyMemory<byte> frame, out byte channel, out Guid msgId, out ReadOnlyMemory<byte> payload)
|
||||
{
|
||||
if (frame.Length < HeaderBytes)
|
||||
{
|
||||
channel = 0;
|
||||
msgId = Guid.Empty;
|
||||
payload = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
channel = frame.Span[0];
|
||||
msgId = new Guid(frame.Span.Slice(1, 16), bigEndian: true);
|
||||
payload = frame[HeaderBytes..];
|
||||
return true;
|
||||
}
|
||||
|
||||
public static byte[] Build(byte channel, Guid msgId, ReadOnlySpan<byte> payload)
|
||||
{
|
||||
byte[] frame = new byte[HeaderBytes + payload.Length];
|
||||
frame[0] = channel;
|
||||
msgId.TryWriteBytes(frame.AsSpan(1, 16), bigEndian: true, out _);
|
||||
payload.CopyTo(frame.AsSpan(HeaderBytes));
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Fleet.Rpc;
|
||||
|
||||
/// <summary>
|
||||
/// 节点间点对点会话的握手(Probe → Agent,TLS 帧)。除了签名认证,双方各生成一个临时 ECDH 密钥,
|
||||
/// 经过 HKDF 派生出数据面使用的 <c>session_id</c> 与 <c>session_key</c>(所以不需要单独的“下发密钥”方法)。
|
||||
/// </summary>
|
||||
public static class PeerMethods
|
||||
{
|
||||
public const string Challenge = "fleet.peer.challenge";
|
||||
public const string Auth = "fleet.peer.auth";
|
||||
}
|
||||
|
||||
public sealed class PeerChallengeParams
|
||||
{
|
||||
[JsonPropertyName("nonce")]
|
||||
public string Nonce { get; set; } = "";
|
||||
|
||||
/// <summary>服务端(监听方)的临时 ECDH 公钥(SPKI,base64)。</summary>
|
||||
[JsonPropertyName("ecdh_pub")]
|
||||
public string EcdhPublicKey { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class PeerAuthParams
|
||||
{
|
||||
[JsonPropertyName("peer_id")]
|
||||
public string PeerId { get; set; } = "";
|
||||
|
||||
/// <summary>发起方的临时 ECDH 公钥(SPKI,base64)。</summary>
|
||||
[JsonPropertyName("ecdh_pub")]
|
||||
public string EcdhPublicKey { get; set; } = "";
|
||||
|
||||
/// <summary>对 <c>fleet|v1|peer|{peer_id}|{nonce}|{client_ecdh_pub}|{server_ecdh_pub}</c> 的签名(base64)。</summary>
|
||||
[JsonPropertyName("signature")]
|
||||
public string Signature { get; set; } = "";
|
||||
|
||||
/// <summary>发起方的程序版本(可选,只用于展示)。</summary>
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { get; set; }
|
||||
}
|
||||
|
||||
public sealed class PeerAuthResult
|
||||
{
|
||||
[JsonPropertyName("session_id")]
|
||||
public string SessionId { get; set; } = "";
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(PeerChallengeParams))]
|
||||
[JsonSerializable(typeof(PeerAuthParams))]
|
||||
[JsonSerializable(typeof(PeerAuthResult))]
|
||||
public sealed partial class PeerRpcContext : JsonSerializerContext;
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Fleet.Core;
|
||||
|
||||
namespace Fleet.Rpc;
|
||||
|
||||
/// <summary>JSON-RPC 2.0 信封。请求、响应、通知共用一个类型,缺失的字段不序列化。</summary>
|
||||
public sealed class RpcMessage
|
||||
{
|
||||
[JsonPropertyName("jsonrpc")]
|
||||
public string Jsonrpc { get; set; } = "2.0";
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public long? Id { get; set; }
|
||||
|
||||
[JsonPropertyName("method")]
|
||||
public string? Method { get; set; }
|
||||
|
||||
[JsonPropertyName("params")]
|
||||
public JsonElement? Params { get; set; }
|
||||
|
||||
[JsonPropertyName("result")]
|
||||
public JsonElement? Result { get; set; }
|
||||
|
||||
[JsonPropertyName("error")]
|
||||
public RpcError? Error { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsRequest => Method is not null && Id is not null;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsNotification => Method is not null && Id is null;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsResponse => Method is null && Id is not null;
|
||||
|
||||
public static RpcMessage Request<T>(long id, string method, T value, JsonTypeInfo<T> typeInfo) =>
|
||||
new() { Id = id, Method = method, Params = JsonSerializer.SerializeToElement(value, typeInfo) };
|
||||
|
||||
public static RpcMessage Notification<T>(string method, T value, JsonTypeInfo<T> typeInfo) =>
|
||||
new() { Method = method, Params = JsonSerializer.SerializeToElement(value, typeInfo) };
|
||||
|
||||
public static RpcMessage Success<T>(long id, T value, JsonTypeInfo<T> typeInfo) =>
|
||||
new() { Id = id, Result = JsonSerializer.SerializeToElement(value, typeInfo) };
|
||||
|
||||
public static RpcMessage Failure(long? id, FleetError error) =>
|
||||
new() { Id = id, Error = RpcError.From(error) };
|
||||
}
|
||||
|
||||
public sealed class RpcError
|
||||
{
|
||||
public const int MethodNotFound = -32601;
|
||||
public const int InvalidParams = -32602;
|
||||
public const int InternalError = -32603;
|
||||
public const int ApplicationError = -32000;
|
||||
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
[JsonPropertyName("message")]
|
||||
public string Message { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("data")]
|
||||
public RpcErrorData? Data { get; set; }
|
||||
|
||||
public static RpcError From(FleetError error)
|
||||
{
|
||||
int code = error.Code == FleetErrors.Unsupported ? MethodNotFound
|
||||
: error.Code == FleetErrors.InvalidParams ? InvalidParams
|
||||
: error.Code == FleetErrors.Internal ? InternalError
|
||||
: ApplicationError;
|
||||
return new RpcError { Code = code, Message = error.Message, Data = new RpcErrorData { Code = error.Code.Value } };
|
||||
}
|
||||
|
||||
public FleetError ToFleetError() =>
|
||||
new(new ErrorCode(Data?.Code ?? "fleet_internal"), Message);
|
||||
}
|
||||
|
||||
public sealed class RpcErrorData
|
||||
{
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("detail")]
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Fleet.Core;
|
||||
|
||||
namespace Fleet.Rpc;
|
||||
|
||||
/// <summary>方法路由,与传输方式无关。<typeparamref name="TContext"/> 由传输层提供(例如调用方节点)。</summary>
|
||||
public sealed class RpcRouter<TContext>
|
||||
{
|
||||
private delegate ValueTask<Result<JsonElement?>> Route(TContext context, JsonElement? parameters, CancellationToken ct);
|
||||
|
||||
private static readonly JsonElement s_emptyObject = JsonDocument.Parse("{}").RootElement.Clone();
|
||||
|
||||
private readonly Dictionary<string, Route> _routes = new(StringComparer.Ordinal);
|
||||
private readonly Action<string, Exception>? _onUnhandled;
|
||||
|
||||
public RpcRouter(Action<string, Exception>? onUnhandled = null)
|
||||
{
|
||||
_onUnhandled = onUnhandled;
|
||||
}
|
||||
|
||||
public bool Has(string method) => _routes.ContainsKey(method);
|
||||
|
||||
public IReadOnlyCollection<string> Methods => _routes.Keys;
|
||||
|
||||
public RpcRouter<TContext> On<TParams, TResult>(
|
||||
string method,
|
||||
JsonTypeInfo<TParams> paramsInfo,
|
||||
JsonTypeInfo<TResult> resultInfo,
|
||||
Func<TContext, TParams, CancellationToken, ValueTask<Result<TResult>>> handler)
|
||||
{
|
||||
_routes[method] = async (context, raw, ct) =>
|
||||
{
|
||||
if (!TryDeserialize(raw, paramsInfo, out TParams? p))
|
||||
{
|
||||
return Result<JsonElement?>.Fail(FleetErrors.InvalidParams, "params could not be parsed");
|
||||
}
|
||||
|
||||
Result<TResult> r = await handler(context, p, ct);
|
||||
return r.TryGet(out TResult? value, out FleetError? error)
|
||||
? Result<JsonElement?>.Ok(JsonSerializer.SerializeToElement(value, resultInfo))
|
||||
: Result<JsonElement?>.Fail(error);
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public RpcRouter<TContext> OnNotify<TParams>(
|
||||
string method,
|
||||
JsonTypeInfo<TParams> paramsInfo,
|
||||
Func<TContext, TParams, CancellationToken, ValueTask> handler)
|
||||
{
|
||||
_routes[method] = async (context, raw, ct) =>
|
||||
{
|
||||
if (!TryDeserialize(raw, paramsInfo, out TParams? p))
|
||||
{
|
||||
return Result<JsonElement?>.Fail(FleetErrors.InvalidParams, "params could not be parsed");
|
||||
}
|
||||
|
||||
await handler(context, p, ct);
|
||||
return Result<JsonElement?>.Ok(null);
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理一条请求或通知。请求返回响应;通知返回 null(失败时只丢弃)。
|
||||
/// 处理函数里的异常属于程序缺陷:在这个边界捕获,回复 internal 错误,并交给 <c>onUnhandled</c> 记录。
|
||||
/// </summary>
|
||||
public async ValueTask<RpcMessage?> DispatchAsync(TContext context, RpcMessage message, CancellationToken ct)
|
||||
{
|
||||
string method = message.Method ?? "";
|
||||
if (!_routes.TryGetValue(method, out Route? route))
|
||||
{
|
||||
return message.Id is null ? null : RpcMessage.Failure(message.Id, new FleetError(FleetErrors.Unsupported, $"unknown method {method}"));
|
||||
}
|
||||
|
||||
Result<JsonElement?> result;
|
||||
try
|
||||
{
|
||||
result = await route(context, message.Params, ct);
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_onUnhandled?.Invoke(method, ex);
|
||||
result = Result<JsonElement?>.Fail(FleetErrors.Internal, "internal error");
|
||||
}
|
||||
|
||||
if (message.Id is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.TryGet(out JsonElement? value, out FleetError? error)
|
||||
? new RpcMessage { Id = message.Id, Result = value ?? s_emptyObject }
|
||||
: RpcMessage.Failure(message.Id, error);
|
||||
}
|
||||
|
||||
private static bool TryDeserialize<T>(JsonElement? raw, JsonTypeInfo<T> info, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out T? value)
|
||||
{
|
||||
value = default;
|
||||
try
|
||||
{
|
||||
value = raw is null ? JsonSerializer.Deserialize("{}", info) : raw.Value.Deserialize(info);
|
||||
return value is not null;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Threading.Channels;
|
||||
using Fleet.Core;
|
||||
|
||||
namespace Fleet.Rpc;
|
||||
|
||||
public enum FrameKind : byte
|
||||
{
|
||||
Text = 0,
|
||||
Binary = 1,
|
||||
}
|
||||
|
||||
public readonly record struct RpcFrame(FrameKind Kind, ReadOnlyMemory<byte> Data);
|
||||
|
||||
/// <summary>
|
||||
/// 一条双向的、保序的消息通道:文本帧承载 JSON-RPC,二进制帧承载发件箱。
|
||||
/// 节点到服务端用 WebSocket,Probe 到 Agent 用 TLS 帧;会话引擎对两者一视同仁。
|
||||
/// </summary>
|
||||
public interface IRpcTransport : IAsyncDisposable
|
||||
{
|
||||
ValueTask SendAsync(RpcFrame frame, CancellationToken ct);
|
||||
|
||||
/// <summary>对端关闭时返回 null。</summary>
|
||||
ValueTask<RpcFrame?> ReceiveAsync(CancellationToken ct);
|
||||
|
||||
ValueTask CloseAsync(string reason, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class RpcSessionOptions
|
||||
{
|
||||
public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <summary>待处理的入站请求上限;超过时接收循环等待(背压)。</summary>
|
||||
public int InboundQueue { get; set; } = 256;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 传输无关的会话:请求与响应的关联、方法路由、二进制帧。
|
||||
/// 入站的请求和通知由独立的任务按顺序处理,接收循环只负责读帧和完成响应,
|
||||
/// 所以处理函数里可以等待向对端发起的请求而不会死锁。
|
||||
/// </summary>
|
||||
public sealed class RpcSession<TContext> : IAsyncDisposable
|
||||
{
|
||||
private readonly IRpcTransport _transport;
|
||||
private readonly RpcRouter<TContext> _router;
|
||||
private readonly TContext _context;
|
||||
private readonly TimeProvider _time;
|
||||
private readonly RpcSessionOptions _options;
|
||||
private readonly ConcurrentDictionary<long, TaskCompletionSource<RpcMessage>> _pending = new();
|
||||
private readonly Channel<RpcMessage> _inbound;
|
||||
private readonly CancellationTokenSource _stop = new();
|
||||
private long _nextId;
|
||||
private long _lastReceive;
|
||||
|
||||
public RpcSession(IRpcTransport transport, RpcRouter<TContext> router, TContext context, TimeProvider time, RpcSessionOptions? options = null)
|
||||
{
|
||||
_transport = transport;
|
||||
_router = router;
|
||||
_context = context;
|
||||
_time = time;
|
||||
_lastReceive = time.GetTimestamp();
|
||||
_options = options ?? new RpcSessionOptions();
|
||||
_inbound = Channel.CreateBounded<RpcMessage>(new BoundedChannelOptions(_options.InboundQueue) { SingleReader = true, FullMode = BoundedChannelFullMode.Wait });
|
||||
}
|
||||
|
||||
/// <summary>收到二进制帧:<c>u8 channel | 正文</c>(发件箱帧再往里解析)。</summary>
|
||||
public Func<ReadOnlyMemory<byte>, CancellationToken, ValueTask>? OnBinary { get; set; }
|
||||
|
||||
/// <summary>对端上一次发来任何帧到现在的时间。静默死亡(断电、进程被冻结)的对端不会关闭连接,调用方据此设置空闲超时。</summary>
|
||||
public TimeSpan IdleFor => _time.GetElapsedTime(Volatile.Read(ref _lastReceive));
|
||||
|
||||
/// <summary>读帧并分发,直到连接关闭或被取消。</summary>
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct, _stop.Token);
|
||||
Task dispatcher = Task.Run(() => DispatchAsync(linked.Token), CancellationToken.None);
|
||||
try
|
||||
{
|
||||
while (!linked.IsCancellationRequested)
|
||||
{
|
||||
RpcFrame? frame = await _transport.ReceiveAsync(linked.Token);
|
||||
if (frame is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Volatile.Write(ref _lastReceive, _time.GetTimestamp());
|
||||
if (frame.Value.Kind == FrameKind.Binary)
|
||||
{
|
||||
if (OnBinary is not null)
|
||||
{
|
||||
await OnBinary(frame.Value.Data, linked.Token);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
RpcMessage? message = Parse(frame.Value.Data);
|
||||
if (message is null)
|
||||
{
|
||||
continue; // 格式不对的帧忽略,连接保持可用
|
||||
}
|
||||
|
||||
if (message.IsResponse)
|
||||
{
|
||||
if (_pending.TryGetValue(message.Id!.Value, out TaskCompletionSource<RpcMessage>? waiter))
|
||||
{
|
||||
waiter.TrySetResult(message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _inbound.Writer.WriteAsync(message, linked.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_inbound.Writer.TryComplete();
|
||||
foreach (TaskCompletionSource<RpcMessage> waiter in _pending.Values)
|
||||
{
|
||||
waiter.TrySetCanceled(CancellationToken.None);
|
||||
}
|
||||
|
||||
await linked.CancelAsync();
|
||||
try
|
||||
{
|
||||
await dispatcher;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 正常结束
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<JsonElement>> RequestAsync(string method, JsonElement? parameters, CancellationToken ct, TimeSpan? timeout = null)
|
||||
{
|
||||
long id = Interlocked.Increment(ref _nextId);
|
||||
var waiter = new TaskCompletionSource<RpcMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_pending[id] = waiter;
|
||||
try
|
||||
{
|
||||
await SendAsync(new RpcMessage { Id = id, Method = method, Params = parameters }, ct);
|
||||
using var timer = new CancellationTokenSource(timeout ?? _options.RequestTimeout, _time);
|
||||
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timer.Token);
|
||||
RpcMessage reply = await waiter.Task.WaitAsync(linked.Token);
|
||||
return reply.Error is not null
|
||||
? Result<JsonElement>.Fail(reply.Error.ToFleetError())
|
||||
: Result<JsonElement>.Ok(reply.Result ?? default);
|
||||
}
|
||||
catch (OperationCanceledException) when (!ct.IsCancellationRequested)
|
||||
{
|
||||
return Result<JsonElement>.Fail(FleetErrors.Timeout, $"{method} timed out or the session closed");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pending.TryRemove(id, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<TResult>> RequestAsync<TParams, TResult>(
|
||||
string method, TParams parameters, JsonTypeInfo<TParams> paramsInfo, JsonTypeInfo<TResult> resultInfo, CancellationToken ct, TimeSpan? timeout = null)
|
||||
{
|
||||
Result<JsonElement> raw = await RequestAsync(method, JsonSerializer.SerializeToElement(parameters, paramsInfo), ct, timeout);
|
||||
if (!raw.TryGet(out JsonElement value, out FleetError? error))
|
||||
{
|
||||
return Result<TResult>.Fail(error);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TResult? result = value.Deserialize(resultInfo);
|
||||
return result is null ? Result<TResult>.Fail(FleetErrors.Internal, "empty result") : Result<TResult>.Ok(result);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return Result<TResult>.Fail(FleetErrors.Internal, "result could not be parsed");
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask NotifyAsync<TParams>(string method, TParams parameters, JsonTypeInfo<TParams> paramsInfo, CancellationToken ct) =>
|
||||
SendAsync(RpcMessage.Notification(method, parameters, paramsInfo), ct);
|
||||
|
||||
public ValueTask SendAsync(RpcMessage message, CancellationToken ct) =>
|
||||
_transport.SendAsync(new RpcFrame(FrameKind.Text, JsonSerializer.SerializeToUtf8Bytes(message, FleetRpcContext.Default.RpcMessage)), ct);
|
||||
|
||||
public ValueTask SendBinaryAsync(ReadOnlyMemory<byte> frame, CancellationToken ct) =>
|
||||
_transport.SendAsync(new RpcFrame(FrameKind.Binary, frame), ct);
|
||||
|
||||
public ValueTask CloseAsync(string reason, CancellationToken ct) => _transport.CloseAsync(reason, ct);
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _stop.CancelAsync();
|
||||
await _transport.DisposeAsync();
|
||||
_stop.Dispose();
|
||||
}
|
||||
|
||||
private async Task DispatchAsync(CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
await foreach (RpcMessage message in _inbound.Reader.ReadAllAsync(ct))
|
||||
{
|
||||
RpcMessage? response = await _router.DispatchAsync(_context, message, ct);
|
||||
if (response is not null)
|
||||
{
|
||||
await SendAsync(response, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 会话结束
|
||||
}
|
||||
}
|
||||
|
||||
private static RpcMessage? Parse(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize(data.Span, FleetRpcContext.Default.RpcMessage);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user