@levinzhang
2022-02-10T12:48:58.000000Z
字数 9674
阅读 950
by
在本文中,作者介绍了gRPC背后的核心概念以及如何使用它进行API开发。文章还通过情景分析的方式介绍了使用gRPC替换REST的优点和缺点。文中包含了一个逐步展开的教程,阐述了如何使用.NET开发基于gRPC的流服务。
从本质上来讲,API就是服务器和客户端之间的一个协议,指定了服务器如何基于客户端的请求提供特定的数据。
/filters:no_upscale()/articles/getting-started-grpc-dotnet/en/resources/1gRPC-api-request-data-1638895461217.jpeg)
在构建API的时候,我们会想到不同的技术。根据需求不同,我们所选择的开发API的技术也会随之发生变化。在目前的这个时代,主要有两种用于创建API的技术:
这两种技术都使用HTTP作为传输机制。尽管使用了相同的底层传输机制,但是它们的实现却是完全不同的。
我们先对比一下这两项技术,然后再深入了解gRPC。
REST是一套架构约束,而不是协议或标准。API开发人员可以使用各种方式来实现REST。
为了让一个API被认作是RESTful的,我们需要遵循一些约束条件:
gRPC构建在RPC(远程过程调用,Remote Procedure Call)协议坚实的基础之上,它也进入了API的领域之中。gRPC是由谷歌开发的免费、开源的框架,它使用HTTP/2进行API通信,为API的设计者隐藏了HTTP实现。
gRPC有很多特征,所以不管是在微服务还是在web/移动API通信方面,都使其成为下一代web应用的基础模块:
与docker和kubernetes类似,gRPC是云原生基金会(CNCF)的一部分。
简而言之,gRPC的好处包括:
为了使用gRPC:
.proto文件都能支持12种不同的语言。默认情况下,gRPC会使用谷歌开源的Protocol Buffers机制来进行结构化数据的序列化:
/filters:no_upscale()/articles/getting-started-grpc-dotnet/en/resources/1gRPC-request-response-1638895461217.jpeg)
案例学习:
在如今的技术趋势下,比较现代的方式是构建微服务。在本例中,我们学习一下构建航空售票系统的过程:
/filters:no_upscale()/articles/getting-started-grpc-dotnet/en/resources/1gRPC-flight-1638895461217.jpeg)
上图展现了一个基于微服务的航空售票系统。在这里,有几个与这种类型的架构相关的关键点,我们需要注意:
假设我们现在有使用不同语言编写的微服务,它们之间要互相进行交流。当这些微服务想要交换信息的时候,它们需要就一些事情达成共识,比如:
REST是最流行的构建API的方案。但是,这个决策取决于很多与我们的实现相关的架构考量:
考虑到这些因素,我们再来看一下gRPC和REST的差异:
gRPC
*.proto文件中定义的,它们是gRPC的核心。这是以一种语言中立的方式来定义API。这些文件随后可以被其他编程语言用来生成代码(如强类型的客户端和消息类)。REST API
基于这些对比,我们可以看到这两种方式各有其优点。但是,我们可以看到,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文件的内容如下所示:
// 声明我们可以使用的最新模式syntax = "proto3";// 为该proto定义命名空间,通常与我们的Grpc服务器相同option csharp_namespace = "GrpcService";package greet;// 我们可以把一个服务看做一个类service Greeter {// 发送问候语rpc SayHello (HelloRequest) returns (HelloReply);}// 请求消息类似于C#中的一个模型,其中会定义属性// 这里的数字用来对属性进行排序message HelloRequest {string name = 1;}// 响应消息包含了问候语message HelloReply {string message = 1;}
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文件夹中创建,它的内容如下所示:
syntax = "proto3";option csharp_namespace = "GrpcService";package customers;service Customer {rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);}message CustomerFindModel {int32 userId = 1; // bool, int32, float, double, string}message CustomerDataModel {string firstName = 1;string lastName = 2;}
保存完上述文件之后,我们需要将它添加到.csproj文件中:
<ItemGroup><Protobuf Include="Protos\\customers.proto" GrpcServices="Server" /></ItemGroup>
现在,我们需要构建应用:
dotnet build
下一步是添加我们的CustomerService类到Services文件夹中并更新其内容,如下所示:
public class CustomerService : Customer.CustomerBase{private readonly ILogger<CustomerService> _logger;public CustomerService(ILogger<CustomerService> logger){_logger = logger;}public override Task<CustomerDataModel> GetCustomerInfo(CustomerFindModel request, ServerCallContext context){CustomerDataModel result = new CustomerDataModel();// 这是一个用于演示的代码// 在实际的场景中,这些信息应该从数据库中获取// 应用中的数据不应该被硬编码if(request.UserId == 1) {result.FirstName = "Mohamad";result.LastName = "Lawand";} else if(request.UserId == 2) {result.FirstName = "Richard";result.LastName = "Feynman";} else if(request.UserId == 3) {result.FirstName = "Bruce";result.LastName = "Wayne";} else {result.FirstName = "James";result.LastName = "Bond";}return Task.FromResult(result);}}
/filters:no_upscale()/articles/getting-started-grpc-dotnet/en/resources/1image-4-1638895461217.jpg)
现在,我们需要更新Startup.cs类,以通知我们的应用程序,我们新创建的服务有了一个新的端点。为了实现这一点,在Configure方法(位于app.UserEndpoints中)里面,我们需要添加如下的代码:
endpoints.MapGrpcService<CustomerService>();
MacOS下的注意事项:
因为MacOS不支持TLS之上的HTTP/2,所以我们需要采用如下的方案来更新Program.cs文件:
webBuilder.ConfigureKestrel(options =>{// 设置无需TLS的HTTP/2端点options.ListenLocalhost(5000, o => o.Protocols =HttpProtocols.Http2);});
下一步就是创建我们的客户端应用:
dotnet new console -o GrpcGreeterClient
现在,我们需要添加必要的包到客户端控制台应用中,使其能够识别gRPC。这可以通过在GrpcGreeterClient类中实现:
dotnet add package Grpc.Net.Clientdotnet add package Google.Protobufdotnet add package Grpc.Tools
因为我们需要客户端具有和服务器端相同的契约,所以需要将前面步骤中创建的.proto文件添加到客户端应用中。为了实现这一点:
1.首先,我们需要添加一个名为Protos的文件夹到客户端项目中。
2.我们需要复制gRPC greeter服务中Protos文件夹里的内容到gRPC客户端项目,即
3.在粘贴完文件之后,我们需要更新命名空间,使其与客户端应用相同:
option csharp_namespace = "GrpcGreeterClient";
4.我们需要更新GrpcGreeterClient.csproj文件,以便让它知道我们新增加的.proto文件:
<ItemGroup><Protobuf Include="Protos\\greet.proto" GrpcServices="Client" /></ItemGroup><ItemGroup><Protobuf Include="Protos\\customers.proto" GrpcServices="Client" /></ItemGroup>
这个Protobuf元素是代码自动生成特性了解.proto文件的方式。通过上面的改动,我们在这里表明,希望客户端使用我们新添加的.proto文件。
我们需要构建客户端并确保所有内容都能构建成功:
dotnet run
现在,我们添加一些代码到控制台应用中,以便于调用服务器端。在Program.cs文件中,我们需要做如下的改动:
// 我们创建一个通道,它代表了客户端到服务器的连接// 我们在这里添加的URL是由服务器的Kestrel所提供的var channel = GrcpChannel.ForAddress("<https://localhost:5001>");// 这个强类型的客户端是当我们添加.proto文件时,由代码生成功能所创建的var client = new Greeter.GreeterClient(channel);var response = await client.SayHelloAsync(new HelloRequest{Name = "Mohamad"});Console.WriteLine("From Server: " + response.Message);var customerClient = new Customer.CustomerClient(channel);var result = await customerClient.GetCustomerInfoAsync(new CustomerFindModel(){UserId = 1});Console.WriteLine($"First Name: {result.FirstName} - Last Name: {result.LastName}");
现在,我们为应用添加流处理的功能。
我们回到customers.proto文件并在Customer服务中添加一个流方法:
// 我们要返回一个消费者的列表// 但是在gRPC中我们不能返回列表,而是需要返回一个流rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);
正如我们所看到的,在返回中,我们添加了stream关键字,这意味着我们正在添加由“多个”回复所组成的stream。
同时,我们还需要添加一个空消息
// 在gRPC中,我们不能定义具有空参数的方法// 所以,我们定义一个空消息message AllCustomerModel {}
要实现这个方法,我们需要到Services文件夹下并添加如下的代码到CustomerService类中:
public override async Task GetAllCustomers(AllCustomerModel request, IServerStreamWriter<CustomerDataModel> responseStream, ServerCallContext context){var allCustomers = new List<CustomerDataModel>();var c1 = new CustomerDataModel();c1.Name = "Mohamad Lawand";c1.Email = "mohamad@mail.com";allCustomers.Add(c1);var c2 = new CustomerDataModel();c2.Name = "Richard Feynman";c2.Email = "richard@physics.com";allCustomers.Add(c2);var c3 = new CustomerDataModel();c3.Name = "Bruce Wayne";c3.Email = "bruce@gotham.com";allCustomers.Add(c3);var c4 = new CustomerDataModel();c4.Name = "James Bond";c4.Email = "007@outlook.com";allCustomers.Add(c4);foreach(var item in allCustomers){await responseStream.WriteAsync(item);}}
现在,我们需要复制服务器端customers.proto文件的变化到客户端的customers.proto文件中:
service Customer {rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);// 我们要返回一个消费者的列表// 但是在gRPC中我们不能返回列表,而是需要返回一个流rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);}// 在gRPC中,我们不能定义具有空参数的方法// 所以,我们定义一个空消息message AllCustomerModel {}
现在,我们需要再次构建应用:
dotnet build
我们下一步需要更新GrpcClientApp中的Program.cs文件以处理新的流方法:
var customerCall = customerClient.GetAllCustomers(new AllCustomerModel());await foreach(var customer in customerCall.ResponseStream.ReadAllAsync()){Console.WriteLine($"{customer.Name} {customer.Email}");}
现在,我们回到GrpcGreeter并更新greet.proto文件,为其添加流方法:
rpc SayHelloStream(HelloRequest) returns (stream HelloReply);
可以看到,在返回中我们添加了关键字stream,这意味着我们正在添加由“多个”回复所组成的stream。要实现这个方法,我们需要到Services文件夹下,并在GreeterService中添加如下的内容:
public override async Task SayHelloStream(HelloRequest request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context){for (int i = 0; i < 10; i ++){await responseStream.WriteAsync(new HelloReply{Message = "Hello " + request.Name + " " + i});await Task.Delay(TimeSpan.FromSeconds(1));}}
现在,我们需要将greet.proto文件的变更从服务器端复制到客户端,并对其进行构建。在客户端应用的greet.proto文件中,我们添加如下这行代码:
rpc SayHelloStream(HelloRequest) returns (stream HelloReply);
确保在保存.proto文件后,对应用进行构建。
dotnet build
现在,我们可以打开Program.cs并使用新的方法:
var call = client.SayHelloStream(new HelloRequest{Name = "Mohamad"});await foreach(var item in call.ResponseStream.ReadAllAsync()){Console.WriteLine("Result " + item.Message);}
该样例阐述了我们如何在.NET 5中实现gRPC的客户端-服务器应用。
我们可以看到gRPC在构建应用程序中的力量,但要发挥这种力量并不容易,因为构建gRPC服务需要更多的搭建时间以及客户端与服务器之间的协调。而使用REST的时候,我们几乎不需要任何搭建过程就可以直接开始消费端点。
gRPC不一定会取代REST,因为这两种技术都有其特定的应用场景。请基于你的业务场景和需求,为自己的项目选择合适的技术。
Mohamad Lawand是一位坚定的、具有前瞻性的技术架构师,拥有13年以上的工作经验,工作范围涉及从金融机构到政府实体等众多行业。他积极主动,适应性强,擅长跨多平台的SaaS和区块链技术。Mohamad还拥有一个Youtube频道,他会在那里分享自己的知识。