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
<PackageReference Include="YtSoft.Gmc.Core.Rpc" Version="1.0.2" />
<PackageVersion Include="YtSoft.Gmc.Core.Rpc" Version="1.0.2" />
<PackageReference Include="YtSoft.Gmc.Core.Rpc" />
paket add YtSoft.Gmc.Core.Rpc --version 1.0.2
#r "nuget: YtSoft.Gmc.Core.Rpc, 1.0.2"
#:package YtSoft.Gmc.Core.Rpc@1.0.2
#addin nuget:?package=YtSoft.Gmc.Core.Rpc&version=1.0.2
#tool nuget:?package=YtSoft.Gmc.Core.Rpc&version=1.0.2
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 | Versions 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. |
-
net6.0
- Google.Protobuf (>= 3.27.2)
- Grpc.AspNetCore (>= 2.67.0)
- Grpc.Net.Client (>= 2.67.0)
- nacos-sdk-csharp (>= 1.3.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.