gRPC在跨主机、跨进程的通信中,有着广泛的应用。但对于新手来说,如果按照官方文档的步骤:Quick start | C++ | gRPC,是无法通过编译并运行examples程序的。
因此,把编译gRPC本身、编译examples程序、从零开始实现一个gRPC的服务器与客户端的步骤整理、记录下来。
首先需要安装编译工具:
sudo apt install -y cmake build-essential autoconf libtool pkg-config
为了避免编译出来的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等目录和文件。
这个步骤,在官方文档里面并没有提及。但是后面的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相关的库文件。
从最基本的hello world开始,编译这个程序:
cd examples/cpp/helloworld
mkdir -p cmake/build
pushd cmake/build
cmake -DCMAKE_PREFIX_PATH=$GRPC_INSTALL_DIR ../..
make -j 4
在两个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
为了实现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()
再编辑一个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端实现对接口的调用。