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.