Client Methods of the Qt GRPC Service

gRPC lets you define four kinds of service methods:

  • Unary call, where the client sends a single request to the server and gets a single response back:
     rpc PingPong (Ping) returns (Pong);
    
  • Server stream, where the client sends a single request to the server and gets one or more responses back:
     rpc PingSeveralPong (Ping) returns (stream Pong);
    
  • Client stream, where the client sends one or more requests to the server and gets a single response back:
     rpc SeveralPingPong (stream Ping) returns (Pong);
    
  • Bidirectional stream, where the client sends one or more requests to the server and gets one or more responses back:
     rpc SeveralPingSeveralPong (stream Ping) returns (stream Pong);
    

    Note that the number of responses might not be aligned with the number of requests, nor the request and response sequence. This is controlled by the application business logic.

gRPC communication always starts at the client side and ends at the server side. The client initiates the communication by sending the first message to the server. The server ends the communication of any type by replying with the status code.

To use the Qt GRPC C++ API, start with defining the pingpong.proto schema for your project:

 syntax = "proto3";

 package ping.pong;

 message Ping {
     uint64 time = 1;
     sint32 num = 2;
 }

 message Pong {
     uint64 time = 1;
     sint32 num = 2;
 }

 service PingPongService {
     // Unary call
     rpc PingPong (Ping) returns (Pong);

     // Server stream
     rpc PingSeveralPong (Ping) returns (stream Pong);

     // Client stream
     rpc SeveralPingPong (stream Ping) returns (Pong);

     // Bidirectional stream
     rpc SeveralPingSeveralPong (stream Ping) returns (stream Pong);
 }

Generate the C++ client code using the above schema and the Qt GRPC CMake API:

 find_package(Qt6 COMPONENTS Protobuf Grpc)

 qt_add_executable(pingpong ...)

 qt_add_protobuf(pingpong PROTO_FILES pingpong.proto)
 qt_add_grpc(pingpong CLIENT PROTO_FILES pingpong.proto)

Both the generated protobuf messages and the client gRPC code will be added to the pingpong CMake target.

Using unary calls in Qt GRPC

Let's start with the simplest communication scenario - a unary gRPC call. In this RPC type, the client sends a single request message and receives a single response message from the server. The communication ends once the server sends a status code.

For unary calls, the qtgrpcgen tool generates two alternative asynchronous methods:

 namespace ping::pong {
 namespace PingPongService {

 class Client : public QGrpcClientBase {
     Q_OBJECT
 public:
     std::shared_ptr<QGrpcCallReply> PingPong(const ping::pong::Ping &arg,
                                             const QGrpcCallOptions &options = {});
     Q_INVOKABLE void PingPong(const ping::pong::Ping &arg, const QObject *context,
                             const std::function<void(std::shared_ptr<QGrpcCallReply>)> &callback,
                             const QGrpcCallOptions &options = {});
     ...
 };
 } // namespace PingPongService
 } // namespace ping::pong

Call reply handling using QGrpcCallReply

The first variant returns the QGrpcCallReply gRPC operation. QGrpcCallReply reads the message received from the server and gets the notifications about errors or the end of call.

After creating PingPongService::Client and attaching QGrpcHttp2Channel to it, call the PingPong method:

 qint64 requestTime = QDateTime::currentMSecsSinceEpoch();
 ping::pong::Ping request;
 request.setTime(requestTime);

 auto reply = cl.PingPong(request,{});
 QObject::connect(reply.get(), &QGrpcCallReply::finished, reply.get(),
                  [requestTime, replyPtr = reply.get()]() {
                      if (const auto response = replyPtr->read<ping::pong::Pong>())
                         qDebug() << "Ping-Pong time difference" << response->time() - requestTime;
                     qDebug() << "Failed deserialization";
                  });

 QObject::connect(reply.get(), &QGrpcCallReply::errorOccurred, stream.get()
                  [](const QGrpcStatus &status) {
                      qDebug() << "Error occurred: " << status.code() << status.message();
                  });

After the server responds to the request, the QGrpcCallReply::finished signal is emitted. The reply object contains the raw response data received from the server and can be deserialized to the ping::pong::Pong protobuf message using the QGrpcCallReply::read method.

If the server does not respond or the request caused an error in the server, the QGrpcCallReply::errorOccurred signal is emitted with the corresponding status code. If the server answered with the QtGrpc::StatusCode::Ok code, the QGrpcCallReply::errorOccurred signal is not emitted.

Call reply handling using callbacks

The overloaded function is similar to the one that returns the QGrpcCallReply, but instead of returning the reply, the function passes it as an argument to the callback function that is used in the call:

 ...
 cl.PingPong(request, &a, [requestTime](std::shared_ptr<QGrpcCallReply> reply) {
     if (const auto response = reply->read<ping::pong::Pong>())
         qDebug() << "Ping and Pong time difference" << response->time() - requestTime;
 });

This variant makes a connection to the QGrpcCallReply::finished signal implicitly, but you cannot cancel the call using the QGrpcOperation::cancel function.

Using the server streams in Qt GRPC

Server streams extend the unary call scenario and allow the server to respond multiple times to the client request. The communication ends once the server sends a status code.

For server streams, the qtgrpcgen tool generates the method that returns the pointer to QGrpcServerStream:

 std::shared_ptr<QGrpcServerStream> pingSeveralPong(const ping::pong::Ping &arg,
                                                          const QGrpcCallOptions &options = {});

QGrpcServerStream is similar to QGrpcCallReply, but it emits the QGrpcServerStream::messageReceived when the server response is received.

 QObject::connect(stream.get(), &QGrpcServerStream::messageReceived, stream.get(),
                  [streamPtr = stream.get(), requestTime]() {
                      if (const auto response = streamPtr->read<ping::pong::Pong>()) {
                         qDebug() << "Ping-Pong next response time difference"
                                 << response->time() - requestTime;
                      }
                  });

 QObject::connect(stream.get(), &QGrpcServerStream::errorOccurred, stream.get()
                  [](const QGrpcStatus &status) {
                      qDebug() << "Error occurred: " << status.code() << status.message();
                  });

 QObject::connect(stream.get(), &QGrpcServerStream::finished, stream.get(),
                  []{
                      qDebug() << "Bye";
                  });

Note: QGrpcServerStream overrides the internal buffer when receiving a new message from the server. After the server finished the communication, you can read only the last message received from the server.

Using the client streams in Qt GRPC

Client streams extend the unary call scenario and allow the client to send multiple requests. The server responds only once before ending the communication.

For server streams, the qtgrpcgen tool generates the method that returns the pointer to QGrpcClientStream:

 std::shared_ptr<QGrpcClientStream> severalPingPong(const ping::pong::Ping &arg,
                                                          const QGrpcCallOptions &options = {});

To send multiple requests to the server, use the QGrpcClientStream::writeMessage method:

 auto stream = cl.severalPingPong(request);

 QTimer timer;
 QObject::connect(&timer, &QTimer::timeout, stream.get(),
                  [streamPtr = stream.get()](){
                      ping::pong::Ping request;
                      request.setTime(QDateTime::currentMSecsSinceEpoch());
                      streamPtr->writeMessage(request);
                  });

 QObject::connect(stream.get(), &QGrpcServerStream::finished, stream.get(),
                  [streamPtr = stream.get(), &timer]{
                      if (const auto response = streamPtr->read<ping::pong::Pong>()) {
                         qDebug() << "Slowest Ping time: " << response->time();
                      }
                      timer.stop();
                  });

 QObject::connect(stream.get(), &QGrpcServerStream::errorOccurred, stream.get()
                  [&timer](const QGrpcStatus &status){
                      qDebug() << "Error occurred: " << status.code() << status.message();
                      timer.stop();
                  });

 timer.start(1000);
 return a.exec();

After the server receives enough Ping requests from the client, it responds with Pong, which contains the slowest Ping time.

Using the bidirectional streams in Qt GRPC

Bidirectional streams combine the functionality of server and client streams. The generated method returns the pointer to QGrpcBidiStream, which provides the API from both server and client streams:

 std::shared_ptr<QGrpcBidiStream> severalPingSeveralPong(const ping::pong::Ping &arg,
                                                         const QGrpcCallOptions &options = {});

Use the bidirectional streams to organize the two-sided communication without breaking the connection session:

 auto stream = cl.severalPingSeveralPong(request);

 qint64 maxPingPongTime = 0;
 QTimer timer;
 QObject::connect(&timer, &QTimer::timeout, stream.get(),
                  [streamPtr = stream.get(), &requestTime](){
                      requestTime = QDateTime::currentMSecsSinceEpoch();
                      ping::pong::Ping request;
                      request.setTime(requestTime);
                      streamPtr->writeMessage(request);
                  });

 QObject::connect(stream.get(), &QGrpcBidiStream::messageReceived, stream.get(),
                  [streamPtr = stream.get(), &timer, &maxPingPongTime, &requestTime]{
                      if (const auto response = streamPtr->read<ping::pong::Pong>())
                         maxPingPongTime = std::max(maxPingPongTime, response->time() - requestTime);
                  });

 QObject::connect(stream.get(), &QGrpcBidiStream::finished, stream.get(),
                  [streamPtr = stream.get(), &timer, &maxPingPongTime]{
                      qDebug() << "Maximum Ping-Pong time: " << maxPingPongTime;
                      timer.stop();
                  });

 QObject::connect(stream.get(), &QGrpcBidiStream::errorOccurred, stream.get(),
                  [&timer](const QGrpcStatus &status){
                      qDebug() << "Error occurred: " << status.code() << status.message();
                      timer.stop();
                  });

 timer.start(1000);

Every time the client sends the Ping requests, the server responds with the Pong message. The maximum Ping-Pong time is evaluated until the server ends the communication by sending a status code to the client.

Note: QGrpcBidiStream overrides the internal buffer when receiving a new message from the server. After server finished the communication, you can read only the last message received from the server.