手写grpc c++ 简单demo
自己动手写一个grpc c++的demo,自己写protobuf文件,编译文件和源码
实现一个最简单的grpc功能,客户端向服务端发送一个消息,服务端接收到消息后把结果返回给客户端
demo的文件结构
首先定义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的官方教程中是通过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官方教程。
简单来说是这样的: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 CONFIG) 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了开两个终端分别运行服务端和客户端程序
这只是一个走流程的demo,目的是清楚怎样自己做一个grpc c++的工程,编译脚本做的还不够自动化,可以通过一个shell脚本整个进行控制,实现功能也比较简单,只是single stream的消息发送与接收,官方教程中还有多输入单输出,单输入多输出,还有多输入多输出的教程
https://grpc.io/docs/languages/cpp/basics/