写点什么

gRPC C++ 快速编译与上手

作者:王玉川
  • 2023-11-29
    上海
  • 本文字数:7817 字

    阅读完需:约 26 分钟

gRPC 在跨主机、跨进程的通信中,有着广泛的应用。但对于新手来说,如果按照官方文档的步骤:Quick start | C++ | gRPC,是无法通过编译并运行 examples 程序的。


因此,把编译 gRPC 本身、编译 examples 程序、从零开始实现一个 gRPC 的服务器与客户端的步骤整理、记录下来。

环境准备

首先需要安装编译工具:


sudo apt install -y cmake build-essential autoconf libtool pkg-config
复制代码

编译 gRPC

为了避免编译出来的 gRPC 文件影响到开发环境,选择把它们放到自己建立的目录 GRPC_INSTALL_DIR,而不是直接放到系统目录。


mkdir gitcd gitexport GRPC_INSTALL_DIR=$HOME/git/grpc/installexport PATH="$GRPC_INSTALL_DIR/bin:$PATH"
git clone --recurse-submodules -b v1.58.0 --depth 1 --shallow-submodules https://github.com/grpc/grpccd grpcmkdir -p cmake/buildpushd cmake/buildcmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=$GRPC_INSTALL_DIR ../..make -j 4make installpopd
复制代码


经过一段时间的 clone 和 make,可以在 GRPC_INSTALL_DIR 目录下看到 gRPC 编译生成的 bin、include、lib、share 等目录和文件。

编译 Abseil C++

这个步骤,在官方文档里面并没有提及。但是后面的 examples 程序会依赖 absl 的一堆库,没有这一步,编译是失败的。简直了,为了一个命令行解释的功能,就引入了很多的依赖……


#回到gRPC根目录#创建存放编译结果的目录mkdir -p third_party/abseil-cpp/cmake/build#进入编译目录pushd third_party/abseil-cpp/cmake/build#生成编译 abseil-cpp 的 Makefile 文件cmake -DCMAKE_INSTALL_PREFIX=$GRPC_INSTALL_DIR -DCMAKE_POSITION_INDEPENDENT_CODE=TRUE ../..#编译make -j 4#安装make installpopd
复制代码


这步结束之后,可以在 GRPC_INSTALL_DIR 的 lib 目录下发现多了 80 几个 libabsl 相关的库文件。

编译 examples 程序

从最基本的 hello world 开始,编译这个程序:


cd examples/cpp/helloworldmkdir -p cmake/buildpushd cmake/buildcmake -DCMAKE_PREFIX_PATH=$GRPC_INSTALL_DIR ../..make -j 4
复制代码

运行 hello world 程序

在两个 Terminal,分别运行 server 端和 client 端的程序。


cd examples/cpp/helloworld/cmake/build./greeter_server
复制代码


cd examples/cpp/helloworld/cmake/build./greeter_cliente
复制代码


可以发现 client 给 server 发了一个 hello,然后 server 回复。


至此,一个 Hello World 的 gRPC 通信已经实现。




后面,我们自己实现一个支持 gRPC 的程序。


先在 examples/cpp 目录下新建一个 scratch 目录:


cd examples/cppmkdir scratchcd scratch
复制代码

实现 Protocol Buffers 部分

为了实现 gRPC,我们首先需要编辑 scratch.proto 文件,在文件里定义通信的 Service、来回消息的格式:


// Proto of a example from scratch
syntax = "proto3";
option java_multiple_files = true;option java_package = "com.ycwang.scratch";option java_outer_classname = "ScratchProto";option objc_class_prefix = "Scratch";
package scratch;
// The greeting service definition.service ScratchService { // Echo, reply with a little more string rpc Echo (EchoRequest) returns (EchoReply) {}
// Math Pow rpc Pow (MathPowRequest) returns (MathPowReply) {}}
message EchoRequest { string id = 1; string msg = 2;}
message EchoReply { string msg = 1;}
message MathPowRequest { int32 base = 1; int32 exp = 2;}
message MathPowReply { int32 power = 1;}
复制代码


在这个文件里,定义了一个 Service,两个 RPC 接口,分别实现了字符串的 echo、整数的幂计算。

实现代码部分

然后,分别实现 server 端和 client 端。另外,为了去除对 absl 的依赖,直接使用 getopt 来获取命令行参数。


scratch_server.cc:


// Server running gRPC
#include <iostream>#include <memory>#include <string>#include <sstream>// For getopt#include <unistd.h>
#include <grpcpp/ext/proto_server_reflection_plugin.h>#include <grpcpp/grpcpp.h>#include <grpcpp/health_check_service_interface.h>
// Generated by protoc#include "scratch.grpc.pb.h"
using grpc::Server;using grpc::ServerBuilder;using grpc::ServerContext;using grpc::Status;
// Application specific using scratch::EchoReply;using scratch::EchoRequest;using scratch::MathPowReply;using scratch::MathPowRequest;using scratch::ScratchService;
// gRPC server, implement the RPC methods defined in proto fileclass ScratchServiceImpl final : public ScratchService::Service{ Status Echo(ServerContext *context, const EchoRequest *request, EchoReply *reply) override { std::cout << "Receive echo request from: " << request->id() << std::endl; std::stringstream ss; ss << request->id() << ", your message is: " << request->msg(); reply->set_msg(ss.str()); return Status::OK; }
Status Pow(ServerContext *context, const MathPowRequest *request, MathPowReply *reply) override { std::cout << "Receive math pow request for base: " << request->base() << ", and exponent: " << request->exp() << std::endl; int result = pow(request->base(), request->exp()); reply->set_power(result); std::cout << "Power result is: " << result << std::endl; return Status::OK; }};

void RunServer(uint16_t port){ std::stringstream ss; ss << "0.0.0.0:" << port; std::string server_address = ss.str();
ScratchServiceImpl service; grpc::EnableDefaultHealthCheckService(true); grpc::reflection::InitProtoReflectionServerBuilderPlugin(); ServerBuilder builder; // Listen on the given address without any authentication mechanism. builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); // Register "service" as the instance through which we'll communicate with // clients. In this case it corresponds to an *synchronous* service. builder.RegisterService(&service); // Finally assemble the server. std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "gRPC Server is listening on: " << server_address << std::endl;
// Wait for the server to shutdown. Note that some other thread must be // responsible for shutting down the server for this call to ever return. server->Wait();}
int main(int argc, char **argv){ uint16_t port = 45678; int opt = 0; while((opt = getopt(argc, argv, "p:h")) != -1) { switch (opt) { case 'p': port = atoi(optarg); break; case 'h': std::cout << "Usage: scratch_server -p Port" << std::endl; break; default: std::cout << "Listening on default port " << port << std::endl; break; } }
RunServer(port); return 0;}
复制代码


Server 端需要实现 proto 文件所定义的接口:收到 Client 发来的数据后,如何进行处理、回复。


scratch_client.cc:


// Client running gRPC
#include <iostream>#include <memory>#include <string>#include <sstream>
#include <grpcpp/grpcpp.h>
// Generated by protoc#include "scratch.grpc.pb.h"
using grpc::Channel;using grpc::ClientContext;using grpc::Status;
// Application specific using scratch::EchoReply;using scratch::EchoRequest;using scratch::MathPowReply;using scratch::MathPowRequest;using scratch::ScratchService;
// gRPC Client, using the service and RPC methods defined in proto fileclass ScratchClient{public: ScratchClient(std::shared_ptr<Channel> channel) : stub_(ScratchService::NewStub(channel)) {}
// Assembles the client's payload, sends it and presents the response back from the server. int PowReq(int base, int exp) { // Data we are sending to the server. MathPowRequest request; request.set_base(base); request.set_exp(exp);
// Container for the data we expect from the server. MathPowReply reply; // Context for the client. It could be used to convey extra information to // the server and/or tweak certain RPC behaviors. ClientContext context;
// The actual RPC. Status status = stub_->Pow(&context, request, &reply); if (status.ok()) { return reply.power(); } else { std::cout << status.error_code() << ": " << status.error_message() << std::endl; return 0; } }
// Assembles the client's payload, sends it and presents the response back from the server. std::string EchoReq(const std::string& id, const std::string& msg) { // Data we are sending to the server. EchoRequest request; request.set_id(id); request.set_msg(msg);
// Container for the data we expect from the server. EchoReply reply; // Context for the client. It could be used to convey extra information to // the server and/or tweak certain RPC behaviors. ClientContext context;
// The actual RPC. Status status = stub_->Echo(&context, request, &reply); if (status.ok()) { std::cout << "Receive message from server: " << reply.msg() <<std::endl; return reply.msg(); } else { std::cout << status.error_code() << ": " << status.error_message() << std::endl; return "RPC failed"; } }
private: std::unique_ptr<ScratchService::Stub> stub_;};

int main(int argc, char **argv){ uint16_t port = 45678; int opt = 0; while((opt = getopt(argc, argv, "p:h")) != -1) { switch (opt) { case 'p': port = atoi(optarg); break; case 'h': std::cout << "Usage: scratch_client -p Port" << std::endl; break; default: std::cout << "Connect to default port " << port << std::endl; break; } }
std::stringstream ss; ss << "0.0.0.0:" << port; std::string server_address = ss.str(); std::cout << "gRPC Client is connecting to: " << server_address << std::endl;
// We indicate that the channel isn't authenticated (use of InsecureChannelCredentials()). ScratchClient client(grpc::CreateChannel(server_address, grpc::InsecureChannelCredentials()));
// gRPC call std::string id("ABC"); std::string msg("How are you and how old are you? "); std::string reply = client.EchoReq(id, msg); std::cout << "Echo received: " << reply << std::endl;
// gRPC call int power = client.PowReq(111, 2); std::cout << "Math Power returns: " << power << std::endl;
return 0;}
复制代码


客户端根据所定义的 RPC 接口,构建消息发给 Server 端,并得到回复。

实现编译部分

编译过程涉及到根据 proto 文件产生 h/cpp 文件、编译连接 server 和 client 代码等步骤。为了简单起见,把这些步骤合并到 Makefile 里面。


CMakeLists.txt:


cmake_minimum_required(VERSION 3.8)
project(Scratch C CXX)
include(../cmake/common.cmake)
# Proto fileget_filename_component(scratch_proto "scratch.proto" ABSOLUTE)get_filename_component(scratch_proto_path "${scratch_proto}" PATH)
# Generated sourcesset(scratch_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/scratch.pb.cc")set(scratch_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/scratch.pb.h")set(scratch_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/scratch.grpc.pb.cc")set(scratch_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/scratch.grpc.pb.h")add_custom_command( OUTPUT "${scratch_proto_srcs}" "${scratch_proto_hdrs}" "${scratch_grpc_srcs}" "${scratch_grpc_hdrs}" COMMAND ${_PROTOBUF_PROTOC} ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}" --cpp_out "${CMAKE_CURRENT_BINARY_DIR}" -I "${scratch_proto_path}" --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}" "${scratch_proto}" DEPENDS "${scratch_proto}")
# Include generated *.pb.h filesinclude_directories("${CMAKE_CURRENT_BINARY_DIR}")
# scratch_grpc_protoadd_library(scratch_grpc_proto ${scratch_grpc_srcs} ${scratch_grpc_hdrs} ${scratch_proto_srcs} ${scratch_proto_hdrs})target_link_libraries(scratch_grpc_proto ${_REFLECTION} ${_GRPC_GRPCPP} ${_PROTOBUF_LIBPROTOBUF})
# Targets scratch_(client|server)foreach(_target scratch_client scratch_server) add_executable(${_target} "${_target}.cc") target_link_libraries(${_target} scratch_grpc_proto ${_REFLECTION} ${_GRPC_GRPCPP} ${_PROTOBUF_LIBPROTOBUF})endforeach()
复制代码


Makefile:



PROTOBUF_UTF8_RANGE_LINK_LIBS = -lutf8_validity
HOST_SYSTEM = $(shell uname | cut -f 1 -d_)SYSTEM ?= $(HOST_SYSTEM)CXX = g++CPPFLAGS += `pkg-config --cflags protobuf grpc`CXXFLAGS += -std=c++14ifeq ($(SYSTEM),Darwin)LDFLAGS += -L/usr/local/lib `pkg-config --libs --static protobuf grpc++`\ $(PROTOBUF_UTF8_RANGE_LINK_LIBS) \ -pthread\ -lgrpc++_reflection\ -ldlelseLDFLAGS += -L/usr/local/lib `pkg-config --libs --static protobuf grpc++`\ $(PROTOBUF_UTF8_RANGE_LINK_LIBS) \ -pthread\ -Wl,--no-as-needed -lgrpc++_reflection -Wl,--as-needed\ -ldlendifPROTOC = protocGRPC_CPP_PLUGIN = grpc_cpp_pluginGRPC_CPP_PLUGIN_PATH ?= `which $(GRPC_CPP_PLUGIN)`
PROTOS_PATH = ./
vpath %.proto $(PROTOS_PATH)
all: system-check scratch_client scratch_server
scratch_client: scratch.pb.o scratch.grpc.pb.o scratch_client.o $(CXX) $^ $(LDFLAGS) -o $@
scratch_server: scratch.pb.o scratch.grpc.pb.o scratch_server.o $(CXX) $^ $(LDFLAGS) -o $@
.PRECIOUS: %.grpc.pb.cc%.grpc.pb.cc: %.proto $(PROTOC) -I $(PROTOS_PATH) --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN_PATH) $<
.PRECIOUS: %.pb.cc%.pb.cc: %.proto $(PROTOC) -I $(PROTOS_PATH) --cpp_out=. $<
clean: rm -f *.o *.pb.cc *.pb.h scratch_client scratch_server

# The following is to test your system and ensure a smoother experience.# They are by no means necessary to actually compile a grpc-enabled software.
PROTOC_CMD = which $(PROTOC)PROTOC_CHECK_CMD = $(PROTOC) --version | grep -q 'libprotoc.3\|libprotoc [0-9][0-9]\.'PLUGIN_CHECK_CMD = which $(GRPC_CPP_PLUGIN)HAS_PROTOC = $(shell $(PROTOC_CMD) > /dev/null && echo true || echo false)ifeq ($(HAS_PROTOC),true)HAS_VALID_PROTOC = $(shell $(PROTOC_CHECK_CMD) 2> /dev/null && echo true || echo false)endifHAS_PLUGIN = $(shell $(PLUGIN_CHECK_CMD) > /dev/null && echo true || echo false)
SYSTEM_OK = falseifeq ($(HAS_VALID_PROTOC),true)ifeq ($(HAS_PLUGIN),true)SYSTEM_OK = trueendifendif
system-check:ifneq ($(HAS_VALID_PROTOC),true) @echo " DEPENDENCY ERROR" @echo @echo "You don't have protoc 3.0.0 or newer installed in your path." @echo "Please install an up-to-date version of Google protocol buffers." @echo "You can find it here:" @echo @echo " https://github.com/protocolbuffers/protobuf/releases" @echo @echo "Here is what I get when trying to evaluate your version of protoc:" @echo -$(PROTOC) --version @echo @echoendififneq ($(HAS_PLUGIN),true) @echo " DEPENDENCY ERROR" @echo @echo "You don't have the grpc c++ protobuf plugin installed in your path." @echo "Please install grpc. You can find it here:" @echo @echo " https://github.com/grpc/grpc" @echo @echo "Here is what I get when trying to detect if you have the plugin:" @echo -which $(GRPC_CPP_PLUGIN) @echo @echoendififneq ($(SYSTEM_OK),true) @falseendif
复制代码


再编辑一个 shell 脚本方便使用:


build.sh:


!/bin/bash
export GRPC_INSTALL_DIR=$HOME/git/grpc/installexport PATH="$GRPC_INSTALL_DIR/bin:$PATH"
mkdir -p cmake/buildpushd cmake/buildcmake -DCMAKE_PREFIX_PATH=$GRPC_INSTALL_DIR ../..makepopd
复制代码


这样,运行这个脚本就可以实现整个程序的编译过程了:


chmod a+x build.sh./build.sh
复制代码

运行部分

在两个 Terminal,分别进入 cmake/build 目录,运行 server 和 client 程序,可以得到结果:


./scratch_servergRPC Server is listening on: 0.0.0.0:45678Receive echo request from: ABCReceive math pow request for base: 111, and exponent: 2Power result is: 12321
复制代码


./scratch_clientgRPC Client is connecting to: 0.0.0.0:45678Receive message from server: ABC, your message is: How are you and how old are you? Echo received: ABC, your message is: How are you and how old are you? Math Power returns: 12321
复制代码


这样,就完成了基于 gRPC 的通信。


基本的步骤就是三步走:根据接口修改 proto 文件 -> 在 server 端实现接口的响应 -> 在 client 端实现对接口的调用。


用户头像

王玉川

关注

https://yuchuanwang.github.io/ 2018-11-13 加入

https://www.linkedin.com/in/yuchuan-wang/

评论

发布
暂无评论
gRPC C++快速编译与上手_c++_王玉川_InfoQ写作社区