gRPC在C#中需严控配置:csharp_namespace与package必须对齐,Protobuf需正确设置GrpcServices属性,GrpcChannel须单例复用,流式调用必须监听ServerCallContext.CancellationToken。

Grpc 在 C# 中不是“用不用”的问题,而是“怎么搭才不崩、怎么调才不慢、怎么改才不裂”。它不像 HttpClient 那样写完就能跑,稍有配置偏差,轻则连不上,重则生成代码缺失、字段错位、流式调用卡死。
proto 文件定义时 csharp_namespace 和 package 必须对齐
这是最常被忽略的“隐性依赖”。csharp_namespace 决定生成类的命名空间,package 影响服务全限定名(尤其在跨语言场景下)。两者不一致会导致客户端找不到服务类型,或反序列化时字段映射失败。
-
option csharp_namespace = "MyCompany.Services.Product";→ 生成的ProductServiceClient类就在这个命名空间里 -
package product.v1;→ 服务注册路径默认为/product.v1.ProductService/GetProduct,gRPCurl 或网关依赖这个 - 如果只改了
csharp_namespace没同步改package,ASP.NET Core 的MapGrpcService<T>()仍能注册,但客户端用GrpcChannel.ForAddress连过去后,调用会返回StatusCode.Unimplemented
Grpc.AspNetCore 项目中 Protobuf 引用必须带 GrpcServices 属性
很多人直接把 .proto 文件拖进项目,加个 <Protobuf> 就以为完事。但没指定 GrpcServices,生成的代码可能只有 DTO,没有服务基类或客户端代理——导致编译通过、运行时报 TypeLoadException 或 “no service base class found”。
- 服务端项目需写:
<Protobuf Include="Protos\product.proto" GrpcServices="Server" /> - 客户端项目需写:
<Protobuf Include="Protos\product.proto" GrpcServices="Client" /> - 共用协议项目(如
.NET Standard类库)建议用GrpcServices="Both",但要确保该类库不直接承载 HTTP 请求 - 生成的
*.Grpc.cs文件不会出现在解决方案资源管理器中,它在obj/下,所以“找不到 ProductServiceBase” 八成是GrpcServices漏配或拼写错误
客户端复用 GrpcChannel 是硬性要求,不是优化建议
GrpcChannel 是重量级对象,内部维护连接池、HTTP/2 流控、TLS 握手缓存。每次调用都新建 GrpcChannel.ForAddress(),不仅性能暴跌,还会快速耗尽端口(TIME_WAIT),在高并发下直接触发 SocketException: Too many open files。
- 必须作为单例或静态字段持有:
private static readonly GrpcChannel _channel = GrpcChannel.ForAddress("https://localhost:5001"); - 不要在
using块里创建 ——GrpcChannel实现了IAsyncDisposable,但它的DisposeAsync()是终结连接,不是“释放资源” - 若用 DI 容器(如 ASP.NET Core 的
IServiceCollection),应注册为Singleton:services.AddSingleton<GrpcChannel>(sp => GrpcChannel.ForAddress(...)); - 注意:通道不可跨 TLS/非 TLS 地址复用,
http://和https://必须分开实例
stream 方法里 ServerCallContext.CancellationToken 容易被忽略
一元调用(unary)靠方法返回 Task 自然响应取消;但服务端流(server streaming)或双向流(bidi streaming)中,客户端断连或超时后,服务端还在往响应流里写数据,就会抛出 RpcException: Status(StatusCode=Cancelled, Detail="") —— 这不是 bug,是未监听取消信号的表现。
- 所有流式方法实现里,必须检查
context.CancellationToken.IsCancellationRequested - 更稳妥的是在
while循环中传入context.CancellationToken,例如:await responseStream.WriteAsync(msg, context.CancellationToken).ConfigureAwait(false); - 不处理的结果:服务端线程卡在
WriteAsync,连接无法释放,监控看到大量 hanging RPC
真正麻烦的从来不是“怎么写通”,而是“怎么让通了之后不掉链子”:字段编号一旦提交就不能动、GrpcChannel 生命周期必须管住、ServerCallContext 的取消信号得真听——这些点不写进代码注释里,过三个月自己都得重读 proto 才想得起来。



















