1.JSON 字符编码方式使得传输数据量较大,而后端一般并不需要直接操作 JSON,都会将 JSON 转为平台专有类型后再处理;既然需要转换,为什么不选择一个数据量更小,转换更方便的格式呢?
2.调用双方要事先约定数据结构和调用接口,稍有变动就要手动更新相关代码(Model 类和方法签名);是否可以将约定固化为文档,服务提供者维护该文档,调用方根据该文档可以方便地生成自己需要的代码,在文档变化时代码也可以自动更新?
3.[之前] WebApi 基于的 Http[1.1] 协议已经诞生 20 多年,其定义的交互模式在今日已经捉襟见肘;业界需要一个更有效率的协议。
syntax = "proto3"; // 指定自动生成的类所在的命名空间,如果不指定则以下面的 package 为命名空间,这主要便于本项目内部的模块划分 option csharp_namespace = "Demo.Grpc"; // 对外提供服务的命名空间 package TestDemo; // 服务 service Greeter { // 接口 rpc SayHello (HelloRequest) returns (HelloReply); } // 不太好的一点是就算只有一个基础类型字段,也要新建一个 message 进行包装 message HelloRequest { string name = 1; } message HelloReply { string message = 1; }然后把它包含到项目文件中:
<ItemGroup> <Protobuf Include="Protos\greeter.proto" GrpcServices="Server" /> </ItemGroup>编译一下,Grpc.Tools 将帮我们生成 GreeterBase 类及两个模型类:
public abstract partial class GreeterBase { public virtual Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { throw new RpcException(new Status(StatusCode.Unimplemented, "")); } } public class HelloRequest { public string Name { get; set; } } public class HelloReply { public string Message { get; set; } }这里的 SayHello 是个空实现,我们新建一个实现类并填充业务逻辑,比如:
public class GreeterService : GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" }); } }最后将服务添加到路由管道,对外暴露:
using Demo.Grpc.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddGrpc(); var app = builder.Build(); // Configure the HTTP request pipeline. app.MapGrpcService<GreeterService>(); app.Run();protobuf-net.Grpc
using ProtoBuf.Grpc; using System.Runtime.Serialization; using System.ServiceModel; using System.Threading.Tasks; namespace Demo.Grpc; [DataContract] public class HelloReply { [DataMember(Order = 1)] public string Message { get; set; } } [DataContract] public class HelloRequest { [DataMember(Order = 1)] public string Name { get; set; } } [ServiceContract(Name = "TestDemo.GreeterService")] public interface IGreeterService { [OperationContract] Task<HelloReply> SayHelloAsync(HelloRequest request, CallContext context = default); }注意其中特性的修饰。
public class GrpcGlobalExceptionInterceptor : Interceptor { private readonly ILogger<GrpcGlobalExceptionInterceptor> _logger; public GrpcGlobalExceptionInterceptor(ILogger<GrpcGlobalExceptionInterceptor> logger) { _logger = logger; } public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation) { try { return await continuation(request, context); } catch (Exception ex) { _logger.LogError(new EventId(ex.HResult), ex, ex.Message); // do something // then you can choose throw the exception again throw ex; } } }上述代码在处理完异常后重新抛出,旨在让客户端接收处理该异常,然而,实际上客户端是无法接收到该异常信息的,除非服务端抛出的是RpcException;同时,为使客户端得到正确的 HttpStatusCode(默认是 200,即使客户端得到是 RpcException),需要显式给HttpContext.Response.StatusCode赋值,如下:
// ... catch(Exception ex) { var httpContext = context.GetHttpContext(); httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; // 注意,RpcException 的 StatusCode 和 Http 的 StatusCode 不是一一对应的 throw new RpcException(new Status(StatusCode.XXX, "some messages")); } // ...我们可以在构造 RpcException 对象时传递Metadata,用于携带额外的数据到客户端,如果需要传递复杂对象,那么要先按约定序列化成字节数组。
builder.Services.AddGrpc(options => { options.Interceptors.Add<GrpcGlobalExceptionInterceptor>(); });测试
// ... builder.Services.AddGrpcReflection(); var app = builder.Build(); // ... IWebHostEnvironment env = app.Environment; if (env.IsDevelopment()) { app.MapGrpcReflectionService(); }客户端
<ItemGroup> <Protobuf Include="Protos\greeter.proto" GrpcServices="Client" /> </ItemGroup>注意,如果只需要服务端提供的部分接口,那么 .proto 文件中只保留必要的接口即可,真正做到按需索取:)。我们还可以更改 .proto 文件中 message 的字段名(只要不改动字段类型和顺序),不会影响服务的调用。这也直接反映了 protobuf 不是按字段名而是事先定义的字段标识编码的。
// .proto 文件中的 package using TestDemo; // 这里注入的服务是 Transient 模式 builder.Services.AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); });如此,其它地方就可以愉快地使用客户端调用远程服务了。同服务端一样,我们可以给客户端配置统一的拦截器。如果服务端返回上文提到的 RpcException,客户端得到后是直接抛出的(就像是本地异常),我们可以新建一个专门的异常拦截器处理 RpcException 异常。
builder.Services .AddGrpcClient<Greeter.GreeterClient>(o => { o.Address = new Uri("https://localhost:5001"); }) .AddInterceptor<ExceptionInterceptor>(); // 默认创建一次,并在 GreeterClient 实例之间共享 //.AddInterceptor<ExceptionInterceptor>(InterceptorScope.Client); // 每个 GreeterClient 实例拥有自己的拦截器具体的异常处理逻辑就不举例了。提一下,通过 RpcException.Trailers 可以获取异常的 metadata 数据。另外,对于异常处理来说,如果项目是普通的 ASP.NET Core Web 服务,那么使用原先的 ActionFilterAttribute、IExceptionFilter等拦截器也是一样的,因为既然运行时出现了异常,这两者肯定也能捕获到。