The Qt GRPC client guide.
Service Methods
In gRPC™, service methods can be defined in a protobuf schema to specify the communication between clients and servers. The protobuf compiler, protoc
, can then generate the required server and client interfaces based on these definitions. gRPC supports four types of service methods:
- Unary Calls — The client sends a single request and receives a single response.
rpc UnaryCall (Request) returns (Response);
The corresponding client handler is QGrpcCallReply.
- Server Streaming — The client sends a single request and receives multiple responses.
rpc ServerStreaming (Request) returns (stream Response);
The corresponding client handler is QGrpcServerStream.
- Client Streaming — The client sends multiple requests and receives a single response.
rpc ClientStreaming (stream Request) returns (Response);
The corresponding client handler is QGrpcClientStream.
- Bidirectional Streaming — The client and server exchange multiple messages.
rpc BidirectionalStreaming (stream Request) returns (stream Response);
The corresponding client handler is QGrpcBidiStream.
gRPC communication always starts with the client, which initiates the remote procedure call (RPC) by sending the first message to the server. The server then concludes any type of communication by returning a StatusCode.
All client RPC handlers are derived from the QGrpcOperation class, which provides shared functionality. Due to the asynchronous nature of RPCs, they are naturally managed through Qt's Signals & Slots mechanism.
A key signal common to all RPC handlers is finished, which indicates the completion of an RPC. The handler emits this signal exactly once during its lifetime. This signal delivers the corresponding QGrpcStatus, providing additional information about the success or failure of the RPC.
There are also operation-specific functionalities, such as messageReceived for incoming messages, writeMessage for sending messages to the server, and writesDone for closing client-side communication. The table below outlines the supported functionality of the RPC client handlers:
Functionality | QGrpcCallReply | QGrpcServerStream | QGrpcClientStream | QGrpcBidiStream |
---|---|---|---|---|
finished | ✓ (read final response) | ✓ | ✓ (read final response) | ✓ |
messageReceived | ✗ | ✓ | ✗ | ✓ |
writeMessage | ✗ | ✗ | ✓ | ✓ |
writesDone | ✗ | ✗ | ✓ | ✓ |
Getting Started
To use the Qt GRPC C++ API, start by using an already available protobuf schema or define your own. We will use the clientguide.proto
file as an example:
syntax = "proto3"; package client.guide; // enclosing namespace message Request { int64 time = 1; sint32 num = 2; } message Response { int64 time = 1; sint32 num = 2; } service ClientGuideService { rpc UnaryCall (Request) returns (Response); rpc ServerStreaming (Request) returns (stream Response); rpc ClientStreaming (stream Request) returns (Response); rpc BidirectionalStreaming (stream Request) returns (stream Response); }
To use this .proto file for our Qt GRPC client in C++, we must run the protoc
compiler with the Qt generator plugins on it. Fortunately, Qt provides the qt_add_grpc and qt_add_protobuf CMake functions to streamline this process.
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/clientguide.proto") find_package(Qt6 COMPONENTS Protobuf Grpc) qt_standard_project_setup(REQUIRES 6.8) qt_add_executable(clientguide_client main.cpp) # Using the executable as input target will append the generated files to it. qt_add_protobuf(clientguide_client PROTO_FILES ${proto_files} ) qt_add_grpc(clientguide_client CLIENT PROTO_FILES ${proto_files} ) target_link_libraries(clientguide_client PRIVATE Qt6::Protobuf Qt6::Grpc)
This results in two header files being generated in the current build directory:
- clientguide.qpb.h: Generated by qtprotobufgen. Declares the
Request
andResponse
protobuf messages from the schema. - clientguide_client.grpc.qpb.h: Generated by qtgrpcgen. Declares the client interface for calling the methods of a gRPC server implementing the
ClientGuideService
from the schema.
The following client interface is generated:
namespace client::guide { namespace ClientGuideService { class Client : public QGrpcClientBase { ... std::unique_ptr<QGrpcCallReply> UnaryCall(const client::guide::Request &arg); std::unique_ptr<QGrpcServerStream> ServerStreaming(const client::guide::Request &arg); std::unique_ptr<QGrpcClientStream> ClientStreaming(const client::guide::Request &arg); std::unique_ptr<QGrpcBidiStream> BidirectionalStreaming(const client::guide::Request &arg); ... }; } // namespace ClientGuideService } // namespace client::guide
Note: Users are responsible for managing the unique RPC handlers returned by the Client
interface, ensuring their existence at least until the finished signal is emitted. After receiving this signal, the handler can be safely reassigned or destroyed.
Server Setup
The server implementation for the ClientGuideService
follows a straightforward approach. It validates the request message's time
field, returning the INVALID_ARGUMENT
status code if the time is in the future:
const auto time = now(); if (request->time() > time) return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };
Additionally, the server sets the current time in every response message:
response->set_num(request->num()); response->set_time(time); return grpc::Status::OK;
For valid time
requests, the service methods behave as follows:
UnaryCall
: Responds with thenum
field from the request.ServerStreaming
: Sendsnum
responses matching the request message.ClientStreaming
: Counts the number of request messages and sets this count asnum
.BidirectionalStreaming
: Immediately responds with thenum
field from each incoming request message.
Client Setup
We begin by including the generated header files:
#include "clientguide.qpb.h" #include "clientguide_client.grpc.qpb.h"
For this example, we create the ClientGuide
class to manage all communication, making it easier to follow. We begin by setting up the backbone of all gRPC communication: a channel.
auto channel = std::make_shared<QGrpcHttp2Channel>( QUrl("http://localhost:50056") /* without channel options. */ ); ClientGuide clientGuide(channel);
The Qt GRPC library offers QGrpcHttp2Channel, which you can attach to the generated client interface:
explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel) { m_client.attachChannel(std::move(channel)); }
With this setup, the client will communicate over HTTP/2 using TCP as the transport protocol. The communication will be unencrypted (i.e. without SSL/TLS setup).
Creating a request message
Here's a simple wrapper to create request messages:
static guide::Request createRequest(int32_t num, bool fail = false) { guide::Request request; request.setNum(num); // The server-side logic fails the RPC if the time is in the future. request.setTime(fail ? std::numeric_limits<int64_t>::max() : QDateTime::currentMSecsSinceEpoch()); return request; }
This function takes an integer and an optional boolean. By default its messages use the current time, so the server logic should accept them. When called with fail
set to true
, however, it produces messages that the server shall reject.
Single Shot RPCs
There are different paradigms for working with RPC client handlers. Specifically, you can choose a class-based design where the RPC handler is a member of the enclosing class, or you can manage the lifetime of the RPC handler through the finished signal.
There are two important things to remember when applying the single-shot paradigm. The code below demonstrates how it would work for unary calls, but it's the same for any other RPC type.
std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(requestMessage); const auto *replyPtr = reply.get(); // 1 QObject::connect( replyPtr, &QGrpcCallReply::finished, replyPtr, [reply = std::move(reply)](const QGrpcStatus &status) { ... }, Qt::SingleShotConnection // 2 );
- 1: Since we manage the lifetime of the unique RPC object within the lambda, moving it into the lambda's capture would invalidate
get()
and other member functions. Therefore, we must copy the pointers address before moving it. - 2: The finished signal is emitted only once, making this a true single-shot connection. It is important to mark this connection as SingleShotConnection! If not, the capture of
reply
will not be destroyed, leading to a hidden memory leak that is hard to discover.
The SingleShotConnection argument in the connect
call ensures that the slot functor (the lambda) is destroyed after being emitted, freeing the resources associated with the slot, including its captures.
Remote Procedure Calls
Unary Calls
Unary calls require only the finished signal to be handled. When this signal is emitted, we can check the status of the RPC to determine if it was successful. If it was, we can read the single and final response from the server.
In this example, we use the single-shot paradigm. Ensure you carefully read the Single Shot RPCs section.
void unaryCall(const guide::Request &request) { std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(request); const auto *replyPtr = reply.get(); QObject::connect( replyPtr, &QGrpcCallReply::finished, replyPtr, [reply = std::move(reply)](const QGrpcStatus &status) { if (status.isOk()) { if (const auto response = reply->read<guide::Response>()) qDebug() << "Client (UnaryCall) finished, received:" << *response; else qDebug("Client (UnaryCall) deserialization failed"); } else { qDebug() << "Client (UnaryCall) failed:" << status; } }, Qt::SingleShotConnection); }
The function starts the RPC by invoking the UnaryCall
member function of the generated client interface m_client
. The lifetime is solely managed by the finished signal.
Running the code
In main
, we simply invoke this function three times, letting the second invocation fail:
clientGuide.unaryCall(ClientGuide::createRequest(1)); clientGuide.unaryCall(ClientGuide::createRequest(2, true)); // fail the RPC clientGuide.unaryCall(ClientGuide::createRequest(3));
A possible output of running this could look like the following:
Welcome to the clientguide! Starting the server process ... Server listening on: localhost:50056 Server (UnaryCall): Request( time: 1733498584776, num: 1 ) Server (UnaryCall): Request( time: 9223372036854775807, num: 2 ) Server (UnaryCall): Request( time: 1733498584776, num: 3 ) Client (UnaryCall) finished, received: Response( time: 1733498584778257 , num: 1 ) Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::InvalidArgument, message: "Request time is in the future!" ) Client (UnaryCall) finished, received: Response( time: 1733498584778409 , num: 3 )
We see the server receiving the three messages, with the second containing a large value for its time. On the client side, the first and last calls returned an Ok status code, but the second message failed with the InvalidArgument status code due to the message time being in the future.
Server Streaming
In a server stream, the client sends an initial request, and the server responds with one or more messages. In addition to the finished signal you also have to handle the messageReceived signal.
In this example, we use the single-shot paradigm to manage the streaming RPC lifecycle. Ensure you carefully read the Single Shot RPCs section.
As with any RPC, we connect to the finished signal first:
void serverStreaming(const guide::Request &initialRequest) { std::unique_ptr<QGrpcServerStream> stream = m_client.ServerStreaming(initialRequest); const auto *streamPtr = stream.get(); QObject::connect( streamPtr, &QGrpcServerStream::finished, streamPtr, [stream = std::move(stream)](const QGrpcStatus &status) { if (status.isOk()) qDebug("Client (ServerStreaming) finished"); else qDebug() << "Client (ServerStreaming) failed:" << status; }, Qt::SingleShotConnection);
To handle the server messages, we connect to the messageReceived signal and read the response when the signal is emitted.
QObject::connect(streamPtr, &QGrpcServerStream::messageReceived, streamPtr, [streamPtr] { if (const auto response = streamPtr->read<guide::Response>()) qDebug() << "Client (ServerStream) received:" << *response; else qDebug("Client (ServerStream) deserialization failed"); }); }
Running the code
The server logic streams back the amount received in the initial request to the client. We create such a request and invoke the function.
clientGuide.serverStreaming(ClientGuide::createRequest(3));
A possible output of running the server streaming could look like this:
Welcome to the clientguide! Starting the server process ... Server listening on: localhost:50056 Server (ServerStreaming): Request( time: 1733504435800, num: 3 ) Client (ServerStream) received: Response( time: 1733504435801724 , num: 0 ) Client (ServerStream) received: Response( time: 1733504435801871 , num: 1 ) Client (ServerStream) received: Response( time: 1733504435801913 , num: 2 ) Client (ServerStreaming) finished
Once the server starts, it receives a request with a num value of 3 and responds with three Response
messages before completing the communication.
Client Streaming
In a client stream, the client sends one or more requests, and the server responds with a single final response. The finished signal must be handled, and messages can be sent using the writeMessage function. The writesDone function can then be used to indicate that the client has finished writing and that no more messages will be sent.
We use a class-based approach to interact with the streaming RPC, incorporating the handler as a member of the class. As with any RPC, we connect to the finished signal:
void clientStreaming(const guide::Request &initialRequest) { m_clientStream = m_client.ClientStreaming(initialRequest); for (int32_t i = 1; i < 3; ++i) m_clientStream->writeMessage(createRequest(initialRequest.num() + i)); m_clientStream->writesDone(); QObject::connect(m_clientStream.get(), &QGrpcClientStream::finished, m_clientStream.get(), [this](const QGrpcStatus &status) { if (status.isOk()) { if (const auto response = m_clientStream->read<guide::Response>()) qDebug() << "Client (ClientStreaming) finished, received:" << *response; m_clientStream.reset(); } else { qDebug() << "Client (ClientStreaming) failed:" << status; qDebug("Restarting the client stream"); clientStreaming(createRequest(0)); } }); }
The function starts the client stream with an initial message. Then it continues to write two additional messages before signaling the end of communication by calling writesDone. If the streaming RPC succeeds, we read the final response from the server and reset
the RPC object. If the RPC fails, we retry by invoking the same function, which overwrites the m_clientStream
member and reconnects the finished signal. We cannot simply reassign the m_clientStream
member within the lambda, as this would lose the necessary connection.
Running the code
In main
, we invoke the clientStreaming
function with a failing message, triggering an RPC failure and executing the retry logic.
clientGuide.clientStreaming(ClientGuide::createRequest(0, true)); // fail the RPC
A possible output of running the client streaming could look like this:
Welcome to the clientguide! Starting the server process ... Server listening on: localhost:50056 Server (ClientStreaming): Request( time: 9223372036854775807, num: 0 ) Client (ClientStreaming) failed: QGrpcStatus( code: QtGrpc::StatusCode::InvalidArgument, message: "Request time is in the future!" ) Restarting the client stream Server (ClientStreaming): Request( time: 1733912946696, num: 0 ) Server (ClientStreaming): Request( time: 1733912946697, num: 1 ) Server (ClientStreaming): Request( time: 1733912946697, num: 2 ) Client (ClientStreaming) finished, received: Response( time: 1733912946696922 , num: 3 )
The server receives an initial message that causes the RPC to fail, triggering the retry logic. The retry starts the RPC with a valid message, after which three messages are sent to the server before completing gracefully.
Bidirectional Streaming
Bidirectional streaming offers the most flexibility, allowing both the client and server to send and receive messages simultaneously. It requires the finished and messageReceived signal to be handled and provides the write functionality through writeMessage.
We use a class-based approach with member function slot connections to demonstrate the functionality, incorporating the handler as a member of the class. Additionally, we utilize the pointer-based read function. The two members used are:
std::unique_ptr<QGrpcBidiStream> m_bidiStream; guide::Response m_bidiResponse;
We create a function to start the bidirectional streaming from an initial message and connect the slot functions to the respective finished and messageReceived signals.
void bidirectionalStreaming(const guide::Request &initialRequest) { m_bidiStream = m_client.BidirectionalStreaming(initialRequest); connect(m_bidiStream.get(), &QGrpcBidiStream::finished, this, &ClientGuide::bidiFinished); connect(m_bidiStream.get(), &QGrpcBidiStream::messageReceived, this, &ClientGuide::bidiMessageReceived); }
The slot functionality is straightforward. The finished slot simply prints and resets the RPC object:
void bidiFinished(const QGrpcStatus &status) { if (status.isOk()) qDebug("Client (BidirectionalStreaming) finished"); else qDebug() << "Client (BidirectionalStreaming) failed:" << status; m_bidiStream.reset(); }
The messageReceived slot reads into the m_bidiResponse
member, continuing to write messages until the received response number hits zero. At that point, we half-close the client-side communication using writesDone.
void bidiMessageReceived() { if (m_bidiStream->read(&m_bidiResponse)) { qDebug() << "Client (BidirectionalStreaming) received:" << m_bidiResponse; if (m_bidiResponse.num() > 0) { m_bidiStream->writeMessage(createRequest(m_bidiResponse.num() - 1)); return; } } else { qDebug("Client (BidirectionalStreaming) deserialization failed"); } m_bidiStream->writesDone(); }
Running the code
The server logic simply returns a message as soon as it reads something, creating a response with the number from the request. In main
, we create such a request, which ultimately serves as a counter.
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));
A possible output of running the bidirectional streaming could look like this:
Welcome to the clientguide! Starting the server process ... Server listening on: localhost:50056 Server (BidirectionalStreaming): Request( time: 1733503832107, num: 3 ) Client (BidirectionalStreaming) received: Response( time: 1733503832108708 , num: 3 ) Server (BidirectionalStreaming): Request( time: 1733503832109, num: 2 ) Client (BidirectionalStreaming) received: Response( time: 1733503832109024 , num: 2 ) Server (BidirectionalStreaming): Request( time: 1733503832109, num: 1 ) Client (BidirectionalStreaming) received: Response( time: 1733503832109305 , num: 1 ) Server (BidirectionalStreaming): Request( time: 1733503832109, num: 0 ) Client (BidirectionalStreaming) received: Response( time: 1733503832109529 , num: 0 ) Client (BidirectionalStreaming) finished
© 2024 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.