mirror of https://github.com/qt/qtgrpc.git
Add the clientguide example
It contains in depth documentation to get started with Qt GRPC and replaces the 'Client Methods of the Qt GRPC Service' guide. This will enable users to also run the code, experiment with it and show us the error directly in case they fail to compile. Task-number: QTBUG-125406 Fixes: QTBUG-129588 Pick-to: 6.9 6.8 Change-Id: I8f16156a13da1683bce0e31001ee6b2ff57b1824 Reviewed-by: Alexey Edelev <alexey.edelev@qt.io>
This commit is contained in:
parent
298f6666fa
commit
dedd3edd3f
|
|
@ -1,15 +1,18 @@
|
|||
# Copyright (C) 2023 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
if(TARGET Qt6::ProtobufQuick
|
||||
AND TARGET Qt6::QuickControls2
|
||||
AND TARGET Qt6::qtprotobufgen
|
||||
AND TARGET Qt6::qtgrpcgen)
|
||||
if(TARGET Qt6::qtprotobufgen AND TARGET Qt6::qtgrpcgen)
|
||||
qt_internal_add_example(clientguide)
|
||||
if(TARGET Qt6::ProtobufQuick AND TARGET Qt6::QuickControls2)
|
||||
qt_internal_add_example(magic8ball)
|
||||
|
||||
if(TARGET Qt6::ProtobufWellKnownTypes)
|
||||
qt_internal_add_example(vehicle)
|
||||
endif()
|
||||
|
||||
if(QT_FEATURE_clipboard)
|
||||
qt_internal_add_example(chat)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright (C) 2024 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(ClientGuide LANGUAGES CXX)
|
||||
|
||||
if(NOT DEFINED INSTALL_EXAMPLESDIR)
|
||||
set(INSTALL_EXAMPLESDIR "examples")
|
||||
endif()
|
||||
|
||||
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/grpc/clientguide")
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
add_subdirectory(server)
|
||||
if (NOT TARGET clientguide_server)
|
||||
message(WARNING "The Qt GRPC Client Guide requires the 'clientguide_server' target")
|
||||
return()
|
||||
endif()
|
||||
|
||||
add_subdirectory(client)
|
||||
add_dependencies(clientguide_client clientguide_server)
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright (C) 2024 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#! [0]
|
||||
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/clientguide.proto")
|
||||
|
||||
find_package(Qt6 COMPONENTS Protobuf Grpc)
|
||||
qt_standard_project_setup(REQUIRES 6.9)
|
||||
|
||||
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)
|
||||
#! [0]
|
||||
|
||||
install(TARGETS clientguide_client
|
||||
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
)
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
//! [gen-includes]
|
||||
#include "clientguide.qpb.h"
|
||||
#include "clientguide_client.grpc.qpb.h"
|
||||
//! [gen-includes]
|
||||
|
||||
#include <QtGrpc/QGrpcHttp2Channel>
|
||||
#include <QtGrpc/qgrpcstream.h>
|
||||
|
||||
#include <QtCore/QCommandLineParser>
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QProcess>
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QUrl>
|
||||
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
// We use part of the namespace to clarify the source.
|
||||
using namespace client;
|
||||
|
||||
void startServerProcess();
|
||||
QDebug operator<<(QDebug debug, const guide::Response &response);
|
||||
|
||||
class ClientGuide : public QObject
|
||||
{
|
||||
public:
|
||||
//! [basic-1]
|
||||
explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel)
|
||||
{
|
||||
m_client.attachChannel(std::move(channel));
|
||||
}
|
||||
//! [basic-1]
|
||||
|
||||
//! [basic-2]
|
||||
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;
|
||||
}
|
||||
//! [basic-2]
|
||||
|
||||
//! [unary-0]
|
||||
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);
|
||||
}
|
||||
//! [unary-0]
|
||||
|
||||
//! [sstream-0]
|
||||
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);
|
||||
//! [sstream-0]
|
||||
//! [sstream-1]
|
||||
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");
|
||||
});
|
||||
}
|
||||
//! [sstream-1]
|
||||
|
||||
// ! [cstream-0]
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
// ! [cstream-0]
|
||||
|
||||
// ! [bstream-1]
|
||||
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);
|
||||
}
|
||||
// ! [bstream-1]
|
||||
|
||||
private slots:
|
||||
// ! [bstream-2]
|
||||
void bidiFinished(const QGrpcStatus &status)
|
||||
{
|
||||
if (status.isOk())
|
||||
qDebug("Client (BidirectionalStreaming) finished");
|
||||
else
|
||||
qDebug() << "Client (BidirectionalStreaming) failed:" << status;
|
||||
m_bidiStream.reset();
|
||||
}
|
||||
// ! [bstream-2]
|
||||
|
||||
// ! [bstream-3]
|
||||
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();
|
||||
}
|
||||
// ! [bstream-3]
|
||||
|
||||
private:
|
||||
guide::ClientGuideService::Client m_client;
|
||||
std::unique_ptr<QGrpcClientStream> m_clientStream;
|
||||
// ! [bstream-0]
|
||||
std::unique_ptr<QGrpcBidiStream> m_bidiStream;
|
||||
guide::Response m_bidiResponse;
|
||||
// ! [bstream-0]
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
// Use the -U, -S, -C, -B options to control execution
|
||||
QCommandLineParser parser;
|
||||
QCommandLineOption enableUnary("U", "Enable UnaryCalls");
|
||||
QCommandLineOption enableSStream("S", "Enable ServerStream");
|
||||
QCommandLineOption enableCStream("C", "Enable ClientStream");
|
||||
QCommandLineOption enableBStream("B", "Enable BiDiStream");
|
||||
|
||||
parser.addHelpOption();
|
||||
parser.addOption(enableUnary);
|
||||
parser.addOption(enableSStream);
|
||||
parser.addOption(enableCStream);
|
||||
parser.addOption(enableBStream);
|
||||
parser.process(app);
|
||||
|
||||
bool defaultRun = !parser.isSet(enableUnary) && !parser.isSet(enableSStream)
|
||||
&& !parser.isSet(enableCStream) && !parser.isSet(enableBStream);
|
||||
|
||||
qDebug("Welcome to the clientguide!");
|
||||
qDebug("Starting the server process ...");
|
||||
startServerProcess();
|
||||
|
||||
//! [basic-0]
|
||||
auto channel = std::make_shared<QGrpcHttp2Channel>(
|
||||
QUrl("http://localhost:50056")
|
||||
/* without channel options. */
|
||||
);
|
||||
ClientGuide clientGuide(channel);
|
||||
//! [basic-0]
|
||||
|
||||
if (defaultRun || parser.isSet(enableUnary)) {
|
||||
//! [unary-1]
|
||||
clientGuide.unaryCall(ClientGuide::createRequest(1));
|
||||
clientGuide.unaryCall(ClientGuide::createRequest(2, true)); // fail the RPC
|
||||
clientGuide.unaryCall(ClientGuide::createRequest(3));
|
||||
//! [unary-1]
|
||||
}
|
||||
|
||||
if (defaultRun || parser.isSet(enableSStream)) {
|
||||
//! [sstream-2]
|
||||
clientGuide.serverStreaming(ClientGuide::createRequest(3));
|
||||
// ! [sstream-2]
|
||||
}
|
||||
|
||||
if (defaultRun || parser.isSet(enableCStream)) {
|
||||
// ! [cstream-1]
|
||||
clientGuide.clientStreaming(ClientGuide::createRequest(0, true)); // fail the RPC
|
||||
// ! [cstream-1]
|
||||
}
|
||||
|
||||
if (defaultRun || parser.isSet(enableBStream)) {
|
||||
// ! [bstream-4]
|
||||
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));
|
||||
// ! [bstream-4]
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
void startServerProcess()
|
||||
{
|
||||
// For the purpose of this example, we launch the server directly from the
|
||||
// client. In a real-world scenario, the server should be running
|
||||
// independently, and this code would not be necessary. This approach is
|
||||
// used here solely for convenience in demonstrating the full interaction.
|
||||
static QProcess serverProcess;
|
||||
QObject::connect(&serverProcess, &QProcess::readyReadStandardOutput, [] {
|
||||
auto msgs = serverProcess.readAll().split('\n');
|
||||
msgs.removeIf([](const QByteArray &s) { return s.isEmpty(); });
|
||||
for (const auto &m : std::as_const(msgs)) {
|
||||
qDebug().noquote().nospace() << " " << m;
|
||||
}
|
||||
});
|
||||
serverProcess.setProcessChannelMode(QProcess::MergedChannels);
|
||||
serverProcess.setReadChannel(QProcess::StandardOutput);
|
||||
serverProcess.start("./clientguide_server");
|
||||
if (!serverProcess.waitForStarted()) {
|
||||
qFatal() << "Couldn't start the server: " << serverProcess.errorString();
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
// give the process some time to properly start up the server
|
||||
QThread::currentThread()->msleep(250);
|
||||
}
|
||||
|
||||
QDebug operator<<(QDebug debug, const guide::Response &response)
|
||||
{
|
||||
return debug << "Response( time: " << response.time() << ", num: " << response.num() << " )";
|
||||
}
|
||||
|
|
@ -0,0 +1,449 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
|
||||
|
||||
/*!
|
||||
\example clientguide
|
||||
\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<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
|
||||
\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<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
|
||||
);
|
||||
\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
|
||||
*/
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
//! [0]
|
||||
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);
|
||||
}
|
||||
//! [0]
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# Copyright (C) 2024 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)
|
||||
|
||||
find_package(protobuf)
|
||||
find_package(gRPC)
|
||||
|
||||
if(NOT TARGET gRPC::grpc_cpp_plugin OR NOT TARGET WrapProtoc::WrapProtoc
|
||||
OR NOT TARGET gRPC::grpc++)
|
||||
message(WARNING "Dependencies of ${PROJECT_NAME} not found. Skipping.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(MINGW)
|
||||
message(WARNING "${PROJECT_NAME} uses reference grpc++ library that doesn't officially support"
|
||||
" MinGW. Please use the MSVC compiler to build this example. Correct operation is not"
|
||||
" guaranteed otherwise.")
|
||||
endif()
|
||||
|
||||
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/clientguide.proto")
|
||||
set(proto_out "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
set(generated_files
|
||||
"${proto_out}/clientguide.pb.h" "${proto_out}/clientguide.pb.cc"
|
||||
"${proto_out}/clientguide.grpc.pb.h" "${proto_out}/clientguide.grpc.pb.cc")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${generated_files}
|
||||
COMMAND $<TARGET_FILE:WrapProtoc::WrapProtoc>
|
||||
ARGS
|
||||
--grpc_out "${proto_out}"
|
||||
--cpp_out "${proto_out}"
|
||||
-I "${CMAKE_CURRENT_LIST_DIR}/../proto"
|
||||
--plugin=protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>
|
||||
"${proto_files}"
|
||||
WORKING_DIRECTORY ${proto_out}
|
||||
DEPENDS "${proto_files}"
|
||||
COMMENT "Generating gRPC ${target} sources..."
|
||||
COMMAND_EXPAND_LISTS
|
||||
VERBATIM
|
||||
)
|
||||
set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE)
|
||||
|
||||
add_executable(clientguide_server
|
||||
main.cpp
|
||||
${generated_files})
|
||||
|
||||
target_include_directories(clientguide_server PRIVATE ${proto_out})
|
||||
|
||||
target_link_libraries(clientguide_server
|
||||
PRIVATE
|
||||
protobuf::libprotobuf
|
||||
gRPC::grpc++
|
||||
)
|
||||
|
||||
install(TARGETS clientguide_server
|
||||
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||
)
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "clientguide.grpc.pb.h"
|
||||
|
||||
#include <grpcpp/security/server_credentials.h>
|
||||
#include <grpcpp/server.h>
|
||||
#include <grpcpp/server_builder.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
using client::guide::Request;
|
||||
using client::guide::Response;
|
||||
|
||||
static constexpr std::string_view ServerUri = "localhost:50056";
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t now()
|
||||
{
|
||||
return std::chrono::system_clock::now().time_since_epoch().count();
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &stream, const Request &request)
|
||||
{
|
||||
return stream << "Request( time: " << request.time() << ", num: " << request.num() << " )";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ClientGuideService : public client::guide::ClientGuideService::Service
|
||||
{
|
||||
grpc::Status UnaryCall(grpc::ServerContext * /* context */, const Request *request,
|
||||
Response *response) override
|
||||
{
|
||||
std::cout << "Server (UnaryCall): " << *request << std::endl;
|
||||
//! [time]
|
||||
const auto time = now();
|
||||
if (request->time() > time)
|
||||
return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };
|
||||
//! [time]
|
||||
|
||||
//! [response]
|
||||
response->set_num(request->num());
|
||||
response->set_time(time);
|
||||
return grpc::Status::OK;
|
||||
//! [response]
|
||||
}
|
||||
|
||||
grpc::Status ServerStreaming(grpc::ServerContext * /* context */, const Request *request,
|
||||
grpc::ServerWriter<Response> *writer) override
|
||||
{
|
||||
std::cout << "Server (ServerStreaming): " << *request << std::endl;
|
||||
if (request->time() > now())
|
||||
return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };
|
||||
|
||||
Response response;
|
||||
for (int32_t i = 0; i < request->num(); ++i) {
|
||||
response.set_num(i);
|
||||
response.set_time(now());
|
||||
writer->Write(response);
|
||||
}
|
||||
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status ClientStreaming(grpc::ServerContext * /* context */,
|
||||
grpc::ServerReader<Request> *reader, Response *response) override
|
||||
{
|
||||
Request request;
|
||||
int32_t count = 0;
|
||||
while (reader->Read(&request)) {
|
||||
std::cout << "Server (ClientStreaming): " << request << std::endl;
|
||||
if (request.time() > now())
|
||||
return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };
|
||||
++count;
|
||||
}
|
||||
response->set_num(count);
|
||||
response->set_time(now());
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
|
||||
grpc::Status
|
||||
BidirectionalStreaming(grpc::ServerContext * /* context */,
|
||||
grpc::ServerReaderWriter<Response, Request> *stream) override
|
||||
{
|
||||
Request request;
|
||||
Response response;
|
||||
|
||||
while (stream->Read(&request)) {
|
||||
std::cout << "Server (BidirectionalStreaming): " << request << std::endl;
|
||||
const auto time = now();
|
||||
if (request.time() > time)
|
||||
return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };
|
||||
response.set_num(request.num());
|
||||
response.set_time(time);
|
||||
if (!stream->Write(response))
|
||||
return grpc::Status::CANCELLED;
|
||||
}
|
||||
|
||||
return grpc::Status::OK;
|
||||
}
|
||||
};
|
||||
|
||||
int main(int /* argc */, char * /* argv */[])
|
||||
{
|
||||
std::unique_ptr<grpc::Server> server;
|
||||
ClientGuideService service;
|
||||
{
|
||||
grpc::ServerBuilder builder;
|
||||
builder.AddListeningPort(ServerUri.data(), grpc::InsecureServerCredentials());
|
||||
builder.RegisterService(&service);
|
||||
server = builder.BuildAndStart();
|
||||
}
|
||||
std::cout << "Server listening on: " << ServerUri.data() << std::endl;
|
||||
server->Wait();
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
// Copyright (C) 2024 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
|
||||
|
||||
/*!
|
||||
\page qtgrpc-service-client-methods.html
|
||||
\title Client Methods of the Qt GRPC Service
|
||||
|
||||
gRPC lets you define four kinds of service methods:
|
||||
\list
|
||||
\li Unary call, where the client sends a single request to
|
||||
the server and gets a single response back:
|
||||
\badcode
|
||||
rpc PingPong (Ping) returns (Pong);
|
||||
\endcode
|
||||
\li Server stream, where the client sends a single request to
|
||||
the server and gets one or more responses back:
|
||||
\badcode
|
||||
rpc PingSeveralPong (Ping) returns (stream Pong);
|
||||
\endcode
|
||||
\li Client stream, where the client sends one or more
|
||||
requests to the server and gets a single response back:
|
||||
\badcode
|
||||
rpc SeveralPingPong (stream Ping) returns (Pong);
|
||||
\endcode
|
||||
\li Bidirectional stream, where the client sends one or more
|
||||
requests to the server and gets one or more responses back:
|
||||
\badcode
|
||||
rpc SeveralPingSeveralPong (stream Ping) returns (stream Pong);
|
||||
\endcode
|
||||
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.
|
||||
\endlist
|
||||
|
||||
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
|
||||
\l{QtGrpc::StatusCode}{status code}.
|
||||
|
||||
To use the Qt GRPC C++ API, start with defining the \c pingpong.proto schema for
|
||||
your project:
|
||||
\badcode
|
||||
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);
|
||||
}
|
||||
\endcode
|
||||
|
||||
Generate the C++ client code using the above schema and the
|
||||
\l{CMake Commands in Qt6 GRPC}{Qt GRPC CMake API}:
|
||||
\badcode
|
||||
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)
|
||||
\endcode
|
||||
|
||||
Both the generated protobuf messages and the client gRPC code will be added to
|
||||
the \c pingpong CMake target.
|
||||
|
||||
\section1 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 \l {The qtgrpcgen Tool} {qtgrpcgen tool} generates
|
||||
two alternative asynchronous methods:
|
||||
\code
|
||||
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
|
||||
\endcode
|
||||
|
||||
\section2 Call reply handling using QGrpcCallReply
|
||||
The first variant returns the \l QGrpcCallReply gRPC operation.
|
||||
\l QGrpcCallReply reads the message received from the server and gets the
|
||||
notifications about errors or the end of call.
|
||||
|
||||
After creating \c{PingPongService::Client} and attaching \l QGrpcHttp2Channel to
|
||||
it, call the \c PingPong method:
|
||||
\code
|
||||
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();
|
||||
});
|
||||
\endcode
|
||||
|
||||
After the server responds to the request, the \l{QGrpcCallReply::finished}
|
||||
signal is emitted. The \c reply object contains the raw response data received
|
||||
from the server and can be deserialized to the \c ping::pong::Pong protobuf
|
||||
message using the \l{QGrpcCallReply::read} method.
|
||||
|
||||
If the server does not respond or the request caused an error in the server, the
|
||||
\l {QGrpcCallReply::finished} signal is emitted with a \l {QtGrpc::StatusCode}
|
||||
{status code} other than \c Ok.
|
||||
|
||||
\section2 Call reply handling using callbacks
|
||||
The overloaded function is similar to the one that returns the
|
||||
\l QGrpcCallReply, but instead of returning the reply, the function passes it as
|
||||
an argument to the callback function that is used in the call:
|
||||
\code
|
||||
...
|
||||
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;
|
||||
});
|
||||
\endcode
|
||||
This variant makes a connection to the \l{QGrpcCallReply::finished} signal
|
||||
implicitly, but you cannot cancel the call using the \l{QGrpcOperation::cancel}
|
||||
function.
|
||||
|
||||
\section1 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 \l {The qtgrpcgen Tool} {qtgrpcgen tool} generates
|
||||
the method that returns the pointer to \l QGrpcServerStream:
|
||||
|
||||
\code
|
||||
std::shared_ptr<QGrpcServerStream> pingSeveralPong(const ping::pong::Ping &arg,
|
||||
const QGrpcCallOptions &options = {});
|
||||
\endcode
|
||||
|
||||
\l QGrpcServerStream is similar to \l QGrpcCallReply, but it emits the
|
||||
\l QGrpcServerStream::messageReceived when the server response is received.
|
||||
|
||||
\code
|
||||
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";
|
||||
});
|
||||
\endcode
|
||||
|
||||
\note \l QGrpcServerStream overrides the internal buffer when receiving a new
|
||||
message from the server. After the server
|
||||
\l{QGrpcServerStream::finished}{finished} the communication, you can read only
|
||||
the last message received from the server.
|
||||
|
||||
\section1 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 \l {The qtgrpcgen Tool} {qtgrpcgen tool} generates
|
||||
the method that returns the pointer to \l{QGrpcClientStream}:
|
||||
|
||||
\code
|
||||
std::shared_ptr<QGrpcClientStream> severalPingPong(const ping::pong::Ping &arg,
|
||||
const QGrpcCallOptions &options = {});
|
||||
\endcode
|
||||
|
||||
To send multiple requests to the server, use the
|
||||
\l {QGrpcClientStream::writeMessage} method:
|
||||
\code
|
||||
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();
|
||||
\endcode
|
||||
|
||||
After the server receives enough \c Ping requests from the client, it responds
|
||||
with \c Pong, which contains the slowest \c Ping time.
|
||||
|
||||
\section1 Using the bidirectional streams in Qt GRPC
|
||||
|
||||
Bidirectional streams combine the functionality of server and client streams.
|
||||
The generated method returns the pointer to \l QGrpcBidiStream, which provides
|
||||
the API from both server and client streams:
|
||||
\code
|
||||
std::shared_ptr<QGrpcBidiStream> severalPingSeveralPong(const ping::pong::Ping &arg,
|
||||
const QGrpcCallOptions &options = {});
|
||||
\endcode
|
||||
|
||||
Use the bidirectional streams to organize the two-sided communication without
|
||||
breaking the connection session:
|
||||
\code
|
||||
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);
|
||||
\endcode
|
||||
|
||||
Every time the client sends the \c Ping requests, the server responds with the
|
||||
\c Pong message. The maximum Ping-Pong time is evaluated until the server ends
|
||||
the communication by sending a status code to the client.
|
||||
|
||||
\note \l QGrpcBidiStream overrides the internal buffer when receiving a new
|
||||
message from the server. After server \l{QGrpcBidiStream::finished}{finished}
|
||||
the communication, you can read only the last message received from the server.
|
||||
*/
|
||||
|
|
@ -79,8 +79,8 @@
|
|||
|
||||
\section1 Articles and Guides
|
||||
\list
|
||||
\li \l{clientguide} {Qt GRPC Client Guide}
|
||||
\li \l{CMake Commands in Qt6 GRPC}
|
||||
\li \l{Client Methods of the Qt GRPC Service}
|
||||
\endlist
|
||||
|
||||
\section1 References
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
in the Qt GRPC module.
|
||||
|
||||
\list
|
||||
\li \l {Client Methods of the Qt GRPC Service}
|
||||
\li \l {clientguide} {Qt GRPC Client Guide}
|
||||
\li \l {The qtgrpcgen Tool}
|
||||
\li \l {CMake Commands in Qt6 GRPC}{CMake Commands}
|
||||
\list
|
||||
|
|
|
|||
Loading…
Reference in New Issue