[关闭]
@levinzhang 2022-02-10T20:48:58.000000Z 字数 9674 阅读 522

gRPC和.NET入门

by

摘要:

在本文中,作者介绍了gRPC背后的核心概念以及如何使用它进行API开发。文章还通过情景分析的方式介绍了使用gRPC替换REST的优点和缺点。文中包含了一个逐步展开的教程,阐述了如何使用.NET开发基于gRPC的流服务。


核心要点

从本质上来讲,API就是服务器和客户端之间的一个协议,指定了服务器如何基于客户端的请求提供特定的数据。

在构建API的时候,我们会想到不同的技术。根据需求不同,我们所选择的开发API的技术也会随之发生变化。在目前的这个时代,主要有两种用于创建API的技术:

这两种技术都使用HTTP作为传输机制。尽管使用了相同的底层传输机制,但是它们的实现却是完全不同的。

我们先对比一下这两项技术,然后再深入了解gRPC。

REST

REST是一套架构约束,而不是协议或标准。API开发人员可以使用各种方式来实现REST。

为了让一个API被认作是RESTful的,我们需要遵循一些约束条件:

gRPC

gRPC构建在RPC(远程过程调用,Remote Procedure Call)协议坚实的基础之上,它也进入了API的领域之中。gRPC是由谷歌开发的免费、开源的框架,它使用HTTP/2进行API通信,为API的设计者隐藏了HTTP实现。

gRPC有很多特征,所以不管是在微服务还是在web/移动API通信方面,都使其成为下一代web应用的基础模块:

与docker和kubernetes类似,gRPC是云原生基金会(CNCF)的一部分。

简而言之,gRPC的好处包括:

为了使用gRPC:

默认情况下,gRPC会使用谷歌开源的Protocol Buffers机制来进行结构化数据的序列化:

案例学习:

在如今的技术趋势下,比较现代的方式是构建微服务。在本例中,我们学习一下构建航空售票系统的过程:

上图展现了一个基于微服务的航空售票系统。在这里,有几个与这种类型的架构相关的关键点,我们需要注意:

假设我们现在有使用不同语言编写的微服务,它们之间要互相进行交流。当这些微服务想要交换信息的时候,它们需要就一些事情达成共识,比如:

REST是最流行的构建API的方案。但是,这个决策取决于很多与我们的实现相关的架构考量:

考虑到这些因素,我们再来看一下gRPC和REST的差异:

gRPC

REST API

基于这些对比,我们可以看到这两种方式各有其优点。但是,我们可以看到,gRPC为基于微服务的场景提供了一组强大的特性。

使用gRPC创建一个服务器-客户端应用

在开始编码之前,我们在自己的计算机上安装以下软件:

软件安装完成之后,我们需要创建项目结构(在本文中,我们将在终端/命令行中直接使用dotnet命令):

dotnet new grpc -n GrpcService

我们还需要配置SSL信任:

dotnet dev-certs https --trust

接下来,我们在VS Code打开这个新项目,看一下都创建了哪些内容。我们可以看到,我们自动有了如下的内容:

在Protos文件夹中,我们有一个greet.proto文件。正如我们在前文中所提到的,.proto能够以语言中立的方式来定义API。

从这个文件中,我们可以看到,它包含一个Greeter服务和一个SayHello方法。我们可以将Greeter服务视为控制器,将SayHello方法视为一个动作。.proto文件的内容如下所示:

  1. // 声明我们可以使用的最新模式
  2. syntax = "proto3";
  3. // 为该proto定义命名空间,通常与我们的Grpc服务器相同
  4. option csharp_namespace = "GrpcService";
  5. package greet;
  6. // 我们可以把一个服务看做一个类
  7. service Greeter {
  8. // 发送问候语
  9. rpc SayHello (HelloRequest) returns (HelloReply);
  10. }
  11. // 请求消息类似于C#中的一个模型,其中会定义属性
  12. // 这里的数字用来对属性进行排序
  13. message HelloRequest {
  14. string name = 1;
  15. }
  16. // 响应消息包含了问候语
  17. message HelloReply {
  18. string message = 1;
  19. }

SayHello方法接收一个HelloRequest(这是一个消息)并返回一个HelloReply(这也是一个消息)。

GreeterService文件中,我们可以看到有一个GreeterService类,它继承自Greeter.GreeterBase,后者是由.proto文件自动生成的。

SayHello方法中,我们会接收一个请求(HelloRequest)并返回一个响应(HelloReply)。它们也是由.proto文件自动为我们生成的。

代码自动生成会基于.proto文件定义为我们生成所需的文件。gRPC在代码生成、路由和序列化方面为我们做了所有繁重的工作。我们所需要做的就是实现基类并覆盖方法的实现。

接下来,我们尝试运行gRPC服务:

dotnet run

从自动生成的端点的结果中可以看到,我们不能像使用web浏览器作为REST的客户端那样使用gRPC。在这种情况下,我们需要创建一个gRPC客户端与服务进行通信。对于我们的客户端来讲,gRPC也需要.proto文件,因为它是一个契约优先的RPC框架。目前,我们的web浏览器对客户端(我们并没有.proto文件)一无所知,所以它不知道如何处理请求。

我们创建名为customers.proto的自定义.proto文件。这个文件必须要在Protos文件夹中创建,它的内容如下所示:

  1. syntax = "proto3";
  2. option csharp_namespace = "GrpcService";
  3. package customers;
  4. service Customer {
  5. rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);
  6. }
  7. message CustomerFindModel {
  8. int32 userId = 1; // bool, int32, float, double, string
  9. }
  10. message CustomerDataModel {
  11. string firstName = 1;
  12. string lastName = 2;
  13. }

保存完上述文件之后,我们需要将它添加到.csproj文件中:

  1. <ItemGroup>
  2. <Protobuf Include="Protos\\customers.proto" GrpcServices="Server" />
  3. </ItemGroup>

现在,我们需要构建应用:

dotnet build

下一步是添加我们的CustomerService类到Services文件夹中并更新其内容,如下所示:

  1. public class CustomerService : Customer.CustomerBase
  2. {
  3. private readonly ILogger<CustomerService> _logger;
  4. public CustomerService(ILogger<CustomerService> logger)
  5. {
  6. _logger = logger;
  7. }
  8. public override Task<CustomerDataModel> GetCustomerInfo(CustomerFindModel request, ServerCallContext context)
  9. {
  10. CustomerDataModel result = new CustomerDataModel();
  11. // 这是一个用于演示的代码
  12. // 在实际的场景中,这些信息应该从数据库中获取
  13. // 应用中的数据不应该被硬编码
  14. if(request.UserId == 1) {
  15. result.FirstName = "Mohamad";
  16. result.LastName = "Lawand";
  17. } else if(request.UserId == 2) {
  18. result.FirstName = "Richard";
  19. result.LastName = "Feynman";
  20. } else if(request.UserId == 3) {
  21. result.FirstName = "Bruce";
  22. result.LastName = "Wayne";
  23. } else {
  24. result.FirstName = "James";
  25. result.LastName = "Bond";
  26. }
  27. return Task.FromResult(result);
  28. }
  29. }

现在,我们需要更新Startup.cs类,以通知我们的应用程序,我们新创建的服务有了一个新的端点。为了实现这一点,在Configure方法(位于app.UserEndpoints中)里面,我们需要添加如下的代码:

endpoints.MapGrpcService<CustomerService>();

MacOS下的注意事项

因为MacOS不支持TLS之上的HTTP/2,所以我们需要采用如下的方案来更新Program.cs文件:

  1. webBuilder.ConfigureKestrel(options =>
  2. {
  3. // 设置无需TLS的HTTP/2端点
  4. options.ListenLocalhost(5000, o => o.Protocols =
  5. HttpProtocols.Http2);
  6. });

下一步就是创建我们的客户端应用:

dotnet new console -o GrpcGreeterClient

现在,我们需要添加必要的包到客户端控制台应用中,使其能够识别gRPC。这可以通过在GrpcGreeterClient类中实现:

  1. dotnet add package Grpc.Net.Client
  2. dotnet add package Google.Protobuf
  3. dotnet add package Grpc.Tools

因为我们需要客户端具有和服务器端相同的契约,所以需要将前面步骤中创建的.proto文件添加到客户端应用中。为了实现这一点:

1.首先,我们需要添加一个名为Protos的文件夹到客户端项目中。

2.我们需要复制gRPC greeter服务中Protos文件夹里的内容到gRPC客户端项目,即

3.在粘贴完文件之后,我们需要更新命名空间,使其与客户端应用相同:

option csharp_namespace = "GrpcGreeterClient";

4.我们需要更新GrpcGreeterClient.csproj文件,以便让它知道我们新增加的.proto文件:

  1. <ItemGroup>
  2. <Protobuf Include="Protos\\greet.proto" GrpcServices="Client" />
  3. </ItemGroup>
  4. <ItemGroup>
  5. <Protobuf Include="Protos\\customers.proto" GrpcServices="Client" />
  6. </ItemGroup>

这个Protobuf元素是代码自动生成特性了解.proto文件的方式。通过上面的改动,我们在这里表明,希望客户端使用我们新添加的.proto文件。

我们需要构建客户端并确保所有内容都能构建成功:

dotnet run

现在,我们添加一些代码到控制台应用中,以便于调用服务器端。在Program.cs文件中,我们需要做如下的改动:

  1. // 我们创建一个通道,它代表了客户端到服务器的连接
  2. // 我们在这里添加的URL是由服务器的Kestrel所提供的
  3. var channel = GrcpChannel.ForAddress("<https://localhost:5001>");
  4. // 这个强类型的客户端是当我们添加.proto文件时,由代码生成功能所创建的
  5. var client = new Greeter.GreeterClient(channel);
  6. var response = await client.SayHelloAsync(new HelloRequest
  7. {
  8. Name = "Mohamad"
  9. });
  10. Console.WriteLine("From Server: " + response.Message);
  11. var customerClient = new Customer.CustomerClient(channel);
  12. var result = await customerClient.GetCustomerInfoAsync(new CustomerFindModel()
  13. {
  14. UserId = 1
  15. });
  16. Console.WriteLine($"First Name: {result.FirstName} - Last Name: {result.LastName}");

现在,我们为应用添加流处理的功能。

我们回到customers.proto文件并在Customer服务中添加一个流方法:

  1. // 我们要返回一个消费者的列表
  2. // 但是在gRPC中我们不能返回列表,而是需要返回一个流
  3. rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);

正如我们所看到的,在返回中,我们添加了stream关键字,这意味着我们正在添加由“多个”回复所组成的stream

同时,我们还需要添加一个空消息

  1. // 在gRPC中,我们不能定义具有空参数的方法
  2. // 所以,我们定义一个空消息
  3. message AllCustomerModel {
  4. }

要实现这个方法,我们需要到Services文件夹下并添加如下的代码到CustomerService类中:

  1. public override async Task GetAllCustomers(AllCustomerModel request, IServerStreamWriter<CustomerDataModel> responseStream, ServerCallContext context)
  2. {
  3. var allCustomers = new List<CustomerDataModel>();
  4. var c1 = new CustomerDataModel();
  5. c1.Name = "Mohamad Lawand";
  6. c1.Email = "mohamad@mail.com";
  7. allCustomers.Add(c1);
  8. var c2 = new CustomerDataModel();
  9. c2.Name = "Richard Feynman";
  10. c2.Email = "richard@physics.com";
  11. allCustomers.Add(c2);
  12. var c3 = new CustomerDataModel();
  13. c3.Name = "Bruce Wayne";
  14. c3.Email = "bruce@gotham.com";
  15. allCustomers.Add(c3);
  16. var c4 = new CustomerDataModel();
  17. c4.Name = "James Bond";
  18. c4.Email = "007@outlook.com";
  19. allCustomers.Add(c4);
  20. foreach(var item in allCustomers)
  21. {
  22. await responseStream.WriteAsync(item);
  23. }
  24. }

现在,我们需要复制服务器端customers.proto文件的变化到客户端的customers.proto文件中:

  1. service Customer {
  2. rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);
  3. // 我们要返回一个消费者的列表
  4. // 但是在gRPC中我们不能返回列表,而是需要返回一个流
  5. rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);
  6. }
  7. // 在gRPC中,我们不能定义具有空参数的方法
  8. // 所以,我们定义一个空消息
  9. message AllCustomerModel {
  10. }

现在,我们需要再次构建应用:

dotnet build

我们下一步需要更新GrpcClientApp中的Program.cs文件以处理新的流方法:

  1. var customerCall = customerClient.GetAllCustomers(new AllCustomerModel());
  2. await foreach(var customer in customerCall.ResponseStream.ReadAllAsync())
  3. {
  4. Console.WriteLine($"{customer.Name} {customer.Email}");
  5. }

现在,我们回到GrpcGreeter并更新greet.proto文件,为其添加流方法:

rpc SayHelloStream(HelloRequest) returns (stream HelloReply);

可以看到,在返回中我们添加了关键字stream,这意味着我们正在添加由“多个”回复所组成的stream。要实现这个方法,我们需要到Services文件夹下,并在GreeterService中添加如下的内容:

  1. public override async Task SayHelloStream(HelloRequest request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
  2. {
  3. for (int i = 0; i < 10; i ++)
  4. {
  5. await responseStream.WriteAsync(new HelloReply
  6. {
  7. Message = "Hello " + request.Name + " " + i
  8. });
  9. await Task.Delay(TimeSpan.FromSeconds(1));
  10. }
  11. }

现在,我们需要将greet.proto文件的变更从服务器端复制到客户端,并对其进行构建。在客户端应用的greet.proto文件中,我们添加如下这行代码:

rpc SayHelloStream(HelloRequest) returns (stream HelloReply);

确保在保存.proto文件后,对应用进行构建。

dotnet build

现在,我们可以打开Program.cs并使用新的方法:

  1. var call = client.SayHelloStream(new HelloRequest
  2. {
  3. Name = "Mohamad"
  4. });
  5. await foreach(var item in call.ResponseStream.ReadAllAsync())
  6. {
  7. Console.WriteLine("Result " + item.Message);
  8. }

该样例阐述了我们如何在.NET 5中实现gRPC的客户端-服务器应用。

总结

我们可以看到gRPC在构建应用程序中的力量,但要发挥这种力量并不容易,因为构建gRPC服务需要更多的搭建时间以及客户端与服务器之间的协调。而使用REST的时候,我们几乎不需要任何搭建过程就可以直接开始消费端点。

gRPC不一定会取代REST,因为这两种技术都有其特定的应用场景。请基于你的业务场景和需求,为自己的项目选择合适的技术。

关于作者

Mohamad Lawand是一位坚定的、具有前瞻性的技术架构师,拥有13年以上的工作经验,工作范围涉及从金融机构到政府实体等众多行业。他积极主动,适应性强,擅长跨多平台的SaaS和区块链技术。Mohamad还拥有一个Youtube频道,他会在那里分享自己的知识。

查看英文原文:Getting Started with gRPC and .NET

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注