Qt GRPC 客户端指南。
服务方法
在 gRPC™中,服务方法可以在 protobuf 模式中定义,以指定客户端与服务器之间的通信。protoc
gRPC 支持四种服务方法:
- 一元调用- 客户端发送单个请求并接收单个响应。
rpc UnaryCall (Request) returns (Response);
相应的客户端处理程序是QGrpcCallReply 。
- 服务器流- 客户端发送单个请求并接收多个响应。
rpc ServerStreaming (Request) returns (stream Response);
相应的客户端处理程序是QGrpcServerStream 。
- 客户端流- 客户端发送多个请求并接收单个响应。
rpc ClientStreaming (stream Request) returns (Response);
相应的客户端处理程序是QGrpcClientStream 。
- 双向流- 客户端和服务器交换多条信息。
rpc BidirectionalStreaming (stream Request) returns (stream Response);
相应的客户端处理程序是QGrpcBidiStream 。
gRPC 通信总是从客户端开始,客户端通过向服务器发送第一条信息来启动远程过程调用(RPC)。然后,服务器通过返回 结束任何类型的通信。StatusCode
所有客户端 RPC 处理程序都源自提供共享功能的QGrpcOperation 类。由于 RPC 的异步性质,它们自然是通过 Qt 的信号与插槽(Signals & Slots)机制来管理的。
所有 RPC 处理程序共有的一个关键信号是finished ,它表示 RPC 的完成。处理程序在其生命周期内只发出一次该信号。该信号提供相应的QGrpcStatus ,提供有关 RPC 成功或失败的附加信息。
还有一些特定于操作的功能,如messageReceived 用于接收信息,writeMessage 用于向服务器发送信息,writesDone 用于关闭客户端通信。下表概述了 RPC 客户端处理程序支持的功能:
功能 | QGrpcCallReply | QGrpcServerStream | QGrpcClientStream | QGrpcBidiStream |
---|---|---|---|---|
finished | ✓ (read 最终响应) | ✓ | ✓ (read 最终响应) | ✓ |
messageReceived | ✗ | ✓ | ✗ | ✓ |
writeMessage | ✗ | ✗ | ✓ | ✓ |
writesDone | ✗ | ✗ | ✓ | ✓ |
开始
要使用Qt GRPC C++ API,首先要使用已有的 protobuf 模式或定义自己的模式。我们将以clientguide.proto
文件为例:
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); }
要将这个.proto文件用于我们的 C++Qt GRPC 客户端,我们必须运行带有 Qt XML 生成器插件的protoc
编译器。幸运的是,Qt 提供了qt_add_grpc和qt_add_protobufCMake 函数来简化这一过程。
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)
这将在当前编译目录下生成两个头文件:
- clientguide.qpb.h:由qtprotobufgen 生成。声明模式中的
Request
和Response
protobuf 消息。 - clientguide_client.grpc.qpb.h:由 qtprotobufgen 生成:由qtgrpcgen 生成。声明用于调用实现模式中
ClientGuideService
的gRPC 服务器方法的客户端接口。
生成的客户端接口如下:
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
注: 用户负责管理Client
接口返回的唯一 RPC 处理程序,确保其至少在finished 信号发出前一直存在。收到该信号后,处理程序可以安全地重新分配或销毁。
服务器设置
ClientGuideService
服务器的实现方法简单明了。它验证请求消息的time
字段,如果时间在未来,则返回INVALID_ARGUMENT
状态代码:
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); return grpc::Status::OK;
对于有效的time
请求,服务方法的行为如下:
UnaryCall
:使用请求中的num
字段进行响应。ServerStreaming
:发送与请求信息匹配的num
响应。ClientStreaming
:计算请求信息的数量,并将此数量设为num
。BidirectionalStreaming
:立即回应每条接收到的请求信息中的num
字段。
客户端设置
首先,我们将生成头文件:
#include "clientguide.qpb.h" #include "clientguide_client.grpc.qpb.h"
在本示例中,我们创建了ClientGuide
类来管理所有通信,这样就更容易理解了。首先,我们要设置所有gRPC 通信的主干:通道。
auto channel = std::make_shared<QGrpcHttp2Channel>( QUrl("http://localhost:50056") /* without channel options. */ ); ClientGuide clientGuide(channel);
Qt GRPC 库提供了QGrpcHttp2Channel ,您可以将其attach 到生成的客户端接口:
explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel) { m_client.attachChannel(std::move(channel)); }
通过这种设置,客户端将使用 TCP 作为传输协议,通过 HTTP/2 进行通信。通信将是未加密的(即未设置 SSL/TLS)。
创建请求信息
下面是一个创建请求信息的简单封装器:
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; }
该函数接收一个整数和一个可选的布尔值。默认情况下,它的信息使用当前时间,因此服务器逻辑应该接受它们。但是,当fail
设置为true
时,调用该函数产生的信息将被服务器拒绝。
单次 RPC
RPC 客户端处理程序有不同的工作模式。具体来说,可以选择基于类的设计,即 RPC 处理程序是外层类的成员,也可以通过finished 信号管理 RPC 处理程序的生命周期。
在应用单次范例时,有两件重要的事情需要记住。下面的代码演示了它如何用于一元调用,但它同样适用于任何其他 RPC 类型。
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:由于我们在 lambda 中管理唯一 RPC 对象的生命周期,因此将其移动到 lambda 的捕获中会使
get()
和其他成员函数失效。因此,我们必须先复制指针地址,然后再移动它。 - 2:finished 信号只发出一次,因此这是一个真正的单次连接。将此连接标记为SingleShotConnection 非常重要!否则,
reply
的捕获将不会被销毁,从而导致难以发现的隐藏内存泄漏。
connect
调用中的SingleShotConnection 参数可确保槽函数器(lambda)在发射后被销毁,从而释放与槽相关的资源,包括其捕获。
远程过程调用
一元调用
一元调用只需要处理finished 信号。当该信号发出时,我们可以检查 RPC 的status ,以确定是否成功。如果成功,我们就可以read 服务器的单一和最终响应。
在本例中,我们使用的是单次范例。请务必仔细阅读单次 RPC部分。
voidunaryCall(constguide::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)](constQGrpcStatus&status) {if(status.isOk()) {if(const autoresponse= reply->read<guide::Response>()) qDebug() << "Client (UnaryCall) finished, received:" << *response; 不然 qDebug("Client (UnaryCall) deserialization failed"); 否则{ qDebug() << "Client (UnaryCall) failed:" << status; }}, Qt::SingleShotConnection); }
该函数通过调用生成的客户端接口m_client
的UnaryCall
成员函数来启动 RPC。生命周期完全由finished 信号管理。
运行代码
在main
中,我们只需调用该函数三次,让第二次调用失败即可:
clientGuide.unaryCall(ClientGuide::createRequest(1)); clientGuide.unaryCall(ClientGuide::createRequest(2, true)); // fail the RPC clientGuide.unaryCall(ClientGuide::createRequest(3));
运行该函数的可能输出结果如下:
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 )
我们可以看到服务器接收到三条信息,其中第二条包含一个很大的时间值。在客户端,第一次和最后一次调用返回了Ok 状态代码,但第二次调用失败,状态代码为InvalidArgument ,原因是消息时间在未来。
服务器流
在服务器流中,客户端会发送一个初始请求,服务器会用一条或多条信息做出响应。除了finished 信号外,您还必须处理messageReceived 信号。
在本例中,我们使用单次范例来管理流 RPC 生命周期。请务必仔细阅读单次 RPC部分。
与任何 RPC 一样,我们首先连接finished 信号:
voidserverStreaming(constguide::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)](constQGrpcStatus&status) {if(status.isOk()) qDebug("Client (ServerStreaming) finished"); 不然 qDebug() << "Client (ServerStreaming) failed:" << status; }, Qt::SingleShotConnection);
为了处理服务器消息,我们连接到messageReceived 信号,并在信号发出时read 响应。
QObject::connect(streamPtr, &QGrpcServerStream::messageReceived,streamPtr, [streamPtr]{if(const autoresponse= streamPtr->read<guide::Response>()) qDebug() << "Client (ServerStream) received:" << *response; 不然 qDebug("Client (ServerStream) deserialization failed"); }); }
运行代码
服务器逻辑会将初始请求中收到的金额回流到客户端。我们创建这样一个请求并调用函数。
clientGuide.serverStreaming(ClientGuide::createRequest(3));
运行服务器流的可能输出结果如下:
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
服务器启动后,会收到一个num值为 3 的请求,并在完成通信前回复三个Response
消息。
客户端流
在客户端流中,客户端发送一个或多个请求,而服务器则以一个最终响应进行响应。必须处理finished 信号,并使用writeMessage 函数发送信息。然后,writesDone 函数可用于指示客户端已完成写入,不再发送信息。
我们使用基于类的方法与流 RPC 进行交互,将处理程序作为类的一个成员。与任何 RPC 一样,我们连接到finished 信号:
voidclientStreaming(constguide::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连接(m_clientStream.get(), &QGrpcClientStream::finished,m_clientStream.get(),[this](constQGrpcStatus&status) {if(status.isOk()) {if(const autoresponse= 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)); } }); }
该函数以一条初始信息启动客户端流。然后,它继续写入另外两条消息,最后通过调用writesDone 来表示通信结束。如果流 RPC 成功,我们将read 服务器的最终响应,并reset
RPC 对象。如果 RPC 失败,我们会调用相同的函数重试,该函数会覆盖m_clientStream
成员并重新连接finished 信号。我们不能简单地在 lambda 中重新分配m_clientStream
成员,因为这样会失去必要的连接。
运行代码
在main
中,我们调用带有失败消息的clientStreaming
函数,触发 RPC 失败并执行重试逻辑。
clientGuide.clientStreaming(ClientGuide::createRequest(0, true)); // fail the RPC
运行客户端流的可能结果如下:
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 )
服务器收到一条初始信息,导致 RPC 失败,触发重试逻辑。重试会以一条有效信息启动 RPC,然后向服务器发送三条信息,最后优雅地完成重试。
双向流
双向流提供了最大的灵活性,允许客户端和服务器同时发送和接收信息。它需要处理finished 和messageReceived 信号,并通过writeMessage 提供写入功能。
我们使用基于类的方法和成员函数槽连接来演示该功能,并将处理程序作为类的一个成员。此外,我们还使用了基于指针的read 函数。使用的两个成员是
std::unique_ptr<QGrpcBidiStream> m_bidiStream; guide::Response m_bidiResponse;
我们创建了一个从初始信息开始双向流的函数,并将槽函数连接到各自的finished 和messageReceived 信号。
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); }
插槽功能简单明了。finished 插槽只需打印和重置 RPC 对象:
voidbidiFinished(constQGrpcStatus&status) {if(status.isOk() qDebug("Client (BidirectionalStreaming) finished"); 不然 qDebug() << "Client (BidirectionalStreaming) failed:" << status; m_bidiStream.reset(); }
messageReceived 槽reads 进入m_bidiResponse
成员,继续写入信息,直到收到的响应数为零。此时,我们使用writesDone 半关闭客户端通信。
voidbidiMessageReceived() {if(m_bidiStream->read(&m_bidiResponse)) { qDebug() << "Client (BidirectionalStreaming) received:" << m_bidiResponse; 如果(m_bidiResponse.num()> 0) { m_bidiStream->writeMessage(createRequest(m_bidiResponse.num()- 1));return; } }else{ qDebug("Client (BidirectionalStreaming) deserialization failed"); } m_bidiStream->writesDone(); }
运行代码
服务器逻辑只需在读取内容后立即返回一条信息,并用请求中的数字创建一个响应。在main
中,我们创建了这样一个请求,它最终用作计数器。
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));
运行双向流的可能输出结果如下:
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
© 2025 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.