Skip to content

Latest commit

 

History

History
537 lines (391 loc) · 13.6 KB

Cpp_gRPC.md

File metadata and controls

537 lines (391 loc) · 13.6 KB

gRPC C++快速编译与上手

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()

再编辑一个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端实现对接口的调用。