闽公网安备 35020302035485号
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等拦截器也是一样的,因为既然运行时出现了异常,这两者肯定也能捕获到。