Qt wrapper around gRPC framework in C ++

    Hello. Today we will look at how to link the gRPC framework in C ++ and the Qt library. The article provides a code summarizing the use of all four interaction modes in gRPC. In addition, there is a code that allows gRPC to be used through Qt signals and slots. The article may be of interest primarily to Qt developers interested in using gRPC. However, a summary of the four gRPC modes of operation is written in C ++ without using Qt, which will allow developers to adapt the code to non-Qt developers. All interested in asking under the cat.


    Prehistory


    About half a year ago, two projects hung on me, using the client and server parts of gRPC. Both projects fell into production. These projects were written by developers who have already quit. I was glad only that I was actively involved in writing the gRPC server and client code. But that was about a year ago. Therefore, as usual, I had to deal with everything from scratch.


    The gRPC server code was written with the expectation that it will be further generated from the .proto file. The code was written well. However, the server had one big disadvantage: only one client could connect to it.


    The gRPC client was written terribly.


    I figured out the client and server code for gRPC only a few days later. And I realized that if I took a project for a couple of weeks, I would have to deal with the gRPC server and client again.


    It was then that I decided that it was time to write and debug the gRPC client and server so that:


    • You could sleep at night;

    • There was no need to remember how this works every time you need to write a client or gRPC server;

    • It was possible to use the written client and server gRPC in other projects.


    When writing code, I was guided by the following requirements:


    • Both the gRPC client and server can work using the signals and slots of the Qt library in a natural way;

    • The gRPC client and server code do not need to be corrected when the .proto file is changed;

    • The gRPC client must be able to tell the client code the status of the connection to the server.


    The structure of the article is as follows. First, there will be a brief overview of the results of working with client code and small explanations to it. At the end of the review link to the repository. Next will be general things about architecture. Then a description of the server and client code (what is under the hood) and the conclusion.


    Short review


    The simplest pingproto.proto file was used as the .proto file, which defines the RPC of all types of interaction:


    syntax = "proto3";
    package pingpong;
    service ping 
    {
      rpc SayHello (PingRequest) returns (PingReply) {}
      rpc GladToSeeMe(PingRequest) returns (stream PingReply){}
      rpc GladToSeeYou(stream PingRequest) returns (PingReply){}
      rpc BothGladToSee(stream PingRequest) returns (stream PingReply){}
    }
    message PingRequest 
    {
      string name = 1;
      string message = 2;
    }
    message PingReply 
    {
      string message = 1;
    }
    

    The pingpong.proto file repeats the file helloworld.proto from the article about the asynchronous modes of gRPC in C ++, up to names .


    As a result, the written server can be used like this:


    classA:public QObject
    {
        Q_OBJECT;
        QpingServerService pingservice;
    public:
        A()
        {
            bool is_ok;
        	is_ok = connect(&pingservice, SIGNAL(SayHelloRequest(SayHelloCallData*)), this, SLOT(onSayHello(SayHelloCallData*))); assert(is_ok);
        	is_ok = connect(&pingservice, SIGNAL(GladToSeeMeRequest(GladToSeeMeCallData*)), this, SLOT(onGladToSeeMe(GladToSeeMeCallData*))); assert(is_ok);
        	is_ok = connect(&pingservice, SIGNAL(GladToSeeYouRequest(GladToSeeYouCallData*)), this, SLOT(onGladToSeeYou(GladToSeeYouCallData*))); assert(is_ok);
        	is_ok = connect(&pingservice, SIGNAL(BothGladToSeeRequest(BothGladToSeeCallData*)), this, SLOT(onBothGladToSee(BothGladToSeeCallData*))); assert(is_ok);
        }
    public slots:
        voidonSayHello(SayHelloCallData* cd){
            std::cout << "[" << cd->peer() << "][11]: request: " << cd->request.name() << std::endl;
            cd->reply.set_message("hello " + cd->request.name());
            cd->Finish();
        }
        //etc.
    };
    

    When a client calls an RPC, the gRPC server notifies the client code (in this case, class A) with the appropriate signal.


    The gRPC client can be used like this:


    classB :public QObject
    {
        Q_OBJECT
        QpingClientService pingPongSrv;
    public:
        B()
        {
            bool c = false;
            c = connect(&pingPongSrv, SIGNAL(SayHelloResponse(SayHelloCallData*)), this, SLOT(onSayHelloResponse(SayHelloCallData*))); assert(c);
            c = connect(&pingPongSrv, SIGNAL(GladToSeeMeResponse(GladToSeeMeCallData*)), this, SLOT(onGladToSeeMeResponse(GladToSeeMeCallData*))); assert(c);
            c = connect(&pingPongSrv, SIGNAL(GladToSeeYouResponse(GladToSeeYouCallData*)), this, SLOT(onGladToSeeYouResponse(GladToSeeYouCallData*))); assert(c);
            c = connect(&pingPongSrv, SIGNAL(BothGladToSeeResponse(BothGladToSeeCallData*)), this, SLOT(onBothGladToSeeResponse(BothGladToSeeCallData*))); assert(c);
            c = connect(&pingPongSrv, SIGNAL(channelStateChanged(int, int)), this, SLOT(onPingPongStateChanged(int, int))); assert(c);
        }
        voidusage(){
        	//Unary
            PingRequest request;
            request.set_name("user");
            request.set_message("user");
            pingPongSrv.SayHello(request);
            //Server streaming
            PingRequest request2;
            request2.set_name("user");
            pingPongSrv.GladToSeeMe(request2);
            //etc.
        }
    public slots:
        voidSayHelloResponse(SayHelloCallData* response){
            std::cout << "[11]: reply: " <<  response->reply.message() << std::endl;
            if (response->CouldBeDeleted())
                delete response;
        }
        //etc.
    };
    

    The gRPC client allows you to call RPC directly, and subscribe to the server response using the appropriate signals.


    The gRPC client also has a signal:

    channelStateChanged(int, int);
    

    which reports past and current connection status to the server. All code with examples of use is in the qgrpc repository .

    How it works


    The principle of including the client and gRPC server in the project is shown in the figure.



    The .pro project file contains the .proto files on which gRPC will work. The grpc.pri file contains commands for generating gRPC and QgRPC files. The protoc compiler generates [protofile] .grpc.pb.h and [protofile] .grpc.pb.cc gRPC files. [protofile] is the name of the .proto file passed to the input of the compiler.


    Generating QgRPC files [protofile] .qgrpc. [Config] .h is handled by the script genQGrpc.py. [config] is either "server" or "client".

    The generated QgRPC files contain a Qt wrapper around the gRPC classes and calls with the appropriate signals. In the previous examples, the QpingServerService and QpingClientService classes are declared in the generated pingpong.qgrpc.server.h and pingpong.qgrpc.client.h, respectively. The generated QgRPC files are added to moc processing.


    The generated QgRPC files include QGrpc [config] .h files, in which all the main work takes place. Read more about this below.


    To connect all this construction to the project, in the .pro project file you need to include the grpc.pri file and specify three variables. The GRPC variable defines the .proto files to be transferred to the inputs of the protoc compiler and the genQGrpc.py script. The variable QGRPC_CONFIG determines the configuration value of the generated QgRPC files and can contain the values ​​“server” or “client”. You can also define an optional variable GRPC_VERSION to indicate the version of gRPC.


    Read more about everything said in the grpc.pri file and the .pro example files.


    Server architecture


    The class diagram of the server is shown in the figure.



    Thick arrows show class inheritance hierarchy, and thin ones show class members and methods belonging. In general, a service is generated for the service class Q [servicename] ServerService, where servicename is the name of the service declared in the .proto file. RPCCallData are control structures generated for each RPC in the service. In the constructor of the QpingServerService class, the base class QGrpcServerService is initialized by the asynchronous service gRPC pingpong :: ping :: AsyncService. To start the service, you need to call the Start () method with the address and port on which the service will run. The Start () function implements a standard service start procedure.


    At the end of the Start () function, the pure virtual function makeRequests () is called, which is implemented in the generated QpingServerService class:


    void makeRequests()
    {
        needAnotherCallData< SayHello_RPCtypes, SayHelloCallData >();
        needAnotherCallData< GladToSeeMe_RPCtypes, GladToSeeMeCallData >();
        needAnotherCallData< GladToSeeYou_RPCtypes, GladToSeeYouCallData >();
        needAnotherCallData< BothGladToSee_RPCtypes, BothGladToSeeCallData >();
    }
      

    The second template parameter of the needAnotherCallData function is the generated RPCCallData structures. These same structures are the parameters of the signals in the generated class of the Qt service.


    The generated RPCCallData structures are inherited from the ServerCallData class. In turn, the ServerCallData class is inherited from the ServerResponder responder. Thus, the creation of the object of the generated structures leads to the creation of the responder object.


    The constructor for the ServerCallData class takes two parameters: signal_func and request_func. signal_func is a generated signal that is invoked after receiving a tag from a queue. request_func is a function that should be called when creating a new responder. For example, in this case it could be the RequestSayHello () function. The call to request_func occurs exactly in the needAnotherCallData () function. This is done in order to manage responders (create and delete) in the service.


    The needAnotherCallData () function code consists of creating a responder object and calling a function that associates the responder with an RPC call:


    template<classRPCCallData, classRPCTypes>
    voidneedAnotherCallData()
    {
        RPCCallData* cd = new RPCCallData();
        //...
        RequestRPC<RPCTypes::kind, ...>
        (service_, cd->request_func_, cd->responder, ..., (void*)cd);
    }
      

    RequestRPC () functions are template functions for four kinds of interactions. As a result, the RequestRPC () call is reduced to a call:


    service_->(cd->request_func_)(...,cd->responder, (void*)cd);

    where service_ is a gRPC service. In this case, it is pingpong :: ping :: AsyncService.


    To synchronously or asynchronously check the event queue, you must call the CheckCQ () or AsyncCheckCQ () functions, respectively. The function code CheckCQ () is reduced to calls to the function of synchronous retrieving a tag from a queue and processing this tag:


    virtualvoidCheckCQ() override
    {
        void* tag; bool ok;
        server_cq_->Next(&tag, &ok);
        //tagActions_ callif (!tag)
            return;
        AbstractCallData* cd = (AbstractCallData*)tag;
        if (!started_.load())
        {
            destroyCallData(cd);
            return;
        }
        cd->cqReaction(this, ok);
    }
      

    After receiving the tag from the queue, the validity of the tag and the start of the server are checked. If the server is turned off, then the tag is no longer needed - you can delete it. After that, the cqReaction () function is called, defined in the ServerCallData class:


    voidcqReaction(const QGrpcServerService* service_, bool ok){    
        if (!first_time_reaction_)
        {
            first_time_reaction_ = true;
            service_->needAnotherCallData<RPC, RPCCallData>();
        }
        auto genRpcCallData = dynamic_cast<RPCCallData*>(this);
        void* tag = static_cast<void*>(genRpcCallData); 
        if (this->CouldBeDeleted())
        {
            service_->destroyCallData(this); 
            return;
        }
        if (!this->processEvent(tag, ok)) return;
        //call generated service signal with generated call data argument
        service_->(*signal_func_)(genRpcCallData);
    }
      

    The flag first_time_reaction_ says that you need to create a new responder for the called RPC. The CouldBeDeleted () and ProcessEvent () functions are inherited from the corresponding type of Responder ServerResponder class. The CouldBeDeleted () function returns a sign that the responder object can be deleted. The processEvent () function processes the tag and the ok flag. So, for example, for the Client Streaming view responder, the function looks like this:


    boolprocessEvent(void* tag, bool ok)
    {
        this->tag_ = tag;
        read_mode_ = ok;
        returntrue;
    }
    

    The ProcessEvent () function, regardless of the type of responder, always returns true. The return value of this function is left for possible extension of functionality and, theoretically, to eliminate errors.


    After processing the event, the call follows:

    service_->(*signal_func_)(genRpcCallData);

    The variable service_ is an instance of the generated service, in our case QpingServerService. The variable signal_func_ is a service signal corresponding to a specific RPC. For example, SayHelloRequest (). The variable genRpcCallData is the responder object of the corresponding type. From the point of view of the calling code, the genRpcCallData variable is an object of one of the generated RPCCallData structures.


    Client architecture


    Whenever possible, the names of the classes and functions of the client coincide with the names of the classes and functions of the server. The client class diagram is shown in the figure.



    Thick arrows show class inheritance hierarchy, and thin ones show class members and methods belonging. In the general case, a Q [servicename] ClientService class is generated for the service, where servicename is the name of the service declared in the .proto file. RPCCallData are control structures generated for each RPC in the service. For an RPC call, the generated class provides functions whose names exactly match the RPC declared in the .proto file. In our example, in the .proto file the RPC SayHello () is declared as:

    rpc SayHello (PingRequest) returns (PingReply) {}
    

    In the generated QpingClientService class, the corresponding RPC function looks like this:


    void SayHello(PingRequest request)
    {
        if(!connected()) return;
        SayHelloCallData* call = new SayHelloCallData;
        call->request = request;
        call->responder = stub_->AsyncSayHello(&call->context, request, &cq_);
        call->responder->Finish(&call->reply, &call->status, (void*)call);
    }
    

    The generated RPCCallData structures, as in the case of the server, are ultimately inherited from the ClientResponder class. Therefore, the creation of an object of the generated structure leads to the creation of a responder. After the responder is created, an RPC call is made and the responder is bound to the event of receiving a response from the server. From the point of view of client code, an RPC call looks like this:


    void ToSayHello()
    {
        PingRequest request;
        request.set_name("user");
        request.set_message("user");
        pingPongSrv.SayHello(request);
    }
    

    Unlike the generated QpingServerService server class, the QpingClientService class is inherited from two template classes: ConnectivityFeatures and MonitorFeatures.


    The ConnectivityFeatures class is responsible for connecting the client to the server and provides three functions for use: grpc_connect (), grpc_disconnect (), grpc_reconnect (). The grpc_disconnect () function simply removes all the data structures responsible for interacting with the server. A call to the grpc_connect function is reduced to calls to the grpc_connect_ () function, which creates control data structures:


    void grpc_connect_()
    {
        channel_ = grpc::CreateChannel(target_, creds_);
        stub_ = GRPCService::NewStub(channel_);
        channelFeatures_ = std::make_unique<ChannelFeatures>(channel_);
        channelFeatures_->checkChannelState();
    }
    

    The ChannelFeatures class monitors the state of the channel_ communication channel with the server. The ConnectivityFeatures class encapsulates an object of the class ChannelFeatures and with this object implements the abstract functions channelState (), checkChannelState () and connected (). The channelState () function returns the last observed state of the communication channel with the server. The checkChannelState () function actually gives the current status of the channel. The connected () function returns the sign of the client connecting to the server.


    The MonitorFeatures class is responsible for receiving and processing events from the server and provides the CheckCQ () function for use:


    boolCheckCQ(){
        auto service_ = dynamic_cast< SERVICE* >(this);
        //connection stateauto old_state = conn_->channelState();
        auto new_state = conn_->checkChannelState();
        if (old_state != new_state)
            service->*channelStateChangedSignal_(old_state, new_state);
        //end of connection statevoid* tag;
        bool ok = false;
        grpc::CompletionQueue::NextStatus st;
        st = cq_.AsyncNext(&tag, &ok, deadlineFromMSec(100));
        if ((st == grpc::CompletionQueue::SHUTDOWN) || (st == grpc::CompletionQueue::TIMEOUT))
            returnfalse;
        (AbstractCallData< SERVICE >*)(tag)->cqActions(service_, ok);
        returntrue;
    }
    

    The code structure is the same as in the server case. Unlike the server, a block of code is added to the client that is responsible for handling the current state. If the link status has changed, the signal is called channelStateChangedSignal_ (). In all generated services, this is a signal:

    voidchannelStateChanged(int, int);
    

    Also, unlike the server, here the AsyncNext () function is used instead of Next (). This was done for several reasons. First, when using AsyncNext (), the client code can learn about the change in the state of the communication channel. Secondly, when using AsyncNext (), it is possible to call various RPCs in the client code any number of times. Using the Next () function in this case will lead to blocking the thread before receiving the event from the queue and, as a result, to the loss of the two possibilities described.

    After receiving the event from the queue, as in the case of the server, the cqReaction () function, defined in the ClientCallData class, is called:


    voidcqActions(RPC::Service* service, bool ok){
        auto response = dynamic_cast<RPCCallData*>(this);
        void* tag = static_cast<void*>(response);
        if (!this->processEvent(tag, ok)) return;
        service->*func_( response );
    }
    

    As with the server, the processEvent () function processes the tag and the ok flag and always returns true. As in the case of the server, after the event is processed, the signal of the generated service is called. However, there are two significant differences from the same-name server function. The first difference is that the creation of responders does not occur in this function. Creation of responders, as shown above, occurs when calling RPC. The second difference is that in this function the responders are not deleted. The lack of deletion of responders is done for two reasons. First, client code can use pointers to the generated RPCCallData structures for their own purposes. Deleting the contents of this pointer, hidden from the client code, can lead to unpleasant consequences. Secondly, deleting the responder will result in that the data signal will not be generated. Consequently, the client code will not receive the last server message. Among several alternative solutions to the indicated problems, a solution was chosen to shift the deletion of the responder (generated structures) to the client code. Thus, signal handler functions (slots) should contain the following code:


    voidResponseHandler(RPCCallData* response){
        if (response->CouldBeDeleted())
            delete response;
        //process response
    }
    

    Failure to delete the responder in the client code will lead not only to a memory leak, but also to possible problems with the communication channel. Signal handlers of all kinds of RPC interactions are implemented in the example code.


    Conclusion


    In conclusion, we note two points. The first point is related to calling the CheckCQ () functions of the client and server. They work, as was shown above, according to one principle: if there is an event in the queue, a signal with the corresponding generated RPCCallData structure is “emitted”. You can call this function manually and check (in the case of a client) the presence of an event. But initially the idea was to transfer the entire network portion associated with gRPC to another stream. For these purposes, QGrpcSrvMonitor for the gRPC server and QGrpcCliServer for the gRPC client were written. Both classes work on the same principle: they create a separate stream, put the generated service into this stream, and periodically call the CheckCQ () function of this service. In this way, when using both auxiliary classes, there is no need to call CheckCQ () functions in the client code. The signals of the generated service, in this case, "come" from another thread. Client and server examples are implemented using these helper classes.


    The second point concerns the majority of developers who do not use the Qt library in their work. Qt classes and macros in QgRPC are used only in two places: in the generated service files, and in files containing helper classes: QGrpcServerMonitor.h and QGrpcClientMonitor.h. The rest of the files with the Qt library are not related. It was planned to add an assembly using cmake, and stub some Qt directives. In particular, the QObject class and the Q_OBJECT macro. But this simply did not reach the hands. Therefore, any suggestions are welcome.


    That's all. Thanks to all!


    Links



    Also popular now: