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 클라이언트 핸들러에서 지원되는 기능이 간략하게 나와 있습니다:
기능 | QGrpcCallReply | QGrpcServerStream | QGrpcClientStream | QGrpcBidiStream |
---|---|---|---|---|
finished | ✓ (read 최종 응답) | ✓ | ✓ (read 최종 응답) | ✓ |
messageReceived | ✗ | ✓ | ✗ | ✓ |
writeMessage | ✗ | ✗ | ✓ | ✓ |
writesDone | ✗ | ✗ | ✓ | ✓ |
시작하기
Qt GRPC C++ API를 사용하려면 먼저 이미 사용 가능한 프로토뷰 스키마를 사용하거나 직접 정의하세요. 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); }
Qt GRPC 클라이언트에서 이 .proto 파일을 C++로 사용하려면 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.8) 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: 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: 람다 내에서 고유한 RPC 객체의 수명을 관리하므로 이를 람다의 캡처로 이동하면
get()
및 다른 멤버 함수가 무효화됩니다. 따라서 포인터 주소를 이동하기 전에 복사해야 합니다. - 2: finished 신호는 한 번만 전송되므로 진정한 의미의 단발성 연결입니다. 이 연결을 SingleShotConnection 로 표시하는 것이 중요합니다! 그렇지 않으면
reply
의 캡처가 파괴되지 않아 발견하기 어려운 숨겨진 메모리 누수가 발생할 수 있습니다.
connect
호출의 SingleShotConnection 인수는 슬롯 함수(람다)가 방출된 후 소멸되도록 하여 캡처를 포함하여 슬롯과 관련된 리소스를 확보합니다.
원격 프로시저 호출
단항 호출
단항 호출은 finished 신호만 처리하면 됩니다. 이 신호가 전송되면 RPC의 status 를 확인하여 성공 여부를 확인할 수 있습니다. 성공했다면 서버로부터 단일하고 최종적인 응답을 read 받을 수 있습니다.
이 예에서는 싱글샷 패러다임을 사용합니다. 싱글 샷 RPC 섹션을 주의 깊게 읽어보세요.
void unaryCall(const guide::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)](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, 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 신호에 먼저 연결합니다:
void serverStreaming(const guide::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)](const QGrpcStatus &status) { if (status.isOk()) qDebug("Client (ServerStreaming) finished"); else qDebug() << "Client (ServerStreaming) failed:" << status; }, Qt::SingleShotConnection);
서버 메시지를 처리하기 위해 messageReceived 신호에 연결하고 신호가 전송되면 read 응답을 받습니다.
QObject::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(); QObject::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, 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 객체를 인쇄하고 재설정합니다:
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) 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.