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 git
cd git
export GRPC_INSTALL_DIR=$HOME/git/grpc/install
export PATH="$GRPC_INSTALL_DIR/bin:$PATH"
git clone --recurse-submodules -b v1.58.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc
cd grpc
mkdir -p cmake/build
pushd cmake/build
cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF -DCMAKE_INSTALL_PREFIX=$GRPC_INSTALL_DIR ../..
make -j 4
make install
popd
经过一段时间的 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 install
popd
这步结束之后,可以在 GRPC_INSTALL_DIR 的 lib 目录下发现多了 80 几个 libabsl 相关的库文件。
编译 examples 程序
从最基本的 hello world 开始,编译这个程序:
cd examples/cpp/helloworld
mkdir -p cmake/build
pushd cmake/build
cmake -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/cpp
mkdir scratch
cd 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 file
class 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 file
class 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 file
get_filename_component(scratch_proto "scratch.proto" ABSOLUTE)
get_filename_component(scratch_proto_path "${scratch_proto}" PATH)
# Generated sources
set(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 files
include_directories("${CMAKE_CURRENT_BINARY_DIR}")
# scratch_grpc_proto
add_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++14
ifeq ($(SYSTEM),Darwin)
LDFLAGS += -L/usr/local/lib `pkg-config --libs --static protobuf grpc++`\
$(PROTOBUF_UTF8_RANGE_LINK_LIBS) \
-pthread\
-lgrpc++_reflection\
-ldl
else
LDFLAGS += -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\
-ldl
endif
PROTOC = protoc
GRPC_CPP_PLUGIN = grpc_cpp_plugin
GRPC_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)
endif
HAS_PLUGIN = $(shell $(PLUGIN_CHECK_CMD) > /dev/null && echo true || echo false)
SYSTEM_OK = false
ifeq ($(HAS_VALID_PROTOC),true)
ifeq ($(HAS_PLUGIN),true)
SYSTEM_OK = true
endif
endif
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
@echo
endif
ifneq ($(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
@echo
endif
ifneq ($(SYSTEM_OK),true)
@false
endif
再编辑一个 shell 脚本方便使用:
build.sh:
!/bin/bash
export GRPC_INSTALL_DIR=$HOME/git/grpc/install
export PATH="$GRPC_INSTALL_DIR/bin:$PATH"
mkdir -p cmake/build
pushd cmake/build
cmake -DCMAKE_PREFIX_PATH=$GRPC_INSTALL_DIR ../..
make
popd
这样,运行这个脚本就可以实现整个程序的编译过程了:
chmod a+x build.sh
./build.sh
运行部分
在两个 Terminal,分别进入 cmake/build 目录,运行 server 和 client 程序,可以得到结果:
./scratch_server
gRPC Server is listening on: 0.0.0.0:45678
Receive echo request from: ABC
Receive math pow request for base: 111, and exponent: 2
Power result is: 12321
./scratch_client
gRPC Client is connecting to: 0.0.0.0:45678
Receive 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/
评论