// Copyright (C) 2024 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only /*! \example clientguide \title Qt GRPC Client Guide \ingroup qtgrpc-examples \examplecategory {Networking} \meta tag {network,protobuf,grpc,serialization,overview} \brief The Qt GRPC client guide. \section1 Service Methods In \gRPC, service methods can be defined in a protobuf schema to specify the communication between clients and servers. The protobuf compiler, \c{protoc}, can then generate the required server and client interfaces based on these definitions. \gRPC supports four types of service methods: \list \li \l{Unary Calls} — The client sends a single request and receives a single response. \badcode rpc UnaryCall (Request) returns (Response); \endcode The corresponding client handler is QGrpcCallReply. \li \l{Server Streaming} — The client sends a single request and receives multiple responses. \badcode rpc ServerStreaming (Request) returns (stream Response); \endcode The corresponding client handler is QGrpcServerStream. \li \l{Client Streaming} — The client sends multiple requests and receives a single response. \badcode rpc ClientStreaming (stream Request) returns (Response); \endcode The corresponding client handler is QGrpcClientStream. \li \l{Bidirectional Streaming} — The client and server exchange multiple messages. \badcode rpc BidirectionalStreaming (stream Request) returns (stream Response); \endcode The corresponding client handler is QGrpcBidiStream. \endlist \gRPC communication always starts with the client, which initiates the \l{https://en.wikipedia.org/wiki/Remote_procedure_call}{remote procedure call} (RPC) by sending the first message to the server. The server then concludes any type of communication by returning a \l{QtGrpc::} {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 \l{Signals & Slots} mechanism. A key signal common to all RPC handlers is \l{QGrpcOperation::} {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 \l{QGrpcServerStream::} {messageReceived} for incoming messages, \l{QGrpcClientStream::} {writeMessage} for sending messages to the server, and \l{QGrpcBidiStream::} {writesDone} for closing client-side communication. The table below outlines the supported functionality of the RPC client handlers: \table 85 % \header \li Functionality \li QGrpcCallReply \li QGrpcServerStream \li QGrpcClientStream \li QGrpcBidiStream \row \li \l {QGrpcOperation::}{finished} \li ✓ (\l{QGrpcOperation::}{read} final response) \li ✓ \li ✓ (\l{QGrpcOperation::}{read} final response) \li ✓ \row \li \l {QGrpcServerStream::}{messageReceived} \li ✗ \li ✓ \li ✗ \li ✓ \row \li \l {QGrpcClientStream::}{writeMessage} \li ✗ \li ✗ \li ✓ \li ✓ \row \li \l {QGrpcBidiStream::}{writesDone} \li ✗ \li ✗ \li ✓ \li ✓ \endtable \section1 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 \c {clientguide.proto} file as an example: \snippet clientguide/proto/clientguide.proto 0 To use this \e {.proto} file for our Qt GRPC client in C++, we must run the \c protoc compiler with the Qt generator plugins on it. Fortunately, Qt provides the \l{qt_add_grpc} and \l{qt_add_protobuf} CMake functions to streamline this process. \snippet clientguide/client/CMakeLists.txt 0 This results in two header files being generated in the current build directory: \list \li \e {clientguide.qpb.h}: Generated by \l{The qtprotobufgen Tool}{qtprotobufgen}. Declares the \c Request and \c Response protobuf messages from the schema. \li \e {clientguide_client.grpc.qpb.h}: Generated by \l{The qtgrpcgen Tool}{qtgrpcgen}. Declares the client interface for calling the methods of a \gRPC server implementing the \c ClientGuideService from the schema. \endlist The following client interface is generated: \code namespace client::guide { namespace ClientGuideService { class Client : public QGrpcClientBase { ... std::unique_ptr UnaryCall(const client::guide::Request &arg); std::unique_ptr ServerStreaming(const client::guide::Request &arg); std::unique_ptr ClientStreaming(const client::guide::Request &arg); std::unique_ptr BidirectionalStreaming(const client::guide::Request &arg); ... }; } // namespace ClientGuideService } // namespace client::guide \endcode \include qtgrpc-shared.qdocinc rpc-lifetime-note \section2 Server Setup The server implementation for the \c ClientGuideService follows a straightforward approach. It validates the request message's \c time field, returning the \c INVALID_ARGUMENT status code if the time is in the future: \snippet clientguide/server/main.cpp time Additionally, the server sets the current time in every response message: \snippet clientguide/server/main.cpp response For valid \c time requests, the service methods behave as follows: \list \li \c{UnaryCall}: Responds with the \c num field from the request. \li \c{ServerStreaming}: Sends \c num responses matching the request message. \li \c{ClientStreaming}: Counts the number of request messages and sets this count as \c num. \li \c{BidirectionalStreaming}: Immediately responds with the \c num field from each incoming request message. \endlist \section2 Client Setup We begin by including the generated header files: \snippet clientguide/client/main.cpp gen-includes For this example, we create the \c ClientGuide class to manage all communication, making it easier to follow. We begin by setting up the backbone of all \gRPC communication: a channel. \snippet clientguide/client/main.cpp basic-0 The Qt GRPC library offers QGrpcHttp2Channel, which you can \l{QGrpcClientBase::attachChannel} {attach} to the generated client interface: \snippet clientguide/client/main.cpp basic-1 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). \section3 Creating a request message Here's a simple wrapper to create request messages: \snippet clientguide/client/main.cpp basic-2 This function takes an integer and an optional boolean. By default its messages use the current time, so the \l{Server Setup}{server logic} should accept them. When called with \c fail set to \c true, however, it produces messages that the server shall reject. \section2 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 \l {QGrpcOperation::} {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. \code std::unique_ptr 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 ); \endcode \list \li \b{1}: Since we manage the lifetime of the unique RPC object within the lambda, moving it into the lambda's capture would invalidate \c {get()} and other member functions. Therefore, we must copy the pointers address before moving it. \li \b{2}: The \l{QGrpcOperation::}{finished} signal is emitted only once, making this a true single-shot connection. It is \b{important} to mark this connection as \l{Qt::}{SingleShotConnection}! If not, the capture of \c reply will not be destroyed, leading to a \b {hidden memory leak} that is hard to discover. \endlist The \l{Qt::}{SingleShotConnection} argument in the \c{connect} call ensures that the slot functor (the lambda) is destroyed after being emitted, freeing the resources associated with the slot, including its captures. \section1 Remote Procedure Calls \section2 Unary Calls Unary calls require only the \l {QGrpcOperation::} {finished} signal to be handled. When this signal is emitted, we can check the \l {QGrpcStatus} {status} of the RPC to determine if it was successful. If it was, we can \l {QGrpcOperation::} {read} the single and final response from the server. In this example, we use the single-shot paradigm. Ensure you carefully read the \l {Single Shot RPCs} section. \snippet clientguide/client/main.cpp unary-0 The function starts the RPC by invoking the \c UnaryCall member function of the generated client interface \c m_client. The lifetime is solely managed by the \l{QGrpcCallReply::} {finished} signal. \details {Running the code} In \c main, we simply invoke this function three times, letting the second invocation fail: \snippet clientguide/client/main.cpp unary-1 A possible output of running this could look like the following: \badcode 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 ) \endcode 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 \l {QtGrpc::StatusCode::} {Ok} status code, but the second message failed with the \l {QtGrpc::StatusCode::} {InvalidArgument} status code due to the message time being in the future. \enddetails \section2 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 \l{QGrpcOperation::} {finished} signal you also have to handle the \l{QGrpcServerStream::} {messageReceived} signal. In this example, we use the single-shot paradigm to manage the streaming RPC lifecycle. Ensure you carefully read the \l {Single Shot RPCs} section. As with any RPC, we connect to the \l{QGrpcOperation::} {finished} signal first: \snippet clientguide/client/main.cpp sstream-0 To handle the server messages, we connect to the \l{QGrpcServerStream::} {messageReceived} signal and \l{QGrpcOperation::} {read} the response when the signal is emitted. \snippet clientguide/client/main.cpp sstream-1 \details {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. \snippet clientguide/client/main.cpp sstream-2 A possible output of running the server streaming could look like this: \badcode 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 \endcode Once the server starts, it receives a request with a \e num value of 3 and responds with three \c Response messages before completing the communication. \enddetails \section2 Client Streaming In a client stream, the client sends one or more requests, and the server responds with a single final response. The \l {QGrpcOperation::} {finished} signal must be handled, and messages can be sent using the \l{QGrpcClientStream::} {writeMessage} function. The \l{QGrpcClientStream::} {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 \l {QGrpcOperation::} {finished} signal: \snippet clientguide/client/main.cpp cstream-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 \l{QGrpcClientStream::}{writesDone}. If the streaming RPC succeeds, we \l {QGrpcOperation::} {read} the final response from the server and \c reset the RPC object. If the RPC fails, we retry by invoking the same function, which overwrites the \c m_clientStream member and reconnects the \l {QGrpcOperation::} {finished} signal. We cannot simply reassign the \c m_clientStream member within the lambda, as this would lose the necessary connection. \details {Running the code} In \c main, we invoke the \c clientStreaming function with a failing message, triggering an RPC failure and executing the retry logic. \snippet clientguide/client/main.cpp cstream-1 A possible output of running the client streaming could look like this: \badcode 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 ) \endcode 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. \enddetails \section2 Bidirectional Streaming Bidirectional streaming offers the most flexibility, allowing both the client and server to send and receive messages simultaneously. It requires the \l {QGrpcOperation::} {finished} and \l {QGrpcBidiStream::} {messageReceived} signal to be handled and provides the write functionality through \l {QGrpcBidiStream::} {writeMessage}. We use a class-based approach with member function \e slot connections to demonstrate the functionality, incorporating the handler as a member of the class. Additionally, we utilize the pointer-based \l {QGrpcOperation::} {read} function. The two members used are: \snippet clientguide/client/main.cpp bstream-0 We create a function to start the bidirectional streaming from an initial message and connect the slot functions to the respective \l {QGrpcOperation::} {finished} and \l {QGrpcBidiStream::} {messageReceived} signals. \snippet clientguide/client/main.cpp bstream-1 The slot functionality is straightforward. The \l {QGrpcOperation::} {finished} slot simply prints and resets the RPC object: \snippet clientguide/client/main.cpp bstream-2 The \l {QGrpcBidiStream::} {messageReceived} slot \l {QGrpcOperation::} {read}s into the \c 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 \l {QGrpcBidiStream::} {writesDone}. \snippet clientguide/client/main.cpp bstream-3 \details {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 \c main, we create such a request, which ultimately serves as a counter. \snippet clientguide/client/main.cpp bstream-4 A possible output of running the bidirectional streaming could look like this: \badcode 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 \endcode \enddetails */