Введение
С готовым .proto-контрактом (статья 323) и настроенной сборкой (статья 322) переходим к реализации самого простого вида RPC-вызова — unary (один запрос, один ответ), концептуально аналогичного обычному HTTP-запросу из основного цикла статей про сеть, но с типобезопасными структурами вместо ручного разбора JSON.
Концепция
gRPC-клиент создаётся через Channel (представляющий установленное соединение с конкретным сервером) и Stub (типобезопасный, сгенерированный из .proto-файла класс, предоставляющий методы, соответствующие RPC-методам сервиса) — вызов метода стаба (GetLatestReading) синхронно отправляет запрос и блокирует выполнение до получения ответа (или ошибки), возвращая статус вызова через объект grpc::Status. Сервер реализуется через наследование от сгенерированного базового класса сервиса, переопределяя методы, соответствующие каждому RPC из .proto-файла.
Пример кода: Unary RPC
// server/telemetry_server.cpp — реализация сервера на стороне C++
#include <grpcpp/grpcpp.h>
#include "device_service.grpc.pb.h"
class TelemetryServiceImpl final : public telemetry::TelemetryService::Service
{
public:
grpc::Status GetLatestReading(grpc::ServerContext *context,
const telemetry::ReadingRequest *request,
telemetry::SensorReading *response) override
{
const std::string &deviceId = request->device_id();
// В реальном проекте здесь было бы обращение к базе данных (цикл статей 230-239)
// или к кешу последних показаний устройств; для примера — фиктивное значение
if (deviceId.empty()) {
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "device_id не может быть пустым");
}
response->set_device_id(deviceId);
response->set_temperature(23.5);
response->set_humidity(45.0);
response->set_timestamp_unix(static_cast<int64_t>(time(nullptr)));
response->set_status(telemetry::DEVICE_STATUS_ONLINE);
return grpc::Status::OK;
}
};
void runServer()
{
std::string serverAddress("0.0.0.0:50051");
TelemetryServiceImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort(serverAddress, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Сервер слушает на " << serverAddress << std::endl;
server->Wait(); // блокирует основной поток до остановки сервера
}
// client/telemetryclient.h — Qt-клиент, использующий синхронный gRPC-вызов
#pragma once
#include <grpcpp/grpcpp.h>
#include "device_service.grpc.pb.h"
#include <QString>
#include <optional>
struct SensorReadingData
{
QString deviceId;
double temperature;
double humidity;
qint64 timestampUnix;
};
class TelemetryClient
{
public:
explicit TelemetryClient(const std::string &serverAddress)
{
auto channel = grpc::CreateChannel(serverAddress, grpc::InsecureChannelCredentials());
m_stub = telemetry::TelemetryService::NewStub(channel);
}
std::optional<SensorReadingData> getLatestReading(const QString &deviceId)
{
telemetry::ReadingRequest request;
request.set_device_id(deviceId.toStdString());
telemetry::SensorReading response;
grpc::ClientContext context;
// Синхронный вызов — блокирует текущий поток до получения ответа или ошибки;
// для Qt-приложения с UI это требует отдельного потока (статья 327)
grpc::Status status = m_stub->GetLatestReading(&context, request, &response);
if (!status.ok()) {
qWarning() << "Ошибка gRPC-вызова:" << QString::fromStdString(status.error_message());
return std::nullopt;
}
return SensorReadingData{
QString::fromStdString(response.device_id()),
response.temperature(),
response.humidity(),
response.timestamp_unix()
};
}
private:
std::unique_ptr<telemetry::TelemetryService::Stub> m_stub;
};
Пояснения к коду
TelemetryServiceImpl переопределяет метод GetLatestReading, сигнатура которого (принимающая ServerContext, указатель на запрос и указатель на ответ для заполнения, возвращающая grpc::Status) сгенерирована автоматически из объявления rpc GetLatestReading(...) в .proto-файле — реализация просто заполняет переданный по указателю объект ответа и возвращает grpc::Status::OK при успехе, или конкретный код ошибки (INVALID_ARGUMENT) при проблеме с входными данными, что аналогично кодам состояния HTTP в REST API, но строго типизировано через перечисление grpc::StatusCode.
TelemetryClient::getLatestReading() показывает клиентскую сторону — m_stub->GetLatestReading(&context, request, &response) выполняет синхронный вызов, заполняя response при успехе; функция возвращает std::optional<SensorReadingData>, явно показывая в сигнатуре, что вызов может не дать результата (аналогично паттерну из repository-слоя цикла про базы данных, статья 230) — при ошибке (!status.ok()) возвращается std::nullopt, а конкретное сообщение об ошибке логируется для диагностики.
Подводные камни
- Синхронный вызов gRPC-метода непосредственно из основного потока Qt-приложения с пользовательским интерфейсом блокирует обработку событий интерфейса на всё время ожидания ответа сервера, аналогично проблеме синхронных сетевых вызовов из основного цикла статей про сеть — для отзывчивого UI такой вызов должен выполняться в отдельном потоке или через асинхронный gRPC API, что подробно разбирается в статье 327.
- Использование
grpc::InsecureChannelCredentials()/InsecureServerCredentials()(как в примере, ради простоты демонстрации) означает полностью незашифрованное, неаутентифицированное соединение — это совершенно неприемлемо для любого реального, не чисто локального тестового сценария, и production-конфигурация требует TLS и аутентификации, разбираемых в статье 329. - Отсутствие установки тайм-аута (deadline) для клиентского вызова — без явного указания предельного времени ожидания через
context.set_deadline()клиент может неопределённо долго ждать ответа от недоступного или медленно отвечающего сервера, что для интерактивного приложения создаёт ощущение «зависшего» интерфейса; явная настройка deadline разбирается подробнее в статье 328. - Игнорирование валидации входных данных на стороне сервера (хотя в примере есть минимальная проверка на пустой
device_id) — поскольку gRPC обеспечивает типобезопасность структуры данных (нельзя передать строку, где ожидается число), это не освобождает от необходимости валидировать содержательную корректность значений (отрицательная температура там, где это физически невозможно, несуществующийdevice_id) точно так же, как это было необходимо для входных данных REST API в основном цикле статей про сеть и безопасность.