YtSoft.Gmc.Core.Rpc 1.0.2

dotnet add package YtSoft.Gmc.Core.Rpc --version 1.0.2
                    
NuGet\Install-Package YtSoft.Gmc.Core.Rpc -Version 1.0.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="YtSoft.Gmc.Core.Rpc" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="YtSoft.Gmc.Core.Rpc" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="YtSoft.Gmc.Core.Rpc" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add YtSoft.Gmc.Core.Rpc --version 1.0.2
                    
#r "nuget: YtSoft.Gmc.Core.Rpc, 1.0.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package YtSoft.Gmc.Core.Rpc@1.0.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=YtSoft.Gmc.Core.Rpc&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=YtSoft.Gmc.Core.Rpc&version=1.0.2
                    
Install as a Cake Tool

YtSoft.Gmc.Core.Rpc 接入指南

YtSoft.Gmc.Core.Rpc 将普通 C# 类的方法发布为统一的 gRPC RPC 接口,使用 JSON 传递业务参数和返回值,并可选接入 Nacos 注册与发现。本文依据当前目录源码,面向需要引用中间件的其他系统;示例地址均为本地地址,部署时请替换为调用方可访问的地址。

通过 NuGet 包接入时的独立完整指南(安装、服务端、客户端、Nacos、排查)见 NUGET-USAGE.md。

迁移后的命名空间统一使用 YtSoft.Gmc.Core.Rpc 前缀;配置节继续使用 RpcMiddleware,gRPC 服务路径继续使用 /rpcbridge.ServiceRpc/Invoke。

1. 组件与边界

本项目只生成一个程序集 YtSoft.Gmc.Core.Rpc,命名空间分工如下:

命名空间 作用 主要入口
YtSoft.Gmc.Core.Rpc.Server 扫描服务、绑定参数、执行方法和托管 gRPC 入口 AddRpcServer、MapRpcServer
YtSoft.Gmc.Core.Rpc.Client 序列化调用、选择实例、执行 gRPC 请求 AddRpcClient、IRpcClient
YtSoft.Gmc.Core.Rpc.Contracts 服务标记、发现接口、调用上下文、JSON 和头部规则 [RpcServer](类或方法)、IRpcServiceDiscovery、RpcCallContext
YtSoft.Gmc.Core.Rpc.Protocol 从 Proto 生成的协议消息和 gRPC 客户端/服务基类 RpcRequest、RpcReply、ServiceRpc
YtSoft.Gmc.Core.Rpc.Nacos Nacos SDK 配置、后台注册、后台发现和实例目录 AddRpcNacos、AddRpcNacosServer、AddRpcNacosDiscovery

调用链为:调用方 → IRpcClient → IRpcServiceDiscovery 选择地址 → gRPC ServiceRpc.Invoke → 服务名/方法名分发 → DI 服务实例。Nacos 只负责注册和发现,业务调用直接连接服务端。无 Nacos 时通过 Endpoints 地址表直连,也可接入自定义发现接口。

当前提供字符串服务名/方法名的调用 API,没有自动生成业务接口强类型代理。协议只有一个 unary gRPC 方法,不提供流式 RPC;也没有内置业务认证授权、业务调用自动重试或主动健康探测。Nacos 目录对合格实例进行简单轮询,权重大于零是筛选条件,不是按权重分配流量。

源码:服务端入口、客户端入口、发现接口、实例目录。

2. 环境与项目引用

项目文件 当前目标为 net6.0,启用 nullable 和隐式 using,并引用 Microsoft.AspNetCore.App 共享框架。现有依赖如下,接入时沿用项目版本即可:

依赖 版本
Google.Protobuf 3.27.2
Grpc.AspNetCore、Grpc.Net.Client、Grpc.Tools 2.67.0
nacos-sdk-csharp 1.3.10

仓库未在此提供已发布 NuGet 包的信息,推荐先使用 ProjectReference。以下假定新增工程与 YtSoft.Gmc.Core.Rpc 目录同级;其他布局请调整相对路径。

服务端 RpcDemo.Server.csproj:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="../YtSoft.Gmc.Core.Rpc/YtSoft.Gmc.Core.Rpc.csproj" />
  </ItemGroup>
</Project>

控制台客户端 RpcDemo.Client.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <ProjectReference Include="../YtSoft.Gmc.Core.Rpc/YtSoft.Gmc.Core.Rpc.csproj" />
    <None Update="appsettings.json" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
  </ItemGroup>
</Project>

ASP.NET Core 客户端工程可直接采用 Web SDK。服务端、客户端共享 DTO 时,可将 DTO 放到独立的普通类库并分别引用;DTO 无需添加 RPC 特性。

3. 最小接入:无 Nacos 直连

3.1 服务端:完整 Program.cs

统一入口默认在 5101 端口监听明文 HTTP/2(h2c),绑定所有本机 IP。缺少 RpcMiddleware:Enabled 时不启用注册中心。服务端使用第 2 节的 Web SDK,Program.cs:

using YtSoft.Gmc.Core.Rpc.Server;
using YtSoft.Gmc.Core.Rpc.Contracts;

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ContentRootPath = AppContext.BaseDirectory
});
builder.Services.AddRpcServer(builder.Configuration);

var app = builder.Build();
app.Run();

[RpcServer("calculator")]
public sealed class CalculatorService
{
    public int Add(int left, int right) => left + right;
}

这一行自动读取配置、扫描入口程序集、配置 RPC HTTP/2 监听并映射路由,无需再调用 ConfigureKestrel 或 MapRpcServer。普通 HTTP API 的监听和 MapControllers 等仍由宿主管理,可以与 RPC 共存。

需要扫描其他程序集时,在同一次调用中通过 assemblies: new[] { typeof(CalculatorService).Assembly } 指定。服务类必须为 public;不能重复调用 AddRpcServer。

3.2 客户端:完整配置与 Program.cs

使用第 2 节的控制台客户端项目,项目文件已设置将配置复制到构建和发布输出。appsettings.json:

{
  "RpcMiddleware": {
    "Enabled": false,
    "Endpoints": { "calculator": "http://localhost:5101" }
  }
}

Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using YtSoft.Gmc.Core.Rpc.Client;

using var host = Host.CreateDefaultBuilder(args)
    .UseContentRoot(AppContext.BaseDirectory)
    .ConfigureServices((context, services) =>
    {
        services.AddRpcClient(context.Configuration);
    })
    .Build();

var client = host.Services.GetRequiredService<IRpcClient>();
var result = await client.CallAsync<int>("calculator", "Add", new { left = 20, right = 22 });
Console.WriteLine(result); // 42

无需自行实现 IRpcServiceDiscovery。Endpoints 的键为服务别名,忽略大小写,值为绝对 HTTP/HTTPS RPC 地址;每个服务可以配置一个地址。参数仍是 {"left":20,"right":22},不是位置参数数组。

直连不需要 StartAsync,host 用于提供 DI 和释放客户端。禁用 Nacos 后,未配置地址表时仍可启动,调用没有地址的服务返回 404。已提前注册的 IRpcServiceDiscovery 会保留,适合已有发现机制的系统。

3.3 使用参数覆盖配置

统一入口先读取配置,再执行一次配置回调。以下分别是服务端和客户端的替换调用片段,已有 builder:

builder.Services.AddRpcServer(builder.Configuration, options =>
{
    options.Registration.RpcPort = 5101;
    options.MaxMessageSize = 8 * 1024 * 1024;
    options.Nacos.Enabled = false;
});
builder.Services.AddRpcClient(builder.Configuration, options =>
{
    options.CallTimeoutSeconds = 10;
    options.Nacos.Enabled = false;
    options.Endpoints["calculator"] = "http://localhost:5101";
});

服务端用 options.Registration 设置注册地址、端口、重试间隔和 TLS,用 options.Nacos 设置注册中心连接。客户端用 options.Discovery 设置扫描间隔,用 options.Nacos 设置注册中心连接;JsonOptions 仍可添加自定义转换器。回调中的设置优先于已加载的文件、环境变量及命令行配置。

4. 业务服务契约

服务与方法如何发布

扫描规则 在注册时校验:

  • 类型只要有类级 [RpcServer],或至少一个当前类声明的方法带 [RpcServer],就会注册为服务;服务类型必须是公开、非抽象、已闭合的具体类。类级特性可以从基类继承,但方法只扫描当前类声明的成员。
  • 类级 [RpcServer("orders")] 显式设置服务别名;名称留空([RpcServer]、[RpcServer("")])时依次尝试去掉类名末尾的 Controller 或 Service,然后转为小写。只有方法级特性时也按类名推导服务名。服务名只能包含字母、数字、_、-、.,不能重复。
  • 类中没有方法级特性时,发布本类所有符合条件的 public 实例方法(旧行为);类中只要有一个方法级特性,就只发布被标注的方法(白名单),未标注的方法既不发布也不参与签名校验。
  • 方法级 [RpcServer("add")] 设置 RPC 方法名;名称留空([RpcServer]、[RpcServer("")])时使用 CLR 方法名。方法级模式允许重载,只要最终 RPC 方法名忽略大小写后唯一;类级-only 模式仍拒绝同名重载。
  • 服务名、方法名匹配均忽略大小写。方法级特性只作用于当前类声明的方法;接口或基类方法上的特性不会被继承,重写方法需自行标注。
  • 方法级特性标在静态、非公开、属性/事件访问器、object 方法重写或 Dispose / DisposeAsync 上会明确报错。不支持泛型方法、ref / out、按引用或指针类型、忽略大小写后重复的参数名、async void。
  • 没有可发布方法、最终 RPC 方法名重复或服务别名冲突会在注册时失败。
[RpcServer("orders")]
public sealed class OrderService
{
    [RpcServer("get")]
    public OrderDto? Get(string id) => null; // 实际实现略

    [RpcServer("cancel")]
    public bool Cancel(string id) => true;   // 实际实现略

    // 白名单模式下未标注,不发布
    public int Count() => 0;
}

参数绑定与 JSON 类型

分发器 将 UTF-8 JSON 对象的属性名绑定到方法参数名,忽略大小写。空 body 按 {} 处理。无参数调用可不传 arguments。

方法参数 JSON 要求
int left, int right {"left":20,"right":22}
OrderDto order {"order":{"id":"A001","quantity":2}},必须保留外层参数名
string? note 仍必须提供 note,可显式为 null
string? note = null 可省略 note,省略时使用默认值
CancellationToken cancellationToken 服务端注入,不在 JSON 中传递
RpcCallContext context 服务端注入,不在 JSON 中传递

缺失没有默认值的参数、未知参数、重复参数键(忽略大小写)、类型不匹配、非对象 JSON,以及用 JSON 覆盖注入参数均返回 400 invalid_arguments。非 nullable 值类型不能为 null;启用 nullable 注解的非空引用参数也不能为 null。DTO 内部属性仍按 JSON 序列化器规则处理,中间件不自动执行数据注解或业务校验。

默认 JSON 配置 基于 JsonSerializerDefaults.Web,属性输出使用 camelCase、读取忽略大小写,枚举按字符串输出,增加了 DateOnly / TimeOnly 转换器:

  • 基础数值、布尔、字符串、char、Guid、DateTime、DateTimeOffset、TimeSpan、Uri、nullable、普通 DTO 等使用 System.Text.Json 可处理的形状。
  • 数组、交错数组、列表、集合、支持的字典等按 JSON 数组/对象传递;byte[] 使用 Base64 字符串。
  • DateOnly 使用 yyyy-MM-dd;TimeOnly 接受 HH:mm、HH:mm:ss、带最多 7 位小数秒的时间字符串。
  • JsonElement / JsonDocument 可表示任意 JSON;object 反序列化通常得到 JsonElement。返回依赖 JsonDocument 的元素前注意 Clone() 和文档生命周期。
  • 不保证任意 CLR 类型均可序列化,例如默认不支持多维数组 int[,]。绑定不支持的类型返回 400,结果序列化失败返回 500。

服务端 RpcServerOptions.JsonOptions 和客户端 RpcClientOptions.JsonOptions 可在注册回调中增加转换器;两端需采用一致契约。示例类型见 ValuesService(原 TestRpcService/Service_Test1/Services/ValuesService.cs)、ShapesService(原 TestRpcService/Service_Test1/Services/ShapesService.cs)。

返回值与 DI 生命周期

支持同步返回值、Task<T>、ValueTask<T>;同步 void、Task、ValueTask 成功时均返回 JSON null,状态码为 200。返回空 Task 或抛异常均返回 500 {"error":"invocation_failed"},不把业务异常详情发给调用方。业务返回值本身不会被解释成自定义 HTTP 状态码。

每次调用创建独立的异步 DI scope,解析服务、等待异步结果、序列化响应后释放。扫描到的服务默认用 TryAddScoped 注册,已存在的服务注册会保留;构造函数依赖需自行注册。RPC scope 不是外部 HTTP 请求的 scope,业务服务应通过 RpcCallContext 获取协议上下文。不要让作用域对象或依赖其生命周期的数据逃逸到调用结束以后。

5. 接入 Nacos

Nacos 连接逐项优先读取进程环境变量 NACOS_SERVERS、NACOS_NAMESPACE、NACOS_USERNAME、NACOS_PASSWORD;未设置或仅空白的字段分别回退到 NacosConfig.ServerAddresses/Namespace/UserName/Password。变量名不带 ${};JSON 不需要使用占位符。NACOS_SERVERS 支持单个地址、逗号分隔列表或 JSON 字符串数组,缺少协议时补 http://。NACOS_NAMESPACE=public 表示 Nacos 公共命名空间。已提供但格式错误的地址会报配置错误,不静默回退。代码回调仍有最高优先级。

5.1 注册和发现的配套要求

服务端仍只调用 AddRpcServer(builder.Configuration),客户端仍只调用 AddRpcClient(configuration)。两者根据 RpcMiddleware:Enabled 自动注册 SDK 和后台任务:

配置节 统一入口的用途
RpcMiddleware 开关、服务名前缀 ServiceNamePrefix、SDK 超时、服务端监听和注册参数、客户端超时、消息上限、直连 Endpoints、发现扫描间隔;参数直接放在节内
NacosConfig 连接地址、命名空间、分组、集群和认证的本地回退配置

RpcMiddleware:Enabled 默认 false。服务端关闭后仍监听 RPC;客户端关闭后使用直连地址表或已注册的自定义发现。未配置参数时使用默认值,不要求配置地址表;没有地址的服务调用返回 404。显式填写的非法参数仍会被拒绝。启用时,两端须使用相同注册中心、Namespace、GroupName;ServiceNamePrefix 由服务端配置,客户端无需配置。

两端优先读取 RpcMiddleware;缺少该节时,服务端兼容原来的 RpcTransport:Server、RpcRegistration,客户端使用默认值,不再读取 RpcTransport:Client、RpcDiscovery。Enabled、DefaultTimeOut 缺少新字段时兼容 NacosConfig 下的旧字段,均未配置则使用默认值。ServiceNamePrefix 优先读取 RpcMiddleware,缺少时兼容 NacosConfig,默认 rpc.。配置回调最后执行一次,仍通过 options.Registration、options.Discovery、options.Nacos 覆盖相应参数。

同一宿主同时注册客户端和服务端时,共用一套 Nacos SDK 与服务目录配置;连接、命名空间、分组、集群、前缀、认证和超时参数需保持一致。重复注册相同配置会复用,冲突则立即报错,避免后注册的角色覆盖前者配置。

别名 calculator 和服务端前缀 rpc.demo1. 对应 Nacos 服务名 rpc.demo1.calculator,客户端调用 API 中传完整名 rpc.demo1.calculator。前缀与别名直接拼接,不自动补 .,不能含 @、/、\。客户端按完整名发现,可同时调用不同前缀的服务;旧客户端需将短名调用改为完整名。服务端仍接受原来的短名,也接受自身前缀加别名的完整名;完整名与某个短名重合时优先按完整名分发。直接组合旧的 AddRpcNacos 等低层接口时,开关仍需宿主自行判断。

服务查找忽略大小写,同一分组内不能发布仅大小写不同的完整服务名。发现时会跳过这类冲突服务并记录警告,其他服务仍正常发现;消除冲突后,下次扫描自动恢复。

5.2 Nacos 服务端:完整配置与 Program.cs

使用第 2 节的服务端项目文件。appsettings.json:

{
  "NacosConfig": {
    "ServerAddresses": ["http://localhost:8848"],
    "Namespace": "",
    "GroupName": "DEFAULT_GROUP",
    "ClusterName": "DEFAULT",
    "UserName": "",
    "Password": ""
  },
  "RpcMiddleware": {
    "Enabled": true,
    "ServiceNamePrefix": "rpc.demo1.",
    "DefaultTimeOut": 10000,
    "MaxMessageSize": 4194304,
    "RpcPort": 5101,
    "AdvertiseAddress": "",
    "RetryIntervalSeconds": 5,
    "UseTls": false
  }
}

Program.cs:

using YtSoft.Gmc.Core.Rpc.Server;
using YtSoft.Gmc.Core.Rpc.Contracts;

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ContentRootPath = AppContext.BaseDirectory
});
builder.Services.AddRpcServer(builder.Configuration);

var app = builder.Build();
app.Run();

[RpcServer("calculator")]
public sealed class CalculatorService
{
    public int Add(int left, int right) => left + right;
}

AdvertiseAddress 留空时,先根据到 Nacos 的路由选择本机可用 IP;无法按路由选择时从活动网卡中挑选唯一地址,优先 IPv4,仍不唯一则报错。显式设置时必须是可用 IP 字面量,不能使用域名、回环地址、Any 地址、链路本地或组播地址。多网卡、容器、跨网段部署请通过 RpcMiddleware__AdvertiseAddress 指定调用方实际可达的 IP。监听 Any 与注册 Any 是不同概念,后者不允许。

后台任务在宿主启动后,为每个服务别名注册临时实例,携带 rpc.transport=grpc、rpc.version=2、rpc.secure=false/true 元数据。注册失败会按 RetryIntervalSeconds 重试,正常停止时尽力注销。注册实现、地址选择实现。

如果由第三方配置包加载独立的 Nacos RPC 文档,RPC 参数直接放在文档根层;服务端示例:

{
  "Enabled": true,
  "ServiceNamePrefix": "rpc.demo1.",
  "DefaultTimeOut": 10000,
  "MaxMessageSize": 4194304,
  "RpcPort": 6201,
  "AdvertiseAddress": "",
  "RetryIntervalSeconds": 2,
  "UseTls": false
}

宿主先把文档根层映射到 IConfiguration 的 RpcMiddleware 节,再调用 AddRpcServer(builder.Configuration) 或 AddRpcClient(configuration)。客户端需要的 ScanIntervalSeconds、CallTimeoutSeconds、Endpoints 同样直接放在根层,不再套 RpcTransport 或 RpcDiscovery。中间件不直接读取 Nacos 配置中心;连接信息优先读取四个 NACOS_* 环境变量,缺少时回退本地 NacosConfig;ServiceNamePrefix 放在 RpcMiddleware 节。Service_Test1 使用 YtSoft.Gmc.Core.Config 1.0.1 的具体映射、文档名和本地回退规则见 Test1 配置说明(原 TestRpcService/Service_Test1/README.md#配置)。这些参数在启动时读取,修改后需要重启 RPC 宿主。

5.3 Nacos 客户端:完整配置与 Program.cs

使用第 2 节的控制台客户端项目文件;其中的复制规则将配置放到构建和发布目录,下面的 Host 也明确从该目录读取配置。appsettings.json:

{
  "NacosConfig": {
    "ServerAddresses": ["http://localhost:8848"],
    "Namespace": "",
    "GroupName": "DEFAULT_GROUP",
    "ClusterName": "DEFAULT",
    "UserName": "",
    "Password": ""
  },
  "RpcMiddleware": {
    "Enabled": true,
    "DefaultTimeOut": 10000,
    "ScanIntervalSeconds": 5,
    "CallTimeoutSeconds": 30,
    "MaxMessageSize": 4194304
  }
}

Program.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using YtSoft.Gmc.Core.Rpc.Client;

using var host = Host.CreateDefaultBuilder(args)
    .UseContentRoot(AppContext.BaseDirectory)
    .ConfigureServices((context, services) =>
    {
        services.AddRpcClient(context.Configuration);
    })
    .Build();

await host.StartAsync();
Console.WriteLine("已启动服务发现;待 Nacos 首次同步后按 Enter 调用。");
Console.ReadLine();
try
{
    var client = host.Services.GetRequiredService<IRpcClient>();
    var result = await client.CallAsync<int>("rpc.demo1.calculator", "Add", new { left = 20, right = 22 });
    Console.WriteLine(result);
}
catch (RpcCallException exception)
{
    Console.WriteLine($"RPC 失败:{exception.StatusCode} {exception.Message}");
}
finally
{
    await host.StopAsync();
}

启动 Generic Host 才会运行后台发现服务。StartAsync 返回不代表发现已经完成,首次快照前调用可能得到 503。首次同步后发现任务定期分页扫描服务列表,并订阅实例变化;扫描失败保留最近目录。仅选择健康、启用、权重大于零、地址端口有效且协议版本元数据匹配的实例,再简单轮询。此处没有基于业务请求的重试或主动探测。发现实现、目录筛选与选择。

认证信息用环境变量覆盖,不要提交真实凭据:

$env:NACOS_USERNAME = '<NACOS_USERNAME>'
$env:NACOS_PASSWORD = '<NACOS_PASSWORD>'
$env:RpcMiddleware__AdvertiseAddress = '<RPC_HOST_IP>' # 仅服务端需要,替换为有效可达 IP

5.4 默认值、单位和 TLS

选项 中间件默认值 约束 / 单位
RpcMiddleware.Enabled false 统一入口的注册/发现开关
NacosConfig.ServerAddresses ["http://localhost:8848"] 非空列表,绝对 http/https 地址
NacosConfig.Namespace "" 两端必须一致
NacosConfig.GroupName / ClusterName DEFAULT_GROUP / DEFAULT 分组不能为空;ClusterName 用于注册实例
RpcMiddleware.ServiceNamePrefix rpc. 服务端注册前缀,非空;客户端无需配置
NacosConfig.UserName / Password "" / "" 按注册中心认证配置填写
RpcMiddleware.DefaultTimeOut 10000 Nacos SDK 超时,毫秒,必须大于零
RpcMiddleware.RpcPort 5101 0~65535;须与真实 RPC 监听一致
RpcMiddleware.AdvertiseAddress "" 自动选择或填写可用 IP
RpcMiddleware.RetryIntervalSeconds 5 秒,有限且大于零
RpcMiddleware.UseTls false 统一入口配置 HTTPS 监听及元数据;证书需另行配置
RpcMiddleware.ScanIntervalSeconds 5 秒,有限且大于零;Service_Test2 在配置文件中设为 1
RpcMiddleware.CallTimeoutSeconds 30 业务 RPC 超时,秒,范围 (0,86400]
RpcMiddleware.MaxMessageSize 4194304 两端消息大小上限,字节,必须大于零
RpcMiddleware.Endpoints {} 直连地址表,键为服务别名;可不配置

RpcPort=0 可用于动态监听端口;注册实现取 IServerAddressesFeature 中最后一个监听地址的端口,多个监听时需将 RPC 监听放在最后并验证注册结果。普通部署优先明确指定端口,避免登记到 HTTP API 端口。

统一入口在 UseTls=true 时给 RPC HTTP/2 监听调用 UseHttps(),并同步发布 rpc.secure=true。宿主仍须通过 Kestrel:Certificates:Default 提供有效默认证书,例如:

{
  "RpcMiddleware": { "UseTls": true },
  "Kestrel": {
    "Certificates": {
      "Default": { "Path": "certs/rpc.pfx" }
    }
  }
}

证书需要密码时可用环境变量 Kestrel__Certificates__Default__Password 提供。客户端必须信任证书;Nacos 目录按实例 IP 构造 HTTPS 地址,证书也需匹配该 IP。旧的低层 AddRpcNacosServer 只处理注册元数据,不配置监听或证书。

选项来源:NacosOptions、Nacos 配置绑定、注册选项、发现选项。

6. 调用上下文、头部与错误处理

6.1 业务头部与取消

客户端 CallAsync 可用 RpcClientCallOptions.Headers 传入业务头;服务端通过注入的 RpcCallContext.Headers 读取,通过 ResponseHeaders 写回业务响应头。例如将下面方法加入已发布的服务类:

public string Echo(string value, YtSoft.Gmc.Core.Rpc.Contracts.RpcCallContext context)
{
    context.ResponseHeaders["X-Correlation-Id"] =
        context.Headers.TryGetValue("X-Correlation-Id", out var ids) ? ids : new[] { "generated-id" };
    return value;
}

Headers 与 ResponseHeaders 的键忽略大小写,支持多值。服务端会合并同名请求头,并过滤 Host、Connection、Keep-Alive、Proxy-Authenticate、Proxy-Authorization、TE、Trailer、Transfer-Encoding、Upgrade、Content-Length,以及 Connection 中列出的额外头。响应也过滤这些头;服务端固定返回 Content-Type: application/json; charset=utf-8,业务不能通过 ResponseHeaders 覆盖它。头部规则、上下文。

这些头是 RPC 消息的字段,不会自动成为底层 gRPC metadata。业务认证和头部信任规则由宿主实现;传递 Authorization 等业务头不代表中间件已完成认证。CallAsync 仅返回业务结果,需要响应头时使用 InvokeAsync。

调用方可传入 cancellationToken;取消会向 gRPC 和服务端上下文传播,本地取消表现为 OperationCanceledException。业务方法应接收 CancellationToken 或读取 context.CancellationToken,并传给异步 I/O;忽略令牌的业务代码不会被强制中断。

单次超时和请求头片段(已有 IRpcClient client):

using var cancellation = new CancellationTokenSource();
var options = new YtSoft.Gmc.Core.Rpc.Client.RpcClientCallOptions
{
    Timeout = TimeSpan.FromSeconds(5),
    Headers = new Dictionary<string, string[]> { ["X-Correlation-Id"] = new[] { "request-001" } }
};
var result = await client.CallAsync<string>("calculator", "Echo", new { value = "hello" },
    callOptions: options, cancellationToken: cancellation.Token);

6.2 类型调用与原始调用

API 正常返回 上游业务非成功状态
CallAsync<T>(service, method, arguments, ...) 将 2xx body 反序列化为 T 非 2xx 抛 RpcCallException,Message 是上游 JSON 正文
CallAsync(service, method, arguments, ...) 等待 2xx 完成,忽略返回值 同上
InvokeAsync(RpcRequest, ...) 返回原始 RpcReply,可读状态、头和 body 保留 200~599 的状态,不因上游 400/404/500 等状态抛异常

发现或传输失败对两类 API 都会抛 RpcCallException。类型调用反序列化抛出 JsonException 时转换为 502;目标类型本身不受支持等产生的 NotSupportedException 不在此转换范围内。原始调用收到 200~599 之外的状态也抛 502。空服务名/方法名会抛 ArgumentException,无效单次超时会抛 ArgumentOutOfRangeException。客户端实现。

原始调用片段(已有 IRpcClient client):

var reply = await client.InvokeAsync(new YtSoft.Gmc.Core.Rpc.Protocol.RpcRequest
{
    Service = "calculator",
    Method = "Add",
    Body = Google.Protobuf.ByteString.CopyFromUtf8("{\"left\":20,\"right\":22}")
});
Console.WriteLine($"{reply.StatusCode}: {reply.Body.ToStringUtf8()}");

6.3 状态和限制

服务端错误 body 是 {"error":"错误标识"};客户端本地发现/传输错误的 RpcCallException.Message 通常是普通文本,不保证为 JSON。

状态 常见含义
400 服务端 invalid_arguments:JSON、参数名、类型、null 或缺失参数错误
404 服务端 service_not_found / method_not_found;或客户端已有目录但不含服务
413 客户端请求超限;或 gRPC 返回 ResourceExhausted(包括大响应)
500 服务端 invocation_failed:构造/执行/异步任务/序列化失败
502 连接、TLS、协议或其他 gRPC 错误;无效端点/响应状态;类型调用反序列化失败
503 发现尚无快照,或没有合格实例
504 gRPC deadline 超时

服务端与客户端 MaxMessageSize 默认均为 4 MiB = 4194304 字节,必须大于零,约束发送和接收的完整 protobuf 消息;业务 JSON、服务名、方法名和头部共同占用额度。调大时同时考虑两端及网关/代理限制,不能只计算 JSON body。

客户端默认 CallTimeoutSeconds=30,单位秒,允许有限的 (0,86400]。类型调用的 RpcClientCallOptions.Timeout 可覆盖单次超时,范围为 (0,1天];InvokeAsync 使用全局超时。超时从 gRPC 调用发起时计算,服务端可通过取消令牌协作结束工作;中间件没有额外的服务端业务执行超时配置。客户端选项、服务端选项。

7. 非 .NET 系统接入

7.1 经现有 HTTP 网关

原 TestRpcService 工程的 Service_Test2(原 TestRpcService/Service_Test2/README.md) 是独立宿主,提供 HTTP → RPC 转发;YtSoft.Gmc.Core.Rpc 本身不会自动映射普通 HTTP API。网关可通过 Nacos 或直连地址表选择上游。其 6202 只是代码兜底,当前 launchSettings.json 可覆盖该地址;在原 TestRpcService 工程根目录运行以下命令可明确指定示例地址:

dotnet run --project Service_Test2/Service_Test2.csproj --no-launch-profile -- --urls http://localhost:6202

以下请求面向 Service_Test1(原 TestRpcService/Service_Test1/README.md) 发布的 shapes 服务,以服务端前缀 rpc. 为例,需先启动该服务并对齐两端 Nacos 的注册中心、namespace 和 group。网关使用本版本客户端时,Nacos 模式下路径传完整服务名;若接入第 5 节的计算器服务,请改用 /rpc.demo1.calculator/Add。

GET /rpc.shapes/Sum?left=20&right=22 HTTP/1.1
Host: localhost:6202
POST /rpc.shapes/Sum HTTP/1.1
Host: localhost:6202
Content-Type: application/json

{"left":20,"right":22}
  • 路由为 GET/POST /{service}/{method},Nacos 模式下 service 为含服务端前缀的完整名;直连模式与 Endpoints 键一致。其他 HTTP 方法返回 405。
  • GET 仅使用 query:合法 JSON 值保留类型,普通文本及空值作为字符串;重复键转成数组。字符串 "42" 需要 URL 编码为 value=%2242%22,否则 42 作为数字。
  • POST 仅使用 body,query 不参与合并;空 body 按 {},非空 body 要求 application/json 或 +json 内容类型,否则 415。JSON 语法和参数校验由 RPC 服务端执行。
  • 网关对 POST body 有独立的 4 MiB 有界读取检查,随后客户端还会校验完整 RPC 消息;上游状态、响应头和 JSON body 直接转发,发现/传输错误转换成相应 HTTP 状态和 {"error":...}。

完整实现:HTTP 路由(原 TestRpcService/Service_Test2/Controllers/RpcController.cs)、转发与参数转换(原 TestRpcService/Service_Test2/RpcForwarder.cs),现成请求集见 Apifox 使用说明(原 TestRpcService/Service_Test2/Apifox/README.md)。

7.2 直接调用 gRPC 协议

其他语言可从 service_rpc.proto 生成客户端,连接真实 HTTP/2 RPC 端口,调用 /rpcbridge.ServiceRpc/Invoke:

  • RpcRequest.service 是业务别名,method 是方法名,headers 为名称和多值列表。
  • body 为 UTF-8 JSON 参数对象的 bytes;不是 DTO 的 protobuf 编码。无参可使用空 bytes 或 {}。
  • RpcReply.status_code 是应用状态,headers 是响应头,body 是 JSON bytes。成功 gRPC 传输仍可能包含应用层 400/404/500,调用方必须检查 status_code;网络和 deadline 失败则属于 gRPC 错误。
  • 外部系统需自行解决端点发现,或调用 HTTP 网关。直接使用生成的 gRPC 客户端时,也需自行实现消息大小、deadline、头部和 JSON 契约约束。

协议版本为 2,Nacos 实例元数据必须符合当前约定;旧版本或手工注册但缺少元数据的实例不会被目录选中。

8. 验证、部署与排查

在仓库根目录构建:

dotnet build YtSoft.Gmc.Core.sln -m:1 -p:UseSharedCompilation=false

示例宿主与回归工程保留在原 TestRpcService 工程目录(本机为 F:\MyCode\TestRpcService)。以下命令从该目录执行,无需 Nacos:

dotnet run --project tests/RpcMiddleware.Smoke

覆盖自动监听和路由、宿主 HTTP 共存、配置与代码覆盖、开关、无效配置、自定义发现及旧用法。

旧的 AddRpcServer(options => ..., assemblies) + 手动监听 + MapRpcServer()、AddRpcClient(options => ...) + 自定义发现或手动 Nacos 注册方式仍保留。迁移到传入 IConfiguration 的新重载后,应删除重复的 RPC 监听、MapRpcServer 和手动 Nacos 注册。

首次构建需要还原现有 NuGet 依赖。最小接入先运行第 3.1 节服务端,再运行第 3.2 节客户端,预期输出 42;这条链路不需要 Nacos。随后再用第 5 节验证注册中心目录和跨进程调用。

部署时应确认:RPC 端口允许 HTTP/2 且从调用方可达;反向代理支持对应的 gRPC/h2c 或 TLS 配置;Nacos 的实际网络端口允许 SDK 连接;服务端公布的地址和端口与实际转发路径一致。关闭进程时让宿主正常停止,便于释放调用作用域并执行 Nacos 注销/取消订阅。

现象 优先检查
直连模式调用返回 404 检查 RpcMiddleware:Endpoints 中是否配置了对应服务;地址表可省略,省略后仍能启动
服务启动时报扫描异常 服务是否 public、别名是否重复、是否有重载或不支持的签名,程序集是否正确
400 顶层是否对象、外层参数名是否保留、是否多传/漏传、nullable 是否仅允许 null 而非省略
404 Nacos 调用是否传完整注册名;已发现的服务名与发布方法是否一致
503 首次快照是否完成;namespace/group 是否匹配;实例是否健康启用及元数据是否正确
502 是否连到 RPC HTTP/2 端口;h2c/TLS 是否一致;证书是否可信且匹配实例 IP;返回类型是否正确
504 全局/单次 deadline、业务耗时与下游超时;业务是否传递取消令牌
413 请求与响应完整消息是否超限,客户端和服务端上限是否一致
注册到错误网卡或端口 显式指定可达 AdvertiseAddress;检查监听端口与 RpcPort;动态端口确认 RPC 监听最后配置

原 TestRpcService 工程内可继续参考:Service_Test1 服务端(原 TestRpcService/Service_Test1/README.md)、Service_Test2 网关(原 TestRpcService/Service_Test2/README.md)、运行时与生命周期用例(原 TestRpcService/Service_Test1/Services/RuntimeService.cs)、HTTP → RPC smoke 工程(原 TestRpcService/tests/Service_Test2.Smoke/Service_Test2.Smoke.csproj)。现有示例宿主可能覆盖中间件默认值,请以各宿主的实际代码和配置为准。

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2 44 10/21/2026
1.0.1 38 10/20/2026
1.0.0 44 10/20/2026