自己動手寫一個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/
