一个面向 .NET 的高性能 RPC 框架(当前主目标框架为 net10.0),支持:
- Source Generator 自动生成
Proxy/Stub/Codec/Assembly Manifest - Unary、
[Oneway]、客户端流、服务端流、双向流、多流参数 - Protocol v2 协议级取消(
ProtocolV2FrameType.Cancel) - TLS/mTLS、认证、Interceptor、deadline、背压、健康检查与 OpenTelemetry
- 自动服务注册、
Singleton/Connection/Call生命周期、运行时程序集安全注册/注销 Microsoft.Extensions.Hosting、DI、readiness 与优雅排空Socket / NamedPipe / AnonymousPipe / UDS传输,以及实验性的同用户共享内存传输- 内置无反射 DTO Codec,并通过通用 Codec Adapter 接入
SharpPack等复杂图序列化器
核心项目(src/):
SharpLink.Abstractions:契约标记、Protocol v2 公共模型、公共接口、通道与传输抽象SharpLink.Runtime:RpcSession、StreamManager、实例级 Codec Provider、传输实现与底层收发逻辑SharpLink.Sdk:契约项目的单一引用入口,提供分析器、源生成器及 1.0.0 类型转发兼容层SharpLink.Client:客户端 Builder、连接生命周期、请求管理与代理调用通道SharpLink.Server:服务端 Builder、连接管理、Stub 分发、心跳与取消处理SharpLink.Hosting:IServiceCollection扩展与 HostedService 集成SharpLink.Generator:契约/服务分析器与Proxy/Stub代码生成SharpLink.Serializer.SharpPack:精确依赖 SharpPack[1.1.0]的 Codec Adapter(memorypack-binary/v1)
示例(demo/):
HelloWorld:基础调用与多类型参数Streaming:客户端流、服务端流、双向流、多流参数HostApplication:Host 模式完整示例Cancel:协议级取消示例Timeout:默认超时与显式超时示例Oneway:单向调用示例Log:日志配置示例Security:TLS 身份、认证、授权和调用上下文Compression:双向协商压缩AdmissionControl:并发接入和拒绝行为InterceptorsTelemetry:Client/Server Interceptor、Activity 与 MeterResilience:静态端点、Retry 与 Circuit BreakerTransportMatrix:TCP、NamedPipe、UDS、SharedMemory 与 AnonymousPipeMultiCluster:两个独立契约程序集与物理集群路由SeparatedContracts / SeparatedServer / SeparatedClient:分离式契约与多进程示例
测试与基准(test/):
SharpLink.UnitTests:快速单元测试SharpLink.IntegrationTests:真实传输与生成代码集成测试SharpLink.Generator.Tests:分析器与生成器规则测试SharpLink.AotSmoke:AOT/生成器/编解码链路冒烟验证SharpLink.LoadTest*、SharpLink.Benchmarks:压测与基准
环境要求:
- .NET SDK
10.0.102或兼容的10.0SDK
构建与测试:
dotnet build Sharplink.slnx -c Release
dotnet test --project test/SharpLink.UnitTests/SharpLink.UnitTests.csproj -c Release
dotnet test --project test/SharpLink.Generator.Tests/SharpLink.Generator.Tests.csproj -c Release
dotnet run --project test/SharpLink.IntegrationTests/SharpLink.IntegrationTests.csproj -c Release -- --timeout 120s运行示例:
dotnet run --project demo/HelloWorld/HelloWorld.csproj
dotnet run --project demo/Streaming/Streaming.csproj
dotnet run --project demo/HostApplication/HostApplication.csproj
dotnet run --project demo/Cancel/Cancel.csproj
dotnet run --project demo/Timeout/Timeout.csproj
dotnet run --project demo/Oneway/Oneway.csproj
dotnet run --project demo/Log/Log.csproj
dotnet run --project demo/SeparatedServer/SeparatedServer.csproj
dotnet run --project demo/SeparatedClient/SeparatedClient.csproj- RPC 契约接口必须标记
[RpcContract] - RPC 服务实现必须标记
[RpcService] - 契约接口必须继承
IService - 契约及其 containing type 必须 public;公开 nested contract 受支持并获得确定性唯一生成类型名
- RPC route 必须是普通 instance method;
ref/out/in、by-ref return、static method 与 abstract property/indexer/event 会在编译期报告错误 - Contract 所在程序集生成 Descriptor、Proxy、contract-based Stub 和 Codec;Service 所在程序集生成 Activator、生命周期与显式依赖
- 每个生成程序集只有一个可由程序集特性直接定位的 Manifest,不使用
Assembly.GetTypes()扫描 - Generator 会为静态引用的 generated Manifest 发出确定性 bootstrap;应用模块初始化时先注册这些 Manifest,Server
Build()再快照并自动注册[RpcService]。Build 后加载的插件仍需显式RegisterAssembly - 可以通过程序集级特性缩小扫描范围:
[assembly: SharpLinkRpcContracts(typeof(MyContract1), typeof(MyContract2))]SharpLinkMultiClusterClientBuilder 管理多个彼此隔离的子客户端。创建代理时,每个契约只映射到一个子客户端;之后通过该代理发起 RPC 调用时,不会再查询协调器、集群名称或逐调用路由上下文。
静态契约程序集路由应声明在应用或宿主程序集中,不要写入可复用的契约包:
[assembly: SharpLinkClusterContractAssembly("orders", typeof(OrderContractsMarker))]
[assembly: SharpLinkClusterContractAssembly("payments", typeof(PaymentContractsMarker))]每个槽位继续使用现有的子 Builder API 配置。委托内的 UseCluster 仍然只负责该槽位自身的端点拓扑,并不是多集群协调器 API。
var client = SharpLinkMultiClusterClientBuilder.Create()
.AddCluster("orders", child => child.UseTcp("127.0.0.1", 5101))
.AddCluster("payments", child => child.UseTcp("127.0.0.1", 5102))
.Build();
await client.ConnectAsync();
var orders = client.Get<IOrderService>();
var payments = client.Get<IPaymentService>();默认情况下,每个槽位都必须具有契约。专门预留给插件的槽位必须显式启用动态契约:
.AddCluster("plugins", child => child.UseTcp("127.0.0.1", 5103),
slot => slot.AllowDynamicContracts = true)动态契约必须注册到明确指定的槽位,相关 API 为 RegisterAssembly(cluster, assembly)、UnregisterAssemblyAsync(cluster, assembly, timeout) 和 ReplaceAssemblyAsync(cluster, oldAssembly, newAssembly, timeout)。构建完成的协调器也可以原子地新增、替换和移除完整槽位:
await client.AddClusterAsync("search",
child => child.UseTcp("127.0.0.1", 5201).UseRetry(),
slot => slot.AllowDynamicContracts = true);
await client.ReplaceClusterAsync("search",
child => child.UseDnsEndpoints(
"search.internal", 5201, SharpLinkTransportFactories.Sockets()),
TimeSpan.FromSeconds(30));
var removal = await client.RemoveClusterAsync("search", TimeSpan.FromSeconds(30));协调器处于 Ready 状态时,候选槽位会在发布前建立连接。替换只影响之后的 Get<T>() 调用;已经创建的代理仍绑定到退役子客户端,不会被静默重绑。当前不存在默认集群、逐调用集群覆盖、跨集群重试,线上协议也不携带集群标识。生命周期与迁移细节见 doc/dynamic-modules-and-multicluster.md。
SharpLink.Sdk 包会把当前契约写到 obj/<configuration>/<tfm>/SharpLink.Contracts.sharplink.json。JSON 按 Contract、Method、DTO member、enum、union 与 Service route 的稳定 ID 排序,不包含时间戳或源码路径;schemaFingerprint 覆盖规范化后的完整内容,可直接作为 CI 构建产物保存。
把上一个已发布版本的文件保存到仓库,并在项目中指定基线:
<PropertyGroup>
<SharpLinkContractBaseline>contracts/previous.sharplink.json</SharpLinkContractBaseline>
<!-- 可选:覆盖当前 Manifest 的输出位置 -->
<SharpLinkContractManifestOutput>artifacts/contracts/current.sharplink.json</SharpLinkContractManifestOutput>
</PropertyGroup>没有基线时只生成当前 Manifest。存在基线时,SHARPLINK024–SHARPLINK035 与 SHARPLINK037 会在可用的 Contract、Method、DTO member 或 Service 位置报告格式错误和破坏性变化,并在消息中给出修复方式。例如 DTO 成员重命名应显式保留旧 ID:
public sealed class Customer
{
[RpcMember(7)] // 重命名前后都保留 7
public string DisplayName { get; init; } = string.Empty;
}新增 Contract、Method 和 optional DTO member 是兼容变化。多态契约可用 [RpcUnionCase(tag, typeof(CaseType))] 固定 union tag;已发布的 tag 不能改派给其他类型。分析全部发生在编译期,不进入运行时路由或 RPC 热路径,NativeAOT 继续使用生成代码而不做反射扫描。
RPC 可达的常规 DTO 会自动生成无反射 Codec,不需要注册序列化器:
public sealed record Address([property: RpcMember(1)] string City);
public sealed class WorkOrder
{
[RpcRequired]
public string Number { get; init; } = string.Empty;
public Address Address { get; init; } = new("");
public List<string> Tags { get; init; } = [];
[RpcIgnore]
public string LocalCacheKey { get; init; } = string.Empty;
}原生子集包含 primitive、enum、nullable、string、数组、List、Dictionary、Memory、ReadOnlyMemory、ImmutableArray、class/struct/record 及无环嵌套。未直接出现在 RPC 签名中的入口可标记 [RpcSerializable]。默认成员 ID 来自稳定成员名 hash;重命名同时要求 wire 兼容时,应保留显式 [RpcMember(id)]。
循环/多态对象图和第三方运行时类型可以交给编译期选择的 Codec Adapter。引用 SharpLink.Serializer.SharpPack 后,[SharpPackable] 会自动选择 SharpPack Adapter;普通 DTO 仍优先使用 SharpLink 原生 Codec:
using SharpPack;
[SharpPackable]
public partial class PluginGraph
{
public PluginGraph? Parent { get; set; }
public List<PluginGraph> Children { get; set; } = [];
}没有框架自带 Attribute 的第三方类型使用通用显式绑定:
[assembly: RpcCodecAdapter(
typeof(ThirdPartyGraph),
typeof(SharpPackRpcCodecAdapter))]Client/Server 不需要 resolver 或手工注册自动 Adapter Codec。高级自定义 formatter 可由调用方创建 SharpPackSerializerContext,再通过 SharpPackRpcCodec.Create<T>(context) 显式 UseCodec;该 Codec 仍保持最高优先级且 Context 所有权属于调用方。
每个 Adapter Scope 按 Runtime Context × generated Manifest × AdapterId 隔离。同一 Manifest 的闭合类型共享一个 SharpPack Context;自动 Context 拥有独立 formatter graph,不使用进程级默认 formatter slot,不同 Client/Server、插件或替换代际不共享。进程 Catalog 只保存弱 Manifest 引用;动态模块排空后释放 Codec、Scope 和 Context。生成代码直接调用闭合 CreateCodec<T>(),不扫描程序集、不调用 MakeGenericType 或 Activator.CreateInstance。当前设计和迁移约束见 doc/contracts-and-codecs.md 与 doc/migration.md。
压缩默认完全关闭。Client 与 Server 分别按本地偏好注册 Provider;握手有交集时 Server 选择自身列表中的第一个 wire profile,没有交集或只有一端启用时自动发送原始帧:
var server = SharpLinkServerBuilder.Create()
.UseTcp(5000)
.UseRuntime(options =>
{
options.Compression.Providers.Add(
SharpLinkCompressionProviders.CreateBrotli());
options.Compression.MinimumPayloadBytes = 2048;
options.Compression.MinimumSavingsBytes = 96;
options.Compression.MinimumSavingsRatio = 0.08;
})
.Build();内置 Provider 只提供框架自带的 Brotli,并允许为每个方向选择 CompressionLevel。Gzip、Deflate、Zstandard 或其他格式可通过自定义 ISharpLinkCompressionProvider 接入。Provider 的 WireProfile 必须是唯一的 1–64 字节规范 ASCII;dictionary identity 等影响解码的配置必须进入 profile,只影响编码成本的 level 不协商。例如,同一 Zstandard 实现可以分别注册 zstd/v1 与 zstd-dict/0123abcd。实现必须线程安全、NativeAOT 安全,并准确返回 consumed/written bytes。压缩只覆盖业务 payload,路由、deadline、metadata 与 stream ID 保持未压缩;默认收益门槛为 1024 B、64 B 和 5%。完整 wire 格式和故障域见 doc/protocol-v2.md。
压缩在连接握手后按每个方向自动应用,不存在 per-call 强制开关;需要控制是否尝试压缩时,应在对应 Client/Server Runtime Context 配置 Provider 或调整 payload/收益阈值。
服务端可在创建 Service、DI Scope、Codec 调用状态和执行 Interceptor 之前启用累计 admission 规则。默认完全关闭;启用后依次取得 Global → Contract → Method → Partition 中所有已配置的 permit,现有每连接和进程硬并发上限仍作为最后安全边界:
var server = SharpLinkServerBuilder.Create()
.UseTcp(5000)
.UseAdmissionControl(options =>
{
options.Global.UseConcurrency(256);
options.MaxQueuedCalls = 512;
options.MaxQueuedBytes = 16 * 1024 * 1024;
options.MaxQueueDelay = TimeSpan.FromSeconds(2);
options.AddMethod<IOrders>(nameof(IOrders.SubmitAsync), method =>
method.UseTokenBucket(rate =>
{
rate.TokenLimit = 1_000;
rate.TokensPerPeriod = 1_000;
rate.ReplenishmentPeriod = TimeSpan.FromSeconds(1);
}));
options.UsePartition(
context => context.Metadata is { Count: > 0 } metadata ? metadata[0].Value : null,
partition =>
{
partition.MaxPartitions = 1_024;
partition.IdleTimeout = TimeSpan.FromMinutes(5);
partition.UseConcurrency(8);
});
})
.Build();速率策略可选 TokenBucket、FixedWindow 或 SlidingWindow,公共 API 不暴露底层 System.Threading.RateLimiting 类型。所有自动计时周期最多为 2,147,483,647 ms;SlidingWindow 的每个 segment 必须至少覆盖一个 TimeSpan tick。等待队列同时受调用数、保留字节、最长等待、调用 deadline、取消、断连和 Server Draining 限制;任一容量不足立即返回 ResourceExhausted。分区键为空时进入明确的默认分区,池满且没有安全可回收的空闲项时按 partition_capacity 拒绝,不记录真实分区键。
OneWay 默认不排队;被过载策略拒绝时服务方法不会执行,只记录 dropped/resource-exhausted 指标和限频日志。QueueOneWayCalls=true 才允许它进入相同有界队列。客户端本地 await OneWay 成功只表示 SendPump 接受了帧,不代表服务端已经执行。
Admission 指标为 sharplink.admission.permits.active、calls.queued、calls.rejected、queue.duration、oneway.dropped 与 partitions.active;拒绝只使用低基数 scope/reason。功能未启用时普通调用不创建 admission 状态、Task、TagList 或后台任务。
NamedPipe在 Unix/macOS 下最终会映射到 Unix Domain Socket 路径- 当前运行时会对超长 pipe name 做确定性缩短,避免触发平台路径长度限制
- NamedPipe 的未定义
PipeOptionsbit 或PipeTransmissionMode会在 factory/listener 构造时立即拒绝;client 也拒绝仅供 server 使用的FirstPipeInstance - TCP keep-alive time/interval 的最大值为 2,147,483,647 秒,配置会在创建 socket 前冻结并校验
AnonymousPipe当前已覆盖本机连接、断连与本机压测回归;仓库内置 LoadTest 仅支持--mode local- 每组 AnonymousPipe handle 从首次连接尝试开始即为已消费;失败重试必须申请新 offer
- 若自行基于
IAnonymousPipeAllocator将句柄转交外部子进程,应在子进程继承两个 handle 后立即调用offer.CompleteHandleTransfer()(或释放 offer),让 Server 能观察子进程断连;同进程直接包装这些 handle 时不要提前完成交接
平台能力矩阵:
| 传输 | Windows | Linux | macOS | 使用范围 |
|---|---|---|---|---|
| TCP | 支持 | 支持 | 支持 | 本机或跨主机 |
| UDS | 不承诺 | 支持 | 支持 | 本机 |
| NamedPipe | 支持 | 支持(映射到 UDS) | 支持(映射到 UDS) | 本机 |
| AnonymousPipe | 支持 | 支持 | 支持 | 本机协同进程 |
| SharedMemory(实验) | CI 目标,待门禁 | CI 目标,待门禁 | arm64 本机已验证 | 同机、同一用户 |
共享内存传输必须在 Client 与 Server 两端显式选择;创建、映射或握手失败会直接报错,绝不静默降级到其他传输。它只允许同机、同一操作系统用户的进程连接,数据通过每连接双向 SPSC 环传输,命名管道只承载握手、合并唤醒、关闭和存活信号。
var server = SharpLinkServerBuilder.Create()
.UseSharedMemory("orders", options =>
{
options.CapacityPerDirectionBytes = 8 * 1024 * 1024;
options.SpinCount = 8;
options.HandshakeTimeout = TimeSpan.FromSeconds(10);
})
.Build();
var client = SharpClientBuilder.Create()
.UseSharedMemory("orders")
.Build();容量必须是 64 KiB–256 MiB 的 2 的幂;双方不一致时取较小值。显式配置优先于运行时 profile,默认值如下:
| Profile | 每方向容量 | SpinCount |
|---|---|---|
| LowLatency | 1 MiB | 64 |
| Balanced | 8 MiB | 8 |
| Throughput | 32 MiB | 0 |
该传输不提供 TLS;同用户隔离依赖命名管道权限、用户私有映射目录、随机 nonce 和映射头校验。SharpLink RPC 认证、授权、deadline、流控和心跳照常生效。普通日志和性能报告不会记录映射路径、nonce 或 payload。正式支持状态以三平台 JIT/NativeAOT、性能与长稳门禁为准;当前安全边界、容量和调优规则见 doc/transports.md 与 doc/limits-and-tuning.md。
正式 NuGet 包中,SharpLink.Sdk 会携带 SharpLink.Generator Analyzer。通过 NuGet 使用时只需引用 SDK,无需再手工添加 Generator DLL 或 Analyzer 项目引用。
SharpLink.Hosting 提供:
services.AddSharpLinkServer(...)services.AddSharpLinkClient(...)
SharpClientBuilder 定义于 SharpLink.Client,SharpLinkServerBuilder 定义于 SharpLink.Server。
- 运行时失败使用
SharpLinkException和SharpLinkErrorCode区分认证、deadline、资源耗尽、断连和协议错误 await client.ConnectAsync(ct)成功后才返回;连接或握手失败直接抛结构化异常,不再返回bool- 用户
CancellationToken取消保留为本地OperationCanceledException;deadline 到期为SharpLinkException(DeadlineExceeded)
- 默认模式明确为 Anonymous,不存在默认密码。
RequireAuthentication()后没有注册服务端 provider 会在 Build 阶段失败。 - client provider 会为每次连接/重连重新创建有界二进制 payload,适合刷新短期 token:
var clientAuthenticator = SharpLinkAuthenticator.CreateClient(async cancellationToken =>
await tokenProvider.GetPayloadAsync(cancellationToken));
var serverAuthenticator = SharpLinkAuthenticator.CreateServer(async (request, cancellationToken) =>
{
var identity = await tokenValidator.ValidateAsync(request.Payload, cancellationToken);
return identity is null
? SharpLinkAuthenticationResult.Reject()
: SharpLinkAuthenticationResult.Authenticate(
new SharpLinkAuthenticationContext(
subject: identity.Subject,
tenantId: identity.TenantId,
scopes: identity.Scopes,
expiresAt: identity.ExpiresAt));
});
var client = SharpClientBuilder.Create()
.UseAuthenticator(clientAuthenticator);
var server = SharpLinkServerBuilder.Create()
.UseAuthenticator(serverAuthenticator)
.RequireAuthentication();认证 payload 受 handshake/metadata 上限约束,provider 异常只向客户端公开通用认证失败。payload、token 和证书内容不会写入普通日志。认证上下文如果在 handshake 时已经过期会直接返回 AuthenticationExpired。
如果你还需要在服务方法内部读取当前身份上下文,可以直接访问:
var subject = SharpLinkCallContext.Current?.Authentication?.Subject;
var tenantId = SharpLinkCallContext.Current?.Authentication?.TenantId;
var role = SharpLinkCallContext.Current?.Authentication?.GetClaim("role");
var canRead = SharpLinkCallContext.Current?.Authentication?.HasScope("rpc.read") ?? false;
var expiresAt = SharpLinkCallContext.Current?.Authentication?.ExpiresAt;SharpLinkCallContext.Current 仅在服务端 RPC 调用处理期间有值。
TLS 在 TCP 建连后、SharpLink Protocol v2 handshake 前完成,并拥有独立的 10 秒默认超时。客户端默认使用平台证书链和 hostname 校验;框架不提供“接受所有证书”的默认 helper。
var server = SharpLinkServerBuilder.Create()
.UseTcp(5000, new SslServerAuthenticationOptions
{
ServerCertificate = serverCertificate,
ClientCertificateRequired = true
})
.Build();
var client = SharpClientBuilder.Create()
.UseTcp("127.0.0.1", 5000, new SslClientAuthenticationOptions
{
TargetHost = "rpc.example.internal",
ClientCertificates = new X509CertificateCollection { clientCertificate }
})
.Build();UDS、NamedPipe、AnonymousPipe 与 SharedMemory 默认依赖操作系统权限,不叠加 TLS。TLS 建立日志只记录协商协议与 cipher suite,不记录证书私钥、token 或 payload。
契约方法可以在尾部声明一个 SharpLinkCallOptions,并可在其后再声明一个 CancellationToken。控制参数不会进入业务 payload:
ValueTask<Result> ExecuteAsync(
Command command,
SharpLinkCallOptions options,
CancellationToken cancellationToken);
var options = new SharpLinkCallOptions
{
Timeout = TimeSpan.FromSeconds(2),
WaitForReady = true,
Metadata = new SharpLinkMetadata(
new KeyValuePair<string, string>("tenant", "factory-a"))
};绝对 Deadline、相对 Timeout、[Timeout] 和客户端默认值会取最早到期时间。Unary 默认 30 秒;Server/Duplex stream 默认无超时。调用 deadline 到期时,客户端固定得到 SharpLinkException(DeadlineExceeded);这与服务实现是否声明 CancellationToken 无关。DisableRequestTimeout() 只关闭客户端默认值,显式 deadline、Timeout 和 [Timeout] 仍然生效。
建议所有可能等待、访问 I/O 或占用昂贵资源的契约方法都把 CancellationToken 放在参数末尾。Unary 没有 token 时产生 SHARPLINK004 Warning;Streaming 没有 token 时产生 SHARPLINK014 Error。确认业务工作不可取消时可用 [NonCancellable] 显式说明,但不能同时声明该特性和 CancellationToken,否则产生 SHARPLINK015 Error。此时客户端仍会按 deadline 停止等待,服务端会把调用标记为 abandoned、丢弃迟到响应并继续观察业务任务,直到任务结束后才释放该调用的 admission 与 DI scope。Streaming 的框架流泵、dispatcher 和窗口等待仍会被终止,不会因为 [NonCancellable] 保留连接资源。团队可以在 .editorconfig 中将 dotnet_diagnostic.SHARPLINK004.severity = error 提升为编译错误。
服务端可从 SharpLinkCallContext.Current 读取协商后的 deadline 与 metadata。
如果你希望直接在服务方法里做常见授权校验,可以使用:
SharpLinkAuthorization.RequireScope("rpc.read");
SharpLinkAuthorization.RequireTenant("tenant-a");
SharpLinkAuthorization.RequireActiveToken();这些 helper 失败时会抛出带正确 SharpLinkErrorCode 的 SharpLinkException,客户端会收到对应结构化错误,而不是退化成普通字符串异常。
Client/Server interceptor 按注册顺序冻结到实例。没有注册 interceptor 时,调用仍直接进入生成的泛型 invoker/stub,不构建 delegate 链:
var client = SharpClientBuilder.Create()
.UseTcp("rpc.example.internal", 5000)
.AddInterceptor(clientInterceptor)
.Build();
var server = SharpLinkServerBuilder.Create()
.UseTcp(5000)
.AddInterceptor(serverInterceptor)
.UseExceptionMapper(exceptionMapper)
.Build();客户端 interceptor 可通过 SharpLinkClientInvocationContext.Options 增加 metadata,也可以直接返回 SharpLinkClientInvocationResult 短路调用。服务端 context 包含 method descriptor、request ID、deadline、metadata、peer、auth、status 和 elapsed,可用于授权、限流与审计。
默认异常 mapper 会保留显式的 SharpLinkException;其他业务异常只向客户端返回 Internal 与通用消息,Unary 和 stream 使用同一规则。仅在受控开发环境中可显式调用 EnableDetailedErrors()。生产环境建议实现 IRpcExceptionMapper,只公开经过审核的业务状态与消息。
[Idempotent] 只把重试资格写入生成的 RpcMethodDescriptor,核心不会自动重试;后续 Resilience 扩展也只允许显式标记的 Unary 方法参与重试。
SharpLinkTelemetry 暴露两个 ActivitySource 和一个 Meter,可直接加入现有 OpenTelemetry pipeline:
tracerProviderBuilder
.AddSource("SharpLink.Client", "SharpLink.Server");
meterProviderBuilder
.AddMeter("SharpLink");内置指标覆盖 active connections、reconnect、started/completed/failed/active/abandoned calls、duration、sent/received bytes、send queue bytes、pending requests、active streams、迟到响应,以及 protocol/auth/resource-exhausted failures。sharplink.calls.abandoned 使用 rpc.sharplink.termination_reason 区分 deadline、远端取消、consumer abandoned、停机与断连;sharplink.responses.late_dropped 逐次记录被安全丢弃的迟到响应。Activity 和指标不记录完整 payload、token、证书或未审核的业务异常消息。没有 listener 时不会创建 TagList、Activity、Stopwatch 对象或额外调用 observer。
服务实现只需标记 [RpcService]。Server 对 Service 项目的普通 ProjectReference 会在编译期生成静态 Manifest bootstrap,不需要 marker type、Assembly.Load 或手动程序集注册;bootstrap 只引用 Service 程序集拥有的 generated public infrastructure,因此实现类型可以是 internal,且 trimming/NativeAOT 不需要运行时扫描。默认 Singleton 保留无调用 Scope 的快速路径;Connection 按认证成功的物理连接惰性创建,Call 为每次调用创建,并在完整 Unary、OneWay 或 Streaming 调用真正结束后释放:
[RpcService(Lifetime = SharpLinkServiceLifetime.Connection)]
public sealed class MyService(Dependency dependency) : IMyService
{
// Generated activator resolves Dependency from the current scope provider.
}
var server = SharpLinkServerBuilder.Create()
.UseServiceProvider(provider)
.UseTcp(5000)
.Build();可以按 Builder 排除、重新启用或只启用白名单服务。调用方传入的实例始终是 caller-owned Singleton;factory 产物由 SharpLink 按指定生命周期释放:
serverBuilder
.ExcludeService<IMyService>()
.EnableService<IMyService>()
.ReplaceService<IMyService>(existingInstance)
.ReplaceService<IOtherService>(
sp => new OtherService(sp.GetRequiredService<Dependency>()),
SharpLinkServiceLifetime.Call);DisableAutomaticServiceRegistration() 可切换为 EnableService<TContract>() 白名单模式。EnableService 找不到生成服务时 Build 失败,ExcludeService 找不到目标时无操作。Hosting 与 UseServiceProvider 继续管理普通依赖的生命周期,但根 RPC 服务的公共生命周期只由 SharpLinkServiceLifetime 定义。
Build 后加载的插件需要分别注册到使用其 Artifact 的 Client/Server。注册不会用异常表示预期失败,而是原子返回结构化诊断;只有完整 Manifest 验证通过才会发布:
SharpLinkAssemblyRegistrationResult registration = server.RegisterAssembly(pluginAssembly);
if (!registration.Succeeded)
Console.Error.WriteLine($"{registration.Error!.Code}: {registration.Error.Message}");
SharpLinkAssemblyUnregisterResult drained = await server.UnregisterAssemblyAsync(
pluginAssembly,
TimeSpan.FromSeconds(10),
cancellationToken);
SharpLinkAssemblyReplacementResult replaced = await server.ReplaceAssemblyAsync(
pluginAssembly,
nextPluginAssembly,
TimeSpan.FromSeconds(10),
cancellationToken);
if (!replaced.Succeeded)
Console.Error.WriteLine($"{replaced.Error!.Code}: {replaced.Error.Message}");ReplaceAssemblyAsync 在修改线上状态前完成新 Manifest、Codec、Stub、Service 与 route 验证;旧 registration 拥有的 route 可由新程序集接管,但第三方 registration 的 route 仍受冲突保护。提交时只发布一次新不可变路由快照,随后复用注销路径排空旧调用。已进入旧 registration 的 Unary 和 Stream 固定使用旧 Codec、Stub、Service 与 Scope;新请求只读取新快照。
普通注销的排空期间路由继续由原模块占有,新调用得到 Unavailable: RPC module is draining。替换和注销超时都会定点取消旧模块调用和流;业务代码不配合取消时 ReferencesReleased=false,框架在计数最终归零后后台完成释放。Client API 语义相同。NativeAOT 的运行时注册与替换返回 PlatformNotSupported,静态 Manifest 路径不受影响。
客户端可以直接使用协议控制帧检查远端状态,不需要定义业务契约:
var health = await client.CheckHealthAsync(cancellationToken);
if (health.Status != SharpLinkHealthStatus.Ready)
throw new InvalidOperationException($"RPC server is {health.Status}.");ISharpLinkServer.HealthStatus 暴露本地 Ready/Draining/Unhealthy。AddSharpLinkServer 与 AddSharpLinkClient 分别注册 sharplink_server 和 sharplink_remote Microsoft health checks,并带有 ready tag。停机顺序固定为 readiness=false、停止 accept、发送 GoAway、等待 active calls、超时后取消、flush 必要控制帧、释放 session/listener/service scope/provider。
- 日志:
UseLoggerFactory(...) - 心跳:
UseHeartbeat(...) - 握手认证:
ISharpLinkClientAuthenticator/ISharpLinkServerAuthenticator与RequireAuthentication() - 调用管线:Client/Server
AddInterceptor(...)与 ServerUseExceptionMapper(...) - 遥测:
SharpLinkTelemetry.ClientActivitySource、ServerActivitySource与Meter - 服务注册与生命周期:
[RpcService]、EnableService/ExcludeService/ReplaceService、UseServiceProvider(...)与SharpLinkServiceLifetime - 运行时插件:Client/Server
RegisterAssembly(...)与UnregisterAssemblyAsync(...) - 健康检查:
CheckHealthAsync()、ISharpLinkServer.HealthStatus与 Hosting health checks - 请求超时:
UseRequestTimeout(...);需要真正无默认超时时使用DisableRequestTimeout() RpcSessionflush:UseRpcSessionFlush(...)- 实例级 Buffer Writer Pool:
UseBufferWriterPool(...) - 运行时并发容器:
UseStateStoreConcurrency(...) - 性能预设与流控边界:
UseRuntime(options => options.PerformanceProfile = SharpLinkPerformanceProfile.LowLatency) - 客户端连接池:
UseConnectionPool(options => { options.MinConnections = 1; options.MaxConnections = 4; })
客户端默认使用 1/1 单连接池,单连接选择路径不产生随机选择或临时集合。只有在已有连接承载在途请求时,池才会按压力异步扩容;多连接使用 power-of-two choices 比较在途请求数。stream 在创建时固定到同一连接,收到 GoAway 的连接停止接收新调用并在在途请求归零后退出。Throughput 预设在用户未显式配置连接池时使用 1/min(Environment.ProcessorCount, 4),其他预设保持 1/1。
AnonymousPipe 的一次句柄 offer 只支持一个客户端连接,因此其 MaxConnections 必须为 1。
以下会跨 Client/Server 实例互相覆盖状态的旧进程级入口已删除:
RpcCodecRegistry/RpcCodec:业务配置迁移到 Client/Server Builder 的UseCodec<T>(...)或UseSerializer(...);底层组件从所属IRpcRuntimeContext.Codecs解析 Codec。BufferWriterPool:容量和保留策略迁移到 Builder 的UseBufferWriterPool(...);框架内部从所属 Context 的Buffers租借和归还。独立工具代码可直接使用PooledByteBufferWriter。RuntimeConcurrency:迁移到每个 Builder 的UseStateStoreConcurrency(...)。- 旧的 Client 调用排列组合入口已删除;业务调用只通过 Source Generator 代理,底层扩展只实现
IRpcChannel的五类 invoker。
这些配置在 Build() 时冻结;同进程的不同 Client/Server 可以使用不同 Codec、Pool 和并发参数而互不污染。
如果你使用 UseTcp(0, "127.0.0.1") 让系统自动分配端口,可以在 Build() 前通过 serverBuilder.Transport.LocalEndPoint 读取实际监听端口。
- 文档首页与特性/Demo 覆盖矩阵:
doc/index.md - 快速开始:
doc/getting-started.md - 契约、Codec 与压缩:
doc/contracts-and-codecs.md - 调用、Streaming、取消与 deadline:
doc/calls-and-streaming.md - 传输与部署:
doc/transports.md - 安全:
doc/security.md - 服务发现与韧性:
doc/resilience.md - 接入控制:
doc/admission-control.md - Hosting 与服务生命周期:
doc/hosting-and-services.md - Interceptor 与可观测性:
doc/observability.md - 多集群与动态模块:
doc/dynamic-modules-and-multicluster.md - 限制与调优:
doc/limits-and-tuning.md - 故障排查与迁移:
doc/troubleshooting.md、doc/migration.md - 架构、Protocol、负载和性能:
doc/architecture.md、doc/protocol-v2.md、doc/loadtest.md、doc/performance.md - 发布流程:
doc/releasing.md - 贡献与社区:
CONTRIBUTING.md、CODE_OF_CONDUCT.md - 安全漏洞请按
SECURITY.md私下报告,不要创建公开 Issue。 - 更新日志:
CHANGELOG.md