自己动手写一个grpc c++的demo,自己写protobuf文件,编译文件和源码

实现一个最简单的grpc功能,客户端向服务端发送一个消息,服务端接收到消息后把结果返回给客户端

手写grpc c++ 简单demo_#include


 demo的文件结构

手写grpc c++ 简单demo_服务端_02


 首先定义proto文件

官方教程:https://developers.google.com/protocol-buffers/docs/cpptutorial

proto文件的书写非常简单,下面是test1.proto

syntax="proto3";

option java_multiple_files=true;
option java_package="io.grpc.example.test1";
option java_outer_classname="Test1Proto";
option objc_class_prefix="HLW";

package test1;

service TestService{
rpc getData (Data) returns (MsgReply){}
}

message Data{
int32 data=1;
}

message MsgReply{
string message=1;
}

在test1.proto文件中,我定义了一个函数和两个数据类型,函数放在service服务中,启动的时候就启动服务,服务中的这些函数就处于等待响应的状态,getData的功能就是在server端接收一个Data,返回一个MsgReply。Data和MsgReply都是我定义的数据结构用message来表示,可以将message近似看成一个结构体。定义完proto文件后,需要编译proto文件,让他生成如下代码

手写grpc c++ 简单demo_服务端_03


 grpc的官方教程中是通过cmake来进行编译的,需要用到add_custom_command来引入外部命令,比较麻烦,所以我直接通过shell脚本进行生成。

generate_grpc_file.sh如下

mkdir gen_code
protoc -I ./ --grpc_out=./gen_code --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` ./test1.proto
protoc -I ./ --cpp_out=./gen_code ./test1.proto

在编译demo之前需要先运行这个shell脚本

编译完proto文件后,我们得到了生成的四份c++代码,这个生成的代码怎么用,详情可以看前面贴的proto官方教程。

​https://developers.google.com/protocol-buffers/docs/cpptutorial#the-protocol-buffer-api​

简单来说是这样的:proto中的数据结构和server里面的函数转变成了c++代码,生成的c++数据结构怎么用?主要有以下几种方法拿MsgReply为例

message MsgReply{
string message=1;
}

成员message是字符串类型,那么MsgReply中就有

message()直接获取值

has_message()检查message是否存在

clear_message()清空message

set_message()给message赋值

mutable_message() 返回string的指针,貌似int这种简单的数据类型没有这个方法

IsInitialized()是否初始化

CopyFrom()拷贝值

clear()清空

这些函数传入值是指针还是引用还是void看上面贴的链接,不一个个列出来了,大概就是这么用的。


编译完proto文件后接下来要写客户端和服务端的代码了。客户端和服务端是两个可执行程序,分开跑

首先看服务端server

#include <grpc/grpc.h>
#include <grpcpp/security/server_credentials.h>
#include <grpcpp/server.h>
#include <grpcpp/server_builder.h>
#include <grpcpp/server_context.h>
#include "./gen_code/test1.grpc.pb.h"
#include <iostream>
#include <string>
#include <memory>

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::ServerReader;
using grpc::ServerWriter;
using grpc::ServerReaderWriter;
using grpc::Status;
using std::cout;
using std::endl;
using std::string;

using test1::Data;
using test1::MsgReply;
using test1::TestService;

class Test1Impl final:public TestService::Service{
public:
Status getData(ServerContext* context,const Data* data,MsgReply* msg)override
{
cout<<"[get data]: "<<data->data()<<endl;
string tmp("data received 12345");
msg->set_message(tmp);
return Status::OK;
}
};

void RunServer()
{
std::string server_addr("0.0.0.0:50051");
// create an instance of our service implementation class Test1Impl
Test1Impl service;

// Create an instance of factory ServerBuilder class
ServerBuilder builder;

// Specify the address and port we want to use to listen for client requests using the builder’s AddListeningPort() method.
builder.AddListeningPort(server_addr,grpc::InsecureServerCredentials());

// Register our service implementation with the builder.
builder.RegisterService(&service);

// Call BuildAndStart() on the builder to create and start an RPC server for our service.
std::unique_ptr<Server> server(builder.BuildAndStart());
cout<<"Server listening on "<<server_addr<<endl;

// Call Wait() on the server to do a blocking wait until process is killed or Shutdown() is called
server->Wait();
}

int main(int argc,char** argv)
{
RunServer();
return 0;
}

首先要引入grpc server端相应的头文件,然后引入我们生成代码的命名空间,也就是using test1::,把我们自己定义的函数和数据结构引入进来。然后创建一个class,继承我们在proto文件中定义的类的service,在这个类中实例化我们在proto中定义的函数。返回值是Status。在这个函数中首先把data打印出来,然后生成一个string,最后把这个string赋给MsgReply中的message,返回Status::OK。然后我们要写一个函数将server跑起来,首先创建一个server实例,然后创建一个ServerBuilder实例,给ServerBuilder添加监听端口,将我自己的server绑定到ServerBuilder上,将这个ServerBuilder启动起来,使用智能指针接受返回值。server->wait(),等待消息传入。然后再主函数中调用runserver函数,至此server端代码完成。


client端代码

#include <iostream>
#include <memory>
#include <string>
#include <grpc/grpc.h>
#include <grpcpp/channel.h>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include "./gen_code/test1.grpc.pb.h"

using std::endl;
using std::cout;
using std::string;

using grpc::Channel;
using grpc::ClientContext;
using grpc::ClientReader;
using grpc::ClientReaderWriter;
using grpc::ClientWriter;
using grpc::Status;

using test1::TestService;
using test1::Data;
using test1::MsgReply;

class Test1Client{
public:
// create stub
Test1Client(std::shared_ptr<Channel> channel):stub_(TestService::NewStub(channel)){}
void GetReplyMsg()
{
Data data;
MsgReply msg_reply;
data.set_data(123);
GetOneData(data,&msg_reply);
}
private:
bool GetOneData(const Data& data,MsgReply* msg_reply)
{
ClientContext context;
Status status=stub_->getData(&context,data,msg_reply);
if(!status.ok())
{
cout<<"GetData rpc failed."<<endl;
return false;
}
if(msg_reply->message().empty())
{
cout<<"message empty."<<endl;
return false;
}
else
{
cout<<"MsgReply:"<<msg_reply->message()<<endl;
}
return true;
}

std::unique_ptr<TestService::Stub> stub_;
};

int main(int argc,char** argv)
{
// create a gRPC channel for our stub
//grpc::CreateChannel("locakhost:50051",grpc::InsecureChannelCredentials());
Test1Client client1(grpc::CreateChannel("localhost:50051",grpc::InsecureChannelCredentials()));
cout<<"====================="<<endl;
client1.GetReplyMsg();

return 0;
}

客户端代码同样是先引入头文件和命名空间,然后创建一个客户端类,客户端中我们需要一个成员变量Stub(不知道怎么翻译)来调用服务端的函数。所以类中有成员变量std::unique_ptr<TestService::Stub> stub_;并且我们需要在构造函数中对其赋值。然后就是通过stub调用server中的getData方法了,然后根据服务端传回的结果,在客户端进行对应的输出。在客户端main函数中,同样需要新建客户端实例Test1Client,然后进行调用。


代码都完成了,下面开始写编译文件CMakeLists.txt

cmake_minimum_required(VERSION 3.5)
project(test1)
find_package(Threads REQUIRED)
find_package(Protobuf REQUIRED)
set(_PROTOBUF_LIBPROTOBUF protobuf::libprotobuf)
set(_REFLECTION gRPC::grpc++_reflection)
find_package(gRPC CONFIG REQUIRED)
set(_GRPC_GRPCPP gRPC::grpc++)

# Include generated *.pb.h files
include_directories("${CMAKE_CURRENT_BINARY_DIR}/../gen_code")
set(hw_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/../gen_code/test1.pb.cc")
set(hw_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/../gen_code/test1.pb.h")
set(hw_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/../gen_code/test1.grpc.pb.cc")
set(hw_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/../gen_code/test1.grpc.pb.h")
# hw_grpc_proto
add_library(hw_grpc_proto
${hw_grpc_srcs}
${hw_grpc_hdrs}
${hw_proto_srcs}
${hw_proto_hdrs})
target_link_libraries(hw_grpc_proto
${_REFLECTION}
${_GRPC_GRPCPP}
${_PROTOBUF_LIBPROTOBUF})

# Targets greeter_[async_](client|server)
foreach(_target
test1_server test1_client
)
add_executable(${_target} "${_target}.cc")
target_link_libraries(${_target}
hw_grpc_proto
${_REFLECTION}
${_GRPC_GRPCPP}
${_PROTOBUF_LIBPROTOBUF})
endforeach()

因为之前已经通过shell脚本完成了proto文件的编译,也就是该生成的代码已经生成完了,所以这里的CMakeLists.txt文件就不需要像官方example中的CMakeLists.txt那么复杂了,只需要将生成的代码导入(add_library)然后和grpc的库进行链接就可以了。最后在把服务端和客户端代码生成可执行文件就可以了。

编译顺序,先在根目录执行

./generate_grpc_file.sh

注意如果目录中有gen_code文件夹,要把它删掉,我的shell脚本写的比较简单,买考虑文件夹存在的情况,要手动删除。然后进入到build文件,执行

cmake ..
make

就ok了开两个终端分别运行服务端和客户端程序

手写grpc c++ 简单demo_客户端_04


这只是一个走流程的demo,目的是清楚怎样自己做一个grpc c++的工程,编译脚本做的还不够自动化,可以通过一个shell脚本整个进行控制,实现功能也比较简单,只是single stream的消息发送与接收,官方教程中还有多输入单输出,单输入多输出,还有多输入多输出的教程