手寫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/ 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM