@levinzhang
2022-02-10T20:48:58.000000Z
字数 9674
阅读 522
by
在本文中,作者介绍了gRPC背后的核心概念以及如何使用它进行API开发。文章还通过情景分析的方式介绍了使用gRPC替换REST的优点和缺点。文中包含了一个逐步展开的教程,阐述了如何使用.NET开发基于gRPC的流服务。
从本质上来讲,API就是服务器和客户端之间的一个协议,指定了服务器如何基于客户端的请求提供特定的数据。
在构建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机制来进行结构化数据的序列化:
案例学习:
在如今的技术趋势下,比较现代的方式是构建微服务。在本例中,我们学习一下构建航空售票系统的过程:
上图展现了一个基于微服务的航空售票系统。在这里,有几个与这种类型的架构相关的关键点,我们需要注意:
假设我们现在有使用不同语言编写的微服务,它们之间要互相进行交流。当这些微服务想要交换信息的时候,它们需要就一些事情达成共识,比如:
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);
}
}
现在,我们需要更新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.Client
dotnet add package Google.Protobuf
dotnet 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频道,他会在那里分享自己的知识。