本页内容

Qt GRPC 客户指南

Qt GRPC 客户指南。

服务方法

在 gRPC™中,可以在 Protobuf 模式中定义服务方法,以指定客户端与服务器之间的通信。随后,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的信号与槽机制进行管理。

所有RPC处理程序共有的关键信号是finished ,它表示RPC已完成。处理程序在其生命周期内仅会发出该信号一次。该信号会传递相应的QGrpcStatus ,提供有关RPC成功或失败的额外信息。

此外还有针对特定操作的功能,例如用于接收传入消息的messageReceived 、用于向服务器发送消息的writeMessage ,以及用于关闭客户端通信的writesDone 。下表概述了RPC客户端处理程序支持的功能:

功能QGrpcCallReplyQGrpcServerStreamQGrpcClientStreamQGrpcBidiStream
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);
}

若要在 C++ 中的Qt GRPC 客户端中使用此.proto文件,我们必须在protoc 编译器上启用 Qt 生成器插件。幸运的是,Qt 提供了qt_add_grpc和qt_add_protobuf这两项 CMake 函数,可简化此过程。

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 interceptors.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:由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 所有通信的基础架构:一个通道。

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,
                                    ExpectedResult expected = ExpectedResult::Success)
{
    guide::Request request;
    request.setNum(num);
    // The server-side logic fails the RPC if the time is in the future.
    const auto time = expected == ExpectedResult::Failure ?
        std::numeric_limits<int64_t>::max() : now();
    request.setTime(time);
    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 ` 参数可确保槽 functor(即 lambda 表达式)在发出后被销毁,从而释放与该槽相关的资源,包括其捕获对象。

远程过程调用

一元调用

一元调用只需处理finished 信号。当该信号被发出时,我们可以检查RPC的status 来确定调用是否成功。如果成功,我们可以read 从服务器接收到的唯一且最终的响应。

在此示例中,我们采用单次调用模式。请务必仔细阅读“单次调用 RPC”一节。

voidunaryCall(constguide::Request&request, constQGrpcCallOptions&opts ={ })
{
    std::unique_ptr<QGrpcCallReply>reply=m_client.UnaryCall(request,opts);
    const auto *replyPtr =reply.get();
    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;
               else
                    qDebug("Client (UnaryCall) deserialization failed");
            }else{
                qDebug() << "Client (UnaryCall) failed:" << status;
            }
        },
        Qt::SingleShotConnection);
}

该函数通过调用生成的客户端接口m_client 的UnaryCall 成员函数来启动RPC。其生命周期完全由finished 信号管理。

运行代码

在 `main` 中,我们只需调用该函数三次,并让第二次调用失败:

clientGuide.unaryCall(ClientGuide::createRequest(1));
clientGuide.unaryCall(ClientGuide::createRequest(2, ExpectedResult::Failure));
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  )

我们可以看到服务器接收了三条消息,其中第二条消息的time字段包含一个较大的数值。在客户端,第一次和最后一次调用返回了Ok 状态码,但第二次消息因time字段的值位于未来而失败,返回了InvalidArgument 状态码。

服务器流式传输

在服务器流中,客户端发送初始请求,服务器则返回一条或多条消息。除了finished 信号外,您还必须处理messageReceived 信号。

在此示例中,我们使用单次调用(Single Shot)范式来管理流式 RPC 的生命周期。请务必仔细阅读“单次调用 RPC”一节。

与任何 RPC 一样,我们首先连接到finished 信号:

voidserverStreaming(constguide::Request&initialRequest)
{
    std::unique_ptr<QGrpcServerStream>stream=m_client.ServerStreaming(initialRequest);
    const auto *streamPtr =stream.get();

    connect(
        streamPtr, &QGrpcServerStream::finished,streamPtr,
        [stream=std::move(stream)](constQGrpcStatus&status) {
            if(status.isOk())
                qDebug("Client (ServerStreaming) finished");
           else
                qDebug() << "Client (ServerStreaming) failed:" << status;
        },
        Qt::SingleShotConnection);

为了处理服务器消息,我们订阅了messageReceived 信号,并在信号被触发时通过read 处理响应。

    connect(streamPtr, &QGrpcServerStream::messageReceived,streamPtr, [streamPtr]{
        if(const autoresponse= streamPtr->read<guide::Response>())
            qDebug() << "Client (ServerStream) received:" << *response;
       else
            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();

    connect(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, ExpectedResult::Failure));

运行客户端流式处理时的可能输出如下:

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");
   else
        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;
       if(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

ClientGuide 拦截器

有关拦截机制的介绍,以及拦截器接口如何参与 RPC 生命周期的说明,请参阅《Qt GRPC 拦截器概述》。

客户端指南中的示例演示了将三个拦截器组合到一条链中的情况:

  • 一个指标拦截器,用于记录 RPC 统计信息并注入跟踪 ID。
  • 一个身份验证拦截器,用于附加承载令牌。
  • 一个清理拦截器,用于验证和规范化消息。

它们共同展示了拦截器如何在调用生命周期的不同阶段观察、修改和验证 RPC 流量。

指标拦截器

指标拦截器位于拦截链的首位,因此能够观察整个RPC生命周期。它会记录基本统计数据,例如接收到的有效载荷大小、RPC持续时间,以及每个RPC方法的成功或失败次数。

class MetricsInterceptor final : public QGrpcStartInterceptor,
                                 public QGrpcMessageReceivedInterceptor,
                                 public QGrpcFinishedInterceptor
{
public:
    ~MetricsInterceptor() override;

    Continuation onStart(QGrpcInterceptionContext &context,
                         QProtobufMessage &message,
                         QGrpcCallOptions &callOptions) override;

    void onMessageReceived(QGrpcInterceptionContext &context,
                           QByteArray &messageData) override;

    void onFinished(QGrpcInterceptionContext &context,
                    QGrpcStatus &status) override;

private:
    using Clock = std::chrono::steady_clock;
    using Ms = std::chrono::duration<double, std::milli>;

    struct RpcMetrics {
        quint64 bytesReceived = 0;
        quint64 completed = 0;
        quint64 failed = 0;
        QList<double> durations;
    };

    QHash<quint64, Clock::time_point> m_activeRPCs;
    QMap<QtGrpc::RpcDescriptor, RpcMetrics> m_metrics;
};
指标拦截器的实现

当 RPC 开始时,拦截器会记录当前时间戳,并使用唯一的“operationId ”作为键将其存储起来。此外,还会向传出的元数据中添加一个跟踪标识符,以便在不同服务之间对调用进行关联分析。

QGrpcStartInterceptor::Continuation
MetricsInterceptor::onStart(QGrpcInterceptionContext &context,
                            QProtobufMessage & /*message*/,
                            QGrpcCallOptions &callOptions)
{
    m_activeRPCs.insert(context.operationId(), std::chrono::steady_clock::now());
    callOptions.addMetadata("x-trace-id"_L1, QUuid::createUuid().toByteArray());

    return Continuation::Proceed;
}

void MetricsInterceptor::onMessageReceived(QGrpcInterceptionContext &context,
                                           QByteArray &messageData)
{
    m_metrics[context.descriptor()].bytesReceived += messageData.size();
}

void MetricsInterceptor::onFinished(QGrpcInterceptionContext &context,
                                    QGrpcStatus &status)
{
    const auto it = m_activeRPCs.find(context.operationId());
    Q_ASSERT(it != m_activeRPCs.cend());

    const auto duration = Ms(Clock::now() - it.value()).count();
    m_activeRPCs.erase(it);

    auto &metrics = m_metrics[context.descriptor()];
    if (!status.isOk()) {
        ++metrics.failed;
        return;
    }
    ++metrics.completed;
    metrics.durations.append(duration);
}

在 RPC 运行期间,传入的消息会通过累加接收到的有效载荷字节数,为收集的指标提供数据。

当 RPC 结束时,拦截器会计算总持续时间,并按每个 RPC 描述符更新汇总统计数据。

为了演示目的,在拦截器实例被销毁时,会打印收集到的指标。

MetricsInterceptor::~MetricsInterceptor()
{
    Q_ASSERT(m_activeRPCs.isEmpty());
    const autokeys=m_metrics.keys();
    for(const auto &k: keys) {
        qInfo() << "Metrics for:" << k;
       const auto &m =m_metrics[k];
        qInfo() << "  Bytes received    :" << m.bytesReceived;
        qInfo() << "  Failed RPCs       :" << m.failed;
        qInfo() << "  Completed RPCs    :" << m.completed;
        qInfo() << "  Completed Durations (ms):" << m.durations;
    }
}
身份验证拦截器

身份验证拦截器会在发出的 RPC 调用中附加一个承载令牌。如果应用程序已经提供了authorization 头,则该拦截器不会对其进行任何更改。

如果服务器报告身份验证失败,该拦截器会刷新令牌,以便后续的 RPC 调用使用更新的凭据。

class TokenProvider
{
public:
    TokenProvider();
    QByteArray token();
    void refresh();

private:
    QByteArray m_token;
};

class AuthInterceptor final : public QGrpcStartInterceptor,
                              public QGrpcTrailingMetadataInterceptor,
                              public QGrpcFinishedInterceptor
{
public:
    Continuation onStart(QGrpcInterceptionContext &context,
                         QProtobufMessage &message,
                         QGrpcCallOptions &callOptions) override;

    void onTrailingMetadata(QGrpcInterceptionContext &context,
                            QMultiHash<QByteArray, QByteArray> &metadata) override;

    void onFinished(QGrpcInterceptionContext &context,
                    QGrpcStatus &status) override;

private:
    TokenProvider m_tokenProvider;
    QByteArray m_serverAuthHint;
};
身份验证拦截器实现

TokenProvider 封装了令牌管理功能。在此示例中,它仅为演示目的生成一个简化的令牌字符串。该字符串并不代表真实的认证令牌。在实际应用中,令牌通常会通过负责签发和管理认证令牌的外部身份系统来获取和刷新。

TokenProvider::TokenProvider() { refresh(); }
QByteArray TokenProvider::token() { return m_token; }
void TokenProvider::refresh()
{
    // NOTE: This is NOT a real JWT or secure token.
    // It only mimics the structure: header.payload.signature
    // The payload is just a timestamp used for expiry checks.
    auto header = R"({"alg":"none","typ":"JWT"})"_ba; // fake header
    auto payload = QByteArray::number(now()); // fake "iat"
    auto signature = "no_signature"_ba; // fake signing

    m_token = header + '.' + payload + '.' + signature;
}

在服务器端,传入的调用会验证authorization 的元数据。如果验证失败,服务器将返回Unauthenticated 状态码,并在尾部元数据中提供额外信息。

if (auto authStatus = validateAuth(context); !authStatus.ok())
    return authStatus;

当 RPC 开始时,拦截器会检查该调用是否已包含authorization 头。如果没有,则会将承载令牌附加到传出的元数据中。

将检查尾部元数据中是否包含服务器返回的身份验证提示。如果存在,则将该提示存储起来,以便在调用最终因身份验证错误而失败时进行报告。

当 RPC 以“Unauthenticated ”状态结束时,拦截器会刷新令牌。在此示例中,刷新操作仅需生成一个包含新当前时间的令牌。在实际场景中,这通常需要向外部身份系统请求新的令牌。

QGrpcStartInterceptor::Continuation
AuthInterceptor::onStart(QGrpcInterceptionContext& /*context*/,
                         QProtobufMessage& /*消息*/,
                         QGrpcCallOptions&callOptions)
{
    constexpr QByteArrayView AuthKey("authorization");

    if(!callOptions.metadata(QtGrpc::MultiValue).contains(AuthKey))
        callOptions.addMetadata(AuthKey, "Bearer " +m_tokenProvider.token());

    returnContinuation::Proceed;
}

voidAuthInterceptor::onTrailingMetadata(QGrpcInterceptionContext& /*context*/,
                                         QMultiHash<QByteArray,QByteArray> &metadata)
{
    constexpr QByteArrayView AuthErrorKey("www-authenticate");
    const autoit=metadata.constFind(AuthErrorKey);
   if(it==metadata.cend())
        return;
    m_serverAuthHint=it.value();
}

voidAuthInterceptor::onFinished(QGrpcInterceptionContext& /*context*/,QGrpcStatus&status)
{
    // 仅处理“未认证”状态码。
    if(status.code()!=QtGrpc::StatusCode::Unauthenticated)
        return;

    qDebug("[AuthInterceptor] Refreshing token for next call");
   // 注意:此处为示例而简化。实际令牌需通过正确的身份验证流程获取。
    m_tokenProvider.refresh();
    if(!m_serverAuthHint.isEmpty()) {
        status=QGrpcStatus{ status.code(),status.message()+u", 提示: "_s+m_serverAuthHint };
        m_serverAuthHint.clear();
    }
}
数据净化拦截器

净化拦截器用于验证和规范化请求与响应数据。它演示了拦截器如何在发送消息之前和接收响应之后强制执行基本的客户端约束。

在此示例中,拦截器将请求值限制在可接受的范围内,对时间戳进行标准化处理,并拒绝元数据超过预定义大小限制的调用。

class SanitizingInterceptor final : public QGrpcStartInterceptor,
                                    public QGrpcWriteMessageInterceptor,
                                    public QGrpcMessageReceivedInterceptor
{
public:
    Continuation onStart(QGrpcInterceptionContext &context,
                         QProtobufMessage &message,
                         QGrpcCallOptions &callOptions) override;

    void onWriteMessage(QGrpcInterceptionContext &context,
                        QProtobufMessage &message) override;

    void onMessageReceived(QGrpcInterceptionContext &context,
                           QByteArray &messageData) override;
};
数据净化拦截器的实现

使用辅助函数来计算元数据的总大小,并对请求和响应消息应用基本的标准化规则。

qsizetype getTotalBytes(const QMultiHash<QByteArray, QByteArray> &md)
{
    qsizetype total = 0;
    for (const auto &[k, v] : md.asKeyValueRange())
        total += k.size() + v.size();
    return total;
}

template <typename T>
void sanitizeMessage(T &request)
{
    const auto num = request.num();
    if (num < 0 || num > 5)
        request.setNum(qBound(0, num, 5));

    const auto time = request.time();
    const auto currentTime = now();
    if (time < 0 || time > currentTime)
        request.setTime(qMax(0, currentTime));
}

当 RPC 开始时,拦截器首先会验证出站元数据是否超过允许的大小。如果超过限制,则通过返回Continuation::Drop

如果请求消息与预期类型匹配,则在发送 RPC 之前对其字段进行清理。

对于流式调用,传出的消息还会在onWriteMessage 钩子中进行清理。

最后,会拦截传入的响应。在将序列化的有效载荷传递给应用程序之前,会对其进行反序列化、规范化处理,然后再次序列化。

QGrpcStartInterceptor::Continuation
SanitizingInterceptor::onStart(QGrpcInterceptionContext&context,QProtobufMessage&message,
                               QGrpcCallOptions&callOptions)
{
    constexpr quint16 MaxMetadata= 1024 * 8;// 8 KiB
    autototalMdBytes=getTotalBytes(callOptions.metadata(QtGrpc::MultiValue));
    if(totalMdBytes>MaxMetadata) {
        qDebug() << "[Sanitizing] Aborting call with metadata size of" << totalMdBytes << "bytes";
       returnContinuation::Drop;
    }

    onWriteMessage(context,message);

    returnContinuation::Proceed;
}

voidSanitizingInterceptor::onWriteMessage(QGrpcInterceptionContext& /*context*/,
                                           QProtobufMessage&message)
{
    // 为了处理涉及多种 Request 消息类型的更复杂场景,
    // 根据 context.descriptor() 的值进行适当分支处理是合理的。
   if(auto *request =qprotobufmessage_cast<client::guide::Request*>(&message))
        sanitizeMessage(*request);
}

voidSanitizingInterceptor::onMessageReceived(QGrpcInterceptionContext&context,
                                              QByteArray&messageData)
{
    // messageData 只能是 client::guide::Response 类型。若需
    // 更复杂的处理,请通过 context.descriptor() 添加相应逻辑。
    // 请注意,反序列化和序列化会引入额外开销。
   client::guide::Response response;
   autoserializer=context.channel().serializer();
    if(serializer->deserialize(&response,messageData))
        return;
    sanitizeMessage(response);
    messageData= serializer->serialize(&response);
}
拦截器的使用

在辅助函数中构建拦截器链,并将其返回给调用方。

inline QGrpcInterceptorChain createInterceptors()
{
    QGrpcInterceptorChain chain;
    chain.set(
        std::make_unique<MetricsInterceptor>(),
        std::make_unique<AuthInterceptor>(),
        std::make_unique<SanitizingInterceptor>()
    );
    return chain;
}

在将拦截器链附加到通道之前,请验证其是否已成功初始化(例如通过检查 `isEmpty()`),如果创建失败,请应用适当的错误处理策略。

autointerceptors=createInterceptors();
if(interceptors.isEmpty()) {
    qWarning("Failed to create the interceptor chain");
   returnEXIT_FAILURE;// 或其他合适的备用处理
}
channel=std::make_shared<QGrpcHttp2Channel>(
    QUrl("http://localhost:50056"),
    std::move(interceptors)
);

对于一元调用,该示例会安排一系列请求,并在请求之间设置短暂延迟。这使得更容易观察拦截器如何影响后续调用:追踪元数据会被添加,认证失败会刷新后续请求的令牌,而无效的元数据可能会导致调用在发送前被丢弃。

int delayMs = 150;
for (int i = 1; i < 6; ++i) {
    QTimer::singleShot(delayMs, [&, i] {
        auto expected = i % 2 == 0 ? ExpectedResult::Failure : ExpectedResult::Success;
        clientGuide.unaryCall(ClientGuide::createRequest(i, expected));
    });
    delayMs += 150;
}
QTimer::singleShot(delayMs, [&] {
    QGrpcCallOptions invalidOpts;
    invalidOpts.addMetadata("huge_key"_ba, "huge_value"_ba.repeated(8'000));
    clientGuide.unaryCall(guide::Request{ }, invalidOpts); // this call will fail.
});
delayMs += 150;

该示例还演示了双向流式RPC。bidirectionalStreaming() 的实现通常会交换666 消息,但净化拦截器会限制请求值,使得流在经过五次交换后终止。

QTimer::singleShot(delayMs, [&] {
    clientGuide.bidirectionalStreaming(ClientGuide::createRequest(666));
});
运行代码

运行clientguide_client 时,请使用-I 选项以启用拦截器支持。可能的输出如下所示:

Welcome to the clientguide!
Running with Interceptor support
Starting the server process ...
    Server listening on: localhost:50056
    Server (UnaryCall): Request( time: 1774360832277, num: 1 ), Metadata: { x-trace-id: {360bb041-9ab5-4940-95c1-0d4c89491570} }
Client (UnaryCall) finished, received: Response( time: 1774360832278, num: 1 )
    Server (UnaryCall): Request( time: 1774360832427, num: 2 ), Metadata: { x-trace-id: {01817a8f-9f0e-404e-a21c-767f81e74a2d} }
Client (UnaryCall) finished, received: Response( time: 1774360832429, num: 2 )
    Server (UnaryCall): Request( time: 1774360832577, num: 3 ), Metadata: { x-trace-id: {78cb336f-0396-4bbc-b1ee-59914651feef} }
Client (UnaryCall) finished, received: Response( time: 1774360832578, num: 3 )
[AuthInterceptor] Refreshing token for next call
Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::Unauthenticated, message: "Token expired with age: 603 ms, Hint: Bearer realm=\"clientguide\", error=\"invalid_token\"" )
    Server (UnaryCall): Request( time: 1774360832876, num: 5 ), Metadata: { x-trace-id: {63a40327-2f46-43c4-9e2c-44b67f297bd2} }
Client (UnaryCall) finished, received: Response( time: 1774360832877, num: 5 )
[Sanitizing] Aborting call with metadata size of 80129 bytes
Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::Aborted, message: "Interceptors dropped the call" )
    Server (BidirectionalStreaming) accepted call, Metadata: { x-trace-id: {fc945bdb-5c8a-4c65-8bfb-8655800d5774} }
    Server (BidirectionalStreaming): Request( time: 1774360833177, num: 5 )
Client (BidirectionalStreaming) received: Response( time: 1774360833178, num: 5 )
    Server (BidirectionalStreaming): Request( time: 1774360833178, num: 4 )
Client (BidirectionalStreaming) received: Response( time: 1774360833179, num: 4 )
    Server (BidirectionalStreaming): Request( time: 1774360833179, num: 3 )
Client (BidirectionalStreaming) received: Response( time: 1774360833179, num: 3 )
    Server (BidirectionalStreaming): Request( time: 1774360833179, num: 2 )
Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 2 )
    Server (BidirectionalStreaming): Request( time: 1774360833180, num: 1 )
Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 1 )
    Server (BidirectionalStreaming): Request( time: 1774360833180, num: 0 )
Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 0 )
Client (BidirectionalStreaming) finished
All operations completed! Automatically shutting down...
Metrics for: QtGrpc::RpcDescriptor( service: "client.guide.ClientGuideService", method: "BidirectionalStreaming", type: QtGrpc::RpcType::BidiStreaming )
Bytes received    : 52
Failed RPCs       : 0
Completed RPCs    : 1
Completed Durations (ms): QList(3.97233)
Metrics for: QtGrpc::RpcDescriptor( service: "client.guide.ClientGuideService", method: "UnaryCall", type: QtGrpc::RpcType::UnaryCall )
Bytes received    : 36
Failed RPCs       : 2
Completed RPCs    : 4
Completed Durations (ms): QList(1.937, 1.90971, 1.624, 1.50429)

输出展示了拦截器链的运行情况。 发出的单元调用在到达服务器时,已由指标拦截器添加了跟踪元数据。当服务器报告身份验证失败时,身份验证拦截器会为后续调用刷新令牌,并根据服务器提供的提示补充错误消息。随后的一次调用因其元数据超过了配置的限制,被净化拦截器在本地拒绝。 该双向流式调用表明,拦截器也适用于流式 RPC:净化拦截器对请求值进行了限制,因此流在交换五条消息后终止,而非从 `666` 继续。最后,当客户端关闭时,指标拦截器会打印每个 RPC 方法的收集统计数据。

示例项目 @ code.qt.io

© 2026 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.