Работа с сетью в Qt

Unary RPC: простой запрос-ответ между Qt-клиентом и gRPC-сервером

Введение

С готовым .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 в основном цикле статей про сеть и безопасность.