Qt обертка вокруг фреймворка gRPC в C++
Всем привет. Сегодня мы рассмотрим, как можно связать фреймворк gRPC в C++ и библиотеку Qt. В статье приведен код, обобщающий использование всех четырех режимов взаимодействия в gRPC. Помимо этого, приведен код, позволяющий использовать gRPC через сигналы и слоты Qt. Статья может быть интересна в первую очередь Qt разработчикам, заинтересованных в использовании gRPC. Тем не менее, обобщение четырех режимов работы gRPC написано на C++ без использования Qt, что позволит адаптировать код разработчикам, не связанных с Qt. Всех заинтересовавшихся прошу под кат.
Предыстория
Около полугода назад на мне висело два проекта, использующих клиентскую и серверную части gRPC. Оба проекта падали в продакшене. Эти проекты писали разработчики, которые уже уволились. Радовало только то, что я принимал активное участие в написании кода сервера и клиента gRPC. Но это было около года назад. Поэтому, как водится, пришлось разбираться со всем с нуля.
Код сервера gRPC писался с расчетом на то, что будет в дальнейшем генерироваться по .proto файлу. Код был написан неплохо. Однако, сервер обладал одним большим недостатком: к нему мог подключиться только один клиент.
Клиент gRPC был написан просто ужасно.
С кодом клиента и сервера gRPC я разобрался только спустя несколько дней. И понял, что возьми я какой-нибудь проект на пару недель, с сервером и клиентом gRPC пришлось бы разбираться заново.
Именно тогда я решил, что самое время написать и отладить клиент и сервер gRPC так, чтобы:
Краткий обзор
В качестве .proto файла был использован простейший pingproto.proto файл, в котором определены RPC всех видов взаимодействия:
Файл pingpong.proto с точностью до имен повторяет файл helloworld.proto из статьи об .
В итоге написанный сервер можно использовать примерно так:
Когда клиент вызывает RPC, сервер gRPC уведомляет об этом клиентский код (в данном случае класс А) при помощи соответствующего сигнала.
Клиент gRPC можно использовать так:
Клиент gRPC позволяет вызывать RPC напрямую, и подписаться на ответ сервера с помощью соответствующих сигналов.
Клиент gRPC также имеет сигнал:
который сообщает о прошлом и текущем состояниях подключения к серверу. Весь код с примерами использования находится в .
Как это работает
Принцип включения клиента и сервера gRPC в проект изображен на рисунке.
В .pro файле проекта указываются .proto файлы, на основе которых будет работать gRPC. В файле grpc.pri прописаны команды для генерации gRPC и QgRPC файлов. Компилятор protoc генерирует gRPC файлы [protofile].grpc.pb.h и [protofile].grpc.pb.cc. [protofile] — это имя .proto файла, переданного на вход компилятора.
Генерацией QgRPC файлов [protofile].qgrpc.[config].h занимается скрипт genQGrpc.py. [config] — это либо «server», либо «client».
Генерируемые QgRPC файлы содержат обертку Qt вокруг gRPC классов и вызовов с соответствующими сигналами. В предыдущих примерах, классы QpingServerService и QpingClientService объявлены соответственно в сгенерированных файлах pingpong.qgrpc.server.h и pingpong.qgrpc.client.h. Сгенерированные QgRPC файлы добавляются в обработку к moc'у.
В сгенерированных QgRPC файлах происходит включение файлов QGrpc[config].h, в которых и происходит вся основная работа. Подробнее об этом рассказано ниже.
Чтобы подключить всю эту конструкцию в проект, в .pro файле проекта нужно подключить файл grpc.pri и указать три переменные. Переменная GRPC определяет .proto файлы, которые будут переданы на входы компилятора protoc и скрипта genQGrpc.py. Переменная QGRPC_CONFIG определяет значение конфигурации сгенерированных QgRPC файлов и может содержать значения «server» или «client». Также можно определить опциональную переменную GRPC_VERSION для указания версии gRPC.
Подробнее обо всем сказанном читайте файл grpc.pri и .pro файлы примеров.
Архитектура сервера
Диаграмма классов сервера приведена на рисунке.
Толстыми стрелочками показана иерархия наследования классов, а тонкими — принадлежность членов и методов классам. В общем случае, для службы генерируется класс Q[servicename]ServerService, где servicename — имя службы, объявленное в .proto файле. RPCCallData — это управляющие структуры, сгенерированные для каждой RPC в службе. В конструкторе класса QpingServerService происходит инициализация базового класса QGrpcServerService асинхронной службой gRPC pingpong:ing::AsyncService. Для запуска службы нужно вызвать метод Start() с адресом и портом, на которых будет работать служба. В функции Start() реализована стандартная процедура запуска службы.
В конце функции Start() вызывается вызывается чисто виртуальная функция makeRequests(), которая реализована в сгенерированном классе QpingServerService:
Второй шаблонный параметр функции needAnotherCallData — это сгенерированные структуры RPCCallData. Эти же структуры являются параметрами сигналов в сгенерированном классе Qt службы.
Сгенерированные структуры RPCCallData наследуются от класса ServerCallData. В свою очередь, класс ServerCallData наследуется от респондера ServerResponder. Таким образом, создание объекта сгеренированных структур приводит к созданию объекта респондера.
Конструктор класса ServerCallData принимает два параметра: signal_func и request_func. signal_func — это сгенерированный сигнал, который вызывается после получения тэга из очереди. request_func — это функция, которая должна быть вызвана при создании нового респондера. Например, в данном случае это может быть функция RequestSayHello(). Вызов request_func происходит именно в функции needAnotherCallData(). Это сделано для того, чтобы управление респондерами (создание и удаление) происходило в службе.
Код функции needAnotherCallData() состоит из создания объекта респондера и вызова функции, связывающей респондер с вызовом RPC:
где service_ — это служба gRPC. В данном случае, это pingpong:ing::AsyncService.
Для синхронной или асинхронной проверки очереди событий необходимо вызвать функции CheckCQ() или AsyncCheckCQ() соответственно. Код функции CheckCQ() сводится к вызовам функции синхронного получения тэга из очереди и обработки этого тэга:
После получения тэга из очереди идут проверки валидности тэга и старта сервера. Если сервер выключен, то тэг уже не нужен — его можно удалить. После этого вызывается функция cqReaction(), определенная в класса ServerCallData:
Всем привет. Сегодня мы рассмотрим, как можно связать фреймворк gRPC в C++ и библиотеку Qt. В статье приведен код, обобщающий использование всех четырех режимов взаимодействия в gRPC. Помимо этого, приведен код, позволяющий использовать gRPC через сигналы и слоты Qt. Статья может быть интересна в первую очередь Qt разработчикам, заинтересованных в использовании gRPC. Тем не менее, обобщение четырех режимов работы gRPC написано на C++ без использования Qt, что позволит адаптировать код разработчикам, не связанных с Qt. Всех заинтересовавшихся прошу под кат.
Предыстория
Около полугода назад на мне висело два проекта, использующих клиентскую и серверную части gRPC. Оба проекта падали в продакшене. Эти проекты писали разработчики, которые уже уволились. Радовало только то, что я принимал активное участие в написании кода сервера и клиента gRPC. Но это было около года назад. Поэтому, как водится, пришлось разбираться со всем с нуля.
Код сервера gRPC писался с расчетом на то, что будет в дальнейшем генерироваться по .proto файлу. Код был написан неплохо. Однако, сервер обладал одним большим недостатком: к нему мог подключиться только один клиент.
Клиент gRPC был написан просто ужасно.
С кодом клиента и сервера gRPC я разобрался только спустя несколько дней. И понял, что возьми я какой-нибудь проект на пару недель, с сервером и клиентом gRPC пришлось бы разбираться заново.
Именно тогда я решил, что самое время написать и отладить клиент и сервер gRPC так, чтобы:
- Можно было спокойно спать по ночам;
- Не нужно было вспоминать, как это работает каждый раз, когда нужно написать клиента или сервер gRPC;
- Можно было использовать написанных клиента и сервера gRPC в других проектах.
- И клиент и сервер gRPC могут работать с использованием сигналов и слотов библиотеки Qt естественным образом;
- Код клиента и сервера gRPC не нужно исправлять при изменении .proto файла;
- Клиент gRPC должен уметь сообщить клиентскому коду состояние соединения с сервером.
Краткий обзор
В качестве .proto файла был использован простейший pingproto.proto файл, в котором определены RPC всех видов взаимодействия:
Файл pingpong.proto с точностью до имен повторяет файл helloworld.proto из статьи об .
В итоге написанный сервер можно использовать примерно так:
Когда клиент вызывает RPC, сервер gRPC уведомляет об этом клиентский код (в данном случае класс А) при помощи соответствующего сигнала.
Клиент gRPC можно использовать так:
Клиент gRPC позволяет вызывать RPC напрямую, и подписаться на ответ сервера с помощью соответствующих сигналов.
Клиент gRPC также имеет сигнал:
который сообщает о прошлом и текущем состояниях подключения к серверу. Весь код с примерами использования находится в .
Как это работает
Принцип включения клиента и сервера gRPC в проект изображен на рисунке.
В .pro файле проекта указываются .proto файлы, на основе которых будет работать gRPC. В файле grpc.pri прописаны команды для генерации gRPC и QgRPC файлов. Компилятор protoc генерирует gRPC файлы [protofile].grpc.pb.h и [protofile].grpc.pb.cc. [protofile] — это имя .proto файла, переданного на вход компилятора.
Генерацией QgRPC файлов [protofile].qgrpc.[config].h занимается скрипт genQGrpc.py. [config] — это либо «server», либо «client».
Генерируемые QgRPC файлы содержат обертку Qt вокруг gRPC классов и вызовов с соответствующими сигналами. В предыдущих примерах, классы QpingServerService и QpingClientService объявлены соответственно в сгенерированных файлах pingpong.qgrpc.server.h и pingpong.qgrpc.client.h. Сгенерированные QgRPC файлы добавляются в обработку к moc'у.
В сгенерированных QgRPC файлах происходит включение файлов QGrpc[config].h, в которых и происходит вся основная работа. Подробнее об этом рассказано ниже.
Чтобы подключить всю эту конструкцию в проект, в .pro файле проекта нужно подключить файл grpc.pri и указать три переменные. Переменная GRPC определяет .proto файлы, которые будут переданы на входы компилятора protoc и скрипта genQGrpc.py. Переменная QGRPC_CONFIG определяет значение конфигурации сгенерированных QgRPC файлов и может содержать значения «server» или «client». Также можно определить опциональную переменную GRPC_VERSION для указания версии gRPC.
Подробнее обо всем сказанном читайте файл grpc.pri и .pro файлы примеров.
Архитектура сервера
Диаграмма классов сервера приведена на рисунке.
Толстыми стрелочками показана иерархия наследования классов, а тонкими — принадлежность членов и методов классам. В общем случае, для службы генерируется класс Q[servicename]ServerService, где servicename — имя службы, объявленное в .proto файле. RPCCallData — это управляющие структуры, сгенерированные для каждой RPC в службе. В конструкторе класса QpingServerService происходит инициализация базового класса QGrpcServerService асинхронной службой gRPC pingpong:ing::AsyncService. Для запуска службы нужно вызвать метод Start() с адресом и портом, на которых будет работать служба. В функции Start() реализована стандартная процедура запуска службы.
В конце функции Start() вызывается вызывается чисто виртуальная функция makeRequests(), которая реализована в сгенерированном классе QpingServerService:
Второй шаблонный параметр функции needAnotherCallData — это сгенерированные структуры RPCCallData. Эти же структуры являются параметрами сигналов в сгенерированном классе Qt службы.
Сгенерированные структуры RPCCallData наследуются от класса ServerCallData. В свою очередь, класс ServerCallData наследуется от респондера ServerResponder. Таким образом, создание объекта сгеренированных структур приводит к созданию объекта респондера.
Конструктор класса ServerCallData принимает два параметра: signal_func и request_func. signal_func — это сгенерированный сигнал, который вызывается после получения тэга из очереди. request_func — это функция, которая должна быть вызвана при создании нового респондера. Например, в данном случае это может быть функция RequestSayHello(). Вызов request_func происходит именно в функции needAnotherCallData(). Это сделано для того, чтобы управление респондерами (создание и удаление) происходило в службе.
Код функции needAnotherCallData() состоит из создания объекта респондера и вызова функции, связывающей респондер с вызовом RPC:
где service_ — это служба gRPC. В данном случае, это pingpong:ing::AsyncService.
Для синхронной или асинхронной проверки очереди событий необходимо вызвать функции CheckCQ() или AsyncCheckCQ() соответственно. Код функции CheckCQ() сводится к вызовам функции синхронного получения тэга из очереди и обработки этого тэга:
После получения тэга из очереди идут проверки валидности тэга и старта сервера. Если сервер выключен, то тэг уже не нужен — его можно удалить. После этого вызывается функция cqReaction(), определенная в класса ServerCallData: