gRPC lets you define four kinds of service methods:
rpc PingPong (Ping) returns (Pong);
rpc PingSeveralPong (Ping) returns (stream Pong);
rpc SeveralPingPong (stream Ping) returns (Pong);
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.
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 QAbstractGrpcClient { 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
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
方法:
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()]() { auto response = replyPtr->read<ping::pong::Pong>(); qDebug() << "Ping-Pong time difference" << response.time() - requestTime; }); 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
方法。
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
QGrpcStatus::Ok
code, the
QGrpcCallReply::errorOccurred
signal is not emitted.
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) { 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, and to get information about error occurred you need to subscribe to the common QAbstractGrpcClient::errorOccurred 信号。
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> streamPingSeveralPong(const ping::pong::Ping &arg, const QGrpcCallOptions &options = {});
QGrpcServerStream 类似于 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]() { 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"; });
注意: 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.
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> streamSeveralPingPong(const ping::pong::Ping &arg, const QGrpcCallOptions &options = {});
To send multiple requests to the server, use the QGrpcClientStream::sendMessage 方法:
auto stream = cl.streamSeveralPingPong(request); QTimer timer; QObject::connect(&timer, &QTimer::timeout, stream.get(), [streamPtr = stream.get()](){ ping::pong::Ping request; request.setTime(QDateTime::currentMSecsSinceEpoch()); streamPtr->sendMessage(request); }); QObject::connect(stream.get(), &QGrpcServerStream::finished, stream.get(), [streamPtr = stream.get(), &timer]{ 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.
Bidirectional streams combine the functionality of server and client streams. The generated method returns the pointer to QGrpcBidirStream , which provides the API from both server and client streams:
std::shared_ptr<QGrpcBidirStream> streamSeveralPingSeveralPong(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.streamSeveralPingSeveralPong(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->sendMessage(request); }); QObject::connect(stream.get(), &QGrpcBidirStream::messageReceived, stream.get(), [streamPtr = stream.get(), &timer, &maxPingPongTime, &requestTime]{ auto response = streamPtr->read<ping::pong::Pong>(); maxPingPongTime = std::max(maxPingPongTime, response.time() - requestTime); }); QObject::connect(stream.get(), &QGrpcBidirStream::finished, stream.get(), [streamPtr = stream.get(), &timer, &maxPingPongTime]{ qDebug() << "Maximum Ping-Pong time: " << maxPingPongTime; timer.stop(); }); QObject::connect(stream.get(), &QGrpcBidirStream::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.
注意: QGrpcBidirStream 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.