使用 C++ 实现 gRPC






4.94/5 (7投票s)
本文解释了如何编写依赖 gRPC 使用协议缓冲区传输数据的应用程序。
随着应用程序从桌面转向互联网,高速数据传输变得越来越必要。gRPC 是一种强大的网络通信技术,是本文的重点。gRPC 不易理解,但一旦你掌握了学习曲线,你将能够快速开发高性能的客户端-服务器应用程序。
gRPC 开发的目标是为两个系统生成代码:一个请求一个或多个方法的客户端,以及一个能够执行这些方法的服务器。整个过程有三个步骤:
- 编写一个定义服务和消息类型的 proto 文件。
- 将 proto 文件转换为传统的源代码,例如 Python、Java 或 C++。
- 将源代码集成到客户端和服务器应用程序中。
本文重点介绍如何使用 gRPC 在 C++ 中构建一个简单的客户端-服务器应用程序。客户端提交对 `TimesTwo` 方法的请求,并提供一个整数作为请求对象。服务器执行其 `TimesTwo` 例程,并提供一个包含两倍于请求中整数的整数作为响应。
1. gRPC 概述
在网络通信的早期,工程师们寻找一种机制,允许一个系统在另一个系统上执行例程。这被称为远程过程调用 (RPC),并且为此开发了许多技术,包括公共对象请求代理体系结构 (CORBA) 和 Java 远程方法调用 (RMI)。
2015年,谷歌发布了一个实现新型 RPC 通信的软件包。该软件包名为 gRPC,客户端和服务器使用高性能数据结构(称为协议缓冲区)传输数据。本节探讨 gRPC 和协议缓冲区的基本操作,然后展示如何将 proto 文件转换为 C++ 代码。
1.1 gRPC 基础
要理解 gRPC,熟悉客户端可以请求的不同类型过程非常重要。gRPC 将这些过程称为方法,客户端可以要求服务器执行四种类型的方法。方法类型取决于客户端请求和服务器响应中数据的性质。
- 一元 (unary) - 请求包含简单数据,响应包含简单数据
- 服务器流 (server streaming) - 客户端发出请求并接收数据流作为响应
- 客户端流 (client streaming) - 客户端向服务器发送数据流并接收简单响应
- 双向流 (bidirectional streaming) - 客户端向服务器发送数据流并接收数据流作为响应
图 1 说明了一元过程调用的工作原理。服务器在一个服务中提供三个方法,客户端进行远程过程调用以执行其中一个方法。作为 RPC 的一部分,客户端向服务器发送请求,服务器回复响应。
图 1:gRPC 会话中的客户端-服务器交互
对于一元远程过程调用,请求-响应过程包括六个步骤:
- 客户端调用一个启动 RPC 的方法(称为存根)。
- 服务器收到客户端打算请求 RPC 的通知,包括客户端的元数据描述。
- (可选) 服务器可以向客户端发送自身的元数据描述。
- 服务器等待客户端的完整请求消息。
- 服务器执行客户端消息中请求的服务。
- 如果服务执行成功,服务器将向客户端发送其响应。
要在代码中实现这一点,开发人员需要指定可以访问的方法以及请求和响应中包含的数据类型。这通过编写称为 proto 文件的特殊文件来完成。
1.2 编写 Proto 文件
至少,gRPC 应用程序中的 proto 文件需要指定三项内容:
- 一个提供一个或多个可由服务器执行的方法的服务
- 客户端请求中包含的数据类型
- 服务器响应中包含的数据类型
在 proto 文件中,这些项中的每一项都由一个命名的代码块表示。代码块的基本结构相当简单:
type name { ... }
“type”标识符可以设置为 `service`(用于服务定义)或 `message`(用于数据定义)。“name”标识符是服务或消息的唯一名称。
1.2.1 服务定义
要定义服务,必须编写一个将“type”标识符设置为 `service` 的代码块。在大括号内部,该块需要定义每个可以调用的方法。每个方法定义有四个重要属性:
- 它以 `rpc` 开头。
- 它包含服务执行时要调用的方法的名称。
- 方法名称后跟括号中的请求数据类型。
- 请求数据类型后跟 `returns` 和括号中的响应数据类型。
一个例子将阐明服务和方法的定义方式。以下代码定义了一个名为 `SimpleMath` 的服务,其中包含一个名为 `TimesTwo` 的方法定义。
service SimpleMath { rpc TimesTwo(ReqType) returns (RespType); }
要启动 gRPC,客户端访问 `SimpleMath` 服务并调用一个与 `TimesTwo` 方法对应的特殊例程(存根)。当客户端调用 `TimesTwo` 存根时,服务器的 `TimesTwo` 例程将接收 `ReqType` 类型给出的请求数据。执行例程后,服务器创建 `RespType` 类型的响应并将其发送给客户端。
方法后面可以跟选项,这些选项限制服务器将如何执行该方法。每个选项行包含 `option` 关键字,后跟括号中的选项名称,该名称设置为一个值。以下代码演示了如何设置方法选项:
service SimpleMath { rpc TimesTwo(ReqType) returns (RespType) { option(opt_name) = "opt_value"; } }
选项是 gRPC 中的一个高级主题,超出了本文的范围。
1.2.2 消息类型定义
正如服务定义以 `service` 开头一样,消息类型定义是一个以 `message` 开头的块。本质上,消息是一个数据结构,可以作为客户端的请求或服务器的响应。消息类型定义标识消息中包含的数据字段。
一个例子将阐明其工作原理。假设客户端在提交请求时需要向服务器提供一个字符串和一个整数。以下代码定义了一个包含这两个字段的 `ReqData` 消息类型:
message ReqType { string str = 1; int32 num = 2; }
每个字段有三个部分:协议缓冲区支持的数据类型、一个名称和一个唯一标识该字段的字段编号。分配字段编号时,需要注意五点:
- 每个字段编号必须是介于 1 和 536,870,911 之间的整数。
- 应用程序启动后,字段编号不应更改。
- 字段编号不必是连续的,但它们在消息中必须是唯一的。
- 常用字段应分配 1 到 15 之间的编号。
- 字段编号 19,000 到 19,999 由协议缓冲区实现保留。
消息中的字段可以设置为十五种原始数据类型之一,包括 `string` 和 `int32`。表 1 列出了这些内置类型及其在 C++ 中的对应类型。
表 1:消息字段类型和 C++ 类型
内置类型 | C++ 类型 |
---|---|
double | double |
float | float |
int32 | int32 |
int64 | int64 |
uint32 | uint32 |
uint64 | uint64 |
sint32 | int32 |
sint64 | int64 |
fixed32 | uint32 |
fixed64 | uint64 |
sfixed32 | int32 |
sfixed64 | int64 |
bool | bool |
字符串 | 字符串 |
字节 | 字符串 |
除了这些类型之外,消息字段还可以设置为导入类型、枚举类型或另一个消息类型。
1.2.3 枚举类型定义
枚举类型是一种自定义类型,它定义了一组有限的值。从结构上讲,枚举类型定义与消息类型定义类似,但块以 `enum` 而不是 `message` 开头。例如,以下代码定义了一个名为 `Direction` 的枚举类型:
enum Direction { NORTH = 0; EAST = 1; SOUTH = 2; WEST = 3; }
如所示,每个值都需要设置为不同的正整数。与字段编号不同,枚举类型中的值可以设置为 0。事实上,gRPC 要求每个枚举类型中的第一个值设置为 0。这作为该类型的默认值。
一旦定义了枚举类型,消息字段就可以被分配给该类型。例如,以下消息类型定义包含一个名为 `dir` 的字段,其类型为 `Direction`:
message Example { ... Direction dir = 7; ... }
转换为 C++ 后,枚举类型将采用传统的 C++ `enum` 形式。
1.2.4 包和导入
除了上述代码块之外,proto 文件还可以有一个指定包名的语句。这可以防止不同 proto 文件中消息类型之间的名称冲突。例如,以下语句指定文件中的每个类型都属于 `Foo` 包:
package Foo;
一个 proto 文件可以使用 `import` 语句从另一个 proto 文件导入类型。例如,以下语句导入在 other.proto 文件中定义的消息类型:
import "other.proto";
谷歌提供了几个定义有用类型的 proto 文件。如果你想在 proto 文件中使用 `Any` 类型,你需要导入 `google/protobuf/any.proto`。
1.3 编译 Proto 文件
编写好 proto 文件后,下一步是将其转换为可在客户端和服务器上运行的源代码。这种转换通常称为编译,编译器的名称是 protoc。网上有下载二进制文件的地方,但默认编译器只能为消息生成代码,不能为 gRPC 服务定义生成代码。这非常令人沮丧。
因此,我发现有必要从源代码构建 protobuf 包。这并不愉快,但它会生成插件,使编译器能够编译服务定义。构建 protobuf 包的说明在此处。
一旦你构建了编译器及其插件,你就可以在终端中运行 `protoc` 命令来编译 proto 文件。这接受选项和一个或多个 proto 文件的名称。表 2 列出了可以设置的十一个选项。
表 2:protoc 编译器的编译选项
选项 | 描述 |
---|---|
--proto-path=PATH | 设置编译器查找导入的路径 |
--descriptor_set_out=FILE | 生成一个包含所有输入文件的文件 |
--error_format=FORMAT | 标识错误消息应如何打印 |
--fatal_warnings | 告诉编译器将警告视为错误 |
--plugin=NAME=PATH | 告诉编译器在哪里访问插件 |
--grpc_out=DIR | 设置包含生成的 gRPC 代码的目录 |
--cpp_out=DIR | 设置包含生成的 C++ 代码的目录 |
--java_out=DIR | 设置包含生成的 Java 代码的目录 |
--python_out=DIR | 设置包含生成的 Python 代码的目录 |
--ruby_out=DIR | 设置包含生成的 Ruby 代码的目录 |
--kotlin_out=DIR | 设置包含生成的 Kotlin 代码的目录 |
如果你不带任何选项运行 protoc,它不会生成任何代码。要告诉编译器生成什么类型的代码,你需要使用至少一个 `--language_out` 选项。如果你想将 example.proto 中的消息类型编译为 C++,你可以使用以下命令:
protoc --cpp_out=. example.proto
如果您想为 gRPC 应用程序生成 C++ 代码,还需要两个额外的选项:
- `--grpc_out=DIR` - 告诉编译器编译服务定义并将源文件存储在给定目录中
- `--plugin=protoc-gen-grpc=$INSTALL/bin/grpc_cpp_plugin` - 告诉编译器在哪里找到编译 gRPC 服务所需的插件
第二个选项很重要。如果没有帮助,protoc 无法编译 proto 文件中的服务定义。这种帮助必须由插件提供,而使 protoc 能够编译 gRPC 服务的插件称为 grpc_cpp_plugin。如果你已经从源代码构建了 protobuf,你应该在 bin 目录中找到此文件。请注意,`--plugin` 选项必须标识 grcp_cpp_plugin 的完整路径。
在我的系统上,grpc_cpp_plugin 文件位于 /usr/local/bin 目录中。以下命令告诉 protoc 读取 example.proto 并生成 C++ 代码:
protoc --plugin=protoc-gen-grpc=/usr/local/bin/grpc_cpp_plugin \ --grpc_out=. --cpp_out=. example.proto
如果编译成功,编译器将创建四个文件:
- example.pb.h - 声明生成的 `message` 类
- example.pb.cc - `message` 类的实现代码
- example.grpc.pb.h - 声明生成的 `service` 类
- example.grpc.pb.cc - `service` 类的实现代码
这些源文件中有大量的代码,但大多数应用程序只会使用生成函数和数据结构的一小部分。下一节将更深入地讨论这一点。
2. 探索生成的代码
在本节中,我们将放下对 gRPC 和 proto 文件的描述,转而关注 C++ 开发。如果您解压本文随附的 example.zip 文件,您将找到一个名为 example.proto 的文件,其内容如下:
syntax = "proto3"; service SimpleMath { rpc TimesTwo(ReqType) returns (RespType); } message ReqType { int32 num = 1; } message RespType { int32 num = 1; }
当使用 protoc 编译时,`ReqType` 和 `RespType` 消息的 C++ 类将在 example.pb.h 中声明,代表 `SimpleMath` 服务的类将在 example.grpc.pb.h 中声明。这些文件包含数量惊人的代码,本节的目标是深入了解生成的代码实现了什么。
2.1 消息类
如果您查看 example.pb.h 和 example.pb.cc,您可能会惊讶于 `ReqType` 和 `RespType` 结构需要如此多的代码。您不需要了解每个函数和数据结构,但您应该知道 `ReqType` 和 `RespType` 都是 `Message` 类的子类,而 `Message` 类又是 `MessageLite` 类的子类。
每个 `Message` 子类都有一个不接受任何参数的构造函数。此外,每个 `Message` 实例都可以访问表 3 中列出的八个函数。
表 3:消息类的函数
函数 | 描述 |
---|---|
CopyFrom(const Message&) | 将指定消息复制到当前消息 |
MergeFrom(const Message&) | 将指定消息合并到当前消息 |
Swap(Message*) | 将指定消息与当前消息交换 |
Clear() | 清除消息中设置的所有字段 |
DebugString() | 返回描述消息的可读字符串 |
SerializeToString(string*) | 将消息转换为字符串 |
ParseFromString(string_view) | 将字符串解析为消息 |
ByteSizeLong() | 序列化后消息的大小 |
`MessageLite` 类提供了序列化函数,可将消息转换为不同的格式。它还提供了解析(反序列化)函数,可将数据转换回消息。例如,应用程序可以通过调用 `SerializeToString` 将 `Message` 序列化为字符串,然后通过调用 `ParseFromString` 将 `Message` 取回。
客户端和服务器将需要访问消息的各个字段。为了提供这种访问,编译器生成以消息字段命名的函数。在 example.proto 中,`ReqType` 和 `RespType` 都包含一个名为 `num` 的单个字段。因此,这两个类都可以访问三个函数:
- `num()` - 返回 num 字段的值
- `set_num(int)` - 设置 num 字段的值
- `clear_num()` - 清除 num 字段的值
编译器将为消息中的每个字段创建类似的函数。对于本文讨论的简单应用程序,我们唯一需要的函数是 `num()` 和 `set_num()`。
2.2 SimpleMath 类
example.grpc.pb.h 和 example.grpc.pb.cc 文件包含为 gRPC 服务生成的代码。example.proto 中服务的名称是 `SimpleMath`,因此在 example.grpc.pb.h 中声明的类名称是 `SimpleMath`。此类包含几个嵌套类,但对于简单的应用程序,只有三个是必需的:
- `StubInterface` - 存根的抽象类
- `Stub` - 存根的具体类
- `Service` - 定义服务及其方法
本节的目标是解释 `StubInterface`、`Stub` 和 `Service` 类。我们还将探讨 `Channel` 和ServerBuilder类。
2.2.1 StubInterface 和 Stub 类
如前所述,客户端通过调用与服务方法对应的存根例程来启动远程过程调用。在 C++ 应用程序中,客户端通过与服务关联的 `Stub` 嵌套类访问存根例程。服务器根本不访问 `Stub` 类。
`Stub` 类继承自抽象 `StubInterface` 类,当您需要编写自定义存根时,它很有用。这对于执行单元测试的应用程序很常见。`StubInterface` 的所有函数都是纯虚函数,并且服务的每个方法都将有一个相应的函数。
例如,`SimpleMath` 服务有一个名为 `TimesTwo` 的方法,因此生成的 `StubInterface` 类定义了以下函数:
virtual ::grpc::Status TimesTwo(::grpc::ClientContext* context,
const ::ReqType& request, ::RespType* response) = 0;
大多数客户端不需要访问 `StubInterface` 类。相反,他们将通过四步过程启动 RPC:
- 创建一个标识服务器位置和所需凭据的 `Channel`。
- 使用 `Channel` 创建一个新的 `Stub` 实例。
- 创建要传递给服务器的请求消息。
- 调用与所需方法对应的 `Stub` 函数并传递请求消息。
对于第二步,客户端通过调用静态 `NewStub` 函数来实例化一个 `Stub`。`NewStub` 的声明如下:
static std::unique_ptr<Stub> NewStub(
const std::shared_ptr< ::grpc::ChannelInterface>& channel,
const ::grpc::StubOptions& options = ::grpc::StubOptions());
`NewStub` 函数接受两个参数:一个标识通道,另一个为新的 `Stub` 实例设置选项。第二个参数通常可以保留其默认值,但每个客户端都需要提供一个通道。接下来我将讨论这一点。
2.2.2 创建通道
在客户端请求方法之前,它需要指定服务器的位置并提供任何必需的凭据。gRPC API 提供了 `Channel` 类来保存此信息,客户端可以通过调用 `CreateChannel` 来获取指向新 `Channel` 的共享指针:
grpc::CreateChannel(const grpc::string &target,
const std::shared_ptr<ChannelCredentials> &creds)
第一个参数是一个字符串,由服务器的 IP 地址、冒号以及服务器将监听的端口组成。对于 IPv4 地址,地址必须以 `ipv4` 开头,如以下代码所示:
std::string target = "ipv4:192.168.0.1:54321";
由于 gRPC 通信基于 HTTP/2,应用程序可能希望使用常见的 HTTP 端口,如 80 和 443。但只要服务器监听消息,任何端口都是可接受的。如果客户端和服务器在同一个系统上运行,IP 地址可以设置为 `localhost`。
`CreateChannel` 的第二个参数是一个 `ChannelCredentials` 对象,服务器可以访问该对象来验证客户端。目前,gRPC 支持基于 SSL/TLS、ALTS 和 Google 令牌的身份验证。为了演示,以下代码创建了一个用于基于 SSL 的身份验证的 `ChannelCredentials` 对象:
ChannelCredentials creds = grpc::SslCredentials(grpc::SslCredentialsOptions());
如果不需要身份验证,第二个参数可以设置为 `grpc::InsecureChannelCredentials()`。这在以下代码中显示,该代码创建一个 `Channel` 并使用它为 `SimpleMath` 服务创建一个新的 `Stub` 实例。
// Create the Channel
std::shared_ptr<grpc::Channel> ch =
grpc::CreateChannel("localhost:54321", grpc::InsecureChannelCredentials());
// Create the Stub
std::unique_ptr<SimpleMath::Stub> st = SimpleMath::NewStub(ch);
创建 `Stub` 对象后,客户端通过调用与服务方法对应的函数来启动 RPC。稍后的部分将演示这在实践中是如何工作的。
2.2.3 服务类
虽然客户端关注 `SimpleMath` 的 `Stub` 嵌套类,但服务器关注 `Service` 嵌套类。这是一个 `grpc::Service` 的子类,它包含每个可以调用的方法的函数。
例如,如果您编译了 example.proto,您将在 example.grpc.pb.h 中找到以下类定义:
class Service : public ::grpc::Service {
public:
Service();
virtual ~Service();
virtual ::grpc::Status TimesTwo(::grpc::ServerContext* context,
const ::ReqType* request, ::RespType* response);
};
如所示,`Service` 类有一个 `TimesTwo` 函数,对应于 example.proto 中定义的 `TimesTwo` 方法。此函数接受 `ServerContext`、包含客户端请求数据的 `ReqType` 对象和由服务器填充的 `RespType` 对象。`ServerContext` 使得访问超出本文范围的高级 gRPC 功能成为可能。
服务器不实例化这个 `Service` 类,而是创建一个它的子类,配置每个方法(示例中的 `TimesTwo`)的操作方式。然后它创建该子类的一个实例并将其传递给 `ServerBuilder`,`ServerBuilder` 在配置服务器操作中扮演着主要角色。
2.2.4 ServerBuilder 类
定义服务方法的函数后,服务器应用程序需要实例化一个 `ServerBuilder` 来配置和管理服务器的操作。这个类的三个函数特别重要:
- `AddListeningPort` - 接受用于通信的 IP 地址、端口和凭据(类似于前面讨论的 `CreateChannel` 函数)
- `RegisterService` - 接受定义服务方法的 `Service` 子类的实例
- `BuildAndStart` - 创建一个 `Server`,该 `Server` 将监听配置的端口并响应客户端请求
调用 `BuildAndStart` 后,应用程序可以调用 `Server` 的 `Wait` 函数,该函数会阻塞应用程序的线程,直到 gRPC 服务器关闭。下一节将演示这在实践中是如何调用的。
3. 示例项目
至此,您应该对 gRPC 的操作有了扎实的理解,并对客户端和服务器所需的 C++ 代码有了基本的了解。本节将逐步介绍两个可执行文件(名为 `client` 和 `server`)的编码和编译。`server` 可执行文件监听端口 54321 上的请求,`client` 可执行文件在端口 54321 上发送对 `TimesTwo` 方法的请求。这两个可执行文件都配置为在同一系统上运行。
3.1 客户端应用程序
client.cpp 中的代码执行五个操作:
- 创建一个标识服务器 IP 地址和端口的 `Channel`。
- 为 `SimpleMath` 服务创建一个 `Stub`。
- 创建一个包含数字 7 的请求对象。
- 调用 `Stub` 函数以提交对 `TimesTwo` 方法的请求。
- 打印响应的内容。
client.cpp 的内容如下:
int main() {
// Create the channel
std::shared_ptr<grpc::Channel> ch = grpc::CreateChannel("localhost:54321",
grpc::InsecureChannelCredentials());
// Create the stub
std::unique_ptr<SimpleMath::Stub> stub = SimpleMath::NewStub(ch);
// Create the client message
ReqType request;
request.set_num(7);
// Invoke the method
grpc::ClientContext ctx;
RespType response;
grpc::Status status = stub->TimesTwo(&ctx, request, &response);
// Check status
if (status.ok()) {
std::cout << "The result is " << response.num() << std::endl;
} else {
std::cout << status.error_code() << ": "
<< status.error_message() << std::endl;
}
return 0;
}
如所示,客户端通过调用存根的 `TimesTwo` 函数来执行服务器的方法。该函数接受三个参数:一个 `ClientContext`、请求对象和响应对象。`ClientContext` 使客户端能够提供元数据、设置身份验证参数、分配截止日期和配置压缩。这些功能超出了本文的范围。
`TimesTwo` 返回一个 `Status` 对象,该对象标识 RPC 调用是否成功执行。如果调用成功,客户端将打印响应的内容。如果失败,它将打印错误代码和错误消息。
例如,如果服务器根本没有响应,错误代码将是 14。我的系统上打印的消息如下:
14: failed to connect to all addresses; last error: UNKNOWN: ipv4:127.0.0.1:54321: Failed to connect to remote host: Connection refused
3.2 服务器应用程序
服务器代码更复杂,因为它需要定义 `SimpleMath` 的 `Service` 嵌套类的子类。这个子类叫做 `SimpleMathImpl`,代码如下:
class SimpleMathImpl final : public SimpleMath::Service {
grpc::Status TimesTwo(grpc::ServerContext* context, const ReqType* req,
RespType* resp) override {
resp->set_num(req->num() * 2);
return grpc::Status::OK;
}
};
子类需要为服务中的每个方法函数提供代码。`SimpleMath` 服务中唯一的方法是 `TimesTwo`,因此子类为名为 `TimesTwo` 的函数提供代码。该函数接受 `ServerContext`、请求对象和响应对象。服务器通过调用 `req->num()` 访问请求的值,将该值乘以 2,并通过调用 `resp->set_num` 将响应的 `num` 字段设置为结果。
当服务器应用程序执行时,其 `main` 函数执行五个操作:
- 创建一个 `SimpleMathImpl` 实例和一个 `ServerBuilder` 实例。
- 通过调用 `ServerBuilder` 的 `AddListeningPort` 函数设置服务器的 IP 地址和端口。
- 通过调用 `ServerBuilder` 的 `RegisterService` 函数注册 `Service` 子类 (`SimpleMathImpl`)。
- 通过调用 `ServerBuilder` 的 `BuildAndStart` 函数获取 `Server` 实例。
- 通过调用新 Server 实例的 `Wait` 函数开始端口监听。
以下代码显示了这些步骤在 server.cpp 中的实现方式:
int main() {
SimpleMathImpl service;
grpc::ServerBuilder builder;
// Set the server's IP address and port
builder.AddListeningPort("localhost:54321", grpc::InsecureServerCredentials());
// Register the service
builder.RegisterService(&service);
// Obtain a Server instance
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
// Block until the Server is halted
server->Wait();
return 0;
}
当调用 `Server` 的 `Wait` 函数时,可执行文件将阻塞,直到服务器停止。因此,最好在客户端发起 RPC 调用时,在单独的进程中运行此代码。
3.3 构建和执行
本文的示例项目依赖 CMake 来管理构建过程,构建说明在 CMakeLists.txt 中给出。要构建客户端和服务器可执行文件,请打开终端并切换到包含 CMakeLists.txt 的目录。然后输入三个命令:
mkdir build cd build cmake ..
如果 CMake 能够找到构建所需的包,最后一条命令将生成必要的构建文件。完成后,您可以使用以下命令构建可执行文件:
cmake --build .
构建过程需要时间,因为有几个依赖项需要链接到可执行文件中。具体来说,gRPC 包依赖于大量的 Abseil 库。我花了几个小时才弄清楚需要哪些库以及它们在构建中需要列出的顺序。
如果编译成功完成,您将在 `build` 文件夹中拥有两个新的可执行文件(`client` 和 `server`)。您可以使用以下命令在单独的进程中启动服务器:
./server &
然后您可以使用以下命令运行客户端:
./client
客户端将通过存根调用服务器的 `TimesTwo` 例程,如果例程成功执行,客户端将收到服务器的响应并打印以下消息:
The result is 14
尽管极其简单,该应用程序清楚地展示了如何使用 gRPC 编写客户端-服务器应用程序。
4. 历史
本文最初提交于 2024 年 5 月 27 日。