Qt GRPC クライアントガイド
Qt GRPC のクライアントガイド。
サービス提供方法
gRPC™では、クライアントとサーバー間の通信を指定するために、protobufスキーマ内でサービスメソッドを定義できます。これにより、protobufコンパイラであるprotoc は、これらの定義に基づいて必要なサーバーおよびクライアントインターフェースを生成できます。gRPC は、4種類のサービスメソッドをサポートしています:
- 単項呼び出し— クライアントが単一のリクエストを送信し、単一のレスポンスを受け取ります。
rpc UnaryCall (Request) returns (Response);対応するクライアントハンドラはQGrpcCallReply です。
- サーバーストリーミング— クライアントは 1 つのリクエストを送信し、複数のレスポンスを受け取ります。
rpc ServerStreaming (Request) returns (stream Response);対応するクライアントハンドラはQGrpcServerStream です。
- クライアント・ストリーミング— クライアントは複数のリクエストを送信し、1つのレスポンスを受け取ります。
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の完了を示します。ハンドラは、その存続期間中にこのシグナルを正確に1回のみ発信します。このシグナルは対応する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 クライアントで使用するには、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)これにより、現在のビルドディレクトリに2つのヘッダーファイルが生成されます:
- 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ハンドラのライフサイクルを管理することも可能です。
シングルショット・パラダイムを適用する際に覚えておくべき重要な点が2つあります。以下のコードは単項呼び出しの場合の動作を示していますが、他のどの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: 一意の RPC オブジェクトのライフタイムをラムダ式内で管理するため、これをラムダのキャプチャ領域に移動させると、
get()やその他のメンバ関数が無効になってしまいます。したがって、移動する前にポインタのアドレスをコピーしておく必要があります。 - 2:`finished ` シグナルは 1 回しか発火しないため、これは真のシングルショット接続となります。この接続を `SingleShotConnection` とマークすることが重要です!そうしないと、
replyのキャプチャが破棄されず、発見が困難な隠れたメモリリークが発生してしまいます。
connect の呼び出しにおけるSingleShotConnection 引数は、スロットファンクタ(ラムダ)がエミットされた後に破棄されることを保証し、キャプチャを含むスロットに関連付けられたリソースを解放します。
リモートプロシージャコール
単項呼び出し
単項呼び出しでは、「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 では、この関数を単純に 3 回呼び出し、2 回目の呼び出しを失敗させます:
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 )サーバーが3つのメッセージを受信していることがわかります。2番目のメッセージには、大きな時間が含まれています。クライアント側では、最初と最後の呼び出しはOk ステータスコードを返しましたが、2番目のメッセージは、メッセージの時間が未来の日時であったため、InvalidArgument ステータスコードで失敗しました。
サーバーストリーミング
サーバーストリームでは、クライアントが初期リクエストを送信すると、サーバーは1つ以上のメッセージで応答します。「finished 」シグナルに加えて、「messageReceived 」シグナルも処理する必要があります。
この例では、ストリーミング 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のリクエストを受信し、通信を完了する前に3つのResponse メッセージを返します。
クライアントストリーミング
クライアントストリームでは、クライアントが1つ以上のリクエストを送信し、サーバーは1つの最終レスポンスで応答します。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));
}
});
}この関数は、初期メッセージを送信してクライアントストリームを開始します。その後、さらに2つのメッセージを書き込んだ後、writesDone を呼び出して通信の終了を通知します。ストリーミングRPCが成功した場合、サーバーからの最終応答をread で処理し、RPCオブジェクトをreset します。 RPCが失敗した場合は、同じ関数を呼び出して再試行します。これにより、m_clientStream メンバーが上書きされ、finished シグナルが再接続されます。ラムダ式内で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が開始され、その後、正常に完了する前に3つのメッセージがサーバーに送信されます。
双方向ストリーミング
双方向ストリーミングは最も柔軟性が高く、クライアントとサーバーの両方が同時にメッセージの送受信を行うことができます。これには、finished およびmessageReceived シグナルの処理が必要であり、writeMessage を通じて書き込み機能を提供します。
この機能を実証するために、メンバー関数のスロット接続を用いたクラスベースのアプローチを採用し、ハンドラをクラスのメンバーとして組み込んでいます。さらに、ポインタベースのread 関数も利用しています。使用される2つのメンバーは以下の通りです:
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 スロットは、readを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) finishedClientGuide インターセプター
インターセプトの仕組みや、インターセプターインターフェースがRPCのライフサイクルにどのように関与するかについての概要については、『Qt GRPC インターセプターの概要』を参照してください。
このクライアントガイドの例では、3つのインターセプタを1つのチェーンに組み合わせています:
- 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;
};Authインターセプタの実装
TokenProvider は、トークン管理をカプセル化しています。この例では、説明のみを目的として簡略化されたトークン文字列を生成しています。これは実際の認証トークンを表すものではありません。実際のアプリケーションでは、通常、認証トークンの発行と管理を担当する外部のIDシステムを通じて、トークンの取得と更新が行われます。
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 ステータスで終了すると、インターセプタはトークンを更新します。この例では、更新とは単に現在の時刻を反映した新しいトークンを再生成することを意味します。実際のシナリオでは、外部のID管理システムに新しいトークンの発行をリクエストすることになります。
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::続き
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 メッセージがやり取りされますが、サニタイズ用インターセプタがリクエスト値を制限しているため、5回のやり取り後にストリームが終了します。
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 から継続するのではなく、5 回のメッセージ交換後に終了します。最後に、クライアントがシャットダウンすると、メトリクス・インターセプタが各 RPC メソッドについて収集した統計情報を出力します。
© 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.