gRPC

欢迎访问我的博客首页


  gRPC,即 google Remote Procedure Call,谷歌远程过程调用。

1. 编译安装


  我们在 Windows 上使用 MinGW 编译,MinGW 的配置见这里。cartogher 用的是 async_grpc,它在 grpc 的基础上实现了异步功能,因此先安装 grpc 再安装 async_grpc。

  gRPC 更新很快,async_grpc 更新很慢。使用较新的 gRPC 编译 async_grpc 会出现 use of deleted function 错误。根据 cartographer 脚本grpc 版本async_grpc 版本如下。

VERSION="v1.10.0"
# Digest: 474c5950686e3962bd339c93d27e369bf64f568f
git clone --branch ${VERSION} --depth 1 https://github.com/grpc/grpc

git clone https://github.com/cartographer-project/async_grpc
cd async_grpc
git checkout 771af45374af7f7bfc3b622ed7efbe29a4aba403

1.1 更换 HOST


  访问 github 较慢时,可以搜索 github DNS 找到 DNS 查询网站。比如chinaz站长工具,在 A 类型 DNS 查询框中输入 github.com,选择 TTL 值最小的 IP 地址。其它 DNS 查询网站还有卡卡网锐成信息vsping站长工具

  HOSTS 文件不能直接修改。把 C:\Windows\System32\drivers\etc\HOSTS 拖到桌面上,添加查询到的 IP 地址,再拖回原来的位置。最后在命令行输入 ipconfig/flushdns 刷新 DNS 解析缓存。

1.2 下载 grpc


  在 github 下载 grpc。grpc 包括 abseil-cpp、benchmark、bloaty、boringssl、c-ares、data-plane-api、googleapis、googletest、libuv、opencensus-proto、opentelemetry-proto、protobuf、re2、xds、zlib 这些子模块(submodule),这些子模块还可能有自己的子模块,可以使用下面的命令下载 grpc:

git clone --recursive https://github.com/grpc/grpc.git
git clone --branch v1.10.0 --recursive https://github.com/grpc/grpc.git grpc1p50

  选项 --recursive 用于递归下载子模块,选项 --branch 用于下载指定分支,可以在最后面指定接收的文件夹名。因为网络等原因,下载子模块可能会超时中断。如果要继续下载子模块,请在接收的文件夹内执行下面的命令,直到所有子模块下载成功。

git submodule update --init --recursive

  可能会出现下面的提示,忽略即可。

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

  有些子模块可能发生了转移,导致下载失败。比如 grpc v1.41 及之前的版本使用的子模块 bloaty,会使用位于 googlesource 的子模块 Fuzzer。该模块已从 googlesource 迁移,可以在 gitee 找到。因此可以把链接从 googlesource 修改为 gitee:在下面前两行所示的两个文件中,把第四行所示的链接修改为第五行。

grpc\.git\modules\third_party\bloaty\config
grpc\third_party\bloaty\.gitmodules

url = https://chromium.googlesource.com/chromium/llvm-project/llvm/lib/Fuzzer
url = https://gitee.com/csb7929/Fuzzer.git

1.3 准备编译


  编译前需要安装 NASMPerl。前者是一个汇编器,用于编译 .s 文件;后者是一种 MinGW。下载二进制文件安装,会自动添加环境变量。使用 Release 模式编译 grpc 可能会出现下面的编译错误,这是源码被优化而产生的错误。

编译错误:seh _savexmm offset is negative

1.4 编译 grpc 静态库


  编译错误 1:grpc/include/grpc/impl/codegen/port_platform.h:47:2: error: #error “Please compile grpc with _WIN32_WINNT of at least 0x600 (aka Windows Vista)”。

  在 CMakeLists.txt 中添加下面一行。等号后面的值和操作系统有关,在这里查询。

add_definitions(-D_WIN32_WINNT=0x0A00)

  编译错误 2:gRPC_INSTALL will be forced to FALSE because gRPC_CARES_PROVIDER is “module”。

  当依赖以子模块存在时,旧版本 grpc 会报上面的错误。这会导致安装的 grpc 缺少 gRPCTargets.cmake 而不能被 find_package 找到。解决方法是,先把安装目录的 bin 和 lib 添加到环境变量,忽略这个警告进行编译安装。然后通过 cmake-gui 把这些依赖由 module 切换到 package,再次编译安装以覆盖上次的安装就可以了。

  编译错误 3:grpc\third_party\boringssl\crypto\cipher_extra\aead_test.cc:542:54: error: requested alignment 64 is larger than 16 [-Werror=attributes] alignas(64)
uint8_t key[EVP_AEAD_MAX_KEY_LENGTH + 1];

  这种 [-Werror=attributes] 的错误一般都是类型转换之类,可以在配置文件在添加下面一行以忽略:

add_definitions(-Wno-error -Wno-format-security)

  编译错误 4:grpc\third_party\boringssl-with-bazel\tool\transport_common.cc:121:32: error: format ‘%x’ expects argument of type ‘unsigned int’, but argument 4 has type ‘int’ [-Werror=format]
snprintf(buf, sizeof(buf), “unknown error (0x%x)”, error);

  和错误 3 相同,参考

  编译错误 5:grpc\third_party\boringssl\tool\digest.cc:40:13: error: conflicting declaration ‘typedef int ssize_t’。

把 typedef int ssize_t; 改为 typedef int int64;

  编译错误 6 未定义的引用,undefined reference to ‘CBS_get_bytes’, ‘OPENSSL_free’, ‘CBB_int’, ‘CBB_add_u8’ 等:在 grpc\third_party\boringssl-with-bazel\CMakeLists.txt 中添加两行 target_link_libraries:

add_library(
  crypto
  ...
)
target_link_libraries(crypto ws2_32)

add_library(
  ssl
  ...
)
target_link_libraries(ssl crypto)

  编译错误 7:grpc/third_party/boringssl/crypto/bio/socket_helper.c:110: undefined reference to `__imp_getsockopt’。这类错误是没有链接 ws2_32 库。boringssl 是 opensll 的一个分支,需要 ws2_32 的网络通信支持。ws2_32 是 MinGW 自带的库,存在于 MinGW-w64 和 Perl 的安装目录:

MinGW-w64/x86_64-w64-mingw32/lib/libws2_32.a
Perl/c/x86_64-w64-mingw32/lib/libws2_32.a

  这类错误还可能是未定义的:

__imp_socket
__imp_setsockopt
__imp_closesocket
__imp_htons
__imp_inet_pton
__imp_closesocket
__imp_htons
__imp_inet_pton
__imp_closesocket
__imp_connect
__imp_closesocket
__imp_WSAStartup
__imp_shutdown
__imp_recv
__imp_closesocket
__imp_closesocket
__imp_recv
__imp_send
__imp_getaddrinfo
gai_strerrorA
__imp_socket
__imp_freeaddrinfo
__imp_ioctlsocket
__imp_getsockopt

  解决方法:找到报错文件的位置,如 socket_helper.c 的位置,从内层文件夹向外层文件夹找到最近的配置文件 CMakeLists.txt。在其中找到包含 socket_helper.c 的 add_library 或 add_executable,在后面添加 target_link_libraries(XXX ws2_32):

add_library(XXX
	...
	socket_helper.c
	...
)

target_link_libraries(XXX ws2_32)

  编译错误 8 如下面第一行:在 iocp.h 或 iocp.cc 的 include 语句下面添加下面三行语句:

grpc\src\core\lib\event_engine\windows\iocp.cc:143:46: error: 'WSA_FLAG_NO_HANDLE_INHERIT' was not declared in this scope

#ifndef WSA_FLAG_NO_HANDLE_INHERIT
#define WSA_FLAG_NO_HANDLE_INHERIT 0x80
#endif

  编译错误 9:grpc\third_party\boringssl-with-bazel\ssl\test\bssl_shim.cc:344:23: error: unknown conversion type character ‘z’ in format [-Werror=format=] fprintf(stderr, “Received a chain of length %zu instead of %zu.\n”,

  添加 add_definitions(-D__USE_MINGW_ANSI_STDIO),参考

  编译错误 10:grpc\third_party\boringssl-with-bazel\ssl\test\bssl_shim.cc:643:44: error: expected ‘)’ before ‘PRId32’ fprintf(stderr, “Ticket age skew was %” PRId32 “, wanted %d\n”, …)

  添加 add_definitions(-D__STDC_FORMAT_MACROS),参考

  编译错误 11:glog/log_severity.h:66:4: error: #error ERROR macro is defined. Define GLOG_NO_ABBREVIATED_SEVERITIES before including logging.h. See the document for detail.
# error ERROR macro is defined. Define GLOG_NO_ABBREVIATED_SEVERITIES before including logging.h. See the document for detail.

  添加 add_definitions(-DGLOG_NO_ABBREVIATED_SEVERITIES)。

  编译选项如下:

请添加图片描述

编译选项

  开始编译前,把安装目录的下的 bin 和 lib 路径添加到环境变量。因为编译后期需要用到前期生成的 protoc,这两个环境变量还可以使 find_package 找到 grpc 安装位置。

1.5 编译 grpc 动态库


  静态库的链接方式是把链接对象拷贝给自己,这样生成的库和可执行文件都很大,而且生成的时间也很长,因此编译成动态库是更好的选择。可以通过 cmake-gui 界面上的 Add Entry 添加 BOOL 型的 BUILD_SHARED_LIBS 选项并选中,或使用命令 cmake -DBUILD_SHARED_LIBS=ON .. 指定编译动态链接库。

  运行错误 1,无法找到程序入口。选中 build_shared_libs 编译成功,运行 demo 报错如下:

运行错误:无法找到入口:无法定位程序输入点 BIO_clear_retry_flags, BIO_get_mem_data 于动态链接库 libssl.dll 上。

  在 github 上搜索发现,包含这两个输入点的文件是 boringssl/crypto/bio/bio_mem.c,它被编译成 crypto。因此问题出现在 ssl 没有找到生成的 crypto,而是找到了其它没有实现 BIO_get_mem_data 的 crypto。通常我们安装的 MinGW 都会包含 crypto 库,比如 MinGW-w64 和 ​strawberry perl 的 crypto 库分别在下面的位置:

MinGW-w64/opt/lib/libcrypto.a
perl/c/lib/libcrypto.a

  在这些目录中搜索 BIO_get_mem_data,选中文件内容,确实搜不出 crypto,证明这些 crypto 中没有实现 BIO_get_mem_data。为了给 ssl 链接 grpc 子模块中的 crypto,我们可以在配置文件中给子模块生成的 crypto 取别名,比如 crypto_gRPC。 为此需要修改以下两个文件:

grpc\third_party\boringssl-with-bazel\CMakeLists.txt
grpc\cmake\ssl.cmake

  在第一个文件中,把 add_library 和 target_link_libraries 中的 crypto 改成 crypto_gRPC。注意,只改库名不改路径名和文件名。在第二个文件中的下面这部分代码中,也把 crypto 改成 crypto_gRPC。

if(TARGET ssl)
  set(_gRPC_SSL_LIBRARIES ssl crypto_gRPC)
  set(_gRPC_SSL_INCLUDE_DIR ${BORINGSSL_ROOT_DIR}/src/include)
  if(gRPC_INSTALL AND _gRPC_INSTALL_SUPPORTED_FROM_MODULE)
    install(TARGETS ssl crypto_gRPC EXPORT gRPCTargets
      RUNTIME DESTINATION ${gRPC_INSTALL_BINDIR}
      LIBRARY DESTINATION ${gRPC_INSTALL_LIBDIR}
      ARCHIVE DESTINATION ${gRPC_INSTALL_LIBDIR})
  endif()
endif()

1.6 编译 async_grpc


   使用下面的命令下载 async_grpc 的最后一次提交。

git clone --depth=1 https://github.com/cartographer-project/async_grpc.git

  编译错误 1:async_grpc/async_grpc/rpc.h:61:19: error: expected identifier before ‘(’ token
enum Action { DELETE = 0, DO_NOT_DELETE };。

  这个错误有时候也会是 expected unqualified-id before numeric constant。原因是当前使用的变量名 DELETE 与宏定义冲突,也就是同名。可以给当前变量取个其它名字,或者在当前变量的作用区域内屏蔽宏定义

#ifdef DELETE
#undef DELETE

enum Action { DELETE = 0, DO_NOT_DELETE };

#endif // DELETE

  编译错误 2:找不到链接库。

Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -lglog
Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -lgflags
Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -lgrpc++

  这是找不到这三个库的位置,可以在 CMakeLists.txt 中使用 link_directories 指定它们的位置。

  编译错误 3:未定义的引用。

D:/Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: CMakeFiles\async_grpc.client_test.dir/objects.a(client_test.cc.obj): in function `TestBody':
C:/async_grpc/async_grpc/client_test.cc:44: undefined reference to `testing::Message::Message()'
D:/Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/async_grpc/async_grpc/client_test.cc:44: undefined reference to `testing::internal::GetBoolAssertionFailureMessage[abi:cxx11](testing::AssertionResult const&, char const*, char const*, char const*)'
D:/Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/async_grpc/async_grpc/client_test.cc:44: undefined reference to `testing::internal::AssertHelper::AssertHelper(testing::TestPartResult::Type, char const*, int, char const*)'
D:/Strawberry/c/bin/../lib/gcc/x86_64-w64-mingw32/8.3.0/../../../../x86_64-w64-mingw32/bin/ld.exe: C:/async_grpc/async_grpc/client_test.cc:44: undefined reference to `testing::internal::AssertHelper::operator=(testing::Message const&) const'

  使用了 gtest 却没有链接 gtest 库。在生成 gest 的地方链接 gest,如下面倒数第二行。

foreach(RELATIVEPATH ${ALL_TESTS})
  get_filename_component(DIR ${RELATIVEPATH} DIRECTORY)
  get_filename_component(FILENAME ${RELATIVEPATH} NAME_WE)
  # Replace slashes as required for CMP0037.
  string(REPLACE "/" "." TEST_TARGET_NAME "${DIR}/${FILENAME}")
  google_test("${TEST_TARGET_NAME}" ${RELATIVEPATH})
  target_link_libraries(${TEST_TARGET_NAME} PUBLIC ${GMOCK_LIBRARY})
  target_link_libraries("${TEST_TARGET_NAME}" PUBLIC grpc++ gtest)
endforeach()

2. protobuf demo


  protobuf 是 gRPC 的依赖,如果没有安装 gRPC,可以单独安装 protobuf。

2.1 proto 文件


  下面是 proto 文件的内容,它被保存在 example.proto 中。

syntax="proto3";

package calc;

service Calculator{
    rpc add(InParams) returns (OutParams) {}
}

message InParams{
    int32 a=1;
    int32 b=2;
}

message OutParams{
    int32 sum=1;
}

  由于 protobuf 2.x 版本与 3.x 版本差别较大,protobuf 3.x 又向下兼容,所以第 1 行显式指出按照 protobuf 3 编译该文件。缺少该行将按 protobuf 2 编译。

  package 用于指出命名空间名。protoc 是编译 .proto 文件的编译器,它将根据该文件生成一个 calc::Calculator 类。

2.2 demo


  下面是 main.cc 文件中的内容,展示了怎么使用 protobuf。

#include "example.pb.h"

int main() {
    calc::InParams request;
    request.set_a(1);
    request.set_b(260);
    std::cout << request.a() << " " << request.b() << std::endl;
    return 0;
}

2.3 配置文件


  下面是配置文件 CMakeLists.txt 中的内容。

cmake_minimum_required(VERSION 3.5.1)
project(protobuf-demo)

# 1. 查找依赖。
find_package(Protobuf REQUIRED)

# 2. 从环境变量中查找 protoc.exe 的绝对路径。
find_program(PROTOBUF_PROTOC protoc)

# 3. 生成 XXX.pb.cc/h。
execute_process(
    COMMAND ${PROTOBUF_PROTOC}
    --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
    -I ..
    example.proto
)

# 4. 设置包含目录和库目录。
include_directories(
    ${PROTOBUF_INCLUDE_DIRS}
    ${CMAKE_CURRENT_BINARY_DIR}
)

# 5. 生成可执行程序。
add_executable(main
    main.cc
    example.pb.cc
)
target_link_libraries(main
    ${PROTOBUF_LIBRARIES}
)

  find_program 有两个参数,它根据环境变量查找名称为第 2 个参数的可执行文件,并把绝对路径赋值给第 1 个参数。还可以使用 PROTOBUF_GENERATE_CPP 实现同样的功能。

  执行 cmake 命令时,execute_process 命令会生成 2 个代码文件 example.pb.h/cc。execute_process 的 --cpp_out 选项用于指出生成文件的路径,-I 选项用于指出 .proto 文件的位置。

3. gRPC demo


  下面是一个简单的 demo,服务端实现加法,客户端调用服务端的加法得到结果。

3.1 服务端代码


  下面是服务端代码,它被保存在 server.cc 中。

#include "example.grpc.pb.h"
#include <grpc++/grpc++.h>

class CalcualtorService : public calc::Calculator::Service {
public:
    virtual ::grpc::Status add(::grpc::ServerContext *context,
                               const ::calc::InParams *request,
                               ::calc::OutParams *response) override {
        response->set_sum(request->a() + request->b());
        return grpc::Status::OK;
    }
};

int main() {
    CalcualtorService calcService;
    grpc::ServerBuilder builder;
    builder.AddListeningPort("0.0.0.0:5152", grpc::InsecureServerCredentials());
    builder.RegisterService(&calcService);
    std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
    server->Wait();
    return 0;
}

  可以看出,服务端定义了一个类,继承 protoc 生成的 Calculator 的内部类 Service,并重写了其虚函数,实现加法运算。

3.2 客户端代码


  下面是客户端代码,它被保存在 client.cc 中。

#include "example.grpc.pb.h"
#include "grpc++/grpc++.h"
#include <iostream>

class Client {
public:
    Client(std::shared_ptr<grpc::Channel> channel) : stub(calc::Calculator::NewStub(channel)) {}

    google::protobuf::int32 add(google::protobuf::int32 a, google::protobuf::int32 b) {
        calc::InParams request;
        request.set_a(a);
        request.set_b(b);
        calc::OutParams response;
        grpc::ClientContext context;
        grpc::Status status = stub->add(&context, request, &response);
        if (status.ok()) {
            return response.sum();
        } else {
            return -1;
        }
    }

private:
    std::unique_ptr<calc::Calculator::Stub> stub;
};

int main() {
    Client client(grpc::CreateChannel("localhost:5152", grpc::InsecureChannelCredentials()));
    auto result = client.add(1, 260);
    std::cout << "1 + 260 = " << result << std::endl;
    return 0;
}

  可以看出,客户端定义的类使用了 Calculator 的内部类 Stub,该类初始化两个加数并调用服务端得到结果。

3.3 配置文件


  下面是配置文件内容,它被保存在 CMakeLists.txt 中。

cmake_minimum_required(VERSION 3.5.1)
project(gRPC C CXX)

add_definitions(-D_WIN32_WINNT=0x0A00)

# 1. 查找依赖。
find_package(Protobuf REQUIRED)
find_package(gRPC CONFIG REQUIRED)
message(STATUS "gRPC version: ${gRPC_VERSION}")

# 2. 从环境变量中查找 protoc.exe 和 grpc_cpp_plugin.exe 的绝对路径。
find_program(PROTOBUF_PROTOC protoc)
find_program(GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin)

# 3. 生成 XXX.grpc.pb.cc/h 和 XXX.pb.cc/h。
execute_process(
    COMMAND ${PROTOBUF_PROTOC}
    --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
    --grpc_out=${CMAKE_CURRENT_BINARY_DIR}
    --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN_EXECUTABLE}
    -I ..
    example.proto
)

# 4. 设置包含目录。
include_directories(
    ${PROTOBUF_INCLUDE_DIRS}
    ${CMAKE_CURRENT_BINARY_DIR}
)

# 5. 生成库,只需在这里链接 grpc++add_library(${PROJECT_NAME}
    example.grpc.pb.cc
    example.pb.cc
)
target_link_libraries(${PROJECT_NAME}
    gRPC::grpc++
)

# 6. 生成客户端和服务端程序。
add_executable(server
    server.cc
)
target_link_libraries(server
    ${PROJECT_NAME}
)

add_executable(client
    client.cc
)
target_link_libraries(client
    ${PROJECT_NAME}
)

  execute_process 命令执行两个命令,相当于下面两个命令。

execute_process(
    COMMAND ${PROTOBUF_PROTOC}
    --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
    -I ..
    example.proto
)

execute_process(
    COMMAND ${PROTOBUF_PROTOC}
    --grpc_out=${CMAKE_CURRENT_BINARY_DIR}
    --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN_EXECUTABLE}
    -I ..
    example.proto
)

  第一个命令生成 example.pb.h/cc,第二个命令生成 example.grpc.pb.h/cc,这些文件在上面的 server.cc 和 client.cc 中使用。其中 example.grpc.pb.h 中定义了 Calculator 类,该类有 Service 和 Stub 等多个内部类,这些内部类的名字是固定的。

4. async_grpc demo


4.1 服务端代码


  实现和 grpc demo 相同的功能,只是服务端使用 async_grpc。下面的代码保存在 server.cc 中。

#include "example.grpc.pb.h"
#include <async_grpc/server.h>

DEFINE_HANDLER_SIGNATURE(CalculatorSignature,
                         calc::InParams,
                         calc::OutParams,
                         "/calc.Calculator/add")

class CalculatorHandler : public async_grpc::RpcHandler<CalculatorSignature> {
public:
    void OnRequest(const calc::InParams &request) override {
        auto response = std::make_unique<calc::OutParams>();
        response->set_sum(request.a() + request.b());
        Send(std::move(response));
    }
};

int main() {
    async_grpc::Server::Builder server_builder;
    server_builder.SetServerAddress("localhost:5152");
    server_builder.SetNumGrpcThreads(2);
    server_builder.SetNumEventThreads(2);
    server_builder.RegisterHandler<CalculatorHandler>();
    std::unique_ptr<async_grpc::Server> server = server_builder.Build();
    // server->SetExecutionContext();
    server->Start();
    server->WaitForShutdown();
    return 0;
}

  可以看出,使用 async_grpc 实现的客户端通过 RegisterHandler 可以提供多个服务类,实现并行。

4.2 配置文件


  下面是配置文件。

cmake_minimum_required(VERSION 3.5.1)
project(async_grpc_demo)

add_definitions(-D_WIN32_WINNT=0x0A00)
add_definitions(-DGLOG_NO_ABBREVIATED_SEVERITIES)

# 1. 查找依赖。
find_package(Protobuf REQUIRED)
find_package(async_grpc CONFIG REQUIRED)

# 2. 从环境变量中查找 protoc.exe 和 grpc_cpp_plugin.exe 的绝对路径。
find_program(PROTOBUF_PROTOC protoc)
find_program(GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin)

# 3. 生成 XXX.grpc.pb.cc/h 和 XXX.pb.cc/h。
execute_process(
    COMMAND ${PROTOBUF_PROTOC}
    --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
    --grpc_out=${CMAKE_CURRENT_BINARY_DIR}
    --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN_EXECUTABLE}
    -I ..
    example.proto
)

# 4. 设置包含目录和库目录。
include_directories(
    D:/MinGW/libraries/include
    ${CMAKE_CURRENT_BINARY_DIR}
)
link_directories(
    D:/MinGW/libraries/lib
    D:/MinGW/libraries/grpc1p10/lib
)

# 5. 生成可执行程序。
add_executable(server
    server.cc
    example.pb.cc
)
target_link_libraries(server
    async_grpc
)
add_executable(client
    client.cc
    example.pb.cc
    example.grpc.pb.cc
)
target_link_libraries(client
    async_grpc
)

4.3 使用 async_grpc 客户端


  从 4.1 可以看出,grpc 实现的客户端可以请求 async_grpc 实现的服务端。其实 async_grpc 也可以实现客户端,它使用 Write 函数发送请求,使用 response 函数获取响应。

  下面的例子仍基于上面的例子,实现:客户端向服务端发送两个加数,服务端输出这两个加数并发送计算结果,客户端接收并输出结果。

  由于客户端和服务器都要用到 CalculatorHandler 类,因此我们先把该类创建成一个库。下面是 add_calculator_handler.h 和 add_calculator_handler.cc 中的代码。

// add_calculator_handler.h
#ifndef _ADD_CALCULATOR_HANDLER_H_
#define _ADD_CALCULATOR_HANDLER_H_

#include "example.grpc.pb.h"
#include <async_grpc/server.h>

DEFINE_HANDLER_SIGNATURE(CalculatorSignature,
                         calc::InParams,
                         calc::OutParams,
                         "/calc.Calculator/add")

class CalculatorHandler : public async_grpc::RpcHandler<CalculatorSignature> {
public:
    void OnRequest(const calc::InParams &request) override;
};

#endif // !_ADD_CALCULATOR_HANDLER_H_


// add_calculator_handler.cc
#include "add_calculator_handler.h"

void CalculatorHandler::OnRequest(const calc::InParams &request) {
    // 1. 处理客户端发来的数据。
    std::cout << "-- 收到客户端请求:a=" << request.a() << ",b=" << request.b() << "。"
              << std::endl;
    // 2. 发给客户端的数据。
    auto response = std::make_unique<calc::OutParams>();
    response->set_sum(request.a() + request.b());
    Send(std::move(response));
}

  下面是 server.cc 和 client.cc 中的代码。

// server.cc
#include "add_calculator_handler.h"

int main() {
    async_grpc::Server::Builder server_builder;
    server_builder.SetServerAddress("localhost:5152");
    server_builder.SetNumGrpcThreads(2);
    server_builder.SetNumEventThreads(2);
    server_builder.RegisterHandler<CalculatorHandler>();
    std::unique_ptr<async_grpc::Server> server = server_builder.Build();
    // server->SetExecutionContext();
    server->Start();
    server->WaitForShutdown();
    return 0;
}


// client.cc
#include "add_calculator_handler.h"
#include <async_grpc/client.h>

int main() {
    // 1. 超时设置。
    std::chrono::system_clock::time_point deadline(std::chrono::system_clock::now() +
                                                   std::chrono::seconds(10));
    // 2. 使用 grpc 客户端连接服务端。
    const std::string server_address = "localhost:5152";
    std::shared_ptr<::grpc::Channel> client_channel(
        grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials()));
    if (!client_channel->WaitForConnected(deadline)) {
        std::cout << "Failed to connect to " << server_address << "!" << std::endl;
        return 0;
    }
    // 3. 准备请求数据。
    calc::InParams request;
    request.set_a(1);
    request.set_b(260);
    // 4. 使用 async_grpc 客户端发送请求数据并接收响应数据。
    async_grpc::Client<CalculatorSignature> client(client_channel);
    grpc::Status status;
    client.Write(request, &status);
    if (!status.ok()) {
        std::cout << "Failed to request!" << std::endl;
    }
    std::cout << client.response().sum() << std::endl;
    return 0;
}

  可以看出,服务器端使用 CalculatorHandler 创建服务器,客户端使用 CalculatorSignature 创建客户机。然后,接收请求、处理数据、发送响应都由 CalculatorHandler 实现。因此,我们可以把 gRPC 的结构归纳为 服务器 <–> handler <–> 客户机。

  下面是配置文件 CMakeLists.txt 中的内容。

cmake_minimum_required(VERSION 3.5.1)
project(async_grpc_demo)

add_definitions(-D_WIN32_WINNT=0x0A00)
add_definitions(-DGLOG_NO_ABBREVIATED_SEVERITIES)

# 1. 查找依赖。
find_package(Protobuf REQUIRED)
find_package(async_grpc CONFIG REQUIRED)

# 2. 从环境变量中查找 protoc.exe 和 grpc_cpp_plugin.exe 的绝对路径。
find_program(PROTOBUF_PROTOC protoc)
find_program(GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin)

# 3. 生成 XXX.grpc.pb.cc/h 和 XXX.pb.cc/h。
execute_process(
    COMMAND ${PROTOBUF_PROTOC}
    --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
    --grpc_out=${CMAKE_CURRENT_BINARY_DIR}
    --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN_EXECUTABLE}
    -I ..
    example.proto
)

# 4. 设置包含目录和库目录。
include_directories(
    D:/MinGW/libraries/include
    ${CMAKE_CURRENT_BINARY_DIR}
)
link_directories(
    D:/MinGW/libraries/lib
    D:/MinGW/libraries/grpc1p10/lib
)

# 5. 生成库。
add_library(${PROJECT_NAME} SHARED
    add_calculator_handler.cc
    example.pb.cc
)
target_link_libraries(${PROJECT_NAME}
    async_grpc
)

# 6. 生成可执行程序。
add_executable(server
    server.cc
)
target_link_libraries(server
    ${PROJECT_NAME}
)
add_executable(client
    client.cc
)
target_link_libraries(client
    ${PROJECT_NAME}
)

4.4 使用上下文


  服务器调用自己的 SetExecutionContext 函数添加上下文,同时也调用 RegisterHandler 函数添加的所有 handler 的 SetExecutionContext 函数,把上下文添加到各 handler。因此,通过服务器添加的上下文,也自动地被添加到各 handler。

  服务器类型 async_grpc::Server 有一个成员函数 GetContext,handler 的基类 async_grpc::RpcHandler 也有一个成员函数 GetContext。因此,服务器和各 handler 都可以调用 GetContext 函数获取上下文。

  可以调用下面两个函数获取上下文。从它们的定义可以看出,使用前者获取上下文时会对其加锁,使用后者获取上下文时不会加锁。如果获取上下文时加锁,在获取的上下文的生存期内,不允许其它地方修改该上下文。

template <typename T>
ExecutionContext::Synchronized<T> GetContext() {
    return {execution_context_->lock(), execution_context_};
}

template <typename T>
T* GetUnsynchronizedContext() {
    return dynamic_cast<T*>(execution_context_);
}

  下面,我们对 4.3 节的代码稍微改动,演示如何使用上下文。同时,我们还演示 async_grpc::RpcHandler 的 OnReadsDone 函数。函数 OnReadsDone 不是必须的,它可以分担 OnRequest 的一些任务:OnRequest 接收数据并处理以得到响应数据,OnReadsDone 发送响应数据。官网例子在这里

  下面是 handle 的定义。与 4.3 节相比,我们添加了上下文类 CalculatorContext 的定义和使用,同时还重写了 OnReadsDone 函数。

// add_calculator_handler.h
#ifndef _ADD_CALCULATOR_HANDLER_H_
#define _ADD_CALCULATOR_HANDLER_H_

#include "example.grpc.pb.h"
#include <async_grpc/server.h>

DEFINE_HANDLER_SIGNATURE(CalculatorSignature,
                         calc::InParams,
                         calc::OutParams,
                         "/calc.Calculator/add")

class CalculatorHandler : public async_grpc::RpcHandler<CalculatorSignature> {
public:
    void OnRequest(const calc::InParams &request) override;
    void OnReadsDone() override;

private:
    int sum = 0;
};

class CalculatorContext : public async_grpc::ExecutionContext {
public:
    int additional_increment() { return 10; }
};

#endif // !_ADD_CALCULATOR_HANDLER_H_


// add_calculator_handler.cc
#include "add_calculator_handler.h"

void CalculatorHandler::OnRequest(const calc::InParams &request) {
    // 处理客户端发来的数据。
    std::cout << "-- 收到客户端请求:a=" << request.a() << ",b=" << request.b() << "。"
              << std::endl;
    sum = request.a() + request.b();
    // 加上上下文中的额外数据。
    // GetContext 是 CalculatorHandler 的基类 async_grpc::RpcHandler 的成员函数。
    sum += CalculatorHandler::GetContext<CalculatorContext>()->additional_increment();
}
void CalculatorHandler::OnReadsDone() {
    // 发给客户端的数据。
    auto response = std::make_unique<calc::OutParams>();
    response->set_sum(sum);
    Send(std::move(response));
}

  下面是服务器和客户端的实现。与 4.3 节相比,我们添加了上下文类 CalculatorContext 的使用示例。

// server.cc
#include "add_calculator_handler.h"

int main() {
    async_grpc::Server::Builder server_builder;
    server_builder.SetServerAddress("localhost:5152");
    server_builder.SetNumGrpcThreads(2);
    server_builder.SetNumEventThreads(2);
    server_builder.RegisterHandler<CalculatorHandler>();
    // 创建服务器。
    std::unique_ptr<async_grpc::Server> server = server_builder.Build();
    // 添加上下文。
    server->SetExecutionContext(std::make_unique<CalculatorContext>());
    // 服务器获取上下文。
    std::cout << server->GetContext<CalculatorContext>()->additional_increment() << std::endl;
    // 启动服务器。
    server->Start();
    server->WaitForShutdown();
    return 0;
}


// client.cc
#include "add_calculator_handler.h"
#include <async_grpc/client.h>

int main() {
    // 1. 超时设置。
    std::chrono::system_clock::time_point deadline(std::chrono::system_clock::now() +
                                                   std::chrono::seconds(10));
    // 2. 使用 grpc 客户端连接服务端。
    const std::string server_address = "localhost:5152";
    std::shared_ptr<::grpc::Channel> client_channel(
        grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials()));
    if (!client_channel->WaitForConnected(deadline)) {
        std::cout << "Failed to connect to " << server_address << "!" << std::endl;
        return 0;
    }
    // 3. 准备请求数据。
    calc::InParams request;
    request.set_a(1);
    request.set_b(260);
    // 4. 使用 async_grpc 客户端发送请求数据并接收响应数据。
    async_grpc::Client<CalculatorSignature> client(client_channel);
    grpc::Status status;
    client.Write(request, &status);
    if (!status.ok()) {
        std::cout << "Failed to request!" << std::endl;
    }
    std::cout << client.response().sum() << std::endl;
    return 0;
}

5. 参考


  1. gRPC入门,优快云,2022。
  2. 更换 git submodule 链接,优快云,2021。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值