Qt GRPC 고객 가이드
Qt GRPC 고객 가이드.
서비스 방법
이 gRPC™, 프로토버프 스키마에서 서비스 메서드를 정의하여 클라이언트와 서버 간의 통신 방식을 지정할 수 있습니다. 그러면 프로토버프 컴파일러인 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);
}C++에서 Qt GRPC 클라이언트에 이 .proto 파일을 사용하려면, Qt 생성기 플러그인이 포함된 protoc 컴파일러를 실행해야 합니다. 다행히도 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및Responseprotobuf 메시지를 선언합니다. - 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: 람다 내에서 고유한 RPC 객체의 수명을 관리하기 때문에, 이를 람다의 캡처 영역으로 이동시키면 `
get()` 및 기타 멤버 함수가 무효화됩니다. 따라서 이동하기 전에 포인터의 주소를 복사해야 합니다. - 2:` finished ` 신호는 단 한 번만 발생하므로, 이는 진정한 싱글샷 연결이 됩니다. 이 연결을 ` SingleShotConnection`로 표시하는 것이 중요합니다! 그렇지 않으면 `
reply`의 캡처가 소멸되지 않아, 발견하기 어려운 숨겨진 메모리 누수가 발생할 수 있습니다.
connect 호출 시 ` SingleShotConnection ` 인수는 슬롯 펑터(람다)가 방출된 후 파괴되도록 보장하여, 캡처를 포함한 슬롯과 관련된 리소스를 해제합니다.
원격 프로시저 호출(RPC)
단항 호출
단항 호출의 경우 ‘ finished ’ 신호만 처리하면 됩니다. 이 신호가 발생하면 RPC의 ‘ status ’를 확인하여 호출이 성공했는지 판단할 수 있습니다. 성공한 경우, 서버에서 반환된 유일하고 최종적인 응답을 ‘ read ’하여 처리할 수 있습니다.
이 예제에서는 싱글 샷 패러다임을 사용합니다. ‘싱글 샷 RPC’ 섹션을 주의 깊게 읽어보시기 바랍니다.
void unaryCall(const guide::Request &request, const QGrpcCallOptions&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)](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);
}이 함수는 생성된 클라이언트 인터페이스 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 )서버가 세 개의 메시지를 수신하는 것을 볼 수 있으며, 두 번째 메시지는 시간 값이 매우 큽니다. 클라이언트 측에서는 첫 번째와 마지막 호출이 Ok 상태 코드를 반환했지만, 두 번째 메시지는 메시지 시간이 미래로 설정되어 있어 InvalidArgument 상태 코드로 실패했습니다.
서버 스트리밍
서버 스트리밍에서는 클라이언트가 초기 요청을 보내면, 서버가 하나 이상의 메시지로 응답합니다. ` finished ` 신호 외에도 ` messageReceived ` 신호도 처리해야 합니다.
이 예제에서는 싱글샷 패러다임을 사용하여 스트리밍 RPC의 라이프사이클을 관리합니다. ‘싱글샷 RPC’ 섹션을 주의 깊게 읽어보시기 바랍니다.
다른 RPC와 마찬가지로, 먼저 finished 신호에 연결합니다:
void serverStreaming(const guide::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)](const QGrpcStatus&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 auto response = 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 신호에 연결합니다:
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();
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));
}
});
}이 함수는 초기 메시지와 함께 클라이언트 스트림을 시작합니다. 그런 다음 두 개의 추가 메시지를 계속 작성한 후, ` 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를 시작하며, 이후 서버로 세 개의 메시지가 전송된 후 정상적으로 완료됩니다.
양방향 스트리밍
양방향 스트리밍은 클라이언트와 서버가 동시에 메시지를 주고받을 수 있도록 하여 가장 높은 유연성을 제공합니다. 이를 위해서는 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 객체를 출력하고 재설정할 뿐입니다:
void bidiFinished(const QGrpcStatus&status)
{
if (status.isOk())
qDebug("Client (BidirectionalStreaming) finished");
else
qDebug() << "Client (BidirectionalStreaming) failed:" << status;
m_bidiStream.reset();
}messageReceived 슬롯 read은 m_bidiResponse 멤버에 메시지를 삽입하며, 수신된 응답 번호가 0이 될 때까지 메시지 쓰기를 계속합니다. 이때 writesDone 을 사용하여 클라이언트 측 통신을 부분적으로 종료합니다.
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();
}코드 실행
서버 로직은 데이터를 읽자마자 메시지를 반환하며, 요청에 포함된 숫자를 사용하여 응답을 생성합니다. ` 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 의 ‘인터셉터 개요’를 참조하십시오.
다음 클라이언트 가이드 예제에서는 세 개의 인터셉터가 하나의 체인으로 결합된 모습을 보여줍니다:
- 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 auto keys = 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());
return Continuation::Proceed;
}
void AuthInterceptor::onTrailingMetadata(QGrpcInterceptionContext& /*context*/,
QMultiHash<QByteArray, QByteArray> &metadata)
{
constexpr QByteArrayView AuthErrorKey("www-authenticate");
const auto it = metadata.constFind(AuthErrorKey);
if (it== metadata.cend())
return;
m_serverAuthHint = it.value();
}
void AuthInterceptor::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
auto totalMdBytes = getTotalBytes(callOptions.metadata(QtGrpc::MultiValue));
if (totalMdBytes > MaxMetadata) {
qDebug() << "[Sanitizing] Aborting call with metadata size of" << totalMdBytes << "bytes";
return Continuation::Drop;
}
onWriteMessage(context, message);
return Continuation::Proceed;
}
void SanitizingInterceptor::onWriteMessage(QGrpcInterceptionContext& /*context*/,
QProtobufMessage&message)
{
// 다양한 Request 메시지 유형이 포함된 더 복잡한 시나리오를 처리하기 위해서는,
// context.descriptor()에 따라 적절히 분기하는 것이 합리적입니다.
if (auto *request = qprotobufmessage_cast<client::guide::Request *>(&message))
sanitizeMessage(*request);
}
void SanitizingInterceptor::onMessageReceived(QGrpcInterceptionContext&context,
QByteArray&messageData)
{
// messageData는 client::guide::Response 타입일 수만 있습니다. 더
// 복잡한 처리가 필요한 경우 context.descriptor()를 사용하여 적절한 처리를 추가하십시오.
// 역직렬화 및 직렬화 작업은 오버헤드를 유발한다는 점에 유의하십시오.
client::guide::Response response;
auto serializer = 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() 확인), 생성 실패 시 적절한 오류 처리 전략을 적용하십시오.
auto interceptors = createInterceptors();
if (interceptors.isEmpty()) {
qWarning("Failed to create the interceptor chain");
return EXIT_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));
});코드 실행
인터셉터 지원을 활성화하려면 -I 옵션을 지정하여 clientguide_client 를 실행하십시오. 가능한 출력 결과는 다음과 같습니다:
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에도 적용된다는 것을 확인할 수 있습니다. 세정(sanitizing) 인터셉터가 요청 값을 제한하므로, 스트림은 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.