Привет, Хабровчане!

Для тех, кто не в курсе, gRPC - это открытый фреймворк от Google, который был представлен миру в 2016 году. Основываясь на протоколе HTTP/2, gRPC использует Protocol Buffers в качестве языка описания интерфейса.

Основная идея gRPC заключается в создании универсального механизма для эффективного и быстрого обмена данными между различными сервисами и приложениями. В этом его главное отличие от традиционных REST API. gRPC работает на основе протоколов, которые определяют "контракты" между клиентом и сервером, позволяя им общаться независимо от ЯПа.

gRPC

gRPC использует HTTP/2 в качестве транспортного протокола. HTTP/2 обеспечивает более эффективное использование сетевых ресурсов по сравнению с HTTP/1.1, позволяя множеству запросов и ответов передаваться параллельно в рамках одного TCP-соединения. Это уменьшает задержки и увеличивает общую производительность.

gRPC обычно использует Protocol Buffers, язык описания интерфейсов и систему сериализации от Google, в качестве формата для структурирования данных. ProtoBuf позволяет определять структуру данных и интерфейсы сервисов в специальных файлах .proto. Эти файлы компактны, эффективны и позволяют автоматически генерировать исходный код для различных языков программирования.

ProtoBuf сериализует структурированные данные в меньший объем по сравнению с такими форматами, как JSON или XML. Это уменьшает размеры пакетов данных, ускоряя их передачу и обработку. Про ProtoBuf подробней немного позже.

gRPC работает на основе определения сервисов и их методов в файлах .proto. Определяются структуры данных (сообщения) и сервисы с методами, которые принимают и возвращают эти сообщения.

Используются файлы .proto для описания сервисов и методов, которые могут быть вызваны удалённо. Каждый метод сервиса может принимать один тип запроса и возвращать один тип ответа. Используя инструменты gRPC, из этих .proto файлов генерируется код на целевом ЯП. Этот код включает в себя клиентские и серверные стабы, которые разработчики используют для реализации логики приложения.

Контракт gRPC включает в себя набор методов, сгруппированных в сервисы. Каждый метод описывается через его имя, запрос и ответ. В запросе и ответе можно использовать как стандартные типы данных, так и определить собственные структуры. В последнем случае нужно задать имя структуре и описать её с помощью ключевого слова message.

Метод должен принимать на вход и возвращать данные — например, FirstRequest и SecondResponse. Если передача данных не требуется, можно использовать google.protobuf.Empty.

В методе необходимо указать типы данных, с которыми он работает. Если тип данных неизвестен, можно использовать google.protobuf.Any.

Каждое поле в сообщении должно иметь уникальный порядковый номер. Уже использованные и удалённые номера нельзя повторно использовать. Удаленные поля можно отметить с помощью reserved или комментариев.

Основные слова для описания контракта

  • syntax: текущая версия синтаксиса.

  • import: для импорта стандартных библиотек, например, google/protobuf/timestamp.proto.

  • service: для объявления сервиса.

  • rpc: для объявления метода и связанных с ним запросов и ответов.

  • returns: для определения ответа метода.

  • message: для определения структуры данных.

  • enum: для определения перечислений.

  • repeated: для указания повторяющихся полей.

  • reserved: для резервирования полей.

  • oneof: для определения поля, которое может принимать одно из нескольких значений.

Пример сервиса в gRPC:

syntax = "proto3";

import "google/protobuf/timestamp.proto";

message MyRequest {
  string name = 1;
  int32 age = 2;
}

// определение структуры данных для ответа.
message MyResponse {
  string message = 1;
  google.protobuf.Timestamp timestamp = 2;
}

// определение перечисления.
enum Gender {
  UNKNOWN = 0;
  MALE = 1;
  FEMALE = 2;
}

// Определение сервиса.
service MyService {
  // определение метода с использованием rpc.
  rpc GetData(MyRequest) returns (MyResponse) {
    option (my.custom.option) = "some_value"; // Пример опции для метода.
  }
}

Клиентское приложение может вызывать методы удаленного сервиса, как будто это локальные функции, благодаря абстракции RPC (Remote Procedure Call). Запрос отправляется на сервер, который обрабатывает его и возвращает ответ.

Благодаря использованию ProtoBuf и генерации кода, gRPC поддерживает множество языков программирования, включая C#, C++, Dart, Go, Java, Kotlin, Node.js, Objective-C, PHP, Python, и Ruby.

gRPC поддерживает несколько типов взаимодействий:

  • Unary RPC: Это самая базовая и простая модель в gRPC. Клиент отправляет один запрос серверу и получает в ответ одно сообщение. Это аналогично традиционному вызову функции в программировании. Подходит для простых запросов и операций, где требуется однократное взаимодействие, например, получение информации по идентификатору или отправка данных для обработки.

  • Server streaming RPC: В этой модели клиент отправляет один запрос серверу, после чего сервер начинает отправлять поток ответов. Клиент читает ответы по мере их поступления, что может продолжаться неопределенное время. Суперски подходит для сценариев, где сервер должен отправить большое количество данных или постоянно обновляемую информацию, например, при передаче логов или потоковой передачи данных.

  • Client streaming RPC: В этой модели клиент отправляет поток данных серверу. После завершения отправки потока клиент ожидает ответ от сервера. Сервер обрабатывает весь поток данных, прежде чем отправить один ответ. Этот тип подходит для сценариев, где клиенту необходимо отправить большое количество данных или серию сообщений, например, при загрузке больших файлов или пакетной обработке данных.

  • Bidirectional streaming RPC: В двунаправленном потоковом RPC клиент и сервер обмениваются потоками данных в обоих направлениях. Клиент может начать отправку серии сообщений, не дожидаясь ответов сервера, и наоборот. Этот тип RPC наиболее гибкий и подходит для сложных взаимодействий, где клиент и сервер должны активно обмениваться данными в реальном времени, например, в интерактивных приложениях, чатах или системах реального времени.

Протоколы обмена данными в gRPC: ProtoBuf, JSON.

Protocol Buffers (ProtoBuf)

ProtoBuf — это язык описания интерфейса и система сериализации данных, разработанные Google. Они используются для сериализации структурированных данных, подобно XML, но более эффективны, быстры и меньше по размеру. ProtoBuf суперски подходят для разработки программ, которым требуется быстрая и компактная сериализация данных. Структура данных в ProtoBuf описывается в файлах с расширением .proto. Эти файлы содержат определения сообщений (аналогично классам в ООП) и сервисов (опционально).

Сообщения определяют поля с типами данных и уникальными идентификаторами. Эти поля могут быть простыми типами (например, int, string), сложными типами (другими сообщениями) или коллекциями.

ProtoBuf поддерживает различные типы данных, включая скалярные типы (например, int32, float, bool), строковые типы (string, bytes) и сложные типы (другие сообщения).

Также поддерживаются "повторяющиеся" поля (аналог массивов или списков в других языках). Каждому полю в сообщении назначается уникальный номер (тег), который используется в бинарном представлении. Эти теги важны для поддержания совместимости вперед при изменении структуры данных.

ProtoBuf сериализует данные в бинарный формат, что делает его очень эффективным как по размеру, так и по скорости сериализации/десериализации. Бинарный формат ProtoBuf не зависит от архитектуры машины, что обеспечивает переносимость данных между разными системами.

После определения структуры данных в .proto файлах, используется компилятор protoc для генерации кода на различных языках программирования.Это позволяет работать с этими структурами данных в их предпочитаемых языках, как с обычными классами или структурами.

ProtoBuf разработан с учетом совместимости вперед. Можно влёгкую добавлять новые поля к сообщениям без нарушения совместимости со старыми версиями сериализованных данных. Однако, изменение типа или удаление поля требует внимания, так как это может нарушить совместимость с уже существующими данными.

Рассмотрим пример файла .proto, который имеет простую структуру данных для пользователя и сервис, который позволяет получать информацию о пользователе. Это стандартный способ определения данных и сервисов в ProtoBuf:

// Определение версии синтаксиса ProtoBuf
syntax = "proto3";

// Опционально: указываем пакет
package user;

// Определение сообщения User
message User {
  int32 id = 1;         // Уникальный идентификатор пользователя
  string name = 2;      // Имя пользователя
  string email = 3;     // Электронная почта пользователя
  repeated string roles = 4; // Роли пользователя в системе
}

// Определение запроса для получения информации о пользователе
message GetUserRequest {
  int32 user_id = 1;    // Идентификатор пользователя, о котором нужно получить информацию
}

// Определение ответа, содержащего информацию о пользователе
message GetUserResponse {
  User user = 1;        // Данные пользователя
}

// Определение сервиса UserService
service UserService {
  // Определение метода для получения информации о пользователе
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

Мы начинаем с определения версии синтаксиса к примеру proto3. Опционально указываем пакет (в данном случае user), что помогает предотвратить конфликты имен в больших проектах.

Определяем структуру данных для пользователя, включая такие поля, как id, name, email и roles. Каждое поле имеет уникальный тег (например, id имеет тег 1).

Определяем структуры GetUserRequest и GetUserResponse для запроса и ответа соответственно. Это стандартный подход для определения входных и выходных данных для RPC вызовов в gRPC.

Определяем сервис с методом GetUser, который принимает GetUserRequest и возвращает GetUserResponse. Это определение позволяет клиентам вызывать метод GetUser удаленно как часть сервиса.

Этот файл .proto затем можно использовать для генерации исходного кода на различных языках программирования с помощью компилятора protoc.

Пару слов про строгую типизацию

В Proto-файлах определяются пользовательские типы данных с помощью ключевого слова message. Например:

message Person {
  string name = 1;
  int32 age = 2;
}

В примере определен пользовательский тип Person, который содержит два поля: name и age. Каждое поле имеет свой собственный номер (1 и 2), который используется для идентификации полей в сериализованных данных.

В Protocol Buffers строгая схема определяет, какие поля могут присутствовать в сообщении и какого они типа. Если вы создаете сообщение типа Person, вы должны предоставить значения для обоих полей name и age. Например:

Person {
  name: "John"
  age: 30
}

Нельзя пропустить обязательные поля или использовать поля другого типа. Все значения полей преобразуются в бинарное представление с учетом их типа и номера поля.

JSON

JSON — это текстовый формат обмена данными, основанный на JavaScript. Он используется для сериализации и передачи данных между сервером и веб-приложениями.

gRPC по умолчанию использует ProtoBuf из-за его высокой производительности и эффективности. Однако, gRPC также поддерживает JSON и другие форматы, что делает его гибким для различных сценариев использования.

Бегло составим простую табличку сравнения между протобафом и джейсоном для наглядности:

Критерий

Protocol Buffers (ProtoBuf)

JSON

Формат данных

Бинарный

Текстовый

Размер сообщений

Обычно меньше, более компактные

Обычно больше из-за текстового формата

Скорость обработки

Быстрее из-за меньшего размера и бинарной природы

Медленнее, требует парсинга текста

Читаемость

Требует специальных инструментов для чтения и отладки

Легко читаем и отлаживаем человеком

Интероперабельность

Хорошая поддержка между различными япами

Отличная поддержка на всех платформах

Совместимость

Строгая совместимость, требует точного соответствия схемы данных

Гибкая, легко адаптируется к изменениям

Типизация данных

Строго типизированный, требует определения всех полей

Динамически типизированный

Использование

Предпочтительнее для высокопроизводительных и оптимизированных систем

Широко используется для веб-API и легкой интеграции

Если нужна производительность и компактность, то ProtoBuf будет предпочтительным выбором. Если важнее удобство отладки и широкая совместимость, то может быть умнее использовать JSON.


gRPC может упростить процесс разработки, повысить производительность и улучшить общее качество взаимодействия между различными компонентами системы.

Какие сервисы делать на gRPC? Об этом мои коллеги из OTUS расскажут подробнее на бесплатном уроке. Регистрация доступна по ссылке.

Комментарии (6)


  1. Urgen
    18.12.2023 16:05

    Нельзя пропустить обязательные поля или использовать поля другого типа.

    враньё. я могу не заполнять поля в запросе. и это будет корректно с т.з. proto. а вот null выставить явно нельзя. только тут про это ни слова.


  1. Kelbon
    18.12.2023 16:05

    Опять бесполезная реклама, может хватит спамить?


  1. AnatoliiShablov
    18.12.2023 16:05

    Protobuf имеет возможность заполнять не все поля. В версии 2 поля можно было помечать optional required в зависимости от требований. Более того это расширяемый формат, что означает, на более старом клиенте может не быть ещё новых полей и он будет корректен для сервера. Аналогично в обратную сторону, новые поля заполненные клиентом просто не будут отображаться на сервере(будут парситься но не иметь имени.

    В версии 3 все поля опциональны, но при этом нет флага, а есть значения по умолчанию (насколько я помню). В версии 2 это было отдельным флагом


  1. vic_1
    18.12.2023 16:05

    Так все же зачем использовать grpc вместо rest? Он так же не зависит от яп и определяет контракт


    1. ptr128
      18.12.2023 16:05

      Если система загружена слабо и потоковых передач не требуется - можно оставаться на REST по принципу "работает - не трожь". В противном случае, gRPC становится логичным выбором, позволяющим повысить производительность и существенно снизить латентность.


  1. ptr128
    18.12.2023 16:05

    Статья пересказывает то, что не только можно прочитать в обзоре gRPC на одноименном сайте, но ещё и повторяет неоднократно опубликованное на Хабре. Я не нашел в ней вообще ничего нового.

    Нет сравнительного профилирования gRPC с REST, protobuf с json, потоковой передачи с унарной, "хороших" данных для protobuf (небольших положительных чисел) и "плохих" (строк).

    Не рассмотрены проблемы передачи не стандартизированных в protobuf типов данных. Например, decimal. А ведь gRPC позволяет использовать разные классы в разных языках для манипуляции такими типами, как объектами.

    Тема optional вообще не раскрыта. А ведь без его указания в proto3 пропущенное число станет нулем, а вовсе не NULL.

    О совместимости версий схем сказано только вскользь, тогда как в иерархических сообщениях подходы к обеспечению совместимости не всегда очевидны.