1. gRPCを選ぶ理由
1.1.歴史と歴史
长久以来,我们在前后端交互时使用WebApi + JSON方式,后端服务之间调用同样如此(或者更久远之前的WCF + XML方式)。WebApi + JSON 是优选的,很重要的一点是它们两者都是平台无关的三方标准,且足够语义化,便于程序员使用,在异构(前后端、多语言后端)交互场景下是不二选择。然而,在后端服务体系改进特别是后来微服务兴起后,我们发现,前后端交互理所当然认可的 WebApi + JSON 在后端体系内显得有点不太合适:
- JSON文字エンコーディングは送信データ量を大きくし、バックエンドは一般的にJSONを直接操作する必要はなく、JSONをプラットフォーム固有の型に変換して処理します。変換が必要なので、データ量が小さく、変換が便利な形式を選択しませんか?
- 呼び出し側は事前にデータ構造と呼び出しインターフェイスに合意する必要があり、わずかな変更は関連するコード(モデルクラスとメソッド署名)を手動で更新する必要があります。契約をドキュメントに固定することができ、サービスプロバイダはドキュメントを維持し、呼び出し側はドキュメントに応じて簡単に必要なコードを生成でき、ドキュメントが変更されたときにコードを自動的に更新できますか?
- WebApiベースのHTTP[1.1]プロトコルは20年以上前から存在しており、その定義されたインタラクションパターンは今日では限界に達しています。業界はより効率的なプロトコルを必要としています。
1.2.効率的な伝送-Http2.0
我们先来说第 3 个问题,其实很多大厂内部早已开始着手处理,并诞生了一些应用广泛的框架,如阿里开源的Dubbo,直接抛弃了 Http 改为基于 Tcp 实现,效率得到明显提升,不过 Dubbo 依赖 Java 环境,无法跨平台使用,不在我们考虑范围。
另一个大厂 Google,内部也在长期使用自研的Stubby框架,与 Dubbo 不同的是,Studdy 是跨平台的,但是 Google 认为 Studdy 不基于任何标准,而且与其内部基础设施紧密耦合,并不适合公开发布。
同時に、GoogleはHttp1.1プロトコルを強化しています。これは2012 年に提案されたSPDYスキームで、HTTPプロトコル層を最適化し、データストリームの多重化、リクエストの優先順位付け、HTTPヘッダ圧縮などの新機能を追加しています。Googleによると、SPDYプロトコルの導入により、ラボテストではページの読み込みが64%高速になった。この大きな改善により、誰もが古いバージョンのHTTPプロトコルの問題をポジティブに見て解決し始め、Http2.0の誕生を直接加速させました。実際、Http2.0はSPDYをプロトタイプとして議論され標準化されており、もちろんより多くの改良と調整が加えられている。
Http2.0の出現と人気により、Stubbyが提供していない機能を含め、Stubbyと同じ機能の多くがパブリック標準に登場しました。Stubbyをやり直して、この標準化を活用し、Androidなどのモバイルデバイス、IoT(IoT)、ブラウザをバックエンドサービスに接続できる分散コンピューティングのラストマイルまで拡張する時が来たことは明らかです。
2015 年 3 月,Google 决定在公开场合构建下一版 Stubby,以便与业界分享经验,并进行相关合作,也就是本文的主角gRPC。
1.3.効率的なコーディング-protobuf
回头来看第 1 个问题,解决起来相对比较简单,无非是将傻瓜式字符编码转为更有效的二进制编码(比如数字 10000 JSON 编码后是 5 个字节,按整型编码就是 4 个字节),同时加上些事先约定的编码算法使得最终结果更紧凑。常见的平台无关的编码格式有MessagePack和protobuf等,我们以 protobuf 为例。
protobuf 采用 varint 和 处理负数的 ZigZag 两种编码方式使得数值字段占用空间大大减少;同时它约定了字段类型和标识,采用 TLV 方式,将字段名映射为小范围结果集中的一项(比如对于不超过 256 个字段的数据体来说,不管字段名本身的长度多少,每个字段名都只要 1 个字节就能标识),同时移除了分隔符,并且可以过滤空字段(若字段没有被赋值,那么该字段不会出现在序列化结果中)。
1.4.効率的なプログラミング-コード生成ツール
第 2 个问题呢,其实需要的就是[每个平台]一套代码生成工具。生成的代码需要覆盖类的定义、对象的序列化/反序列化、服务接口的暴露和远程调用等等必要的模板代码,如此,开发人员只需要负责接口文档的维护和业务代码的实现(很自然的面向接口编程:))。此时,采用 protobuf 的gRPC自然而然的映入眼帘,因为对于目前所有主要的编程语言和平台,都有 gRPC 工具和库,包括 .NET、Java、Python、Go、C++、Node.js、Swift、Dart、Ruby 以及 PHP。可以说,这些工具和库的提供,使得 gRPC 可以跨多种语言和平台一致地工作,成为一个全面的 RPC 解决方案。
2. NETでのg RPCの使用
ASP.NET Core 3.0 开始,支持gRPC作为 .NET 平台中的“一等公民”。
2.1.サーバー側。
在 VS 中新建ASP.NET Core gRPC 服务,会发现在项目文件中自动引入了Microsoft.NET.Sdk.Web类库,很明显,gRPC 服务仍然是 Web 服务,毕竟它走的是 Http 协议。同时还引入了Grpc.AspNetCore类库,该类库引用了几个子类库需要了解下:
Google.Protobuf:包含 protobuf 预定义 message 类型在 C# 中的实现;Grpc.Tools:上面讲到的代码生成工具,编译时使用,运行时不需要,因此依赖项标记为 PrivateAssets="All";Grpc.AspNetCore.Server:服务端专用;Grpc.Net.ClientFactory:客户端专用,如果只是提供服务的话,那么该类库可以移除。
インタフェース·ファイルを定義する
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クラスと2つのモデルクラスを生成します。
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();
2.1.1. protobuf-net.Grpc
如果觉得写 .proto 文件太别扭,希望可以按传统方式写接口,那么社区项目protobuf-net.Grpc值得尝试,使用它可以它通过特性批注的 .NET 类型来定义应用的 gRPC 服务和消息。
まず、Grpc.AspNetCoreを参照する代わりに、代わりにprotobuf-net.Grpcライブラリを参照します。また、.protoファイルを書く代わりに、インターフェイスクラスを直接書く:
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);
}
その特徴に注意してください。
実装クラスを書き終えたらProgram.csに登録すればよいので、ここでは触れない。
使用 protobuf-net.Grpc,我们不需要写 .proto 文件,但是调用方特别是其他平台的调用方,需要 .proto 文件来生成相应的客户端,难道我们还要另外再写一份吗?别急,我们可以引入protobuf-net.Grpc.AspNetCore.Reflection,它引用的protobuf-net.Grpc.Reflection提供了根据 C# 接口生成 .proto 文件的方法;同时使用它还便于客户端测试,同Grpc.AspNetCore.Server.Reflection的作用一样,下文会讲到。
2.1.2.例外の処理
Netはg RPC用のインターセプタメカニズムを提供し、新しいインターセプタを作成してビジネス例外を統一的に処理する。
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>();
});
2.1.3.テストテスト
服务端完成后,如果要借助Postman或者gRPCurl测试,那么它们其实就是调用服务的客户端,要让它们事先知道服务约定信息,有两种方法:
- サービスに関するすべての情報が. protoファイルで定義されているので、.protoファイルを提供してください。
- サーバは、サービス情報にアクセスできるインタフェースを公開します。
如果要用方法 2,那么要先引入Grpc.AspNetCore.Server.Reflection类库,然后在 Program.cs 中注册接口:
// ...
builder.Services.AddGrpcReflection();
var app = builder.Build();
// ...
IWebHostEnvironment env = app.Environment;
if (env.IsDevelopment())
{
app.MapGrpcReflectionService();
}
2.2.クライアント側は
クライアントはGrpc.AspNetCore.Serverを必要としないので、Google.Protobuf、Grpc.Tools、Grpc. Net.ClientFactoryを直接参照します。
サーバーから提供される.protoファイルをプロジェクトに追加し、プロジェクトファイルに次の項目を含めます。
<ItemGroup>
<Protobuf Include="Protos\greeter.proto" GrpcServices="Client" />
</ItemGroup>
サーバーが提供するインターフェイスの一部だけが必要な場合、.protoファイルには必要なインターフェイスのみが残り、オンデマンドでリクエストすることができます。
protoファイル内のメッセージのフィールド名を変更することもできます(フィールドの型と順序を変更しない限り)。これは、protobufがフィールド名ではなく定義済みのフィールド識別子でエンコードされることを直接反映している。
由此,假如我们有多个 .proto 文件,使用到了相同结构的 message,无所谓字段名是否相同,我们都可以将这些 message 抽离为单独的一个 .proto 文件,然后其他的 .proto 文件使用import "Protos/xxx.proto";引入它。
コンパイルして、Program.csにサービスクライアントを登録します。
// .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等拦截器也是一样的,因为既然运行时出现了异常,这两者肯定也能捕获到。
2.3.高度な知識です
本文未涉及的 .NET-gRPC 的进阶知识诸如单元测试、服务调用中止、负载均衡、健康监控等,以后有机会再与大家分享。其实这方面微软官方文档已经讲解得相当全面了,但也难以覆盖在实操过程中遇到的所有问题,所以有此文以飨读者,还望不吝指教。
3. リファレンス·リソース
この記事は転載から。
著者:ライプニッツ
原文标题:gRPC入門と実操. NET篇
原文へのリンク:https//www.cnblogs.com/newton/archive/2023/01/10/17033789.html