作者 | Mohamad Lawand
译者 | 张卫滨
策划 | 丁晓昀
从本质上来讲,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:
.proto
文件都能支持 12 种不同的语言。默认情况下,gRPC 会使用谷歌开源的 Protocol Buffers 机制来进行结构化数据的序列化:
案例学习:
在如今的技术趋势下,比较现代的方式是构建微服务。在本例中,我们学习一下构建航空售票系统的过程:
上图展现了一个基于微服务的航空售票系统。在这里,有几个与这种类型的架构相关的关键点,我们需要注意:
假设我们现在有使用不同语言编写的微服务,它们之间要互相进行交流。当这些微服务想要交换信息的时候,它们需要就一些事情达成共识,比如:
REST 是最流行的构建 API 的方案。但是,这个决策取决于很多与我们的实现相关的架构考量:
考虑到这些因素,我们再来看一下 gRPC 和 REST 的差异:
gRPC
*.proto
文件中定义的,它们是 gRPC 的核心。这是以一种语言中立的方式来定义 API。这些文件随后可以被其他编程语言用来生成代码(如强类型的客户端和消息类)。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文件的内容如下所示:
// 声明我们可以使用的最新模式
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 频道,他会在那里分享自己的知识。
原文链接:
https://www.infoq.com/articles/getting-started-grpc-dotnet/