Qt GRPC Guía del cliente
La guía del cliente de Qt GRPC.
Métodos de servicio
En gRPC™se pueden definir métodos de servicio en un esquema protobuf para especificar la comunicación entre clientes y servidores. El compilador de protobuf, protoc, puede entonces generar las interfaces servidor y cliente necesarias basándose en estas definiciones. gRPC admite cuatro tipos de métodos de servicio:
- Llamadas unarias - El cliente envía una única petición y recibe una única respuesta.
rpc UnaryCall (Request) returns (Response);
El manejador de cliente correspondiente es QGrpcCallReply.
- Streaming de servidor - El cliente envía una única petición y recibe múltiples respuestas.
rpc ServerStreaming (Request) returns (stream Response);
El controlador de cliente correspondiente es QGrpcServerStream.
- Streaming de cliente: el cliente envía varias solicitudes y recibe una única respuesta.
rpc ClientStreaming (stream Request) returns (Response);
El controlador de cliente correspondiente es QGrpcClientStream.
- Streaming bidireccional - El cliente y el servidor intercambian múltiples mensajes.
rpc BidirectionalStreaming (stream Request) returns (stream Response);
El controlador de cliente correspondiente es QGrpcBidiStream.
gRPC La comunicación comienza siempre con el cliente, que inicia la llamada a procedimiento remoto (RPC) enviando el primer mensaje al servidor. A continuación, el servidor concluye cualquier tipo de comunicación devolviendo un StatusCode.
Todos los manejadores RPC del cliente derivan de la clase QGrpcOperation, que proporciona funcionalidad compartida. Debido a la naturaleza asíncrona de las RPC, se gestionan de forma natural a través del mecanismo Signals & Slots de Qt.
Una señal clave común a todos los gestores de RPC es finished, que indica la finalización de una RPC. El manejador emite esta señal exactamente una vez durante su vida. Esta señal entrega el correspondiente QGrpcStatus, proporcionando información adicional sobre el éxito o fracaso de la RPC.
También existen funcionalidades específicas de la operación, como messageReceived para mensajes entrantes, writeMessage para enviar mensajes al servidor y writesDone para cerrar la comunicación con el cliente. La siguiente tabla muestra las funcionalidades soportadas por los manejadores de cliente RPC:
| Funcionalidad | QGrpcCallReply | QGrpcServerStream | QGrpcClientStream | QGrpcBidiStream |
|---|---|---|---|---|
| finished | ✓ (read respuesta final) | ✓ | ✓ (read respuesta final) | ✓ |
| messageReceived | ✗ | ✓ | ✗ | ✓ |
| writeMessage | ✗ | ✗ | ✓ | ✓ |
| writesDone | ✗ | ✗ | ✓ | ✓ |
Primeros pasos
Para utilizar la API Qt GRPC C++, empieza por utilizar un esquema protobuf ya disponible o define el tuyo propio. Utilizaremos el archivo clientguide.proto como ejemplo:
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);
}Para utilizar este archivo .proto para nuestro cliente Qt GRPC en C++, debemos ejecutar el compilador protoc con los plugins generadores de Qt en él. Afortunadamente, Qt proporciona las funciones CMake qt_add_grpc y qt_add_protobuf para agilizar este proceso.
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/clientguide.proto")
find_package(Qt6 COMPONENTS Protobuf Grpc)
qt_standard_project_setup(REQUIRES 6.9)
qt_add_executable(clientguide_client main.cpp)
# Using the executable as input target will append the generated files to it.
qt_add_protobuf(clientguide_client
PROTO_FILES ${proto_files}
)
qt_add_grpc(clientguide_client CLIENT
PROTO_FILES ${proto_files}
)
target_link_libraries(clientguide_client PRIVATE Qt6::Protobuf Qt6::Grpc)Esto resulta en dos archivos de cabecera que se generan en el directorio de construcción actual:
- clientguide.qpb.h: Generado por qtprotobufgen. Declara los mensajes protobuf
RequestyResponsedel esquema. - clientguide_client.grpc.qpb.h: Generado por qtgrpcgen. Declara la interfaz cliente para llamar a los métodos de un servidor gRPC que implemente el
ClientGuideServicedel esquema.
Se genera la siguiente interfaz de cliente:
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
Nota: Los usuarios son responsables de gestionar los manejadores RPC únicos devueltos por la interfaz Client, asegurando su existencia al menos hasta que se emita la señal finished. Después de recibir esta señal, el manejador puede ser reasignado o destruido de forma segura.
Configuración del servidor
La implementación del servidor para ClientGuideService sigue un enfoque sencillo. Valida el campo time del mensaje de petición, devolviendo el código de estado INVALID_ARGUMENT si la hora es futura:
const auto time = now(); if (request->time() > time) return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };
Además, el servidor establece la hora actual en cada mensaje de respuesta:
response->set_num(request->num()); response->set_time(time); return grpc::Status::OK;
Para solicitudes válidas de time, los métodos de servicio se comportan de la siguiente manera:
UnaryCall: Responde con el camponumde la solicitud.ServerStreaming: Envía respuestasnumque coinciden con el mensaje de la solicitud.ClientStreaming: Cuenta el número de mensajes de petición y establece este recuento comonum.BidirectionalStreaming: Responde inmediatamente con el camponumde cada mensaje de solicitud entrante.
Configuración del cliente
Comenzamos incluyendo los ficheros de cabecera generados:
#include "clientguide.qpb.h" #include "clientguide_client.grpc.qpb.h"
Para este ejemplo, creamos la clase ClientGuide para manejar toda la comunicación, haciéndolo más fácil de seguir. Comenzamos configurando la columna vertebral de toda la comunicación gRPC: un canal.
auto channel = std::make_shared<QGrpcHttp2Channel>( QUrl("http://localhost:50056") /* without channel options. */ ); ClientGuide clientGuide(channel);
La biblioteca Qt GRPC ofrece QGrpcHttp2Channel, que puede attach a la interfaz de cliente generada:
explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel) { m_client.attachChannel(std::move(channel)); }
Con esta configuración, el cliente se comunicará a través de HTTP/2 utilizando TCP como protocolo de transporte. La comunicación será sin cifrar (es decir, sin configuración SSL/TLS).
Creación de un mensaje de solicitud
He aquí un sencillo wrapper para crear mensajes de petición:
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; }
Esta función toma un entero y un booleano opcional. Por defecto, sus mensajes utilizan la hora actual, por lo que la lógica del servidor debería aceptarlos. Sin embargo, cuando se llama con fail establecido en true, produce mensajes que el servidor rechazará.
RPCs de disparo único
Existen diferentes paradigmas para trabajar con manejadores de clientes RPC. Específicamente, puedes elegir un diseño basado en clases donde el manejador RPC es un miembro de la clase que lo encierra, o puedes manejar el tiempo de vida del manejador RPC a través de la señal finished.
Hay dos cosas importantes que debes recordar cuando apliques el paradigma de una sola vez. El código siguiente demuestra cómo funcionaría para llamadas unarias, pero es lo mismo para cualquier otro tipo de 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: Dado que gestionamos el tiempo de vida del objeto RPC unario dentro de la lambda, moverlo a la captura de la lambda invalidaría
get()y otras funciones miembro. Por lo tanto, debemos copiar la dirección del puntero antes de moverlo. - 2: La señal finished se emite sólo una vez, haciendo de ésta una verdadera conexión de un solo disparo. ¡Es importante marcar esta conexión como SingleShotConnection! Si no, la captura de
replyno será destruida, dando lugar a una fuga de memoria oculta difícil de descubrir.
El argumento SingleShotConnection en la llamada connect asegura que el functor de la ranura (la lambda) se destruya después de ser emitido, liberando los recursos asociados a la ranura, incluyendo sus capturas.
Llamadas a procedimientos remotos
Llamadas unarias
Las llamadas unarias sólo requieren que se gestione la señal finished. Cuando se emite esta señal, podemos comprobar la status de la RPC para determinar si tuvo éxito. Si lo fue, podemos read la respuesta única y final del servidor.
En este ejemplo, usamos el paradigma single-shot. Asegúrate de leer cuidadosamente la sección RPCs Single Shot.
void unaryCall(const guide::Request &request) { std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(request); const auto *replyPtr = reply.get(); QObject::connect( respuestaPtr, &QGrpcCallReply::finished, replyPtr,[ reply = std::move(reply)](const QGrpcStatus &status) { if (status.isOk()) { if(const auto response = reply->read<guía::Response>()) qDebug() << "Client (UnaryCall) finished, received:" << *response; si no qDebug("Client (UnaryCall) deserialization failed"); } else { qDebug() << "Client (UnaryCall) failed:" << status; }}, Qt::SingleShotConnection); }
La función inicia la RPC invocando a la función miembro UnaryCall de la interfaz de cliente generada m_client. El tiempo de vida es gestionado únicamente por la señal finished.
Ejecución del código
En main, simplemente invocamos esta función tres veces, dejando que la segunda invocación falle:
clientGuide.unaryCall(ClientGuide::createRequest(1)); clientGuide.unaryCall(ClientGuide::createRequest(2, true)); // fail the RPC clientGuide.unaryCall(ClientGuide::createRequest(3));
Una posible salida de ejecutar esto podría ser la siguiente:
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 )Vemos que el servidor recibe los tres mensajes, con el segundo conteniendo un valor grande para su tiempo. En el lado del cliente, la primera y la última llamada devolvieron un código de estado Ok, pero el segundo mensaje falló con el código de estado InvalidArgument debido a que la hora del mensaje estaba en el futuro.
Streaming del servidor
En un flujo de servidor, el cliente envía una petición inicial, y el servidor responde con uno o más mensajes. Además de la señal finished también hay que manejar la señal messageReceived.
En este ejemplo, usamos el paradigma single-shot para manejar el ciclo de vida del streaming RPC. Asegúrate de leer atentamente la sección RPCs Single Shot.
Como con cualquier RPC, nos conectamos primero a la señal 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"); si no qDebug() << "Client (ServerStreaming) failed:" << status; }, Qt::SingleShotConnection);
Para manejar los mensajes del servidor, nos conectamos a la señal messageReceived y read la respuesta cuando se emite la señal.
QObject::connect(streamPtr, &QGrpcServerStream::mensajeRecibido, streamPtr, [streamPtr] { if(const auto response = streamPtr->read<guía::Respuesta>()) qDebug() << "Client (ServerStream) received:" << *response; si no qDebug("Client (ServerStream) deserialization failed"); }); }
Ejecución del código
La lógica del servidor devuelve al cliente la cantidad recibida en la petición inicial. Creamos dicha petición e invocamos la función.
clientGuide.serverStreaming(ClientGuide::createRequest(3));
Un posible resultado de la ejecución del streaming del servidor podría ser el siguiente:
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) finishedUna vez que el servidor arranca, recibe una petición con un valor num de 3 y responde con tres mensajes Response antes de completar la comunicación.
Streaming cliente
En un streaming cliente, el cliente envía una o más peticiones, y el servidor responde con una única respuesta final. La señal finished debe ser manejada, y los mensajes pueden ser enviados usando la función writeMessage. La función writesDone puede utilizarse entonces para indicar que el cliente ha terminado de escribir y que no se enviarán más mensajes.
Usamos un enfoque basado en clases para interactuar con el streaming RPC, incorporando el manejador como un miembro de la clase. Como con cualquier RPC, nos conectamos a la señal 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<guía::Response>()) qDebug() << "Client (ClientStreaming) finished, received:" << *response; m_clientStream.reset(); } else { qDebug() << "Client (ClientStreaming) failed:" << status; qDebug("Restarting the client stream"); clientStreaming(createRequest(0)); } }); }
La función inicia el flujo del cliente con un mensaje inicial. Luego continúa escribiendo dos mensajes adicionales antes de señalar el final de la comunicación llamando a writesDone. Si el flujo RPC tiene éxito, read la respuesta final del servidor y reset el objeto RPC. Si la RPC falla, volvemos a intentarlo invocando la misma función, que sobrescribe el miembro m_clientStream y vuelve a conectar la señal finished. No podemos simplemente reasignar el miembro m_clientStream dentro de la lambda, ya que perderíamos la conexión necesaria.
Ejecución del código
En main, invocamos la función clientStreaming con un mensaje de fallo, desencadenando un fallo RPC y ejecutando la lógica de reintento.
clientGuide.clientStreaming(ClientGuide::createRequest(0, true)); // fail the RPC
Un posible resultado de la ejecución del cliente de streaming podría ser el siguiente:
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 )El servidor recibe un mensaje inicial que provoca el fallo de la RPC, disparando la lógica de reintento. El reintento inicia la RPC con un mensaje válido, tras lo cual se envían tres mensajes al servidor antes de completarse con gracia.
Streaming bidireccional
La transmisión bidireccional ofrece la mayor flexibilidad, permitiendo al cliente y al servidor enviar y recibir mensajes simultáneamente. Requiere el manejo de las señales finished y messageReceived y proporciona la funcionalidad de escritura a través de writeMessage.
Utilizamos un enfoque basado en clases con conexiones de ranura de función miembro para demostrar la funcionalidad, incorporando el manejador como miembro de la clase. Además, utilizamos la función read basada en punteros. Los dos miembros utilizados son:
std::unique_ptr<QGrpcBidiStream> m_bidiStream; guide::Response m_bidiResponse;
Creamos una función para iniciar el flujo bidireccional a partir de un mensaje inicial y conectamos las funciones de ranura a las respectivas señales finished y 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); }
La funcionalidad de la ranura es sencilla. La ranura finished simplemente imprime y reinicia el objeto RPC:
void bidiFinished(const QGrpcStatus &status) { if (status.isOk()) qDebug("Client (BidirectionalStreaming) finished"); si no qDebug() << "Client (BidirectionalStreaming) failed:" << status; m_bidiStream.reset(); }
La ranura messageReceived read s en el miembro m_bidiResponse, continuando escribiendo mensajes hasta que el número de respuestas recibidas llegue a cero. En ese momento, cerramos a medias la comunicación con el cliente utilizando 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(); }
Ejecutando el código
La lógica del servidor simplemente devuelve un mensaje en cuanto lee algo, creando una respuesta con el número de la petición. En main, creamos dicha petición, que en última instancia sirve como contador.
clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));
Una posible salida de la ejecución del flujo bidireccional podría tener este aspecto:
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© 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.