Обзор Protocol Buffers
Protocol Buffers - это независимый от языка и платформы расширяемый механизм для сериализации структурированных данных.
Что такое Protocol Buffers?
Protocol Buffers — это независимый от языка и платформы расширяемый механизм Google для сериализации структурированных данных — представьте себе XML, но меньше, быстрее и проще. Вы один раз определяете, как должны быть структурированы ваши данные, а затем можете использовать специальный сгенерированный исходный код для легкой записи и чтения ваших структурированных данных в различные потоки данных и из них, используя различные языки программирования.
Выберите свой любимый язык
Protocol Buffers поддерживают генерацию кода для C++, C#, Dart, Go, Java, Kotlin, Objective-C, Python и Ruby. С версией proto3 вы также можете работать с PHP.
Пример реализации
edition = "2024";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
Рисунок 1. Определение в proto.
// Код на Java
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
Рисунок 2. Использование сгенерированного класса для сохранения данных.
// Код на C++
Person john;
fstream input(argv[1],
ios::in | ios::binary);
john.ParseFromIstream(&input);
id = john.id();
name = john.name();
email = john.email();
Рисунок 3. Использование сгенерированного класса для разбора сохраненных данных.
С чего начать?
- Скачайте и установите компилятор protocol buffer.
- Прочтите обзор.
- Попробуйте руководство для выбранного вами языка.
Обзор
Protocol Buffers - это независимый от языка и платформы расширяемый механизм для сериализации структурированных данных.
Это похоже на JSON, но меньше и быстрее, и он генерирует нативные привязки к языкам. Вы один раз определяете, как должны быть структурированы ваши данные, а затем можете использовать специальный сгенерированный исходный код для легкой записи и чтения ваших структурированных данных в различные потоки данных и из них, используя различные языки.
Protocol Buffers — это комбинация языка определений (создаваемого в
файлах .proto), кода, который компилятор proto генерирует для работы с
данными, специфичных для языка библиотек времени выполнения, формата сериализации для данных,
которые записываются в файл (или отправляются по сетевому соединению), и самих
сериализованных данных.
Какие проблемы решают Protocol Buffers?
Protocol Buffers предоставляют формат сериализации для пакетов типизированных, структурированных данных размером до нескольких мегабайт. Формат подходит как для временного сетевого трафика, так и для долгосрочного хранения данных. Protocol Buffers могут быть расширены новой информацией без признания недействительными существующих данных или необходимости обновлять код.
Protocol Buffers — наиболее часто используемый формат данных в Google. Они широко
используются в межсерверных коммуникациях, а также для архивного хранения данных на диске. Сообщения и сервисы Protocol Buffers описываются
инженерами в файлах .proto. Ниже показан пример message:
edition = "2023";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
Компилятор proto запускается во время сборки для файлов .proto, чтобы сгенерировать код
на различных языках программирования (рассматривается в разделе
Кросс-языковая совместимость далее в этой теме) для манипуляции
соответствующим protocol buffer. Каждый сгенерированный класс содержит простые
методы доступа для каждого поля и методы для сериализации и разбора всей структуры
в сырые байты и из них. Ниже показан пример использования этих
сгенерированных методов:
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
Поскольку Protocol Buffers широко используются во всех видах сервисов в Google и данные внутри них могут сохраняться в течение длительного времени, поддержание обратной совместимости имеет crucial. Protocol Buffers позволяют беспрепятственно поддерживать изменения, включая добавление новых полей и удаление существующих полей, для любого protocol buffer без нарушения работы существующих сервисов. Подробнее об этой теме см. Обновление определений Proto без обновления кода далее в этой теме.
Каковы преимущества использования Protocol Buffers?
Protocol Buffers идеально подходят для любой ситуации, когда вам необходимо сериализовать структурированные, похожие на записи, типизированные данные независимым от языка и платформы, расширяемым образом. Они чаще всего используются для определения коммуникационных протоколов (вместе с gRPC) и для хранения данных.
Некоторые преимущества использования Protocol Buffers включают:
- Компактное хранение данных
- Быстрый разбор
- Доступность на многих языках программирования
- Оптимизированная функциональность через автоматически сгенерированные классы
Кросс-языковая совместимость
Одни и те же сообщения могут быть прочитаны кодом, написанным на любом поддерживаемом языке
программирования. Вы можете иметь Java-программу на одной платформе, захватывающую данные из одной программной системы, сериализующую их на основе определения .proto, а затем извлекающую
определенные значения из этих сериализованных данных в отдельном Python-приложении,
работающем на другой платформе.
Следующие языки поддерживаются непосредственно в компиляторе protocol buffers, protoc:
Следующие языки поддерживаются Google, но исходный код проектов находится в репозиториях GitHub. Компилятор protoc использует плагины для этих языков:
Дополнительные языки не поддерживаются напрямую Google, а другими проектами на GitHub. Эти языки рассматриваются в Сторонние дополнения для Protocol Buffers.
Поддержка межпроектного взаимодействия
Вы можете использовать Protocol Buffers в разных проектах, определяя типы message в
файлах .proto, которые находятся вне базового кода конкретного проекта. Если вы
определяете типы message или перечисления (enums), которые, как вы ожидаете, будут широко использоваться
за пределами вашей непосредственной команды, вы можете поместить их в свой собственный файл без
зависимостей.
Пару примеров широко используемых в Google определений proto — это
timestamp.proto
и
status.proto.
Обновление определений Proto без обновления кода
Стандартной практикой для программных продуктов является обратная совместимость, но менее
распространена прямая совместимость. До тех пор, пока вы следуете некоторым
простым практикам
при обновлении определений .proto, старый код будет читать новые сообщения без
проблем, игнорируя любые вновь добавленные поля. Для старого кода удаленные поля будут иметь свое значение по умолчанию, а удаленные повторяющиеся (repeated) поля будут
пустыми. Для получения информации о том, что такое «повторяющиеся» поля, см.
Синтаксис определений Protocol Buffers далее в этой теме.
Новый код также будет прозрачно читать старые сообщения. Новые поля не будут присутствовать в старых сообщениях; в этих случаях Protocol Buffers предоставляют разумное значение по умолчанию.
Когда Protocol Buffers не являются хорошим выбором?
Protocol Buffers подходят не для всех данных. В частности:
- Protocol Buffers склонны предполагать, что все сообщения могут быть загружены в память сразу и не больше графа объектов. Для данных, которые превышают несколько мегабайт, рассмотрите другое решение; при работе с большими данными вы можете эффективно получить несколько копий данных из-за сериализованных копий, что может вызвать неожиданные скачки в использовании памяти.
- Когда Protocol Buffers сериализуются, одни и те же данные могут иметь много разных бинарных сериализаций. Вы не можете сравнить два сообщения на равенство без их полного разбора.
- Сообщения не сжаты. Хотя сообщения можно сжимать zip или gzip, как любой другой файл, специализированные алгоритмы сжатия, подобные используемым JPEG и PNG, произведут гораздо меньшие файлы для данных соответствующего типа.
- Сообщения Protocol Buffers менее чем максимально эффективны как по размеру, так и по скорости для многих научных и инженерных применений, связанных с большими, многомерными массивами чисел с плавающей запятой. Для этих приложений FITS и подобные форматы имеют меньше накладных расходов.
- Protocol Buffers не очень хорошо поддерживаются в не-объектно-ориентированных языках, популярных в научных вычислениях, таких как Fortran и IDL.
- Сообщения Protocol Buffers по своей сути не самоописывают свои данные, но они
имеют полностью рефлективную схему, которую вы можете использовать для реализации
самоописания. То есть вы не можете полностью интерпретировать сообщение без доступа к
его соответствующему файлу
.proto. - Protocol Buffers не являются формальным стандартом какой-либо организации. Это делает их непригодными для использования в средах с юридическими или иными требованиями строить поверх стандартов.
Кто использует Protocol Buffers?
Многие проекты используют Protocol Buffers, включая следующие:
Как работают Protocol Buffers?
На следующей диаграмме показано, как вы используете Protocol Buffers для работы с вашими данными.
Рисунок 1. Workflow Protocol Buffers
Код, сгенерированный Protocol Buffers, предоставляет служебные методы для извлечения данных из файлов и потоков, извлечения отдельных значений из данных, проверки существования данных, сериализации данных обратно в файл или поток и другие полезные функции.
Следующие примеры кода показывают пример этого потока в Java. Как показано
ранее, это определение .proto:
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
Компиляция этого файла .proto создает класс Builder, который вы можете использовать для
создания новых экземпляров, как в следующем Java-коде:
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
Затем вы можете десериализовать данные, используя методы, которые Protocol Buffers создает на других языках, например, C++:
Person john;
fstream input(argv[1], ios::in | ios::binary);
john.ParseFromIstream(&input);
int id = john.id();
std::string name = john.name();
std::string email = john.email();
Синтаксис определений Protocol Buffers
При определении файлов .proto вы можете указать мощность (cardinality) (одиночное (singular) или
повторяющееся (repeated)). В proto2 и proto3 вы также можете указать, является ли поле опциональным (optional).
В proto3 установка поля как optional
изменяет его с неявного присутствия на явное.
После установки мощности поля вы указываете тип данных. Protocol Buffers поддерживают обычные примитивные типы данных, такие как целые числа, логические значения и числа с плавающей запятой. Полный список см. в Скалярные типы значений.
Поле также может быть:
- Типом
message, чтобы вы могли вкладывать части определения, например, для повторяющихся наборов данных. - Типом
enum, чтобы вы могли указать набор значений для выбора. - Типом
oneof, который вы можете использовать, когда сообщение имеет много опциональных полей и не более одного поля будет установлено одновременно. - Типом
map, чтобы добавить пары ключ-значение в ваше определение.
Сообщения могут разрешать расширения (extensions) для определения полей вне самого сообщения. Например, внутренняя схема сообщений библиотеки protobuf позволяет расширения для пользовательских, специфичных для использования опций.
Для получения дополнительной информации о доступных опциях см. руководство по языку для proto2, proto3 или edition 2023.
После установки мощности и типа данных вы выбираете имя для поля. Есть некоторые вещи, которые следует иметь в виду при задании имен полей:
- Иногда может быть трудно или даже невозможно изменить имена полей после того, как они были использованы в production.
- Имена полей не могут содержать дефисы. Подробнее о синтаксисе имен полей см. Имена сообщений и полей.
- Используйте имена во множественном числе для повторяющихся полей.
После присвоения имени полю вы присваиваете номер поля. Номера полей не могут быть перепрофилированы или повторно использованы. Если вы удаляете поле, вам следует зарезервировать его номер поля, чтобы предотвратить случайное повторное использование номера кем-либо.
Поддержка дополнительных типов данных
Protocol Buffers поддерживают множество скалярных типов значений, включая целые числа, которые используют как кодирование переменной длины, так и фиксированные размеры. Вы также можете создавать свои собственные составные типы данных, определяя сообщения, которые сами по себе являются типами данных, которые вы можете назначить полю. В дополнение к простым и составным типам значений, несколько общих типов опубликованы.
История
Чтобы прочитать об истории проекта Protocol Buffers, см. История Protocol Buffers.
Философия открытого исходного кода Protocol Buffers
Protocol Buffers были открыты в 2008 году как способ предоставить разработчикам за пределами Google те же преимущества, которые мы получаем от них внутри компании. Мы поддерживаем сообщество открытого исходного кода с помощью регулярных обновлений языка по мере того, как мы вносим эти изменения для поддержки наших внутренних требований. Хотя мы принимаем избранные pull-запросы от внешних разработчиков, мы не всегда можем расставить приоритеты для запросов на функции и исправления ошибок, которые не соответствуют конкретным потребностям Google.
Сообщество разработчиков
Чтобы получать уведомления о предстоящих изменениях в Protocol Buffers и общаться с разработчиками и пользователями protobuf, присоединяйтесь к Группе Google.
Дополнительные ресурсы
Установка компилятора
Компилятор protocol buffer, protoc, используется для компиляции файлов .proto, которые
содержат определения сервисов и сообщений. Выберите один из приведенных ниже методов
для установки protoc.
Установка предварительно скомпилированных бинарных файлов (любая ОС)
Чтобы установить последнюю версию компилятора протокола из предварительно скомпилированных бинарных файлов, выполните следующие инструкции:
-
На странице https://github.com/google/protobuf/releases вручную загрузите zip-файл, соответствующий вашей операционной системе и архитектуре компьютера (
protoc-<version>-<os>-<arch>.zip), или получите файл с помощью таких команд, как следующая:PB_REL="https://github.com/protocolbuffers/protobuf/releases" curl -LO $PB_REL/download/v30.2/protoc-30.2-linux-x86_64.zip -
Разархивируйте файл в
$HOME/.localили в каталог по вашему выбору. Например:unzip protoc-30.2-linux-x86_64.zip -d $HOME/.local -
Обновите переменную пути (path) вашего окружения, чтобы включить путь к исполняемому файлу
protoc. Например:export PATH="$PATH:$HOME/.local/bin"
Установка с помощью менеджера пакетов
warning
Запустите
protoc --version, чтобы проверить версиюprotocпосле использования менеджера пакетов для установки, и убедитесь, что она достаточно свежая. Версииprotoc, устанавливаемые некоторыми менеджерами пакетов, могут быть довольно старыми. См.страницу поддержки версий, чтобы сравнить вывод проверки версии с номером минорной версии поддерживаемой версии языка(ов), который вы используете.
Вы можете установить компилятор протокола, protoc, с помощью менеджера пакетов в
Linux, macOS или Windows, используя следующие команды.
-
Linux, используя
aptилиapt-get, например:apt install -y protobuf-compiler protoc --version # Убедитесь, что версия компилятора 3+ -
macOS, используя Homebrew:
brew install protobuf protoc --version # Убедитесь, что версия компилятора 3+ -
Windows, используя Winget
> winget install protobuf > protoc --version # Убедитесь, что версия компилятора 3+
Другие варианты установки
Если вы хотите собрать компилятор протокола из исходных кодов или получить доступ к старым версиям предварительно скомпилированных бинарных файлов, см. Загрузка Protocol Buffers.
Новости
Темы новостей предоставляют информацию о прошлых событиях и изменениях в Protocol Buffers, а также о планах предстоящих изменений. Информация доступна как в хронологическом порядке, так и по релизам. Обратите внимание, что не всё включено в темы по релизам, так как некоторый контент не привязан к конкретной версии.
Новые темы новостей также будут публиковаться в списке рассылки protobuf@ с темой [Announcement].
Хронологический порядок
Следующие темы новостей предоставляют информацию в обратном хронологическом порядке.
- 19 сентября 2025 г. - Критические изменения в предстоящем релизе 34.x
- 16 июля 2025 г. - Возобновление поддержки Bazel с MSVC
- 14 июля 2025 г. - Устаревание меток FieldDescriptor
- 27 июня 2025 г. - Edition 2024
- 18 марта 2025 г. - Прекращение поддержки Ruby 3.0
- 23 января 2025 г. - Poison Java gencode
- 18 декабря 2024 г. - Go Protobuf: новый Opaque API
- 13 декабря 2024 г. - Удаление MutableRepeatedFieldRef<T>::Reserve() в v30
- 4 декабря 2024 г. - DebugString заменен
- 7 ноября 2024 г. - Дополнительные критические изменения в предстоящем релизе 30.x
- 2 октября 2024 г. - Критические изменения в предстоящем релизе 30.x
- 1 октября 2024 г. - Изменения в сборках Bazel
- 26 июня 2024 г. - Прекращение поддержки сборки Protobuf Java из исходного кода с Maven
- 27 февраля 2024 г. - Прекращение поддержки старых версий Ruby
- 5 февраля 2024 г. - Критические изменения в Java, C++ и Python в линейке 26.x
- 31 января 2024 г. - Критические изменения в линейке 26.x для Python
- 5 января 2024 г. - Критические изменения в линейке 26.x для Ruby и Python
- 27 декабря 2023 г. - Критические изменения в линейке 26.x для Ruby, PHP, Python и upb
- 13 декабря 2023 г. - Критические изменения в Python и C++ в линейке 26.x
- 5 декабря 2023 г. - Критические изменения в Java в линейке 26.x
- 10 октября 2023 г. - Опубликована документация по функциям Protobuf Editions
- 15 сентября 2023 г. - Перемещение μpb в репозиторий Protobuf на GitHub
- 15 августа 2023 г. - Критическое изменение в Python: замена message.UnknownFields()
- 9 августа 2023 г. - Политика поддержки .NET
- 17 июля 2023 г. - Прекращение поддержки старых версий Bazel
- 6 июля 2023 г. - Прекращение поддержки старых версий PHP, Ruby и Python
- 29 июня 2023 г. - Анонс Protobuf Editions
- 28 апреля 2023 г. - Null больше не разрешен в опциях поля json_name
- 20 апреля 2023 г. - Обновление генератора кода Ruby
- 11 апреля 2023 г. - Удаление рефлексии синтаксиса, выпуск поддержки C++ CORD для единичных байтовых полей в OSS, сохранение опций и прекращение поддержки Bazel <5.3
- 3 августа 2022 г. - Изменения поддержки платформ и предстоящие изменения в линейке C++ 22.x
- 6 июля 2022 г. - Политика критических изменений в библиотеках
- 6 мая 2022 г. - Версионирование, обновления Python и поддержка JavaScript
По релизам
Следующие новые темы предоставляют информацию по релизам. Все записи новостей появляются в хронологическом списке в предыдущем разделе, но только записи, относящиеся к конкретной версии, появляются на страницах, перечисленных в этом разделе.
Эти страницы не заменяют примечания к выпуску, так как примечания к выпуску будут более полными. Также, не всё из хронологического списка будет в этих темах, так как некоторый контент не относится к конкретному релизу.
- Версия 30.x
- Версия 29.x
- Версия 26.x
- Версия 25.x
- Версия 24.x
- Версия 23.x
- Версия 22.x
- Версия 21.x
Руководство программиста
Узнайте, как использовать Protocol Buffers в ваших проектах.
Руководство по языку (editions)
Охватывает, как использовать редакции editions языка Protocol Buffers в вашем проекте.
Руководство по языку (proto 2)
Охватывает, как использовать редакцию proto2 языка Protocol Buffers в вашем проекте.
Руководство по языку (proto 3)
Охватывает, как использовать редакцию proto3 языка Protocol Buffers в вашем проекте.
Ограничения Proto
Охватывает ограничения на количество поддерживаемых элементов в proto-схемах.
Руководство по стилю
Предоставляет рекомендации по оптимальной структуре ваших proto-определений.
Поведение Enum
Объясняет, как перечисления (enums) в настоящее время работают в Protocol Buffers и как они должны работать.
Кодирование
Объясняет, как Protocol Buffers кодирует данные в файлы или для передачи по сети.
Формат ProtoJSON
Охватывает, как использовать утилиты преобразования Protobuf в JSON.
Методы
Описывает некоторые часто используемые шаблоны проектирования для работы с Protocol Buffers.
Сторонние дополнения
Ссылки на множество проектов с открытым исходным кодом, которые добавляют полезную функциональность поверх Protocol Buffers.
Объявления расширений
Подробно описывает, что такое объявления расширений, зачем они нужны и как их использовать.
Примечание: Присутствие полей
Объясняет различные дисциплины отслеживания присутствия для полей protobuf. Также объясняет поведение явного отслеживания присутствия для единичных полей proto3 с базовыми типами.
Сериализация Proto не является канонической
Объясняет, как работает сериализация и почему она не является канонической.
Десериализация отладочных представлений Proto
Как записывать отладочную информацию в Protocol Buffers.
Руководство по языку (editions)
Охватывает, как использовать редакции editions языка Protocol Buffers в вашем проекте.
Это руководство описывает, как использовать язык protocol buffer для структурирования ваших
данных protocol buffer, включая синтаксис файлов .proto и как генерировать классы
доступа к данным из ваших файлов .proto. Оно охватывает edition 2023 и edition
2024 языка protocol buffers. Для информации о том, чем editions концептуально отличаются от proto2 и proto3, см.
Обзор Protobuf Editions.
Для информации о синтаксисе proto2 см. Руководство по языку Proto2.
Для информации о синтаксисе proto3 см. Руководство по языку Proto3.
Это справочное руководство – для пошагового примера, который использует многие из функций, описанных в этом документе, см. обучение для выбранного вами языка.
Определение типа сообщения
Сначала давайте рассмотрим очень простой пример. Допустим, вы хотите определить формат сообщения
поискового запроса, где каждый поисковый запрос имеет строку запроса,
конкретную страницу результатов, которая вас интересует, и количество результатов на
страницу. Вот файл .proto, который вы используете для определения типа сообщения.
edition = "2023";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
-
Первая строка файла указывает, что вы используете edition 2023 спецификации языка protobuf.
edition(илиsyntaxдля proto2/proto3) должна быть первой непустой, не-комментарием строкой файла.- Если
editionилиsyntaxне указана, компилятор protocol buffer предположит, что вы используете proto2.
-
Определение сообщения
SearchRequestзадает три поля (пары имя/значение), по одному для каждой части данных, которые вы хотите включить в этот тип сообщения. Каждое поле имеет имя и тип.
Указание типов полей
В предыдущем примере все поля являются скалярными типами: два целых числа
(page_number и results_per_page) и строка (query). Вы также можете
указать перечисления и составные типы, такие как другие типы сообщений, для
вашего поля.
Назначение номеров полей
Вы должны дать каждому полю в определении вашего сообщения число между 1 и
536,870,911 со следующими ограничениями:
- Данный номер должен быть уникальным среди всех полей этого сообщения.
- Номера полей
19,000до19,999зарезервированы для реализации Protocol Buffers. Компилятор protocol buffer будет жаловаться, если вы используете один из этих зарезервированных номеров полей в вашем сообщении. - Вы не можете использовать любые ранее зарезервированные номера полей или любые номера полей, которые были выделены для расширений.
Этот номер не может быть изменен после того, как ваш тип сообщения используется, потому что он идентифицирует поле в формате передачи сообщения. «Изменение» номера поля эквивалентно удалению этого поля и созданию нового поля с тем же типом, но новым номером. См. Удаление полей для того, как сделать это правильно.
Номера полей никогда не должны использоваться повторно. Никогда не извлекайте номер поля из зарезервированного списка для повторного использования с новым определением поля. См. Последствия повторного использования номеров полей.
Вам следует использовать номера полей от 1 до 15 для наиболее часто устанавливаемых полей. Меньшие значения номеров полей занимают меньше места в формате передачи. Например, номера полей в диапазоне от 1 до 15 занимают один байт для кодирования. Номера полей в диапазоне от 16 до 2047 занимают два байта. Вы можете узнать больше об этом в Кодирование Protocol Buffer.
Последствия повторного использования номеров полей
Повторное использование номера поля делает декодирование сообщений в формате передачи неоднозначным.
Формат передачи protobuf является «легким» и не предоставляет способа обнаружения полей, закодированных с использованием одного определения и декодированных с использованием другого.
Кодирование поля с использованием одного определения и последующее декодирование этого же поля с другим определением может привести к:
- Потере времени разработчика на отладку
- Ошибке разбора/слияния (лучший сценарий)
- Утечке PII/SPII
- Повреждению данных
Распространенные причины повторного использования номеров полей:
-
перенумерация полей (иногда делается для достижения более эстетически приятного порядка номеров для полей). Перенумерация фактически удаляет и заново добавляет все поля, вовлеченные в перенумерацию, что приводит к несовместимым изменениям формата передачи.
-
удаление поля и не резервирование номера для предотвращения будущего повторного использования.
- Это была очень легкая ошибка для совершения с полями расширений по нескольким причинам. Объявления расширений предоставляют механизм для резервирования полей расширений.
Номер поля ограничен 29 битами вместо 32 битов, потому что три бита используются для указания формата передачи поля. Для получения дополнительной информации см. Тема Кодирование.
Указание мощности полей
Поля сообщения могут быть одним из следующих:
-
Одиночное (Singular):
Одиночное поле не имеет явной метки мощности. Оно имеет два возможных состояния:
- поле установлено и содержит значение, которое было явно установлено или разобрано из передачи. Оно будет сериализовано в передачу.
- поле не установлено и будет возвращать значение по умолчанию. Оно не будет сериализовано в передачу.
Вы можете проверить, было ли значение явно установлено.
Неявные поля Proto3, которые были мигрированы в editions, будут использовать функцию
field_presence, установленную в значениеIMPLICIT.Обязательные поля Proto2 (
required), которые были мигрированы в editions, также будут использовать функциюfield_presence, но установленную вLEGACY_REQUIRED. -
repeated: этот тип поля может повторяться ноль или более раз в правильно сформированном сообщении. Порядок повторяющихся значений будет сохранен. -
map: это поле типа пар ключ/значение. См. Карты для получения дополнительной информации об этом типе поля.
Повторяющиеся поля упакованы по умолчанию
В proto editions, repeated поля скалярных числовых типов используют packed
кодирование по умолчанию.
Вы можете узнать больше об packed кодировании в
Кодирование Protocol Buffer.
Правильно сформированные сообщения
Термин «правильно сформированный» (well-formed), когда применяется к сообщениям protobuf, относится к байтам, сериализованным/десериализованным. Парсер protoc проверяет, что данное proto определение файла разбираемо.
Одиночные поля могут появляться более одного раза в байтах формата передачи. Парсер примет ввод, но только последний экземпляр этого поля будет доступен через сгенерированные привязки. См. Последний побеждает для получения дополнительной информации по этой теме.
Добавление дополнительных типов сообщений
Несколько типов сообщений могут быть определены в одном файле .proto. Это полезно,
если вы определяете несколько связанных сообщений – так, например, если вы хотите
определить формат сообщения ответа, который соответствует вашему типу сообщения SearchResponse,
вы можете добавить его в тот же .proto:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
Комбинирование сообщений приводит к раздуванию Хотя несколько типов сообщений (таких как
message, enum и service) могут быть определены в одном файле .proto, это может
также привести к раздуванию зависимостей, когда большое количество сообщений с различными
зависимостями определяется в одном файле. Рекомендуется включать как можно меньше
типов сообщений в каждый файл .proto.
Добавление комментариев
Чтобы добавить комментарии в ваши файлы .proto:
-
Предпочитайте комментарии в стиле C/C++/Java в конце строки '//' в строке перед элементом кода .proto
-
Встрочные/многострочные комментарии в стиле C
/* ... */также принимаются.- При использовании многострочных комментариев предпочтительна строка отступа '*'.
/**
* SearchRequest представляет поисковый запрос с параметрами pagination,
* чтобы указать, какие результаты включать в ответ.
*/
message SearchRequest {
string query = 1;
// Какой номер страницы нам нужен?
int32 page_number = 2;
// Количество результатов, возвращаемых на страницу.
int32 results_per_page = 3;
}
Удаление полей
Удаление полей может вызвать серьезные проблемы, если не сделано правильно.
Когда вам больше не нужно поле и все ссылки были удалены из клиентского кода, вы можете удалить определение поля из сообщения. Однако, вы должны зарезервировать удаленный номер поля. Если вы не зарезервируете номер поля, возможно, что разработчик повторно использует этот номер в будущем.
Вам также следует зарезервировать имя поля, чтобы позволить JSON и TextFormat кодировкам вашего сообщения продолжать разбираться.
Зарезервированные номера полей
Если вы обновляете тип сообщения, полностью удаляя поле, или
комментируя его, будущие разработчики могут повторно использовать номер поля при внесении
своих собственных обновлений в тип. Это может вызвать серьезные проблемы, как описано в
Последствия повторного использования номеров полей. Чтобы убедиться, что этого
не происходит, добавьте ваш удаленный номер поля в список reserved.
Компилятор protoc будет генерировать сообщения об ошибках, если любые будущие разработчики попытаются использовать эти зарезервированные номера полей.
message Foo {
reserved 2, 15, 9 to 11;
}
Диапазоны зарезервированных номеров полей включительны (9 to 11 это то же самое, что 9, 10, 11).
Зарезервированные имена полей
Повторное использование старого имени поля позже обычно безопасно, за исключением случаев использования TextProto
или JSON кодировок, где имя поля сериализуется. Чтобы избежать этого риска, вы
можете добавить удаленное имя поля в список reserved.
Зарезервированные имена влияют только на поведение компилятора protoc, а не на поведение во время выполнения, за одним исключением: реализации TextProto могут отбрасывать неизвестные поля (без вызова ошибки, как с другими неизвестными полями) с зарезервированными именами во время разбора (только реализации C++ и Go делают это сегодня). JSON парсинг во время выполнения не затрагивается зарезервированными именами.
message Foo {
reserved 2, 15, 9 to 11;
reserved foo, bar;
}
Обратите внимание, что вы не можете смешивать имена полей и номера полей в одном операторе reserved.
Что генерируется из вашего .proto?
Когда вы запускаете компилятор protocol buffer на .proto файле,
компилятор генерирует код на выбранном вами языке, который вам понадобится для работы с
типами сообщений, которые вы описали в файле, включая получение и установку значений полей,
сериализацию ваших сообщений в выходной поток и разбор ваших сообщений
из входного потока.
- Для C++ компилятор генерирует файлы
.hи.ccиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для Java компилятор генерирует файл
.javaс классом для каждого типа сообщения, а также специальный классBuilderдля создания экземпляров классов сообщений. - Для Kotlin, в дополнение к сгенерированному Java коду, компилятор
генерирует файл
.ktдля каждого типа сообщения с улучшенным Kotlin API. Это включает DSL, который упрощает создание экземпляров сообщений, метод доступа к nullable полю и функцию копирования. - Python немного отличается — компилятор Python генерирует модуль
со статическим дескриптором каждого типа сообщения в вашем
.proto, который затем используется с метаклассом для создания необходимого класса доступа к данным Python во время выполнения. - Для Go компилятор генерирует файл
.pb.goс типом для каждого типа сообщения в вашем файле. - Для Ruby компилятор генерирует файл
.rbс Ruby модулем, содержащим ваши типы сообщений. - Для Objective-C компилятор генерирует файлы
pbobjc.hиpbobjc.mиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для C# компилятор генерирует файл
.csиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для PHP компилятор генерирует файл сообщения
.phpдля каждого типа сообщения, описанного в вашем файле, и файл метаданных.phpдля каждого.protoфайла, который вы компилируете. Файл метаданных используется для загрузки допустимых типов сообщений в пул дескрипторов. - Для Dart компилятор генерирует файл
.pb.dartс классом для каждого типа сообщения в вашем файле.
Вы можете узнать больше об использовании API для каждого языка, следуя обучению для выбранного языка. Для получения еще более подробной информации об API см. соответствующий справочник по API.
Скалярные типы значений
Скалярное поле сообщения может иметь один из следующих типов – таблица показывает
тип, указанный в файле .proto, и соответствующий тип в
автоматически сгенерированном классе:
| Тип в Proto | Примечания |
|---|---|
| double | |
| float | |
| int32 | Использует кодирование переменной длины. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте sint32 вместо этого. |
| int64 | Использует кодирование переменной длины. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте sint64 вместо этого. |
| uint32 | Использует кодирование переменной длины. |
| uint64 | Использует кодирование переменной длины. |
| sint32 | Использует кодирование переменной длины. Значение со знаком. Они более эффективно кодируют отрицательные числа, чем обычные int32. |
| sint64 | Использует кодирование переменной длины. Значение со знаком. Они более эффективно кодируют отрицательные числа, чем обычные int64. |
| fixed32 | Всегда четыре байта. Более эффективно, чем uint32, если значения часто больше чем 228. |
| fixed64 | Всегда восемь байта. Более эффективно, чем uint64, если значения часто больше чем 256. |
| sfixed32 | Всегда четыре байта. |
| sfixed64 | Всегда восемь байта. |
| bool | |
| string | Строка должна всегда содержать текст в кодировке UTF-8 или 7-битный ASCII и не может быть длиннее 232. |
| bytes | Может содержать любую произвольную последовательность байт не длиннее 232. |
| Тип в Proto | Тип в C++ | Тип в Java/Kotlin[1] | Тип в Python[3] | Тип в Go | Тип в Ruby | Тип в C# | Тип в PHP | Тип в Dart | Тип в Rust |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | f64 |
| float | float | float | float | float32 | Float | float | float | double | f32 |
| int32 | int32_t | int | int | int32 | Fixnum или Bignum (по необходимости) | int | integer | int | i32 |
| int64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| uint32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum или Bignum (по необходимости) | uint | integer | int | u32 |
| uint64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sint32 | int32_t | int | int | int32 | Fixnum или Bignum (по необходимости) | int | integer | int | i32 |
| sint64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| fixed32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum или Bignum (по необходимости) | uint | integer | int | u32 |
| fixed64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sfixed32 | int32_t | int | int | int32 | Fixnum или Bignum (по необходимости) | int | integer | int | i32 |
| sfixed64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | bool |
| string | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String | ProtoString |
| bytes | string | ByteString | str (Python 2), bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string | List |
ProtoBytes |
[1] Kotlin использует соответствующие типы из Java, даже для беззнаковых типов, чтобы обеспечить совместимость в смешанных Java/Kotlin кодовых базах.
[2] В Java беззнаковые 32-битные и 64-битные целые числа представляются с использованием их знаковых аналогов, при этом старший бит просто хранится в бите знака.
[3] Во всех случаях установка значений в поле будет выполнять проверку типа чтобы убедиться, что оно допустимо.
[4] 64-битные или беззнаковые 32-битные целые числа всегда представляются как long при декодировании, но могут быть int, если при установке поля задан int. Во всех случаях значение должно помещаться в тип, представленный при установке. См. [2].
[5] Строки Python представляются как unicode при декодировании, но могут быть str, если задана строка ASCII (это может измениться).
[6] Integer используется на 64-битных машинах, а string используется на 32-битных машинах.
Вы можете узнать больше о том, как эти типы кодируются при сериализации вашего сообщения в Кодирование Protocol Buffer.
Значения полей по умолчанию
Когда сообщение разбирается, если закодированные байты сообщения не содержат конкретное поле, доступ к этому полю в разобранном объекте возвращает значение по умолчанию для этого поля. Значения по умолчанию зависят от типа:
- Для строк значение по умолчанию — пустая строка.
- Для bytes значение по умолчанию — пустые байты.
- Для bools значение по умолчанию — false.
- Для числовых типов значение по умолчанию — ноль.
- Для полей сообщения поле не установлено. Его точное значение зависит от языка. Подробности см. в руководстве по сгенерированному коду.
- Для перечислений значение по умолчанию — первое определенное значение перечисления, которое должно быть 0. См. Значение перечисления по умолчанию.
Значение по умолчанию для повторяющихся полей — пустое (обычно пустой список в соответствующем языке).
Значение по умолчанию для полей map — пустое (обычно пустая карта в соответствующем языке).
Переопределение скалярных значений по умолчанию
В protobuf editions вы можете указать явные значения по умолчанию для одиночных
полей, не являющихся сообщениями. Например, допустим, вы хотите предоставить значение по умолчанию
10 для поля SearchRequest.result_per_page:
int32 result_per_page = 3 [default = 10];
Если отправитель не указывает result_per_page, получатель будет наблюдать
следующее состояние:
- Поле result_per_page отсутствует. То есть метод
has_result_per_page()(hazzer метод) вернетfalse. - Значение
result_per_page(возвращаемое из «геттера») равно10.
Если отправитель отправляет значение для result_per_page, значение по умолчанию 10
игнорируется, и значение отправителя возвращается из «геттера».
См. руководство по сгенерированному коду для вашего выбранного языка для получения более подробной информации о том, как работают значения по умолчанию в сгенерированном коде.
Явные значения по умолчанию не могут быть указаны для полей, которые имеют
функцию field_presence, установленную в IMPLICIT.
Перечисления
Когда вы определяете тип сообщения, вы можете захотеть, чтобы одно из его полей имело
только одно значение из предопределенного списка. Например, допустим, вы хотите добавить
поле corpus для каждого SearchRequest, где corpus может быть UNIVERSAL,
WEB, IMAGES, LOCAL, NEWS, PRODUCTS или VIDEO. Вы можете сделать это очень
просто, добавив enum в определение вашего сообщения с константой для каждого возможного значения.
В следующем примере мы добавили enum с именем Corpus со всеми
возможными значениями и поле типа Corpus:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
Значение перечисления по умолчанию
Значение по умолчанию для поля SearchRequest.corpus — CORPUS_UNSPECIFIED,
потому что это первое значение, определенное в перечислении.
В edition 2023 первое значение, определенное в определении перечисления, должно иметь
значение ноль и должно иметь имя ENUM_TYPE_NAME_UNSPECIFIED или
ENUM_TYPE_NAME_UNKNOWN. Это потому что:
- Нулевое значение должно быть первым элементом для совместимости с семантикой proto2, где первое значение перечисления является значением по умолчанию, если не указано явно другое значение.
- Должно быть нулевое значение для совместимости с семантикой proto3, где нулевое значение используется как значение по умолчанию для всех полей с неявным присутствием, использующих этот тип перечисления.
Также рекомендуется, чтобы это первое, значение по умолчанию не имело семантического значения, кроме «это значение не было указано».
Значение по умолчанию для поля перечисления, такого как поле SearchRequest.corpus, может быть
явно переопределено следующим образом:
Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
Если тип перечисления был мигрирован из proto2 с использованием option features.enum_type = CLOSED;, нет ограничений на первое значение в перечислении. Не
рекомендуется изменять первое значение таких типов перечислений, потому что это
изменит значение по умолчанию для любых полей, использующих этот тип перечисления, без явного
значения по умолчанию для поля.
Псевдонимы значений перечисления
Вы можете определить псевдонимы, назначая одно и то же значение разным константам перечисления.
Чтобы сделать это, вам нужно установить опцию allow_alias в true. В противном случае
компилятор protocol buffer генерирует предупреждение, когда псевдонимы
найдены. Хотя все значения псевдонимов действительны для сериализации, только первое значение
используется при десериализации.
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Раскомментирование этой строки вызовет предупреждение.
ENAA_FINISHED = 2;
}
Константы перечисления
Константы перечислителя должны находиться в диапазоне 32-битного целого числа. Поскольку значения enum
используют
varint кодирование в
передаче, отрицательные значения неэффективны и поэтому не рекомендуются. Вы можете определять
enum внутри определения сообщения, как в предыдущем примере, или снаружи –
эти enum могут быть повторно использованы в любом определении сообщения в вашем файле .proto. Вы
также можете использовать тип enum, объявленный в одном сообщении, в качестве типа поля в
другом сообщении, используя синтаксис _MessageType_._EnumType_.
Языко-специфичные реализации перечислений
Когда вы запускаете компилятор protocol buffer на .proto, который использует enum,
сгенерированный код будет иметь соответствующий enum для Java, Kotlin или C++, или
специальный класс EnumDescriptor для Python, который используется для создания набора
символических констант с целочисленными значениями в классе, сгенерированном во время выполнения.
warning
Сгенерированный код может быть подвержен языко-специфичным ограничениям на количество перечислителей (несколько тысяч для одного языка). Ознакомьтесь с ограничениями для языков, которые вы планируете использовать.
Во время десериализации нераспознанные значения перечисления будут сохранены в сообщении, хотя то, как это представлено при десериализации сообщения, зависит от языка. В языках, которые поддерживают открытые типы перечислений со значениями вне диапазона указанных символов, таких как C++ и Go, неизвестное значение перечисления просто хранится как его базовое целочисленное представление. В языках с закрытыми типами перечислений, таких как Java, случай в перечислении используется для представления нераспознанного значения, и базовое целое число может быть доступно с помощью специальных методов доступа. В любом случае, если сообщение сериализуется, нераспознанное значение все равно будет сериализовано с сообщением.
warning
Для информации о том, как перечисления должны работать, в противовес тому, как они сейчас работают в
разных языках, см.Поведение Enum.
Для получения дополнительной информации о том, как работать с enum сообщений в ваших
приложениях, см. руководство по сгенерированному коду
для выбранного вами языка.
Зарезервированные значения
Если вы обновляете тип перечисления, полностью удаляя запись перечисления, или
комментируя ее, будущие пользователи могут повторно использовать числовое значение при внесении
своих собственных обновлений в тип. Это может вызвать серьезные проблемы, если они позже загрузят старые
экземпляры того же .proto, включая повреждение данных, ошибки конфиденциальности и т.д.
Один из способов убедиться, что этого не произойдет, — указать, что числовые
значения (и/или имена, которые также могут вызывать проблемы для JSON сериализации)
ваших удаленных записей reserved. Компилятор protocol buffer будет жаловаться,
если любые будущие пользователи попытаются использовать эти идентификаторы. Вы можете указать, что ваш
зарезервированный диапазон числовых значений доходит до максимально возможного значения, используя
ключевое слово max.
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved FOO, BAR;
}
Обратите внимание, что вы не можете смешивать имена полей и числовые значения в одном операторе reserved.
Использование других типов сообщений
Вы можете использовать другие типы сообщений в качестве типов полей. Например, допустим, вы
хотели включить сообщения Result в каждое сообщение SearchResponse – чтобы
сделать это, вы можете определить тип сообщения Result в том же .proto и затем
указать поле типа Result в SearchResponse:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
Импорт определений
В предыдущем примере тип сообщения Result определен в том же файле, что и
SearchResponse – что, если тип сообщения, который вы хотите использовать в качестве типа поля,
уже определен в другом файле .proto?
Вы можете использовать определения из других файлов .proto, импортируя их. Чтобы импортировать
определения другого .proto, вы добавляете оператор импорта в начало вашего файла:
import "myproject/other_protos.proto";
Начиная с Edition 2024, вы также можете использовать import option для использования
определений пользовательских опций из других файлов .proto. В отличие от
обычных импортов, это позволяет использовать только определения пользовательских опций, но не
другие определения сообщений или перечислений, чтобы избежать зависимостей в сгенерированном коде.
import option "myproject/other_protos.proto";
По умолчанию вы можете использовать определения только из непосредственно импортированных файлов .proto.
Однако иногда вам может потребоваться переместить файл .proto в новое место.
Вместо прямого перемещения файла .proto и обновления всех мест вызова в
одном изменении, вы можете поместить файл-заполнитель .proto в старое местоположение для
перенаправления всех импортов в новое местоположение с использованием понятия import public.
Обратите внимание, что функциональность публичного импорта недоступна в Java, Kotlin, TypeScript, JavaScript, GCL, а также в целях C++, которые используют статическую рефлексию protobuf.
Зависимости import public могут быть транзитивно использованы любым кодом,
импортирующим proto, содержащий оператор import public. Например:
// new.proto
// Все определения перемещены сюда
// old.proto
// Это proto, которое импортируют все клиенты.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// Вы используете определения из old.proto и new.proto, но не other.proto
Компилятор протокола ищет импортированные файлы в наборе каталогов,
указанных в командной строке компилятора протокола с использованием флага -I/--proto_path.
Если флаг не был задан, он ищет в каталоге, из которого был вызван компилятор.
В общем, вам следует установить флаг --proto_path в корень вашего проекта
и использовать полностью квалифицированные имена для всех импортов.
Видимость символов
Видимость того, какие символы доступны или недоступны при импорте другими
proto, контролируется функцией
features.default_symbol_visibility
и ключевыми словами
export и local,
которые были добавлены в Edition 2024.
Только символы, которые экспортированы, либо через видимость символов по умолчанию, либо с
помощью ключевого слова export, могут быть referenced импортирующим файлом.
Использование типов сообщений proto2 и proto3
Возможно импортировать proto2 и proto3 типы сообщений и использовать их в ваших сообщениях editions, и наоборот.
Вложенные типы
Вы можете определять и использовать типы сообщений внутри других типов сообщений, как в
следующем примере – здесь сообщение Result определено внутри сообщения
SearchResponse:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
Если вы хотите повторно использовать этот тип сообщения вне его родительского типа сообщения, вы
ссылаетесь на него как _Parent_._Type_:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
Вы можете вкладывать сообщения настолько глубоко, насколько хотите. В примере ниже обратите внимание, что
два вложенных типа с именем Inner полностью независимы, поскольку они определены
внутри разных сообщений:
message Outer { // Уровень 0
message MiddleAA { // Уровень 1
message Inner { // Уровень 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Уровень 1
message Inner { // Уровень 2
int32 ival = 1;
bool booly = 2;
}
}
}
Обновление типа сообщения
Если существующий тип сообщения больше не удовлетворяет всем вашим потребностям – например, вы хотите, чтобы формат сообщения имел дополнительное поле – но вы все еще хотите использовать код, созданный со старым форматом, не волнуйтесь! Это очень просто обновить типы сообщений, не нарушая любой из вашего существующего кода, когда вы используете бинарный формат передачи.
note
Если вы используете ProtoJSON или текстовый формат proto
для хранения ваших сообщений protocol buffer, изменения, которые вы можете внести в ваше proto определение, отличаются. Безопасные изменения формата передачи ProtoJSON описаны здесь.
Проверьте Лучшие практики Proto и следующие правила:
Бинарно небезопасные изменения формата передачи
Небезопасные изменения формата передачи – это изменения схемы, которые сломаются, если вы используете разбор данных, которые были сериализованы с использованием старой схемы, с парсером, который использует новую схему (или наоборот). Делайте небезопасные изменения формата передачи только если вы знаете, что все сериализаторы и десериализаторы данных используют новую схему.
- Изменение номеров полей для любого существующего поля небезопасно.
- Изменение номера поля эквивалентно удалению поля и добавлению нового поля с тем же типом. Если вы хотите перенумеровать поле, см. инструкции для удаления поля.
- Перемещение полей в существующий
oneofнебезопасно.
Бинарно безопасные изменения формата передачи
Безопасные изменения формата передачи – это те, при которых полностью безопасно развивать схему таким образом без риска потери данных или новых ошибок разбора.
Обратите внимание, что любые безопасные изменения формата передачи могут быть критическим изменением для кода приложения на данном языке. Например, добавление значения в предсуществующее enum будет критическим изменением компиляции для любого кода с исчерпывающим switch по этому enum. По этой причине Google может избегать внесения некоторых из этих типов изменений в публичные сообщения: AIPs содержат рекомендации о том, какие из этих изменений безопасно делать там.
- Добавление новых полей безопасно.
- Если вы добавляете новые поля, любые сообщения, сериализованные кодом, использующим ваш «старый» формат сообщения, все еще могут быть разобраны вашим новым сгенерированным кодом. Вам следует иметь в виду значения по умолчанию для этих элементов, чтобы новый код мог правильно взаимодействовать с сообщениями, сгенерированными старым кодом. Аналогично, сообщения, созданные вашим новым кодом, могут быть разобраны вашим старым кодом: старые бинарные файлы просто игнорируют новое поле при разборе. См. раздел Неизвестные поля для деталей.
- Удаление полей безопасно.
- Тот же номер поля не должен использоваться снова в вашем обновленном типе сообщения.
Вы можете захотеть переименовать поле вместо этого, возможно, добавив префикс
"OBSOLETE_", или сделать номер поля зарезервированным, чтобы
будущие пользователи вашего
.protoне могли случайно повторно использовать номер.
- Тот же номер поля не должен использоваться снова в вашем обновленном типе сообщения.
Вы можете захотеть переименовать поле вместо этого, возможно, добавив префикс
"OBSOLETE_", или сделать номер поля зарезервированным, чтобы
будущие пользователи вашего
- Добавление дополнительных значений в enum безопасно.
- Изменение одного поля с явным присутствием или расширения на член
нового
oneofбезопасно. - Изменение
oneof, который содержит только одно поле, на поле с явным присутствием безопасно. - Изменение поля на расширение с тем же номером и типом безопасно.
Бинарно совместимые изменения формата передачи (Условно Безопасные)
В отличие от безопасных изменений формата передачи, совместимые означают, что одни и те же данные могут быть разобраны как до, так и после данного изменения. Однако разбор данных может быть потерейным при такой форме изменения. Например, изменение int32 на int64 является совместимым изменением, но если значение больше INT32_MAX записано, клиент, который читает его как int32, отбросит старшие биты числа.
Вы можете вносить совместимые изменения в вашу схему только если вы тщательно управляете развертыванием в вашей системе. Например, вы можете изменить int32 на int64, но обеспечить, чтобы вы продолжали записывать только допустимые значения int32 до тех пор, пока новая схема не будет развернута на всех конечных точках, и затем начать записывать большие значения после этого.
Если ваша схема опубликована за пределами вашей организации, вам обычно не следует вносить совместимые изменения формата передачи, так как вы не можете управлять развертыванием новой схемы, чтобы знать, когда различные диапазоны значений могут быть безопасны для использования.
int32,uint32,int64,uint64иboolвсе совместимы.- Если число разбирается из передачи, которое не помещается в соответствующий тип, вы получите тот же эффект, как если бы вы привели число к этому типу в C++ (например, если 64-битное число читается как int32, оно будет усечено до 32 бит).
sint32иsint64совместимы друг с другом, но не совместимы с другими целочисленными типами.- Если записанное значение было между INT_MIN и INT_MAX включительно, оно будет разобрано как то же значение с любым типом. Если значение sint64 было записано вне этого диапазона и разобрано как sint32, varint усекается до 32 бит, а затем происходит zigzag декодирование (что вызовет наблюдение другого значения).
stringиbytesсовместимы, пока байты являются допустимым UTF-8.- Встроенные сообщения совместимы с
bytes, если байты содержат закодированный экземпляр сообщения. fixed32совместим сsfixed32, иfixed64сsfixed64.- Для
string,bytesи полей сообщений, одиночное совместимо сrepeated.- При получении сериализованных данных повторяющегося поля в качестве входных данных, клиенты, которые ожидают это поле одиночным, возьмут последнее входное значение, если это поле примитивного типа, или объединят все входные элементы, если это тип сообщения. Обратите внимание, что это не вообще безопасно для числовых типов, включая bools и enums. Повторяющиеся поля числовых типов сериализуются в упакованном формате по умолчанию, который не будет правильно разобран, когда ожидается одиночное поле.
enumсовместим сint32,uint32,int64иuint64- Имейте в виду, что клиентский код может обрабатывать их по-разному, когда сообщение
десериализуется: например, нераспознанные значения proto3
enumбудут сохранены в сообщении, но то, как это представлено, когда сообщение десериализуется, зависит от языка.
- Имейте в виду, что клиентский код может обрабатывать их по-разному, когда сообщение
десериализуется: например, нераспознанные значения proto3
- Изменение поля между
map<K, V>и соответствующимrepeatedполем сообщения бинарно совместимо (см. Карты ниже, для макета сообщения и других ограничений).- Однако безопасность изменения зависит от приложения: при
десериализации и повторной сериализации сообщения клиенты, использующие определение
repeatedполя, произведут семантически идентичный результат; однако, клиенты, использующие определениеmapполя, могут переупорядочить записи и отбросить записи с дублирующимися ключами.
- Однако безопасность изменения зависит от приложения: при
десериализации и повторной сериализации сообщения клиенты, использующие определение
Неизвестные поля
Неизвестные поля – это правильно сформированные сериализованные данные protocol buffer, представляющие поля, которые парсер не распознает. Например, когда старый бинарный файл разбирает данные, отправленные новым бинарным файлом с новыми полями, эти новые поля становятся неизвестными полями в старом бинарном файле.
Сообщения Editions сохраняют неизвестные поля и включают их во время разбора и в сериализованном выводе, что соответствует поведению proto2 и proto3.
Сохранение неизвестных полей
Некоторые действия могут привести к потере неизвестных полей. Например, если вы сделаете одно из следующего, неизвестные поля теряются:
- Сериализуете proto в JSON.
- Итерируетесь по всем полям в сообщении, чтобы заполнить новое сообщение.
Чтобы избежать потери неизвестных полей, делайте следующее:
- Используйте бинарный формат; избегайте использования текстовых форматов для обмена данными.
- Используйте API, ориентированные на сообщения, такие как
CopyFrom()иMergeFrom(), для копирования данных, а не копирования поле за полем.
TextFormat – это особый случай. Сериализация в TextFormat печатает неизвестные поля, используя их номера полей. Но разбор данных TextFormat обратно в бинарный proto завершается неудачей, если есть записи, которые используют номера полей.
Расширения
Расширение – это поле, определенное вне своего контейнерного сообщения; обычно в
файле .proto, отдельном от файла .proto контейнерного сообщения.
Зачем использовать расширения?
Есть две основные причины использовать расширения:
- Файл
.protoконтейнерного сообщения будет иметь меньше импортов/зависимостей. Это может улучшить время сборки, разорвать циклические зависимости и иным образом способствовать слабой связанности. Расширения очень хороши для этого. - Позволяют системам прикреплять данные к контейнерному сообщению с минимальной зависимостью
и координацией. Расширения не являются отличным решением для этого из-за
ограниченного пространства номеров полей и
Последствий повторного использования номеров полей. Если ваш случай использования
требует очень низкой координации для большого количества расширений, рассмотрите
использование
типа сообщения
Anyвместо этого.
Пример расширения
Использование расширения – это двухэтапный процесс. Сначала, в сообщении, которое вы хотите расширить ( «контейнер»), вы должны зарезервировать диапазон номеров полей для расширений. Затем, в отдельном файле, вы определяете само поле расширения.
Вот пример, который показывает, как добавить расширение для видео с котятами в универсальное сообщение UserContent.
Шаг 1: Зарезервируйте диапазон расширений в контейнерном сообщении.
Контейнерное сообщение должно использовать ключевое слово extensions для резервирования диапазона
номеров полей для использования другими. Это лучшая практика – также добавить
declaration для конкретного расширения, которое вы планируете добавить. Это объявление действует
как предварительное объявление, упрощая разработчикам обнаружение расширений и избежание повторного использования номеров полей.
// media/user_content.proto
edition = "2023";
package media;
// Контейнер для пользовательского контента.
message UserContent {
extensions 100 to 199 [
declaration = {
number: 126,
full_name: ".kittens.kitten_videos",
type: ".kittens.Video",
repeated: true
}
];
}
Это объявление указывает номер поля, полное имя, тип и мощность расширения, которое будет определено в другом месте.
Шаг 2: Определите расширение в отдельном файле.
Само расширение определяется в другом файле .proto, который обычно
фокусируется на конкретной функции (например, видео с котятами). Это позволяет избежать добавления зависимости от универсального контейнера к конкретной функции.
// kittens/video_ext.proto
edition = "2023";
import "media/user_content.proto"; // Импортирует контейнерное сообщение
import "kittens/video.proto"; // Импортирует тип сообщения расширения
package kittens;
// Это определяет поле расширения.
extend media.UserContent {
repeated Video kitten_videos = 126;
}
Блок extend связывает новое поле kitten_videos обратно с
сообщением media.UserContent, используя номер поля 126, который был зарезервирован в
контейнере.
Нет разницы в кодировании формата передачи полей расширений по сравнению со стандартным полем с тем же номером поля, типом и мощностью. Следовательно, безопасно перемещать стандартное поле из его контейнера, чтобы быть расширением, или перемещать поле расширения в его контейнерное сообщение как стандартное поле, при условии, что номер поля, тип и мощность остаются постоянными.
Однако, поскольку расширения определены вне контейнерного сообщения, никакие
специализированные методы доступа не генерируются для получения и установки конкретных полей расширений.
Для нашего примера компилятор protobuf не сгенерирует методы доступа AddKittenVideos()
или GetKittenVideos(). Вместо этого доступ к расширениям осуществляется через
параметризованные функции, такие как: HasExtension(), ClearExtension(),
GetExtension(), MutableExtension() и AddExtension().
В C++ это будет выглядеть примерно так:
UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetRepeatedExtension(kittens::kitten_videos).size());
user_content.GetRepeatedExtension(kittens::kitten_videos)[0];
Определение диапазонов расширений
Если вы являетесь владельцем контейнерного сообщения, вам нужно будет определить диапазон расширений для расширений вашего сообщения.
Номера полей, выделенные для полей расширений, не могут быть повторно использованы для стандартных полей.
Безопасно расширять диапазон расширений после его определения. Хорошим значением по умолчанию является выделение 1000 относительно маленьких чисел и плотное заполнение этого пространства с использованием объявлений расширений:
message ModernExtendableMessage {
// Все расширения в этом диапазоне должны использовать объявления расширений.
extensions 1000 to 2000 [verification = DECLARATION];
}
При добавлении диапазона для объявлений расширений до фактических расширений, вы
должны добавить verification = DECLARATION, чтобы обеспечить использование объявлений
для этого нового диапазона. Этот заполнитель может быть удален, как только будет добавлено фактическое объявление.
Безопасно разделить существующий диапазон расширений на отдельные диапазоны, которые покрывают тот же общий диапазон. Это может быть необходимо для миграции унаследованного типа сообщения на Объявления расширений. Например, до миграции диапазон может быть определен как:
message LegacyMessage {
extensions 1000 to max;
}
И после миграции (разделения диапазона) это может быть:
message LegacyMessage {
// Унаследованный диапазон, который использовал непроверенную схему выделения.
extensions 1000 to 524999999 [verification = UNVERIFIED];
// Текущий диапазон, который использует объявления расширений.
extensions 525000000 to max [verification = DECLARATION];
}
Небезопасно увеличивать начальный номер поля или уменьшать конечный номер поля для перемещения или сокращения диапазона расширений. Эти изменения могут сделать недействительным существующее расширение.
Предпочитайте использовать номера полей от 1 до 15 для стандартных полей, которые заполняются в большинстве экземпляров вашего proto. Не рекомендуется использовать эти номера для расширений.
Если ваше соглашение о нумерации может включать расширения с очень большими номерами полей, вы можете указать, что ваш диапазон расширений доходит до максимально возможного номера поля, используя ключевое слово max:
message Foo {
extensions 1000 to max;
}
max это 229 - 1, или 536,870,911.
Выбор номеров расширений
Расширения – это просто поля, которые могут быть указаны вне их контейнерных сообщений. Все те же правила для Назначения номеров полей применяются к номерам полей расширений. Те же Последствия повторного использования номеров полей также применяются к повторному использованию номеров полей расширений.
Выбор уникальных номеров полей расширений прост, если контейнерное сообщение использует объявления расширений. При определении нового расширения выберите наименьший номер поля выше всех других объявлений из самого высокого диапазона расширений, определенного в контейнерном сообщении. Например, если контейнерное сообщение определено так:
message Container {
// Унаследованный диапазон, который использовал непроверенную схему выделения
extensions 1000 to 524999999;
// Текущий диапазон, который использует объявления расширений. (самый высокий диапазон расширений)
extensions 525000000 to max [
declaration = {
number: 525000001,
full_name: ".bar.baz_ext",
type: ".bar.Baz"
}
// 525,000,002 - это наименьший номер поля выше всех других объявлений
];
}
Следующее расширение Container должно добавить новое объявление с номером
525000002.
Непроверенное выделение номеров расширений (не рекомендуется)
Владелец контейнерного сообщения может отказаться от объявлений расширений в пользу своей собственной непроверенной стратегии выделения номеров расширений.
Непроверенная схема выделения использует механизм, внешний по отношению к экосистеме protobuf, для выделения номеров полей расширений в выбранном диапазоне расширений. Одним из примеров может быть использование номера коммита монорепозитория. Эта система «непроверена» с точки зрения компилятора protobuf, поскольку нет способа проверить, что расширение использует правильно полученный номер поля расширения.
Преимущество непроверенной системы над проверенной системой, такой как объявления расширений, – это возможность определить расширение без координации с владельцем контейнерного сообщения.
Недостатком непроверенной системы является то, что компилятор protobuf не может защитить участников от повторного использования номеров полей расширений.
Непроверенные стратегии выделения номеров полей расширений не рекомендуются,
потому что Последствия повторного использования номеров полей ложатся на всех
расширителей сообщения (а не только на разработчика, который не следовал
рекомендациям). Если ваш случай использования требует очень низкой координации, рассмотрите
использование
сообщения Any
вместо этого.
Непроверенные стратегии выделения номеров полей расширений ограничены диапазоном от 1 до 524,999,999. Номера полей 525,000,000 и выше могут использоваться только с объявлениями расширений.
Указание типов расширений
Расширения могут быть любого типа поля, кроме oneof и map.
Вложенные расширения (не рекомендуется)
Вы можете объявлять расширения в области видимости другого сообщения:
import "common/user_profile.proto";
package puppies;
message Photo {
extend common.UserProfile {
int32 likes_count = 111;
}
...
}
В этом случае код C++ для доступа к этому расширению:
UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);
Другими словами, единственный эффект заключается в том, что likes_count определен в области видимости puppies.Photo.
Это является частым источником путаницы: Объявление блока extend, вложенного внутрь
типа сообщения, не подразумевает никаких отношений между внешним типом и расширяемым типом. В частности, предыдущий пример не означает, что Photo
является каким-либо подклассом UserProfile. Все это означает, что символ
likes_count объявлен внутри области видимости Photo; это просто статический
член.
Распространенным шаблоном является определение расширений внутри области видимости
типа поля расширения – например, вот расширение для media.UserContent типа
puppies.Photo, где расширение определено как часть Photo:
import "media/user_content.proto";
package puppies;
message Photo {
extend media.UserContent {
Photo puppy_photo = 127;
}
...
}
Однако нет требования, чтобы расширение с типом сообщения было определено внутри этого типа. Вы также можете использовать стандартный шаблон определения:
import "media/user_content.proto";
package puppies;
message Photo {
...
}
// Это может быть даже в другом файле.
extend media.UserContent {
Photo puppy_photo = 127;
}
Этот стандартный (файловый уровень) синтаксис предпочтительнее, чтобы избежать путаницы. Вложенный синтаксис часто ошибочно принимается за наследование пользователями, которые еще не знакомы с расширениями.
Any
Тип сообщения Any позволяет вам использовать сообщения как встроенные типы без наличия
их определения .proto. Any содержит произвольное сериализованное сообщение как
bytes, вместе с URL, который действует как глобально уникальный идентификатор и
разрешается к типу этого сообщения. Чтобы использовать тип Any, вам нужно
импортировать google/protobuf/any.proto.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
URL типа по умолчанию для данного типа сообщения:
type.googleapis.com/_packagename_._messagename_.
Различные языковые реализации будут поддерживать вспомогательные библиотеки времени выполнения для упаковки
и распаковки значений Any типобезопасным образом – например, в Java тип Any
будет иметь специальные методы доступа pack() и unpack(), в то время как в C++ есть
методы PackFrom() и UnpackTo():
// Хранение произвольного типа сообщения в Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Чтение произвольного сообщения из Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... обработка network_error ...
}
}
Если вы хотите ограничить содержащиеся сообщения небольшим количеством типов и
требовать разрешение перед добавлением новых типов в список, рассмотрите использование
расширений с
объявлениями расширений
вместо типов сообщений Any.
Oneof
Если у вас есть сообщение со многими одиночными полями и где не более одного поля будет установлено одновременно, вы можете обеспечить это поведение и сэкономить память, используя функцию oneof.
Поля oneof похожи на одиночные поля, за исключением того, что все поля в oneof разделяют
память, и не более одного поля может быть установлено одновременно. Установка любого члена
oneof автоматически очищает всех других членов. Вы можете проверить, какое значение
в oneof установлено (если есть), используя специальный метод case() или WhichOneof(),
в зависимости от выбранного вами языка.
Обратите внимание, что если установлено несколько значений, последнее установленное значение, определенное порядком в proto, перезапишет все предыдущие.
Номера полей для полей oneof должны быть уникальными в пределах заключающего сообщения.
Использование Oneof
Чтобы определить oneof в вашем .proto, вы используете ключевое слово oneof, за которым следует ваше
имя oneof, в этом случае test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Затем вы добавляете ваши поля oneof в определение oneof. Вы можете добавлять поля
любого типа, кроме полей map и repeated. Если вам нужно добавить
повторяющееся поле в oneof, вы можете использовать сообщение, содержащее повторяющееся поле.
В вашем сгенерированном коде поля oneof имеют те же геттеры и сеттеры, что и обычные поля. Вы также получаете специальный метод для проверки, какое значение (если есть) в oneof установлено. Вы можете узнать больше об API oneof для вашего выбранного языка в соответствующем справочнике по API.
Особенности Oneof
-
Установка поля oneof автоматически очистит всех других членов oneof. Так что если вы установите несколько полей oneof, только последнее поле, которое вы установили, все еще будет иметь значение.
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); // Вызов mutable_sub_message() очистит поле name и установит // sub_message в новый экземпляр SubMessage без установленных полей. message.mutable_sub_message(); CHECK(!message.has_name()); -
Если парсер встречает несколько членов одного и того же oneof в передаче, только последний увиденный член используется в разобранном сообщении. При разборе данных в передаче, начиная с начала байтов, оцените следующее значение и примените следующие правила разбора:
-
Сначала проверьте, установлено ли другое поле в том же oneof в настоящее время, и если да, очистите его.
-
Затем примените содержимое, как если бы поле не было в oneof:
- Примитив перезапишет любое уже установленное значение
- Сообщение объединится с любым уже установленным значением
-
-
Расширения не поддерживаются для oneof.
-
Oneof не может быть
repeated. -
Reflection API работают для полей oneof.
-
Если вы установите поле oneof в значение по умолчанию (например, установите поле oneof int32 в 0), «случай» этого поля oneof будет установлен, и значение будет сериализовано в передаче.
-
Если вы используете C++, убедитесь, что ваш код не вызывает сбоев памяти. Следующий пример кода приведет к сбою, потому что
sub_messageуже был удален вызовом методаset_name().SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Удалит sub_message sub_message->set_... // Сбой здесь -
Снова в C++, если вы
Swap()два сообщения с oneof, каждое сообщение окажется со случаем oneof другого: в примере нижеmsg1будет иметьsub_messageиmsg2будет иметьname.SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK(msg2.has_name());
Проблемы обратной совместимости
Будьте осторожны при добавлении или удалении полей oneof. Если проверка значения oneof возвращает None/NOT_SET, это может означать, что oneof не был установлен или
он был установлен в поле в другой версии oneof. Нет способа
отличить разницу, поскольку нет способа узнать, является ли неизвестное поле в
передаче членом oneof.
Проблемы повторного использования тегов
- Перемещение одиночных полей в oneof или из oneof: Вы можете потерять некоторую вашу информацию (некоторые поля будут очищены) после того, как сообщение будет сериализовано и разобрано. Однако, вы можете безопасно переместить одно поле в новый oneof и, возможно, сможете переместить несколько полей, если известно, что только одно когда-либо установлено. См. Обновление типа сообщения для дальнейших деталей.
- Удаление поля oneof и его возвращение: Это может очистить ваше текуще установленное поле oneof после сериализации и разбора сообщения.
- Разделение или объединение oneof: Это имеет проблемы, похожие на перемещение одиночных полей.
Карты
Если вы хотите создать ассоциативную карту как часть вашего определения данных, protocol buffers предоставляет удобный сокращенный синтаксис:
map<key_type, value_type> map_field = N;
...где key_type может быть любым целочисленным или строковым типом (так, любой
скалярный тип, кроме типов с плавающей точкой и bytes). Обратите внимание, что
ни enum, ни proto сообщения не допустимы для key_type.
value_type может быть любым типом, кроме другой карты.
Итак, например, если вы хотите создать карту проектов, где каждое сообщение Project
связано со строковым ключом, вы можете определить это так:
map<string, Project> projects = 3;
Особенности карт
- Расширения не поддерживаются для карт.
- Поля карт не могут быть
repeated. - Порядок в формате передачи и порядок итерации карты значений не определен, поэтому вы не можете полагаться на то, что элементы вашей карты находятся в определенном порядке.
- При генерации текстового формата для
.protoкарты сортируются по ключу. Числовые ключи сортируются численно. - При разборе из передачи или при слиянии, если есть дублирующиеся ключи карты, используется последний увиденный ключ. При разборе карты из текстового формата разбор может завершиться неудачей, если есть дублирующиеся ключи.
- Если вы предоставляете ключ, но не значение для поля карты, поведение при сериализации поля зависит от языка. В C++, Java, Kotlin и Python сериализуется значение по умолчанию для типа, в то время как в других языках ничего не сериализуется.
- Никакой символ
FooEntryне может существовать в той же области видимости, что и картаfoo, потому чтоFooEntryуже используется реализацией карты.
Сгенерированный API карт в настоящее время доступен для всех поддерживаемых языков. Вы можете узнать больше об API карт для вашего выбранного языка в соответствующем справочнике по API.
Обратная совместимость
Синтаксис карт эквивалентен следующему в передаче, поэтому реализации protocol buffers, которые не поддерживают карты, все еще могут обрабатывать ваши данные:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Любая реализация protocol buffers, которая поддерживает карты, должна и производить, и принимать данные, которые могут быть приняты предыдущим определением.
Пакеты
Вы можете добавить необязательный спецификатор package в файл .proto, чтобы предотвратить конфликты имен
между типами сообщений протокола.
package foo.bar;
message Open { ... }
Затем вы можете использовать спецификатор пакета при определении полей вашего типа сообщения:
message Foo {
...
foo.bar.Open open = 1;
...
}
Способ, которым спецификатор пакета влияет на сгенерированный код, зависит от вашего выбранного языка:
- В C++ сгенерированные классы обернуты внутри пространства имен C++. Например,
Openбудет в пространстве именfoo::bar. - В Java и Kotlin пакет используется как Java пакет, если только
вы явно не предоставите
option java_packageв вашем файле.proto. - В Python директива
packageигнорируется, поскольку модули Python организованы в соответствии с их расположением в файловой системе. - В Go директива
packageигнорируется, и сгенерированный файл.pb.goнаходится в пакете, названном в соответствии с соответствующим правилом Bazelgo_proto_library. Для проектов с открытым исходным кодом вы должны предоставить либо опциюgo_package, либо установить флаг Bazel-M. - В Ruby сгенерированные классы обернуты внутри вложенных пространств имен Ruby,
преобразованных в требуемый стиль капитализации Ruby (первая
буква заглавная; если первый символ не буква, добавляется
PB_). Например,Openбудет в пространстве именFoo::Bar. - В PHP пакет используется как пространство имен после преобразования в
PascalCase, если вы явно не предоставите
option php_namespaceв вашем файле.proto. Например,Openбудет в пространстве именFoo\Bar. - В C# пакет используется как пространство имен после преобразования в
PascalCase, если вы явно не предоставите
option csharp_namespaceв вашем файле.proto. Например,Openбудет в пространстве именFoo.Bar.
Обратите внимание, что даже когда директива package не влияет напрямую на
сгенерированный код, например в Python, все равно настоятельно рекомендуется
указывать пакет для файла .proto, так как в противном случае это может привести к конфликтам имен
в дескрипторах и сделать proto непереносимым для других языков.
Пакеты и разрешение имен
Разрешение имен типов в языке protocol buffer работает как в C++: сначала
ищется самая внутренняя область, затем следующая по внутренности и так далее, причем каждый
пакет считается «внутренним» по отношению к своему родительскому пакету. Начальная '.' (например,
.foo.bar.Baz) означает начать с самой внешней области вместо этого.
Компилятор protocol buffer разрешает все имена типов, разбирая импортированные
файлы .proto. Генератор кода для каждого языка знает, как ссылаться на каждый
тип на этом языке, даже если он имеет разные правила области видимости.
Определение сервисов
Если вы хотите использовать ваши типы сообщений с системой RPC (Удаленный вызов процедур),
вы можете определить интерфейс RPC сервиса в файле .proto, и
компилятор protocol buffer сгенерирует код интерфейса сервиса и заглушки на вашем выбранном языке. Итак, например, если вы хотите определить RPC сервис с
методом, который принимает ваш SearchRequest и возвращает SearchResponse, вы можете
определить это в вашем файле .proto следующим образом:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
Наиболее straightforward RPC система для использования с protocol buffers – это
gRPC: независимая от языка и платформы открытая RPC система,
разработанная в Google. gRPC особенно хорошо работает с protocol buffers и позволяет
вам генерировать соответствующий RPC код непосредственно из ваших файлов .proto с помощью
специального плагина компилятора protocol buffer.
Если вы не хотите использовать gRPC, также возможно использовать protocol buffers с вашей собственной RPC реализацией. Вы можете узнать больше об этом в Руководстве по языку Proto2.
Также существует ряд сторонних проектов по разработке RPC реализаций для Protocol Buffers. Для списка ссылок на проекты, о которых мы знаем, см. вики-страницу сторонних дополнений.
Отображение JSON
Стандартный бинарный формат передачи protobuf является предпочтительным форматом сериализации для общения между двумя системами, которые используют protobuf. Для общения с системами, которые используют JSON, а не формат передачи protobuf, Protobuf поддерживает каноническое кодирование в ProtoJSON.
Опции
Отдельные объявления в файле .proto могут быть аннотированы рядом
опций. Опции не меняют общее значение объявления, но могут
влиять на способ его обработки в определенном контексте. Полный список доступных опций определен в /google/protobuf/descriptor.proto.
Некоторые опции являются опциями уровня файла, что означает, что они должны быть записаны на верхнем уровне, не внутри любого сообщения, enum или определения сервиса. Некоторые опции являются опциями уровня сообщения, что означает, что они должны быть записаны внутри определений сообщений. Некоторые опции являются опциями уровня поля, что означает, что они должны быть записаны внутри определений полей. Опции также могут быть записаны на типах перечислений, значениях перечислений, полях oneof, типах сервисов и методах сервисов; однако, в настоящее время нет полезных опций для любого из этих.
Вот несколько наиболее часто используемых опций:
-
java_package(опция файла): Пакет, который вы хотите использовать для ваших сгенерированных Java/Kotlin классов. Если явная опцияjava_packageне задана в файле.proto, то по умолчанию будет использоваться proto пакет (указанный с помощью ключевого слова "package" в файле.proto). Однако proto пакеты обычно не являются хорошими Java пакетами, поскольку proto пакеты не ожидаются начинающимися с обратных доменных имен. Если не генерируется Java или Kotlin код, эта опция не имеет эффекта.option java_package = "com.example.foo"; -
java_outer_classname(опция файла): Имя класса (и, следовательно, имя файла) для класса-обертки Java, который вы хотите сгенерировать. Если явныйjava_outer_classnameне указан в файле.proto, имя класса будет сконструировано путем преобразования имени файла.protoв camel-case (такfoo_bar.protoстановитсяFooBar.java). Если опцияjava_multiple_filesотключена, то все другие классы/enums/etc., сгенерированные для файла.proto, будут сгенерированы внутри этого внешнего класса-обертки Java как вложенные классы/enums/etc. Если не генерируется Java код, эта опция не имеет эффекта.option java_outer_classname = "Ponycopter"; -
java_multiple_files(опция файла): Если false, будет сгенерирован только один файл.javaдля этого файла.proto, и все Java классы/enums/etc., сгенерированные для сообщений верхнего уровня, сервисов и перечислений, будут вложены внутри внешнего класса (см.java_outer_classname). Если true, отдельные файлы.javaбудут сгенерированы для каждого из Java классов/enums/etc., сгенерированных для сообщений верхнего уровня, сервисов и перечислений, и класс-обертка Java, сгенерированный для этого файла.proto, не будет содержать никаких вложенных классов/enums/etc. Это логическая опция, которая по умолчаниюfalse. Если не генерируется Java код, эта опция не имеет эффекта. Это было удалено в edition 2024 и заменено наfeatures.(pb.java).nest_in_file_classoption java_multiple_files = true; -
optimize_for(опция файла): Может быть установлена вSPEED,CODE_SIZEилиLITE_RUNTIME. Это влияет на генераторы кода C++ и Java (и возможно, сторонние генераторы) следующим образом:SPEED(по умолчанию): Компилятор protocol buffer сгенерирует код для сериализации, разбора и выполнения других общих операций над вашими типами сообщений. Этот код высоко оптимизирован.CODE_SIZE: Компилятор protocol buffer сгенерирует минимальные классы и будет полагаться на общий, основанный на рефлексии код для реализации сериализации, разбора и различных других операций. Сгенерированный код будет таким образом намного меньше, чем сSPEED, но операции будут медленнее. Классы все еще будут реализовывать точно такой же публичный API, как они делают в режимеSPEED. Этот режим наиболее полезен в приложениях, которые содержат очень большое количество файлов.protoи не нуждаются в том, чтобы все они были ослепительно быстрыми.LITE_RUNTIME: Компилятор protocol buffer сгенерирует классы, которые зависят только от «облегченной» библиотеки времени выполнения (libprotobuf-liteвместоlibprotobuf). Облегченная среда выполнения намного меньше, чем полная библиотека (примерно на порядок меньше), но опускает определенные функции, такие как дескрипторы и рефлексия. Это особенно полезно для приложений, работающих на ограниченных платформах, таких как мобильные телефоны. Компилятор все еще будет генерировать быстрые реализации всех методов, как он делает в режимеSPEED. Сгенерированные классы будут реализовывать только интерфейсMessageLiteв каждом языке, который предоставляет только подмножество методов полного интерфейсаMessage.
option optimize_for = CODE_SIZE; -
cc_generic_services,java_generic_services,py_generic_services(опции файла): Универсальные сервисы устарели. Должен ли компилятор protocol buffer генерировать абстрактный сервисный код на основе определений сервисов в C++, Java и Python, соответственно. По legacy причинам, они по умолчаниюtrue. Однако, начиная с версии 2.3.0 (январь 2010), считается предпочтительным, чтобы RPC реализации предоставляли плагины генератора кода для генерации кода, более специфичного для каждой системы, а не полагаться на «абстрактные» сервисы.// Этот файл полагается на плагины для генерации сервисного кода. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false; -
cc_enable_arenas(опция файла): Включает выделение арены для C++ сгенерированного кода. -
objc_class_prefix(опция файла): Устанавливает префикс класса Objective-C, который добавляется ко всем сгенерированным классам и enum Objective-C из этого .proto. Значения по умолчанию нет. Вы должны использовать префиксы, которые состоят из 3-5 заглавных символов, как рекомендовано Apple. Обратите внимание, что все 2-буквенные префиксы зарезервированы Apple. -
packed(опция поля): В protobuf editions эта опция заблокирована наtrue. Чтобы использовать неупакованный формат передачи, вы можете переопределить эту опцию, используя функцию editions. Это обеспечивает совместимость с парсерами до версии 2.3.0 (редко требуется), как показано в следующем примере:repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED]; -
deprecated(опция поля): Если установлено вtrue, указывает, что поле устарело и не должно использоваться новым кодом. В большинстве языков это не имеет фактического эффекта. В Java это становится аннотацией@Deprecated. Для C++, clang-tidy будет генерировать предупреждения всякий раз, когда используются устаревшие поля. В будущем другие языко-специфичные генераторы кода могут генерировать аннотации устаревания на методах доступа поля, что, в свою очередь, вызовет предупреждение при компиляции кода, который пытается использовать поле. Если поле не используется никем и вы хотите предотвратить использование его новыми пользователями, рассмотрите замену объявления поля на зарезервированное утверждение.int32 old_field = 6 [deprecated = true];
Опции значений перечисления
Опции значений перечисления поддерживаются. Вы можете использовать опцию deprecated для
указания, что значение больше не должно использоваться. Вы также можете создавать пользовательские
опции, используя расширения.
Следующий пример показывает синтаксис для добавления этих опций:
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
Код C++ для чтения опции string_name может выглядеть примерно так:
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
См. Пользовательские опции, чтобы узнать, как применять пользовательские опции к значениям перечисления и к полям.
Пользовательские опции
Protocol Buffers также позволяет вам определять и использовать ваши собственные опции. Обратите внимание, что это продвинутая функция, которая большинству людей не нужна. Если вы все же думаете, что вам нужно создать свои собственные опции, см. Руководство по языку Proto2 для деталей. Обратите внимание, что создание пользовательских опций использует расширения.
Начиная с edition 2024, импортируйте определения пользовательских опций с помощью import option. См. Импорт.
Сохранение опций
Опции имеют понятие сохранения (retention), которое контролирует, сохраняется ли опция
в сгенерированном коде. Опции имеют сохранение времени выполнения по умолчанию,
что означает, что они сохраняются в сгенерированном коде и, таким образом, видны во время выполнения в сгенерированном пуле дескрипторов. Однако вы можете установить retention = RETENTION_SOURCE, чтобы указать, что опция (или поле внутри опции) не должна сохраняться во время выполнения. Это называется сохранением исходного кода.
Сохранение опций – это продвинутая функция, о которой большинству пользователей не нужно беспокоиться,
но она может быть полезна, если вы хотите использовать определенные опции без
оплаты стоимости размера кода за их сохранение в ваших бинарных файлах. Опции с
сохранением исходного кода все еще видны protoc и плагинам protoc, поэтому
генераторы кода могут использовать их для настройки своего поведения.
Сохранение может быть установлено непосредственно на опции, вот так:
extend google.protobuf.FileOptions {
int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
Оно также может быть установлено на простом поле, в этом случае оно вступает в силу только когда это поле появляется внутри опции:
message OptionsMessage {
int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
Вы можете установить retention = RETENTION_RUNTIME, если хотите, но это не имеет эффекта,
поскольку это поведение по умолчанию. Когда поле сообщения помечено
RETENTION_SOURCE, все его содержимое отбрасывается; поля внутри него не могут
переопределить это, пытаясь установить RETENTION_RUNTIME.
note
По состоянию на Protocol Buffers 22.0, поддержка сохранения опций все еще находится в разработке, и
только C++ и Java поддерживаются. Go имеет поддержку, начиная с 1.29.0. Поддержка Python завершена, но еще не попала в релиз.
Цели опций
Поля имеют опцию targets, которая контролирует типы сущностей, к которым
поле может применяться при использовании в качестве опции. Например, если поле имеет
targets = TARGET_TYPE_MESSAGE, то это поле не может быть установлено в пользовательской опции
на enum (или любой другой не-сообщенной сущности). Protoc обеспечивает это и будет
выдавать ошибку, если есть нарушение ограничений цели.
На первый взгляд, эта функция может показаться ненужной, учитывая, что каждая пользовательская опция является расширением сообщения опций для конкретной сущности, что уже ограничивает опцию этой одной сущностью. Однако цели опций полезны в случае, когда у вас есть общее сообщение опций, применяемое к нескольким типам сущностей, и вы хотите контролировать использование отдельных полей в этом сообщении. Например:
message MyOptions {
string file_only_option = 1 [targets = TARGET_TYPE_FILE];
int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
MyOptions enum_options = 50000;
}
// OK: это поле разрешено в опциях файла
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: это поле разрешено в опциях сообщения и enum
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Ошибка: file_only_option не может быть установлен на enum.
option (enum_options).file_only_option = "xyz";
}
Генерация ваших классов
Чтобы сгенерировать Java, Kotlin, Python, C++, Go, Ruby, Objective-C или C# код,
который вам нужен для работы с типами сообщений, определенными в файле .proto, вам
нужно запустить компилятор protocol buffer protoc на файле .proto. Если вы
не установили компилятор,
загрузите пакет и следуйте
инструкциям в README. Для Go вам также нужно установить специальный плагин генератора кода для компилятора; вы можете найти его и инструкции по установке в репозитории golang/protobuf на GitHub.
Компилятор протокола вызывается следующим образом:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATHуказывает каталог, в котором нужно искать файлы.protoпри разрешении директивimport. Если опущено, используется текущий каталог. Несколько каталогов импорта могут быть указаны путем передачи опции--proto_pathнесколько раз; они будут искаться в порядке.-I=_IMPORT_PATH_может быть использована как короткая форма--proto_path.
Примечание: Пути к файлам относительно их proto_path должны быть глобально уникальными в данном
бинарном файле. Например, если у вас есть proto/lib1/data.proto и
proto/lib2/data.proto, эти два файла не могут быть использованы вместе с
-I=proto/lib1 -I=proto/lib2, потому что будет неоднозначно, какой файл import "data.proto" будет означать. Вместо этого следует использовать -Iproto/
и глобальные имена будут lib1/data.proto и lib2/data.proto.
Если вы публикуете библиотеку и другие пользователи могут использовать ваши сообщения напрямую,
вы должны включить уникальное имя библиотеки в путь, под которым они ожидаются
использоваться, чтобы избежать конфликтов имен файлов. Если у вас несколько каталогов в
одном проекте, лучшей практикой является предпочтение установки одного -I в каталог верхнего уровня проекта.
-
Вы можете предоставить одну или несколько директив вывода:
--cpp_outгенерирует C++ код вDST_DIR. См. Справочник по сгенерированному C++ коду для получения дополнительной информации.--java_outгенерирует Java код вDST_DIR. См. Справочник по сгенерированному Java коду для получения дополнительной информации.--kotlin_outгенерирует дополнительный Kotlin код вDST_DIR. См. Справочник по сгенерированному Kotlin коду для получения дополнительной информации.--python_outгенерирует Python код вDST_DIR. См. Справочник по сгенерированному Python коду для получения дополнительной информации.--go_outгенерирует Go код вDST_DIR. См. Справочник по сгенерированному Go коду для получения дополнительной информации.--ruby_outгенерирует Ruby код вDST_DIR. См. Справочник по сгенерированному Ruby коду для получения дополнительной информации.--objc_outгенерирует Objective-C код вDST_DIR. См. Справочник по сгенерированному Objective-C коду для получения дополнительной информации.--csharp_outгенерирует C# код вDST_DIR. См. Справочник по сгенерированному C# коду для получения дополнительной информации.--php_outгенерирует PHP код вDST_DIR. См. Справочник по сгенерированному PHP коду для получения дополнительной информации.
В качестве дополнительного удобства, если
DST_DIRзаканчивается на.zipили.jar, компилятор запишет вывод в один архивный файл формата ZIP с заданным именем. Вывод.jarтакже получит файл манифеста, как требуется спецификацией Java JAR. Обратите внимание, что если выходной архив уже существует, он будет перезаписан. -
Вы должны предоставить один или несколько файлов
.protoв качестве входных данных. Несколько файлов.protoмогут быть указаны сразу. Хотя файлы названы относительно текущего каталога, каждый файл должен находиться в одном изIMPORT_PATH, чтобы компилятор мог определить его каноническое имя.
Расположение файлов
Предпочитайте не помещать файлы .proto в тот же
каталог, что и другие языковые исходники. Рассмотрите
создание подпакета proto для файлов .proto под корневым пакетом для
вашего проекта.
Расположение должно быть независимым от языка
При работе с Java кодом удобно помещать связанные файлы .proto в
тот же каталог, что и Java исходники. Однако, если какой-либо не-Java код когда-либо использует те же
protos, префикс пути больше не будет иметь смысла. Поэтому
в общем, помещайте protos в связанный независимый от языка каталог, такой как
//myteam/mypackage.
Исключением из этого правила является случай, когда ясно, что protos будут использоваться только в Java контексте, например, для тестирования.
Поддерживаемые платформы
Для информации о:
- операционных системах, компиляторах, системах сборки и версиях C++, которые поддерживаются, см. Основная политика поддержки C++.
- версиях PHP, которые поддерживаются, см. Поддерживаемые версии PHP.
Руководство по языку (proto 2)
Охватывает, как использовать редакцию proto2 языка Protocol Buffers в вашем проекте.
Это руководство описывает, как использовать язык protocol buffer для структурирования ваших
данных protocol buffer, включая синтаксис файлов .proto и как генерировать классы
доступа к данных из ваших файлов .proto. Оно охватывает редакцию proto2
языка protocol buffers.
Для информации о синтаксисе editions см. Руководство по языку Protobuf Editions.
Для информации о синтаксисе proto3 см. Руководство по языку Proto3.
Это справочное руководство – для пошагового примера, который использует многие из функций, описанных в этом документе, см. обучение для выбранного вами языка.
Определение типа сообщения
Сначала давайте рассмотрим очень простой пример. Допустим, вы хотите определить формат сообщения
поискового запроса, где каждый поисковый запрос имеет строку запроса,
конкретную страницу результатов, которая вас интересует, и количество результатов на
страницу. Вот файл .proto, который вы используете для определения типа сообщения.
syntax = "proto2";
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
-
Первая строка файла указывает, что вы используете редакцию proto2 спецификации языка protobuf.
syntaxдолжна быть первой непустой, не-комментарием строкой файла.- Если
syntaxне указана, компилятор protocol buffer предположит, что вы используете proto2.
-
Определение сообщения
SearchRequestзадает три поля (пары имя/значение), по одному для каждой части данных, которые вы хотите включить в этот тип сообщения. Каждое поле имеет имя и тип.
Указание типов полей
В предыдущем примере все поля являются скалярными типами: два целых числа
(page_number и results_per_page) и строка (query). Вы также можете
указать перечисления и составные типы, такие как другие типы сообщений, для
вашего поля.
Назначение номеров полей
Вы должны дать каждому полю в определении вашего сообщения число между 1 и
536,870,911 со следующими ограничениями:
- Данный номер должен быть уникальным среди всех полей этого сообщения.
- Номера полей
19,000до19,999зарезервированы для реализации Protocol Buffers. Компилятор protocol buffer будет жаловаться, если вы используете один из этих зарезервированных номеров полей в вашем сообщении. - Вы не можете использовать любые ранее зарезервированные номера полей или любые номера полей, которые были выделены для расширений.
Этот номер не может быть изменен после того, как ваш тип сообщения используется, потому что он идентифицирует поле в формате передачи сообщения. «Изменение» номера поля эквивалентно удалению этого поля и созданию нового поля с тем же типом, но новым номером. См. Удаление полей для того, как сделать это правильно.
Номера полей никогда не должны использоваться повторно. Никогда не извлекайте номер поля из зарезервированного списка для повторного использования с новым определением поля. См. Последствия повторного использования номеров полей.
Вам следует использовать номера полей от 1 до 15 для наиболее часто устанавливаемых полей. Меньшие значения номеров полей занимают меньше места в формате передачи. Например, номера полей в диапазоне от 1 до 15 занимают один байт для кодирования. Номера полей в диапазоне от 16 до 2047 занимают два байта. Вы можете узнать больше об этом в Кодирование Protocol Buffer.
Последствия повторного использования номеров полей
Повторное использование номера поля делает декодирование сообщений в формате передачи неоднозначным.
Формат передачи protobuf является «легким» и не предоставляет способа обнаружения полей, закодированных с использованием одного определения и декодированных с использованием другого.
Кодирование поля с использованием одного определения и последующее декодирование этого же поля с другим определением может привести к:
- Потере времени разработчика на отладку
- Ошибке разбора/слияния (лучший сценарий)
- Утечке PII/SPII
- Повреждению данных
Распространенные причины повторного использования номеров полей:
-
перенумерация полей (иногда делается для достижения более эстетически приятного порядка номеров для полей). Перенумерация фактически удаляет и заново добавляет все поля, вовлеченные в перенумерацию, что приводит к несовместимым изменениям формата передачи.
-
удаление поля и не резервирование номера для предотвращения будущего повторного использования.
- Это была очень легкая ошибка для совершения с полями расширений по нескольким причинам. Объявления расширений предоставляют механизм для резервирования полей расширений.
Номер поля ограничен 29 битами вместо 32 битов, потому что три бита используются для указания формата передачи поля. Для получения дополнительной информации см. Тема Кодирование.
Указание мощности полей
Поля сообщения могут быть одним из следующих:
-
Одиночное (Singular):
В proto2 есть два типа одиночных полей:
-
optional: (рекомендуется) Полеoptionalнаходится в одном из двух возможных состояний:- поле установлено и содержит значение, которое было явно установлено или разобрано из передачи. Оно будет сериализовано в передачу.
- поле не установлено и будет возвращать значение по умолчанию. Оно не будет сериализовано в передачу.
Вы можете проверить, было ли значение явно установлено.
-
required: Не используйте. Обязательные поля настолько проблематичны, что были удалены из proto3 и editions. Семантика обязательных полей должна быть реализована на уровне приложения. Когда оно используется, правильно сформированное сообщение должно иметь ровно одно такое поле.
-
-
repeated: этот тип поля может повторяться ноль или более раз в правильно сформированном сообщении. Порядок повторяющихся значений будет сохранен. -
map: это поле типа пар ключ/значение. См. Карты для получения дополнительной информации об этом типе поля.
Используйте упакованное кодирование для новых повторяющихся полей
По историческим причинам, repeated поля скалярных числовых типов (например,
int32, int64, enum) не кодируются так эффективно, как могли бы. Новый
код должен использовать специальную опцию [packed = true] для получения более эффективного
кодирования. Например:
repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];
Вы можете узнать больше об packed кодировании в
Кодирование Protocol Buffer.
Required настоятельно устарел
{{% alert title="Важно" color="warning" %}} Required это навсегда
Как упоминалось ранее, required не должен использоваться для новых полей. Семантика
обязательных полей должна быть реализована на уровне приложения вместо этого.
Существующие required поля должны рассматриваться как постоянные, неизменяемые элементы
определения сообщения. Почти невозможно безопасно изменить поле с
required на optional. Если существует хоть малейшая вероятность, что существует устаревший читатель, он
будет считать сообщения без этого поля неполными и может отклонить или
отбросить их. {{% /alert %}}
Вторая проблема с обязательными полями появляется, когда кто-то добавляет значение в enum. В этом случае нераспознанное значение enum рассматривается как если бы оно было отсутствующим, что также вызывает сбой проверки обязательного значения.
Правильно сформированные сообщения
Термин «правильно сформированный» (well-formed), когда применяется к сообщениям protobuf, относится к байтам, сериализованным/десериализованным. Парсер protoc проверяет, что данное proto определение файла разбираемо.
Одиночные поля могут появляться более одного раза в байтах формата передачи. Парсер примет ввод, но только последний экземпляр этого поля будет доступен через сгенерированные привязки. См. Последний побеждает для получения дополнительной информации по этой теме.
Добавление дополнительных типов сообщений
Несколько типов сообщений могут быть определены в одном файле .proto. Это полезно,
если вы определяете несколько связанных сообщений – так, например, если вы хотите
определить формат сообщения ответа, который соответствует вашему типу сообщения SearchResponse,
вы можете добавить его в тот же .proto:
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
message SearchResponse {
...
}
Комбинирование сообщений приводит к раздуванию Хотя несколько типов сообщений (таких как
message, enum и service) могут быть определены в одном файле .proto, это может
также привести к раздуванию зависимостей, когда большое количество сообщений с различными
зависимостями определяется в одном файле. Рекомендуется включать как можно меньше
типов сообщений в каждый файл .proto.
Добавление комментариев
Чтобы добавить комментарии в ваши файлы .proto:
-
Предпочитайте комментарии в стиле C/C++/Java в конце строки '//' в строке перед элементом кода .proto
-
Встроенные/многострочные комментарии в стиле C
/* ... */также принимаются.- При использовании многострочных комментариев предпочтительна строка отступа '*'.
/**
* SearchRequest представляет поисковый запрос с параметрами pagination,
* чтобы указать, какие результаты включать в ответ.
*/
message SearchRequest {
optional string query = 1;
// Какой номер страницы нам нужен?
optional int32 page_number = 2;
// Количество результатов, возвращаемых на страницу.
optional int32 results_per_page = 3;
}
Удаление полей
Удаление полей может вызвать серьезные проблемы, если не сделано правильно.
Не удаляйте required поля. Это почти невозможно сделать безопасно. Если
вы должны удалить required поле, вы должны сначала пометить поле optional
и deprecated и убедиться, что все системы, которые так или иначе наблюдают за
сообщением, были развернуты с новой схемой. Затем вы можете рассмотреть удаление
поля (но обратите внимание, что это все еще процесс, подверженный ошибкам).
Когда вам больше не нужно поле, которое не required, сначала удалите все
ссылки на поле из клиентского кода, а затем удалите определение поля
из сообщения. Однако, вы должны
зарезервировать удаленный номер поля. Если вы не зарезервируете
номер поля, возможно, что разработчик повторно использует этот номер в будущем
и вызовет поломку.
Вам также следует зарезервировать имя поля, чтобы позволить JSON и TextFormat кодировкам вашего сообщения продолжать разбираться.
Зарезервированные номера полей
Если вы обновляете тип сообщения, полностью удаляя поле, или
комментируя его, будущие разработчики могут повторно использовать номер поля при внесении
своих собственных обновлений в тип. Это может вызвать серьезные проблемы, как описано в
Последствия повторного использования номеров полей. Чтобы убедиться, что этого
не происходит, добавьте ваш удаленный номер поля в список reserved.
Компилятор protoc будет генерировать сообщения об ошибках, если любые будущие разработчики попытаются использовать эти зарезервированные номера полей.
message Foo {
reserved 2, 15, 9 to 11;
}
Диапазоны зарезервированных номеров полей включительны (9 to 11 это то же самое, что 9, 10, 11).
Зарезервированные имена полей
Повторное использование старого имени поля позже обычно безопасно, за исключением случаев использования TextProto
или JSON кодировок, где имя поля сериализуется. Чтобы избежать этого риска, вы
можете добавить удаленное имя поля в список reserved.
Зарезервированные имена влияют только на поведение компилятора protoc, а не на поведение во время выполнения, за одним исключением: реализации TextProto могут отбрасывать неизвестные поля (без вызова ошибки, как с другими неизвестными полями) с зарезервированными именами во время разбора (только реализации C++ и Go делают это сегодня). JSON парсинг во время выполнения не затрагивается зарезервированными именами.
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
Обратите внимание, что вы не можете смешивать имена полей и номера полей в одном операторе reserved.
Что генерируется из вашего .proto?
Когда вы запускаете компилятор protocol buffer на .proto файле,
компилятор генерирует код на выбранном вами языке, который вам понадобится для работы с
типами сообщений, которые вы описали в файле, включая получение и установку значений полей,
сериализацию ваших сообщений в выходной поток и разбор ваших сообщений
из входного потока.
- Для C++ компилятор генерирует файлы
.hи.ccиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для Java компилятор генерирует файл
.javaс классом для каждого типа сообщения, а также специальный классBuilderдля создания экземпляров классов сообщений. - Для Kotlin, в дополнение к сгенерированному Java коду, компилятор
генерирует файл
.ktдля каждого типа сообщения с улучшенным Kotlin API. Это включает DSL, который упрощает создание экземпляров сообщений, метод доступа к nullable полю и функцию копирования. - Python немного отличается — компилятор Python генерирует модуль
со статическим дескриптором каждого типа сообщения в вашем
.proto, который затем используется с метаклассом для создания необходимого класса доступа к данным Python во время выполнения. - Для Go компилятор генерирует файл
.pb.goс типом для каждого типа сообщения в вашем файле. - Для Ruby компилятор генерирует файл
.rbс Ruby модулем, содержащим ваши типы сообщений. - Для Objective-C компилятор генерирует файлы
pbobjc.hиpbobjc.mиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для C# компилятор генерирует файл
.csиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для PHP компилятор генерирует файл сообщения
.phpдля каждого типа сообщения, описанного в вашем файле, и файл метаданных.phpдля каждого.protoфайла, который вы компилируете. Файл метаданных используется для загрузки допустимых типов сообщений в пул дескрипторов. - Для Dart компилятор генерирует файл
.pb.dartс классом для каждого типа сообщения в вашем файле.
Вы можете узнать больше об использовании API для каждого языка, следуя обучению для выбранного языка. Для получения еще более подробной информации об API см. соответствующий справочник по API.
Скалярные типы значений
Скалярное поле сообщения может иметь один из следующих типов – таблица показывает
тип, указанный в файле .proto, и соответствующий тип в
автоматически сгенерированном классе:
| Тип в Proto | Примечания |
|---|---|
| double | |
| float | |
| int32 | Использует кодирование переменной длины. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте sint32 вместо этого. |
| int64 | Использует кодирование переменной длины. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте sint64 вместо этого. |
| uint32 | Использует кодирование переменной длины. |
| uint64 | Использует кодирование переменной длины. |
| sint32 | Использует кодирование переменной длины. Значение со знаком. Они более эффективно кодируют отрицательные числа, чем обычные int32. |
| sint64 | Использует кодирование переменной длины. Значение со знаком. Они более эффективно кодируют отрицательные числа, чем обычные int64. |
| fixed32 | Всегда четыре байта. Более эффективно, чем uint32, если значения часто больше чем 228. |
| fixed64 | Всегда восемь байта. Более эффективно, чем uint64, если значения часто больше чем 256. |
| sfixed32 | Всегда четыре байта. |
| sfixed64 | Всегда восемь байта. |
| bool | |
| string | Строка должна всегда содержать текст в кодировке UTF-8 или 7-битный ASCII и не может быть длиннее 232. |
| bytes | Может содержать любую произвольную последовательность байт не длиннее 232. |
| Тип в Proto | Тип в C++ | Тип в Java/Kotlin[1] | Тип в Python[3] | Тип в Go | Тип в Ruby | Тип в C# | Тип в PHP | Тип в Dart | Тип в Rust |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | *float64 | Float | double | float | double | f64 |
| float | float | float | float | *float32 | Float | float | float | double | f32 |
| int32 | int32_t | int | int | int32 | Fixnum или Bignum (по необходимости) | int | integer | *int32 | i32 |
| int64 | int64_t | long | int/long[4] | *int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| uint32 | uint32_t | int[2] | int/long[4] | *uint32 | Fixnum или Bignum (по необходимости) | uint | integer | int | u32 |
| uint64 | uint64_t | long[2] | int/long[4] | *uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sint32 | int32_t | int | int | int32 | Fixnum или Bignum (по необходимости) | int | integer | *int32 | i32 |
| sint64 | int64_t | long | int/long[4] | *int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| fixed32 | uint32_t | int[2] | int/long[4] | *uint32 | Fixnum или Bignum (по необходимости) | uint | integer | int | u32 |
| fixed64 | uint64_t | long[2] | int/long[4] | *uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sfixed32 | int32_t | int | int | *int32 | Fixnum или Bignum (по необходимости) | int | integer | int | i32 |
| sfixed64 | int64_t | long | int/long[4] | *int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| bool | bool | boolean | bool | *bool | TrueClass/FalseClass | bool | boolean | bool | bool |
| string | string | String | unicode (Python 2), str (Python 3) | *string | String (UTF-8) | string | string | String | ProtoString |
| bytes | string | ByteString | bytes | []byte | String (ASCII-8BIT) | ByteString | string | List |
ProtoBytes |
[1] Kotlin использует соответствующие типы из Java, даже для беззнаковых типов, чтобы обеспечить совместимость в смешанных Java/Kotlin кодовых базах.
[2] В Java беззнаковые 32-битные и 64-битные целые числа представляются с использованием их знаковых аналогов, при этом старший бит просто хранится в бите знака.
[3] Во всех случаях установка значений в поле будет выполнять проверку типа чтобы убедиться, что оно допустимо.
[4] 64-битные или беззнаковые 32-битные целые числа всегда представляются как long при декодировании, но могут быть int, если при установке поля задан int. Во всех случаях значение должно помещаться в тип, представленный при установке. См. [2].
[5] Proto2 обычно никогда не проверяет валидность UTF-8 строковых полей. Однако поведение варьируется между языками, и невалидные данные UTF-8 не должны храниться в строковых полях.
[6] Integer используется на 64-битных машинах, а string используется на 32-битных машинах.
Вы можете узнать больше о том, как эти типы кодируются при сериализации вашего сообщения в Кодирование Protocol Buffer.
Значения полей по умолчанию
Когда сообщение разбирается, если закодированные байты сообщения не содержат конкретное поле, доступ к этому полю в разобранном объекте возвращает значение по умолчанию для этого поля. Значения по умолчанию зависят от типа:
- Для строк значение по умолчанию — пустая строка.
- Для bytes значение по умолчанию — пустые байты.
- Для bools значение по умолчанию — false.
- Для числовых типов значение по умолчанию — ноль.
- Для полей сообщения поле не установлено. Его точное значение зависит от языка. Подробности см. в руководстве по сгенерированному коду для вашего языка.
- Для перечислений значение по умолчанию — первое определенное значение перечисления, которое должно быть 0 (рекомендуется для совместимости с открытыми enums). См. Значение перечисления по умолчанию.
Значение по умолчанию для повторяющихся полей — пустое (обычно пустой список в соответствующем языке).
Значение по умолчанию для полей map — пустое (обычно пустая карта в соответствующем языке).
Переопределение скалярных значений по умолчанию
В proto2 вы можете указать явные значения по умолчанию для одиночных полей, не являющихся сообщениями.
Например, допустим, вы хотите предоставить значение по умолчанию 10 для
поля SearchRequest.results_per_page:
optional int32 results_per_page = 3 [default = 10];
Если отправитель не указывает results_per_page, получатель будет наблюдать
следующее состояние:
- Поле
results_per_pageотсутствует. То есть методhas_results_per_page()(hazzer метод) вернетfalse. - Значение
results_per_page(возвращаемое из «геттера») равно10.
Если отправитель отправляет значение для results_per_page, значение по умолчанию 10
игнорируется, и значение отправителя возвращается из «геттера».
См. руководство по сгенерированному коду для вашего выбранного языка для получения более подробной информации о том, как работают значения по умолчанию в сгенерированном коде.
Поскольку значение по умолчанию для enums является первым определенным значением enum, будьте осторожны при добавлении значения в начало списка значений enum. См. раздел Обновление типа сообщения для рекомендаций о том, как безопасно изменять определения.
Перечисления
Когда вы определяете тип сообщения, вы можете захотеть, чтобы одно из его полей имело
только одно значение из предопределенного списка. Например, допустим, вы хотите добавить
поле corpus для каждого SearchRequest, где corpus может быть UNIVERSAL,
WEB, IMAGES, LOCAL, NEWS, PRODUCTS или VIDEO. Вы можете сделать это очень
просто, добавив enum в определение вашего сообщения с константой для каждого возможного значения.
В следующем примере мы добавили enum с именем Corpus со всеми
возможными значениями и поле типа Corpus:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
optional Corpus corpus = 4;
}
Значение перечисления по умолчанию
Значение по умолчанию для поля SearchRequest.corpus — CORPUS_UNSPECIFIED,
потому что это первое значение, определенное в перечислении.
Настоятельно рекомендуется определять первое значение каждого enum как
ENUM_TYPE_NAME_UNSPECIFIED = 0; или ENUM_TYPE_NAME_UNKNOWN = 0;. Это
из-за способа, которым proto2 обрабатывает неизвестные значения для полей enum.
Также рекомендуется, чтобы это первое, значение по умолчанию не имело семантического значения, кроме «это значение не было указано».
Значение по умолчанию для поля перечисления, такого как поле SearchRequest.corpus, может быть
явно переопределено следующим образом:
optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
Псевдонимы значений перечисления
Вы можете определить псевдонимы, назначая одно и то же значение разным константам перечисления.
Чтобы сделать это, вам нужно установить опцию allow_alias в true. В противном случае
компилятор protocol buffer генерирует предупреждение, когда псевдонимы
найдены. Хотя все значения псевдонимов действительны для сериализации, только первое значение
используется при десериализации.
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Раскомментирование этой строки вызовет предупреждение.
ENAA_FINISHED = 2;
}
Константы перечислителя должны находиться в диапазоне 32-битного целого числа. Поскольку значения enum
используют
varint кодирование в
передаче, отрицательные значения неэффективны и поэтому не рекомендуются. Вы можете определять
enum внутри определения сообщения, как в предыдущем примере, или снаружи –
эти enum могут быть повторно использованы в любом определении сообщения в вашем файле .proto. Вы
также можете использовать тип enum, объявленный в одном сообщении, в качестве типа поля в
другом сообщении, используя синтаксис _MessageType_._EnumType_.
Когда вы запускаете компилятор protocol buffer на .proto, который использует enum,
сгенерированный код будет иметь соответствующий enum для Java, Kotlin или C++, или
специальный класс EnumDescriptor для Python, который используется для создания набора
символических констант с целочисленными значениями в классе, сгенерированном во время выполнения.
{{% alert title="Важно" color="warning" %}} Сгенерированный код может быть подвержен языко-специфичным ограничениям на количество перечислителей (несколько тысяч для одного языка). Ознакомьтесь с ограничениями для языков, которые вы планируете использовать. {{% /alert %}}
{{% alert title="Важно" color="warning" %}} Для информации о том, как перечисления должны работать, в противовес тому, как они сейчас работают в разных языках, см. Поведение Enum. {{% /alert %}}
Удаление значений enum является критическим изменением для сохраненных protos. Вместо
удаления значения пометьте значение ключевым словом reserved, чтобы предотвратить генерацию кода для значения enum,
или сохраните значение, но укажите, что оно будет
удалено позже, используя опцию поля deprecated:
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3 [deprecated = true];
reserved 4,5;
}
Для получения дополнительной информации о том, как работать с enum сообщений в ваших
приложениях, см. руководство по сгенерированному коду
для выбранного вами языка.
Зарезервированные значения
Если вы обновляете тип перечисления, полностью удаляя запись перечисления, или
комментируя ее, будущие пользователи могут повторно использовать числовое значение при внесении
своих собственных обновлений в тип. Это может вызвать серьезные проблемы, если они позже загрузят старые
экземпляры того же .proto, включая повреждение данных, ошибки конфиденциальности и т.д.
Один из способов убедиться, что этого не произойдет, — указать, что числовые
значения (и/или имена, которые также могут вызывать проблемы для JSON сериализации)
ваших удаленных записей reserved. Компилятор protocol buffer будет жаловаться,
если любые будущие пользователи попытаются использовать эти идентификаторы. Вы можете указать, что ваш
зарезервированный диапазон числовых значений доходит до максимально возможного значения, используя
ключевое слово max.
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
Обратите внимание, что вы не можете смешивать имена полей и числовые значения в одном операторе reserved.
Использование других типов сообщений
Вы можете использовать другие типы сообщений в качестве типов полей. Например, допустим, вы
хотели включить сообщения Result в каждое сообщение SearchResponse – чтобы
сделать это, вы можете определить тип сообщения Result в том же .proto и затем
указать поле типа Result в SearchResponse:
message SearchResponse {
repeated Result results = 1;
}
message Result {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
Импорт определений
В предыдущем примере тип сообщения Result определен в том же файле, что и
SearchResponse – что, если тип сообщения, который вы хотите использовать в качестве типа поля,
уже определен в другом файле .proto?
Вы можете использовать определения из других файлов .proto, импортируя их. Чтобы импортировать
определения другого .proto, вы добавляете оператор импорта в начало вашего файла:
import "myproject/other_protos.proto";
По умолчанию вы можете использовать определения только из непосредственно импортированных файлов .proto.
Однако иногда вам может потребоваться переместить файл .proto в новое место.
Вместо прямого перемещения файла .proto и обновления всех мест вызова в
одном изменении, вы можете поместить файл-заполнитель .proto в старое местоположение для
перенаправления всех импортов в новое местоположение с использованием понятия import public.
Обратите внимание, что функциональность публичного импорта недоступна в Java, Kotlin, TypeScript, JavaScript, GCL, а также в целях C++, которые используют статическую рефлексию protobuf.
Зависимости import public могут быть транзитивно использованы любым кодом,
импортирующим proto, содержащий оператор import public. Например:
// new.proto
// Все определения перемещены сюда
// old.proto
// Это proto, которое импортируют все клиенты.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// Вы используете определения из old.proto и new.proto, но не other.proto
Компилятор протокола ищет импортированные файлы в наборе каталогов,
указанных в командной строке компилятора протокола с использованием флага -I/--proto_path.
Если флаг не был задан, он ищет в каталоге, из которого был вызван компилятор.
В общем, вам следует установить флаг --proto_path в корень вашего проекта
и использовать полностью квалифицированные имена для всех импортов.
Использование типов сообщений proto3
Возможно импортировать proto3 и edition 2023 типы сообщений и использовать их в ваших proto2 сообщениях, и наоборот. Однако proto2 enums не могут использоваться напрямую в синтаксисе proto3 (это нормально, если импортированное proto2 сообщение использует их).
Вложенные типы
Вы можете определять и использовать типы сообщений внутри других типов сообщений, как в
следующем примере – здесь сообщение Result определено внутри сообщения
SearchResponse:
message SearchResponse {
message Result {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
Если вы хотите повторно использовать этот тип сообщения вне его родительского типа сообщения, вы
ссылаетесь на него как _Parent_._Type_:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
Вы можете вкладывать сообщения настолько глубоко, насколько хотите. В примере ниже обратите внимание, что
два вложенных типа с именем Inner полностью независимы, поскольку они определены
внутри разных сообщений:
message Outer { // Уровень 0
message MiddleAA { // Уровень 1
message Inner { // Уровень 2
optional int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Уровень 1
message Inner { // Уровень 2
optional int32 ival = 1;
optional bool booly = 2;
}
}
}
Группы
Обратите внимание, что функция групп устарела и не должна использоваться при создании новых типов сообщений. Используйте вложенные типы сообщений вместо этого.
Группы — это другой способ вложения информации в ваши определения сообщений. Например,
другой способ указать SearchResponse, содержащий ряд
Result, выглядит следующим образом:
message SearchResponse {
repeated group Result = 1 {
optional string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
}
Группа просто объединяет вложенный тип сообщения и поле в одно
объявление. В вашем коде вы можете обращаться с этим сообщением так, как если бы оно имело
поле типа Result с именем result (последнее имя преобразуется в нижний регистр
чтобы оно не конфликтовало с первым). Следовательно, этот пример
точно эквивалентен предыдущему SearchResponse, за исключением того, что сообщение имеет
другой формат передачи.
Обновление типа сообщения
Если существующий тип сообщения больше не удовлетворяет всем вашим потребностям – например, вы хотите, чтобы формат сообщения имел дополнительное поле – но вы все еще хотите использовать код, созданный со старым форматом, не волнуйтесь! Это очень просто обновить типы сообщений, не нарушая любой из вашего существующего кода, когда вы используете бинарный формат передачи.
{{% alert title="Примечание" color="note" %}} Если вы используете ProtoJSON или текстовый формат proto для хранения ваших сообщений protocol buffer, изменения, которые вы можете внести в ваше proto определение, отличаются. Безопасные изменения формата передачи ProtoJSON описаны здесь. {{% /alert %}}
Проверьте Лучшие практики Proto и следующие правила:
Бинарно небезопасные изменения формата передачи
Небезопасные изменения формата передачи – это изменения схемы, которые сломаются, если вы используете разбор данных, которые были сериализованы с использованием старой схемы, с парсером, который использует новую схему (или наоборот). Делайте небезопасные изменения формата передачи только если вы знаете, что все сериализаторы и десериализаторы данных используют новую схему.
- Изменение номеров полей для любого существующего поля небезопасно.
- Изменение номера поля эквивалентно удалению поля и добавлению нового поля с тем же типом. Если вы хотите перенумеровать поле, см. инструкции для удаления поля.
- Перемещение полей в существующий
oneofнебезопасно.
Бинарно безопасные изменения формата передачи
Безопасные изменения формата передачи – это те, при которых полностью безопасно развивать схему таким образом без риска потери данных или новых ошибок разбора.
Обратите внимание, что любые безопасные изменения формата передачи могут быть критическим изменением для кода приложения на данном языке. Например, добавление значения в предсуществующее enum будет критическим изменением компиляции для любого кода с исчерпывающим switch по этому enum. По этой причине Google может избегать внесения некоторых из этих типов изменений в публичные сообщения: AIPs содержат рекомендации о том, какие из этих изменений безопасно делать там.
- Добавление новых полей безопасно.
- Если вы добавляете новые поля, любые сообщения, сериализованные кодом, использующим ваш «старый» формат сообщения, все еще могут быть разобраны вашим новым сгенерированным кодом. Вам следует иметь в виду значения по умолчанию для этих элементов, чтобы новый код мог правильно взаимодействовать с сообщениями, сгенерированными старым кодом. Аналогично, сообщения, созданные вашим новым кодом, могут быть разобраны вашим старым кодом: старые бинарные файлы просто игнорируют новое поле при разборе. См. раздел Неизвестные поля для деталей.
- Удаление полей безопасно.
- Тот же номер поля не должен использоваться снова в вашем обновленном типе сообщения.
Вы можете захотеть переименовать поле вместо этого, возможно, добавив префикс
"OBSOLETE_", или сделать номер поля зарезервированным, чтобы
будущие пользователи вашего
.protoне могли случайно повторно использовать номер.
- Тот же номер поля не должен использоваться снова в вашем обновленном типе сообщения.
Вы можете захотеть переименовать поле вместо этого, возможно, добавив префикс
"OBSOLETE_", или сделать номер поля зарезервированным, чтобы
будущие пользователи вашего
- Добавление дополнительных значений в enum безопасно.
- Изменение одного поля с явным присутствием или расширения на член
нового
oneofбезопасно. - Изменение
oneof, который содержит только одно поле, на поле с явным присутствием безопасно. - Изменение поля на расширение с тем же номером и типом безопасно.
Бинарно совместимые изменения формата передачи (Условно Безопасные)
В отличие от безопасных изменений формата передачи, совместимые означают, что одни и те же данные могут быть разобраны как до, так и после данного изменения. Однако разбор данных может быть потерейным при такой форме изменения. Например, изменение int32 на int64 является совместимым изменением, но если значение больше INT32_MAX записано, клиент, который читает его как int32, отбросит старшие биты числа.
Вы можете вносить совместимые изменения в вашу схему только если вы тщательно управляете развертыванием в вашей системе. Например, вы можете изменить int32 на int64, но обеспечить, чтобы вы продолжали записывать только допустимые значения int32 до тех пор, пока новая схема не будет развернута на всех конечных точках, и затем начать записывать большие значения после этого.
Если ваша схема опубликована за пределами вашей организации, вам обычно не следует вносить совместимые изменения формата передачи, так как вы не можете управлять развертыванием новой схемы, чтобы знать, когда различные диапазоны значений могут быть безопасны для использования.
int32,uint32,int64,uint64иboolвсе совместимы.- Если число разбирается из передачи, которое не помещается в соответствующий тип, вы получите тот же эффект, как если бы вы привели число к этому типу в C++ (например, если 64-битное число читается как int32, оно будет усечено до 32 бит).
sint32иsint64совместимы друг с другом, но не совместимы с другими целочисленными типами.- Если записанное значение было между INT_MIN и INT_MAX включительно, оно будет разобрано как то же значение с любым типом. Если значение sint64 было записано вне этого диапазона и разобрано как sint32, varint усекается до 32 бит, а затем происходит zigzag декодирование (что вызовет наблюдение другого значения).
stringиbytesсовместимы, пока байты являются допустимым UTF-8.- Встроенные сообщения совместимы с
bytes, если байты содержат закодированный экземпляр сообщения. fixed32совместим сsfixed32, иfixed64сsfixed64.- Для
string,bytesи полей сообщений, одиночное совместимо сrepeated.- При получении сериализованных данных повторяющегося поля в качестве входных данных, клиенты, которые ожидают это поле одиночным, возьмут последнее входное значение, если это поле примитивного типа, или объединят все входные элементы, если это тип сообщения. Обратите внимание, что это не вообще безопасно для числовых типов, включая bools и enums. Повторяющиеся поля числовых типов сериализуются в упакованном формате по умолчанию, который не будет правильно разобран, когда ожидается одиночное поле.
enumсовместим сint32,uint32,int64иuint64- Имейте в виду, что клиентский код может обрабатывать их по-разному, когда сообщение
десериализуется: например, нераспознанные значения proto3
enumбудут сохранены в сообщении, но то, как это представлено, когда сообщение десериализуется, зависит от языка.
- Имейте в виду, что клиентский код может обрабатывать их по-разному, когда сообщение
десериализуется: например, нераспознанные значения proto3
- Изменение поля между
map<K, V>и соответствующимrepeatedполем сообщения бинарно совместимо (см. Карты ниже, для макета сообщения и других ограничений).- Однако безопасность изменения зависит от приложения: при
десериализации и повторной сериализации сообщения клиенты, использующие определение
repeatedполя, произведут семантически идентичный результат; однако, клиенты, использующие определениеmapполя, могут переупорядочить записи и отбросить записи с дублирующимися ключами.
- Однако безопасность изменения зависит от приложения: при
десериализации и повторной сериализации сообщения клиенты, использующие определение
Неизвестные поля
Неизвестные поля – это правильно сформированные сериализованные данные protocol buffer, представляющие поля, которые парсер не распознает. Например, когда старый бинарный файл разбирает данные, отправленные новым бинарным файлом с новыми полями, эти новые поля становятся неизвестными полями в старом бинарном файле.
Первоначально, proto3 сообщения всегда отбрасывали неизвестные поля во время разбора, но в версии 3.5 мы reintroduced сохранение неизвестных полей, чтобы соответствовать поведению proto2. В версиях 3.5 и позже, неизвестные поля сохраняются во время разбора и включаются в сериализованный вывод.
Сохранение неизвестных полей
Некоторые действия могут привести к потере неизвестных полей. Например, если вы сделаете одно из следующего, неизвестные поля теряются:
- Сериализуете proto в JSON.
- Итерируетесь по всем полям в сообщении, чтобы заполнить новое сообщение.
Чтобы избежать потери неизвестных полей, делайте следующее:
- Используйте бинарный формат; избегайте использования текстовых форматов для обмена данными.
- Используйте API, ориентированные на сообщения, такие как
CopyFrom()иMergeFrom(), для копирования данных, а не копирования поле за полем.
TextFormat – это особый случай. Сериализация в TextFormat печатает неизвестные поля, используя их номера полей. Но разбор данных TextFormat обратно в бинарный proto завершается неудачей, если есть записи, которые используют номера полей.
Расширения
Расширение – это поле, определенное вне своего контейнерного сообщения; обычно в
файле .proto, отдельном от файла .proto контейнерного сообщения.
Зачем использовать расширения?
Есть две основные причины использовать расширения:
- Файл
.protoконтейнерного сообщения будет иметь меньше импортов/зависимостей. Это может улучшить время сборки, разорвать циклические зависимости и иным образом способствовать слабой связанности. Расширения очень хороши для этого. - Позволяют системам прикреплять данные к контейнерному сообщению с минимальной зависимостью
и координацией. Расширения не являются отличным решением для этого из-за
ограниченного пространства номеров полей и
Последствий повторного использования номеров полей. Если ваш случай использования
требует очень низкой координации для большого количества расширений, рассмотрите
использование
типа сообщения
Anyвместо этого.
Пример расширения
Давайте рассмотрим пример расширения:
// файл kittens/video_ext.proto
import "kittens/video.proto";
import "media/user_content.proto";
package kittens;
// Это расширение позволяет видео с котятами в сообщении media.UserContent.
extend media.UserContent {
// Video - это сообщение, импортированное из kittens/video.proto
repeated Video kitten_videos = 126;
}
Обратите внимание, что файл, определяющий расширение (kittens/video_ext.proto), импортирует
файл контейнерного сообщения (media/user_content.proto).
Контейнерное сообщение должно зарезервировать подмножество своих номеров полей для расширений.
// файл media/user_content.proto
package media;
// Контейнерное сообщение для хранения вещей, созданных пользователем.
message UserContent {
// Установите verification в `DECLARATION`, чтобы требовать объявления расширений для всех
// расширений в этом диапазоне.
extensions 100 to 199 [verification = DECLARATION];
}
Файл контейнерного сообщения (media/user_content.proto) определяет сообщение
UserContent, которое резервирует номера полей [100 до 199] для расширений.
Рекомендуется установить verification = DECLARATION для диапазона, чтобы требовать
объявления для всех его расширений.
Когда новое расширение (kittens/video_ext.proto) добавляется, соответствующее
объявление должно быть добавлено в UserContent и verification должно быть
удалено.
// Контейнерное сообщение для хранения вещей, созданных пользователем.
message UserContent {
extensions 100 to 199 [
declaration = {
number: 126,
full_name: ".kittens.kitten_videos",
type: ".kittens.Video",
repeated: true
}
];
}
UserContent объявляет, что номер поля 126 будет использоваться repeated
полем расширения с полностью квалифицированным именем .kittens.kitten_videos и
fully-qualified типом .kittens.Video. Чтобы узнать больше об объявлениях расширений, см.
Объявления расширений.
Обратите внимание, что файл контейнерного сообщения (media/user_content.proto) не
импортирует определение расширения kitten_video (kittens/video_ext.proto)
Нет разницы в кодировании формата передачи полей расширений по сравнению со стандартным полем с тем же номером поля, типом и мощностью. Следовательно, безопасно перемещать стандартное поле из его контейнера, чтобы быть расширением, или перемещать поле расширения в его контейнерное сообщение как стандартное поле, при условии, что номер поля, тип и мощность остаются постоянными.
Однако, поскольку расширения определены вне контейнерного сообщения, никакие
специализированные методы доступа не генерируются для получения и установки конкретных полей расширений.
Для нашего примера компилятор protobuf не сгенерирует методы доступа AddKittenVideos()
или GetKittenVideos(). Вместо этого доступ к расширениям осуществляется через
параметризованные функции, такие как: HasExtension(), ClearExtension(),
GetExtension(), MutableExtension() и AddExtension().
В C++ это будет выглядеть примерно так:
UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);
Определение диапазонов расширений
Если вы являетесь владельцем контейнерного сообщения, вам нужно будет определить диапазон расширений для расширений вашего сообщения.
Номера полей, выделенные для полей расширений, не могут быть повторно использованы для стандартных полей.
Безопасно расширять диапазон расширений после его определения. Хорошим значением по умолчанию является выделение 1000 относительно маленьких чисел и плотное заполнение этого пространства с использованием объявлений расширений:
message ModernExtendableMessage {
// Все расширения в этом диапазоне должны использовать объявления расширений.
extensions 1000 to 2000 [verification = DECLARATION];
}
При добавлении диапазона для объявлений расширений до фактических расширений, вы
должны добавить verification = DECLARATION, чтобы обеспечить использование объявлений
для этого нового диапазона. Этот заполнитель может быть удален, как только будет добавлено фактическое объявление.
Безопасно разделить существующий диапазон расширений на отдельные диапазоны, которые покрывают тот же общий диапазон. Это может быть необходимо для миграции унаследованного типа сообщения на Объявления расширений. Например, до миграции диапазон может быть определен как:
message LegacyMessage {
extensions 1000 to max;
}
И после миграции (разделения диапазона) это может быть:
message LegacyMessage {
// Унаследованный диапазон, который использовал непроверенную схему выделения.
extensions 1000 to 524999999 [verification = UNVERIFIED];
// Текущий диапазон, который использует объявления расширений.
extensions 525000000 to max [verification = DECLARATION];
}
Небезопасно увеличивать начальный номер поля или уменьшать конечный номер поля для перемещения или сокращения диапазона расширений. Эти изменения могут сделать недействительным существующее расширение.
Предпочитайте использовать номера полей от 1 до 15 для стандартных полей, которые заполняются в большинстве экземпляров вашего proto. Не рекомендуется использовать эти номера для расширений.
Если ваше соглашение о нумерации может включать расширения с очень большими номерами полей, вы можете указать, что ваш диапазон расширений доходит до максимально возможного номера поля, используя ключевое слово max:
message Foo {
extensions 1000 to max;
}
max это 229 - 1, или 536,870,911.
Выбор номеров расширений
Расширения – это просто поля, которые могут быть указаны вне их контейнерных сообщений. Все те же правила для Назначения номеров полей применяются к номерам полей расширений. Те же Последствия повторного использования номеров полей также применяются к повторному использованию номеров полей расширений.
Выбор уникальных номеров полей расширений прост, если контейнерное сообщение использует объявления расширений. При определении нового расширения выберите наименьший номер поля выше всех других объявлений из самого высокого диапазона расширений, определенного в контейнерном сообщении. Например, если контейнерное сообщение определено так:
message Container {
// Унаследованный диапазон, который использовал непроверенную схему выделения
extensions 1000 to 524999999;
// Текущий диапазон, который использует объявления расширений. (самый высокий диапазон расширений)
extensions 525000000 to max [
declaration = {
number: 525000001,
full_name: ".bar.baz_ext",
type: ".bar.Baz"
}
// 525,000,002 - это наименьший номер поля выше всех других объявлений
];
}
Следующее расширение Container должно добавить новое объявление с номером
525000002.
Непроверенное выделение номеров расширений (не рекомендуется)
Владелец контейнерного сообщения может отказаться от объявлений расширений в пользу своей собственной непроверенной стратегии выделения номеров расширений.
Непроверенная схема выделения использует механизм, внешний по отношению к экосистеме protobuf, для выделения номеров полей расширений в выбранном диапазоне расширений. Одним из примеров может быть использование номера коммита монорепозитория. Эта система «непроверена» с точки зрения компилятора protobuf, поскольку нет способа проверить, что расширение использует правильно полученный номер поля расширения.
Преимущество непроверенной системы над проверенной системой, такой как объявления расширений, – это возможность определить расширение без координации с владельцем контейнерного сообщения.
Недостатком непроверенной системы является то, что компилятор protobuf не может защитить участников от повторного использования номеров полей расширений.
Непроверенные стратегии выделения номеров полей расширений не рекомендуются,
потому что Последствия повторного использования номеров полей ложатся на всех
расширителей сообщения (а не только на разработчика, который не следовал
рекомендациям). Если ваш случай использования требует очень низкой координации, рассмотрите
использование
сообщения Any
вместо этого.
Непроверенные стратегии выделения номеров полей расширений ограничены диапазоном от 1 до 524,999,999. Номера полей 525,000,000 и выше могут использоваться только с объявлениями расширений.
Указание типов расширений
Расширения могут быть любого типа поля, кроме oneof и map.
Вложенные расширения (не рекомендуется)
Вы можете объявлять расширения в области видимости другого сообщения:
import "common/user_profile.proto";
package puppies;
message Photo {
extend common.UserProfile {
optional int32 likes_count = 111;
}
...
}
В этом случае код C++ для доступа к этому расширению:
UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);
Другими словами, единственный эффект заключается в том, что likes_count определен в области видимости puppies.Photo.
Это является частым источником путаницы: Объявление блока extend, вложенного внутрь
типа сообщения, не подразумевает никаких отношений между внешним типом и расширяемым типом. В частности, предыдущий пример не означает, что Photo
является каким-либо подклассом UserProfile. Все это означает, что символ
likes_count объявлен внутри области видимости Photo; это просто статический
член.
Распространенным шаблоном является определение расширений внутри области видимости
типа поля расширения – например, вот расширение для media.UserContent типа
puppies.Photo, где расширение определено как часть Photo:
import "media/user_content.proto";
package puppies;
message Photo {
extend media.UserContent {
optional Photo puppy_photo = 127;
}
...
}
Однако нет требования, чтобы расширение с типом сообщения было определено внутри этого типа. Вы также можете использовать стандартный шаблон определения:
import "media/user_content.proto";
package puppies;
message Photo {
...
}
// Это может быть даже в другом файле.
extend media.UserContent {
optional Photo puppy_photo = 127;
}
Этот стандартный (файловый уровень) синтаксис предпочтительнее, чтобы избежать путаницы. Вложенный синтаксис часто ошибочно принимается за наследование пользователями, которые еще не знакомы с расширениями.
Any
Тип сообщения Any позволяет вам использовать сообщения как встроенные типы без наличия
их определения .proto. Any содержит произвольное сериализованное сообщение как
bytes, вместе с URL, который действует как глобально уникальный идентификатор и
разрешается к типу этого сообщения. Чтобы использовать тип Any, вам нужно
импортировать google/protobuf/any.proto.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
URL типа по умолчанию для данного типа сообщения:
type.googleapis.com/_packagename_._messagename_.
Различные языковые реализации будут поддерживать вспомогательные библиотеки времени выполнения для упаковки
и распаковки значений Any типобезопасным образом – например, в Java тип Any
будет иметь специальные методы доступа pack() и unpack(), в то время как в C++ есть
методы PackFrom() и UnpackTo():
// Хранение произвольного типа сообщения в Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Чтение произвольного сообщения из Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... обработка network_error ...
}
}
Если вы хотите ограничить содержащиеся сообщения небольшим количеством типов и
требовать разрешение перед добавлением новых типов в список, рассмотрите использование
расширений с
объявлениями расширений
вместо типов сообщений Any.
Oneof
Если у вас есть сообщение со многими опциональными полями и где не более одного поля будет установлено одновременно, вы можете обеспечить это поведение и сэкономить память, используя функцию oneof.
Поля oneof похожи на опциональные поля, за исключением того, что все поля в oneof разделяют
память, и не более одного поля может быть установлено одновременно. Установка любого члена
oneof автоматически очищает всех других членов. Вы можете проверить, какое значение
в oneof установлено (если есть), используя специальный метод case() или WhichOneof(),
в зависимости от выбранного вами языка.
Обратите внимание, что если установлено несколько значений, последнее установленное значение, определенное порядком в proto, перезапишет все предыдущие.
Номера полей для полей oneof должны быть уникальными в пределах заключающего сообщения.
Использование Oneof
Чтобы определить oneof в вашем .proto, вы используете ключевое слово oneof, за которым следует ваше
имя oneof, в этом случае test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Затем вы добавляете ваши поля oneof в определение oneof. Вы можете добавлять поля
любого типа, кроме полей map, но вы не можете использовать ключевые слова required, optional или
repeated. Если вам нужно добавить повторяющееся поле в oneof, вы можете использовать
сообщение, содержащее повторяющееся поле.
В вашем сгенерированном коде поля oneof имеют те же геттеры и сеттеры, что и
обычные optional поля. Вы также получаете специальный метод для проверки, какое
значение (если есть) в oneof установлено. Вы можете узнать больше об API oneof
для вашего выбранного языка в соответствующем
справочнике по API.
Особенности Oneof
-
Установка поля oneof автоматически очистит всех других членов oneof. Так что если вы установите несколько полей oneof, только последнее поле, которое вы установили, все еще будет иметь значение.
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); // Вызов mutable_sub_message() очистит поле name и установит // sub_message в новый экземпляр SubMessage без установленных полей. message.mutable_sub_message(); CHECK(!message.has_name()); -
Если парсер встречает несколько членов одного и того же oneof в передаче, только последний увиденный член используется в разобранном сообщении. При разборе данных в передаче, начиная с начала байтов, оцените следующее значение и примените следующие правила разбора:
-
Сначала проверьте, установлено ли другое поле в том же oneof в настоящее время, и если да, очистите его.
-
Затем примените содержимое, как если бы поле не было в oneof:
- Примитив перезапишет любое уже установленное значение
- Сообщение объединится с любым уже установленным значением
-
-
Расширения не поддерживаются для oneof.
-
Oneof не может быть
repeated. -
Reflection API работают для полей oneof.
-
Если вы установите поле oneof в значение по умолчанию (например, установите поле oneof int32 в 0), «случай» этого поля oneof будет установлен, и значение будет сериализовано в передаче.
-
Если вы используете C++, убедитесь, что ваш код не вызывает сбоев памяти. Следующий пример кода приведет к сбою, потому что
sub_messageуже был удален вызовом методаset_name().SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Удалит sub_message sub_message->set_... // Сбой здесь -
Снова в C++, если вы
Swap()два сообщения с oneof, каждое сообщение окажется со случаем oneof другого: в примере нижеmsg1будет иметьsub_messageиmsg2будет иметьname.SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK(msg2.has_name());
Проблемы обратной совместимости
Будьте осторожны при добавлении или удалении полей oneof. Если проверка значения oneof возвращает None/NOT_SET, это может означать, что oneof не был установлен или
он был установлен в поле в другой версии oneof. Нет способа
отличить разницу, поскольку нет способа узнать, является ли неизвестное поле в
передаче членом oneof.
Проблемы повторного использования тегов
- Перемещение опциональных полей в oneof или из oneof: Вы можете потерять некоторую вашу информацию (некоторые поля будут очищены) после того, как сообщение будет сериализовано и разобрано. Однако, вы можете безопасно переместить одно поле в новый oneof и, возможно, сможете переместить несколько полей, если известно, что только одно когда-либо установлено. См. Обновление типа сообщения для дальнейших деталей.
- Удаление поля oneof и его возвращение: Это может очистить ваше текуще установленное поле oneof после сериализации и разбора сообщения.
- Разделение или объединение oneof: Это имеет проблемы, похожие на перемещение
optionalполей.
Карты
Если вы хотите создать ассоциативную карту как часть вашего определения данных, protocol buffers предоставляет удобный сокращенный синтаксис:
map<key_type, value_type> map_field = N;
...где key_type может быть любым целочисленным или строковым типом (так, любой
скалярный тип, кроме типов с плавающей точкой и bytes). Обратите внимание, что
ни enum, ни proto сообщения не допустимы для key_type.
value_type может быть любым типом, кроме другой карты.
Итак, например, если вы хотите создать карту проектов, где каждое сообщение Project
связано со строковым ключом, вы можете определить это так:
map<string, Project> projects = 3;
Особенности карт
- Расширения не поддерживаются для карт.
- Карты не могут быть
repeated,optionalилиrequired. - Порядок в формате передачи и порядок итерации карты значений не определен, поэтому вы не можете полагаться на то, что элементы вашей карты находятся в определенном порядке.
- При генерации текстового формата для
.protoкарты сортируются по ключу. Числовые ключи сортируются численно. - При разборе из передачи или при слиянии, если есть дублирующиеся ключи карты, используется последний увиденный ключ. При разборе карты из текстового формата разбор может завершиться неудачей, если есть дублирующиеся ключи.
- Если вы предоставляете ключ, но не значение для поля карты, поведение при сериализации поля зависит от языка. В C++, Java, Kotlin и Python сериализуется значение по умолчанию для типа, в то время как в других языках ничего не сериализуется.
- Никакой символ
FooEntryне может существовать в той же области видимости, что и картаfoo, потому чтоFooEntryуже используется реализацией карты.
Сгенерированный API карт в настоящее время доступен для всех поддерживаемых языков. Вы можете узнать больше об API карт для вашего выбранного языка в соответствующем справочнике по API.
Обратная совместимость
Синтаксис карт эквивалентен следующему в передаче, поэтому реализации protocol buffers, которые не поддерживают карты, все еще могут обрабатывать ваши данные:
message MapFieldEntry {
optional key_type key = 1;
optional value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Любая реализация protocol buffers, которая поддерживает карты, должна и производить, и принимать данные, которые могут быть приняты предыдущим определением.
Пакеты
Вы можете добавить необязательный спецификатор package в файл .proto, чтобы предотвратить конфликты имен
между типами сообщений протокола.
package foo.bar;
message Open { ... }
Затем вы можете использовать спецификатор пакета при определении полей вашего типа сообщения:
message Foo {
...
optional foo.bar.Open open = 1;
...
}
Способ, которым спецификатор пакета влияет на сгенерированный код, зависит от вашего выбранного языка:
- В C++ сгенерированные классы обернуты внутри пространства имен C++. Например,
Openбудет в пространстве именfoo::bar. - В Java и Kotlin пакет используется как Java пакет, если только
вы явно не предоставите
option java_packageв вашем файле.proto. - В Python директива
packageигнорируется, поскольку модули Python организованы в соответствии с их расположением в файловой системе. - В Go директива
packageигнорируется, и сгенерированный файл.pb.goнаходится в пакете, названном в соответствии с соответствующим правилом Bazelgo_proto_library. Для проектов с открытым исходным кодом вы должны предоставить либо опциюgo_package, либо установить флаг Bazel-M. - В Ruby сгенерированные классы обернуты внутри вложенных пространств имен Ruby,
преобразованных в требуемый стиль капитализации Ruby (первая
буква заглавная; если первый символ не буква, добавляется
PB_). Например,Openбудет в пространстве именFoo::Bar. - В PHP пакет используется как пространство имен после преобразования в
PascalCase, если вы явно не предоставите
option php_namespaceв вашем файле.proto. Например,Openбудет в пространстве именFoo\Bar. - В C# пакет используется как пространство имен после преобразования в
PascalCase, если вы явно не предоставите
option csharp_namespaceв вашем файле.proto. Например,Openбудет в пространстве именFoo.Bar.
Обратите внимание, что даже когда директива package не влияет напрямую на
сгенерированный код, например в Python, все равно настоятельно рекомендуется
указывать пакет для файла .proto, так как в противном случае это может привести к конфликтам имен
в дескрипторах и сделать proto непереносимым для других языков.
Пакеты и разрешение имен
Разрешение имен типов в языке protocol buffer работает как в C++: сначала
ищется самая внутренняя область, затем следующая по внутренности и так далее, причем каждый
пакет считается «внутренним» по отношению к своему родительскому пакету. Начальная '.' (например,
.foo.bar.Baz) означает начать с самой внешней области вместо этого.
Компилятор protocol buffer разрешает все имена типов, разбирая импортированные
файлы .proto. Генератор кода для каждого языка знает, как ссылаться на каждый
тип на этом языке, даже если он имеет разные правила области видимости.
Определение сервисов
Если вы хотите использовать ваши типы сообщений с системой RPC (Удаленный вызов процедур),
вы можете определить интерфейс RPC сервиса в файле .proto, и
компилятор protocol buffer сгенерирует код интерфейса сервиса и заглушки на вашем выбранном языке. Итак, например, если вы хотите определить RPC сервис с
методом, который принимает ваш SearchRequest и возвращает SearchResponse, вы можете
определить это в вашем файле .proto следующим образом:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
По умолчанию компилятор протокола затем сгенерирует абстрактный интерфейс
called SearchService и соответствующую «заглушку» (stub) реализацию. Заглушка
пересылает все вызовы на RpcChannel, который, в свою очередь, является абстрактным интерфейсом
который вы должны определить сами в терминах вашей собственной RPC системы. Например, вы
можете реализовать RpcChannel, который сериализует сообщение и отправляет его на сервер
через HTTP. Другими словами, сгенерированная заглушка предоставляет типобезопасный
интерфейс для выполнения RPC вызовов на основе protocol buffer,
не привязывая вас к какой-либо конкретной RPC реализации. Так, в C++, вы можете получить код, похожий на
этот:
using google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void DoSearch() {
// Вы предоставляете классы MyRpcChannel и MyRpcController, которые реализуют
// абстрактные интерфейсы protobuf::RpcChannel и protobuf::RpcController.
channel = new MyRpcChannel("somehost.example.com:1234");
controller = new MyRpcController;
// Компилятор протокола генерирует класс SearchService на основе
// определения, данного ранее.
service = new SearchService::Stub(channel);
// Настройка запроса.
request.set_query("protocol buffers");
// Выполнение RPC.
service->Search(controller, &request, &response,
protobuf::NewCallback(&Done));
}
void Done() {
delete service;
delete channel;
delete controller;
}
Все сервисные классы также реализуют интерфейс Service, который предоставляет способ
вызывать конкретные методы, не зная имени метода или его входных и выходных
типов во время компиляции. На стороне сервера это может быть использовано для реализации RPC
сервера, в который вы могли бы регистрировать сервисы.
using google::protobuf;
class ExampleSearchService : public SearchService {
public:
void Search(protobuf::RpcController* controller,
const SearchRequest* request,
SearchResponse* response,
protobuf::Closure* done) {
if (request->query() == "google") {
response->add_result()->set_url("http://www.google.com");
} else if (request->query() == "protocol buffers") {
response->add_result()->set_url("http://protobuf.googlecode.com");
}
done->Run();
}
};
int main() {
// Вы предоставляете класс MyRpcServer. Он не должен реализовывать какой-либо
// конкретный интерфейс; это просто пример.
MyRpcServer server;
protobuf::Service* service = new ExampleSearchService;
server.ExportOnPort(1234, service);
server.Run();
delete service;
return 0;
}
Если вы не хотите подключать вашу собственную существующую RPC систему, вы можете использовать
gRPC: независимую от языка и платформы
открытую RPC систему, разработанную в Google. gRPC особенно хорошо работает с
protocol buffers и позволяет вам генерировать соответствующий RPC код непосредственно из ваших
файлов .proto с помощью специального плагина компилятора protocol buffer. Однако, поскольку
существуют потенциальные проблемы совместимости между клиентами и серверами, сгенерированными
с proto2 и proto3, мы рекомендуем вам использовать proto3 или edition 2023 для
определения gRPC сервисов. Вы можете узнать больше о синтаксисе proto3 в
Руководстве по языку Proto3 и
о edition 2023 в
Руководстве по языку Edition 2023.
В дополнение к gRPC, также существует ряд сторонних проектов по разработке RPC реализаций для Protocol Buffers. Для списка ссылок на проекты, о которых мы знаем, см. вики-страницу сторонних дополнений.
Отображение JSON
Стандартный бинарный формат передачи protobuf является предпочтительным форматом сериализации для общения между двумя системами, которые используют protobuf. Для общения с системами, которые используют JSON, а не формат передачи protobuf, Protobuf поддерживает каноническое кодирование в JSON.
Опции
Отдельные объявления в файле .proto могут быть аннотированы рядом
опций. Опции не меняют общее значение объявления, но могут
влиять на способ его обработки в определенном контексте. Полный список доступных опций определен в /google/protobuf/descriptor.proto.
Некоторые опции являются опциями уровня файла, что означает, что они должны быть записаны на верхнем уровне, не внутри любого сообщения, enum или определения сервиса. Некоторые опции являются опциями уровня сообщения, что означает, что они должны быть записаны внутри определений сообщений. Некоторые опции являются опциями уровня поля, что означает, что они должны быть записаны внутри определений полей. Опции также могут быть записаны на типах перечислений, значениях перечислений, полях oneof, типах сервисов и методах сервисов; однако, в настоящее время нет полезных опций для любого из этих.
Вот несколько наиболее часто используемых опций:
-
java_package(опция файла): Пакет, который вы хотите использовать для ваших сгенерированных Java/Kotlin классов. Если явная опцияjava_packageне задана в файле.proto, то по умолчанию будет использоваться proto пакет (указанный с помощью ключевого слова "package" в файле.proto). Однако proto пакеты обычно не являются хорошими Java пакетами, поскольку proto пакеты не ожидаются начинающимися с обратных доменных имен. Если не генерируется Java или Kotlin код, эта опция не имеет эффекта.option java_package = "com.example.foo"; -
java_outer_classname(опция файла): Имя класса (и, следовательно, имя файла) для класса-обертки Java, который вы хотите сгенерировать. Если явныйjava_outer_classnameне указан в файле.proto, имя класса будет сконструировано путем преобразования имени файла.protoв camel-case (такfoo_bar.protoстановитсяFooBar.java). Если опцияjava_multiple_filesотключена, то все другие классы/enums/etc., сгенерированные для файла.proto, будут сгенерированы внутри этого внешнего класса-обертки Java как вложенные классы/enums/etc. Если не генерируется Java код, эта опция не имеет эффекта.option java_outer_classname = "Ponycopter"; -
java_multiple_files(опция файла): Если false, будет сгенерирован только один файл.javaдля этого файла.proto, и все Java классы/enums/etc., сгенерированные для сообщений верхнего уровня, сервисов и перечислений, будут вложены внутри внешнего класса (см.java_outer_classname). Если true, отдельные файлы.javaбудут сгенерированы для каждого из Java классов/enums/etc., сгенерированных для сообщений верхнего уровня, сервисов и перечислений, и класс-обертка Java, сгенерированный для этого файла.proto, не будет содержать никаких вложенных классов/enums/etc. Это логическая опция, которая по умолчаниюfalse. Если не генерируется Java код, эта опция не имеет эффекта.option java_multiple_files = true; -
optimize_for(опция файла): Может быть установлена вSPEED,CODE_SIZEилиLITE_RUNTIME. Это влияет на генераторы кода C++ и Java (и возможно, сторонние генераторы) следующим образом:SPEED(по умолчанию): Компилятор protocol buffer сгенерирует код для сериализации, разбора и выполнения других общих операций над вашими типами сообщений. Этот код высоко оптимизирован.CODE_SIZE: Компилятор protocol buffer сгенерирует минимальные классы и будет полагаться на общий, основанный на рефлексии код для реализации сериализации, разбора и различных других операций. Сгенерированный код будет таким образом намного меньше, чем сSPEED, но операции будут медленнее. Классы все еще будут реализовывать точно такой же публичный API, как они делают в режимеSPEED. Этот режим наиболее полезен в приложениях, которые содержат очень большое количество файлов.protoи не нуждаются в том, чтобы все они были ослепительно быстрыми.LITE_RUNTIME: Компилятор protocol buffer сгенерирует классы, которые зависят только от «облегченной» библиотеки времени выполнения (libprotobuf-liteвместоlibprotobuf). Облегченная среда выполнения намного меньше, чем полная библиотека (примерно на порядок меньше), но опускает определенные функции, такие как дескрипторы и рефлексия. Это особенно полезно для приложений, работающих на ограниченных платформах, таких как мобильные телефоны. Компилятор все еще будет генерировать быстрые реализации всех методов, как он делает в режимеSPEED. Сгенерированные классы будут реализовывать только интерфейсMessageLiteв каждом языке, который предоставляет только подмножество методов полного интерфейсаMessage.
option optimize_for = CODE_SIZE; -
cc_generic_services,java_generic_services,py_generic_services(опции файла): Универсальные сервисы устарели. Должен ли компилятор protocol buffer генерировать абстрактный сервисный код на основе определений сервисов в C++, Java и Python, соответственно. По legacy причинам, они по умолчаниюtrue. Однако, начиная с версии 2.3.0 (январь 2010), считается предпочтительным, чтобы RPC реализации предоставляли плагины генератора кода для генерации кода, более специфичного для каждой системы, а не полагаться на «абстрактные» сервисы.// Этот файл полагается на плагины для генерации сервисного кода. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false; -
cc_enable_arenas(опция файла): Включает выделение арены для C++ сгенерированного кода. -
objc_class_prefix(опция файла): Устанавливает префикс класса Objective-C, который добавляется ко всем сгенерированным классам и enum Objective-C из этого .proto. Значения по умолчания нет. Вы должны использовать префиксы, которые состоят из 3-5 заглавных символов, как рекомендовано Apple. Обратите внимание, что все 2-буквенные префиксы зарезервированы Apple. -
message_set_wire_format(опция сообщения): Если установлено вtrue, сообщение использует другой бинарный формат, предназначенный для совместимости со старым форматом, используемым внутри Google, под названиемMessageSet. Пользователи вне Google, вероятно, никогда не будут нуждаться в этой опции. Сообщение должно быть объявлено точно следующим образом:message Foo { option message_set_wire_format = true; extensions 4 to max; } -
packed(опция поля): Если установлено вtrueна повторяющемся поле базового числового типа, это вызывает использование более компактного кодирования. Единственная причина не использовать эту опцию — если вам нужна совместимость с парсерами до версии 2.3.0. Эти старые парсеры игнорировали упакованные данные, когда это не ожидалось. Следовательно, было невозможно изменить существующее поле на упакованный формат без нарушения совместимости передачи. В 2.3.0 и позже, это изменение безопасно, так как парсеры для упаковываемых полей всегда принимают оба формата, но будьте осторожны, если вам приходится иметь дело со старыми программами, использующими старые версии protobuf.repeated int32 samples = 4 [packed = true]; -
deprecated(опция поля): Если установлено вtrue, указывает, что поле устарело и не должно использоваться новым кодом. В большинстве языков это не имеет фактического эффекта. В Java это становится аннотацией@Deprecated. Для C++, clang-tidy будет генерировать предупреждения всякий раз, когда используются устаревшие поля. В будущем другие языко-специфичные генераторы кода могут генерировать аннотации устаревания на методах доступа поля, что, в свою очередь, вызовет предупреждение при компиляции кода, который пытается использовать поле. Если поле не используется никем и вы хотите предотвратить использование его новыми пользователями, рассмотрите замену объявления поля на зарезервированное утверждение.optional int32 old_field = 6 [deprecated = true];
Опции значений перечисления
Опции значений перечисления поддерживаются. Вы можете использовать опцию deprecated для
указания, что значение больше не должно использоваться. Вы также можете создавать пользовательские
опции, используя расширения.
Следующий пример показывает синтаксис для добавления этих опций:
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
Код C++ для чтения опции string_name может выглядеть примерно так:
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
См. Пользовательские опции, чтобы узнать, как применять пользовательские опции к значениям перечисления и к полям.
Пользовательские опции
Protocol Buffers также позволяет вам определять и использовать ваши собственные опции. Обратите внимание, что
это продвинутая функция, которая большинству людей не нужна. Поскольку опции
определены сообщениями, определенными в google/protobuf/descriptor.proto (как
FileOptions или FieldOptions), определение ваших собственных опций — это просто вопрос
расширения этих сообщений. Например:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
Здесь мы определили новую опцию уровня сообщения, расширив MessageOptions.
Когда мы затем используем опцию, имя опции должно быть заключено в круглые скобки, чтобы
указать, что это расширение. Теперь мы можем прочитать значение my_option в
C++ примерно так:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
Здесь MyMessage::descriptor()->options() возвращает протокольное сообщение MessageOptions
для MyMessage. Чтение пользовательских опций из него — это так же, как чтение любого другого
расширения.
Аналогично, в Java мы бы написали:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
.getExtension(MyProtoFile.myOption);
В Python это было бы:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
.Extensions[my_proto_file_pb2.my_option]
Пользовательские опции могут быть определены для каждого вида конструкций в языке Protocol Buffers. Вот пример, который использует каждый вид опции:
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {
optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50007;
}
option (my_file_option) = "Hello world!";
message MyMessage {
option (my_message_option) = 1234;
optional int32 foo = 1 [(my_field_option) = 4.5];
optional string bar = 2;
oneof qux {
option (my_oneof_option) = 42;
string quux = 3;
}
}
enum MyEnum {
option (my_enum_option) = true;
FOO = 1 [(my_enum_value_option) = 321];
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO;
rpc MyMethod(RequestType) returns(ResponseType) {
// Примечание: my_method_option имеет тип MyMessage. Мы можем установить каждое поле
// внутри него, используя отдельную строку "option".
option (my_method_option).foo = 567;
option (my_method_option).bar = "Some string";
}
}
Обратите внимание, что если вы хотите использовать пользовательскую опцию в пакете, отличном от того, в котором она была определена, вы должны добавить префикс имени опции с именем пакета, точно так же, как вы делаете для имен типов. Например:
// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
option (foo.my_option) = "Hello world!";
}
Последнее: Поскольку пользовательские опции являются расширениями, они должны быть назначены номера полей как любое другое поле или расширение. В примерах ранее мы использовали номера полей в диапазоне 50000-99999. Этот диапазон зарезервирован для внутреннего использования внутри отдельных организаций, поэтому вы можете свободно использовать числа в этом диапазоне для внутренних приложений. Однако, если вы планируете использовать пользовательские опции в публичных приложениях, тогда важно, чтобы вы убедились, что ваши номера полей глобально уникальны. Чтобы получить глобально уникальные номера полей, отправьте запрос на добавление записи в глобальный реестр расширений protobuf. Обычно вам нужно только одно число расширения. Вы можете объявить несколько опций с только одним номером расширения, поместив их в под-сообщение:
message FooOptions {
optional int32 opt1 = 1;
optional string opt2 = 2;
}
extend google.protobuf.FieldOptions {
optional FooOptions foo_options = 1234;
}
// использование:
message Bar {
optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
// альтернативный агрегатный синтаксис (использует TextFormat):
optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}
Также обратите внимание, что каждый тип опции (уровень файла, уровень сообщения, уровень поля и т.д.) имеет свое собственное пространство номеров, так что, например, вы могли бы объявить расширения FieldOptions и MessageOptions с одним и тем же номером.
Сохранение опций
Опции имеют понятие сохранения (retention), которое контролирует, сохраняется ли опция
в сгенерированном коде. Опции имеют сохранение времени выполнения по умолчанию,
что означает, что они сохраняются в сгенерированном коде и, таким образом, видны во время выполнения в сгенерированном пуле дескрипторов. Однако вы можете установить retention = RETENTION_SOURCE, чтобы указать, что опция (или поле внутри опции) не должна сохраняться во время выполнения. Это называется сохранением исходного кода.
Сохранение опций – это продвинутая функция, о которой большинству пользователей не нужно беспокоиться,
но она может быть полезна, если вы хотите использовать определенные опции без
оплаты стоимости размера кода за их сохранение в ваших бинарных файлах. Опции с
сохранением исходного кода все еще видны protoc и плагинам protoc, поэтому
генераторы кода могут использовать их для настройки своего поведения.
Сохранение может быть установлено непосредственно на опции, вот так:
extend google.protobuf.FileOptions {
optional int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
Оно также может быть установлено на простом поле, в этом случае оно вступает в силу только когда это поле появляется внутри опции:
message OptionsMessage {
optional int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
Вы можете установить retention = RETENTION_RUNTIME, если хотите, но это не имеет эффекта,
поскольку это поведение по умолчанию. Когда поле сообщения помечено
RETENTION_SOURCE, все его содержимое отбрасывается; поля внутри него не могут
переопределить это, пытаясь установить RETENTION_RUNTIME.
{{% alert title="Примечание" color="note" %}} По состоянию на Protocol Buffers 22.0, поддержка сохранения опций все еще находится в разработке, и только C++ и Java поддерживаются. Go имеет поддержку, начиная с 1.29.0. Поддержка Python завершена, но еще не попала в релиз. {{% /alert %}}
Цели опций
Поля имеют опцию targets, которая контролирует типы сущностей, к которым
поле может применяться при использовании в качестве опции. Например, если поле имеет
targets = TARGET_TYPE_MESSAGE, то это поле не может быть установлено в пользовательской опции
на enum (или любой другой не-сообщенной сущности). Protoc обеспечивает это и будет
выдавать ошибку, если есть нарушение ограничений цели.
На первый взгляд, эта функция может показаться ненужной, учитывая, что каждая пользовательская опция является расширением сообщения опций для конкретной сущности, что уже ограничивает опцию этой одной сущностью. Однако цели опций полезны в случае, когда у вас есть общее сообщение опций, применяемое к нескольким типам сущностей, и вы хотите контролировать использование отдельных полей в этом сообщении. Например:
message MyOptions {
optional string file_only_option = 1 [targets = TARGET_TYPE_FILE];
optional int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
optional MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
optional MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
optional MyOptions enum_options = 50000;
}
// OK: это поле разрешено в опциях файла
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: это поле разрешено в опциях сообщения и enum
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Ошибка: file_only_option не может быть установлен на enum.
option (enum_options).file_only_option = "xyz";
}
Генерация ваших классов
Чтобы сгенерировать Java, Kotlin, Python, C++, Go, Ruby, Objective-C или C# код,
который вам нужен для работы с типами сообщений, определенными в файле .proto, вам
нужно запустить компилятор protocol buffer protoc на файле .proto. Если вы
не установили компилятор,
загрузите пакет и следуйте
инструкциям в README. Для Go вам также нужно установить специальный плагин генератора кода для компилятора; вы можете найти его и инструкции по установке в репозитории golang/protobuf на GitHub.
Компилятор протокола вызывается следующим образом:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATHуказывает каталог, в котором нужно искать файлы.protoпри разрешении директивimport. Если опущено, используется текущий каталог. Несколько каталогов импорта могут быть указаны путем передачи опции--proto_pathнесколько раз; они будут искаться в порядке.-I=_IMPORT_PATH_может быть использована как короткая форма--proto_path.
Примечание: Пути к файлам относительно их proto_path должны быть глобально уникальными в данном
бинарном файле. Например, если у вас есть proto/lib1/data.proto и
proto/lib2/data.proto, эти два файла не могут быть использованы вместе с
-I=proto/lib1 -I=proto/lib2, потому что будет неоднозначно, какой файл import "data.proto" будет означать. Вместо этого следует использовать -Iproto/
и глобальные имена будут lib1/data.proto и lib2/data.proto.
Если вы публикуете библиотеку и другие пользователи могут использовать ваши сообщения напрямую,
вы должны включить уникальное имя библиотеки в путь, под которым они ожидаются
использоваться, чтобы избежать конфликтов имен файлов. Если у вас несколько каталогов в
одном проекте, лучшей практикой является предпочтение установки одного -I в каталог верхнего уровня проекта.
-
Вы можете предоставить одну или несколько директив вывода:
--cpp_outгенерирует C++ код вDST_DIR. См. Справочник по сгенерированному C++ коду для получения дополнительной информации.--java_outгенерирует Java код вDST_DIR. См. Справочник по сгенерированному Java коду для получения дополнительной информации.--kotlin_outгенерирует дополнительный Kotlin код вDST_DIR. См. Справочник по сгенерированному Kotlin коду для получения дополнительной информации.--python_outгенерирует Python код вDST_DIR. См. Справочник по сгенерированному Python коду для получения дополнительной информации.--go_outгенерирует Go код вDST_DIR. См. Справочник по сгенерированному Go коду для получения дополнительной информации.--ruby_outгенерирует Ruby код вDST_DIR. См. Справочник по сгенерированному Ruby коду для получения дополнительной информации.--objc_outгенерирует Objective-C код вDST_DIR. См. Справочник по сгенерированному Objective-C коду для получения дополнительной информации.--csharp_outгенерирует C# код вDST_DIR. См. Справочник по сгенерированному C# коду для получения дополнительной информации.--php_outгенерирует PHP код вDST_DIR. См. Справочник по сгенерированному PHP коду для получения дополнительной информации.
В качестве дополнительного удобства, если
DST_DIRзаканчивается на.zipили.jar, компилятор запишет вывод в один архивный файл формата ZIP с заданным именем. Вывод.jarтакже получит файл манифеста, как требуется спецификацией Java JAR. Обратите внимание, что если выходной архив уже существует, он будет перезаписан. -
Вы должны предоставить один или несколько файлов
.protoв качестве входных данных. Несколько файлов.protoмогут быть указаны сразу. Хотя файлы названы относительно текущего каталога, каждый файл должен находиться в одном изIMPORT_PATH, чтобы компилятор мог определить его каноническое имя.
Расположение файлов
Предпочитайте не помещать файлы .proto в тот же
каталог, что и другие языковые исходники. Рассмотрите
создание подпакета proto для файлов .proto под корневым пакетом для
вашего проекта.
Расположение должно быть независимым от языка
При работе с Java кодом удобно помещать связанные файлы .proto в
тот же каталог, что и Java исходники. Однако, если какой-либо не-Java код когда-либо использует те же
protos, префикс пути больше не будет иметь смысла. Поэтому
в общем, помещайте protos в связанный независимый от языка каталог, такой как
//myteam/mypackage.
Исключением из этого правила является случай, когда ясно, что protos будут использоваться только в Java контексте, например, для тестирования.
Поддерживаемые платформы
Для информации о:
- операционных системах, компиляторах, системах сборки и версиях C++, которые поддерживаются, см. Основная политика поддержки C++.
- версиях PHP, которые поддерживаются, см. Поддерживаемые версии PHP.
Руководство по языку (proto 3)
Охватывает, как использовать редакцию proto3 языка Protocol Buffers в вашем проекте.
Это руководство описывает, как использовать язык буферов протокола для структурирования ваших данных, включая синтаксис файлов .proto и генерацию классов доступа к данным из ваших файлов .proto. Оно охватывает proto3 редакцию языка буферов протокола.
Для получения информации о синтаксисе editions, см. Руководство по языку Protobuf Editions.
Для получения информации о синтаксисе proto2, см. Руководство по языку Proto2.
Это справочное руководство – пошаговый пример, использующий многие функции, описанные в этом документе, см. в руководстве для выбранного вами языка.
Определение типа сообщения
Сначала рассмотрим очень простой пример. Допустим, вы хотите определить формат сообщения для поискового запроса, где каждый запрос имеет строку запроса, номер интересующей страницы результатов и количество результатов на странице. Вот файл .proto, который вы используете для определения типа сообщения.
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
- Первая строка файла указывает, что вы используете редакцию proto3 спецификации языка protobuf.
edition(илиsyntaxдля proto2/proto3) должна быть первой непустой, не комментарием строкой файла.- Если
editionилиsyntaxне указана, компилятор буферов протокола предположит, что вы используете proto2.
- Определение сообщения
SearchRequestзадает три поля (пары имя/значение), по одному для каждой части данных, которые вы хотите включить в этот тип сообщения. Каждое поле имеет имя и тип.
Указание типов полей
В предыдущем примере все поля являются скалярными типами: два целых числа (page_number и results_per_page) и строка (query). Вы также можете указать перечисления и составные типы, такие как другие типы сообщений для вашего поля.
Назначение номеров полей
Вы должны дать каждому полю в определении вашего сообщения номер от 1 до 536,870,911 со следующими ограничениями:
- Данный номер должен быть уникальным среди всех полей этого сообщения.
- Номера полей с
19,000до19,999зарезервированы для реализации Protocol Buffers. Компилятор буферов протокола будет жаловаться, если вы используете один из этих зарезервированных номеров полей в вашем сообщении. - Вы не можете использовать любые ранее зарезервированные номера полей или любые номера полей, выделенные для расширений.
Этот номер нельзя изменить после того, как ваш тип сообщения используется, потому что он идентифицирует поле в формате сообщения на проводе. «Изменение» номера поля эквивалентно удалению этого поля и созданию нового поля с тем же типом, но новым номером. См. Удаление полей для правильного выполнения.
Номера полей никогда не должны использоваться повторно. Никогда не извлекайте номер поля из зарезервированного списка для повторного использования с новым определением поля. См. Последствия повторного использования номеров полей.
Вы должны использовать номера полей от 1 до 15 для наиболее часто устанавливаемых полей. Меньшие значения номеров полей занимают меньше места в wire-формате. Например, номера полей в диапазоне от 1 до 15 занимают один байт для кодирования. Номера полей в диапазоне от 16 до 2047 занимают два байта. Подробнее об этом можно узнать в Кодировании Protocol Buffer.
Последствия повторного использования номеров полей
Повторное использование номера поля делает декодирование сообщений в wire-формате неоднозначным.
Формат провода protobuf является lean и не предоставляет способа обнаружения полей, закодированных с использованием одного определения и декодированных с использованием другого.
Кодирование поля с использованием одного определения и последующее декодирование этого же поля с другим определением может привести к:
- Потере времени разработчика на отладку
- Ошибке разбора/слияния (лучший сценарий)
- Утечке PII/SPII
- Повреждению данных
Распространенные причины повторного использования номеров полей:
- перенумерация полей (иногда делается для достижения более эстетически приятного порядка номеров полей). Перенумерация фактически удаляет и заново добавляет все поля, вовлеченные в перенумерацию, что приводит к несовместимым изменениям wire-формата.
- удаление поля и не резервирование номера для предотвращения будущего повторного использования.
Номер поля ограничен 29 битами вместо 32 бит, потому что три бита используются для указания формата провода поля. Для получения дополнительной информации см. тему Кодирование.
Указание мощности полей
Поля сообщения могут быть одним из следующих:
-
Одиночные (Singular):
В proto3 есть два типа одиночных полей:
-
optional: (рекомендуется) Полеoptionalнаходится в одном из двух возможных состояний:- поле установлено и содержит значение, которое было явно задано или разобрано из провода. Оно будет сериализовано в провод.
- поле не установлено и будет возвращать значение по умолчанию. Оно не будет сериализовано в провод.
Вы можете проверить, было ли значение явно установлено.
optionalрекомендуется вместо неявных полей для максимальной совместимости с редакциями protobuf и proto2.
-
неявные (implicit): (не рекомендуется) Неявное поле не имеет явной метки мощности и ведет себя следующим образом:
- если поле является типом сообщения, оно ведет себя так же, как поле
optional. - если поле не является сообщением, оно имеет два состояния:
- поле установлено в не-умолчательное (ненулевое) значение, которое было явно задано или разобрано из провода. Оно будет сериализовано в провод.
- поле установлено в значение по умолчанию (ноль). Оно не будет сериализовано в провод. Фактически, вы не можете определить, было ли значение по умолчанию (ноль) установлено, разобрано из провода или не предоставлено вовсе. Для получения дополнительной информации по этой теме см. Присутствие поля.
- если поле является типом сообщения, оно ведет себя так же, как поле
-
-
repeated: этот тип поля может повторяться ноль или более раз в правильно сформированном сообщении. Порядок повторяющихся значений сохраняется. -
map: это поле типа пар ключ/значение. См. Карты для получения дополнительной информации об этом типе поля.
Поля Repeated упаковываются по умолчанию
В proto3 поля repeated скалярных числовых типов используют packed кодирование по умолчанию.
Вы можете узнать больше об packed кодировании в Кодировании Protocol Buffer.
Поля типа сообщения всегда имеют присутствие поля
В proto3 поля типа сообщения уже имеют присутствие поля. Из-за этого добавление модификатора optional не меняет присутствие поля для этого поля.
Определения для Message2 и Message3 в следующем примере кода генерируют одинаковый код для всех языков, и нет разницы в представлении в бинарном, JSON и TextFormat:
syntax="proto3";
package foo.bar;
message Message1 {}
message Message2 {
Message1 foo = 1;
}
message Message3 {
optional Message1 bar = 1;
}
Правильно сформированные сообщения
Термин «правильно сформированный» (well-formed), применяемый к сообщениям protobuf, относится к байтам, сериализованным/десериализованным. Парсер protoc проверяет, что данный файл определения proto разбирается.
Одиночные поля могут появляться более одного раза в байтах wire-формата. Парсер примет ввод, но только последний экземпляр этого поля будет доступен через сгенерированные привязки. См. Последний побеждает для получения дополнительной информации по этой теме.
Добавление большего количества типов сообщений
В одном файле .proto можно определить несколько типов сообщений. Это полезно, если вы определяете несколько связанных сообщений – так, например, если вы хотите определить формат сообщения-ответа, соответствующий вашему типу сообщения SearchResponse, вы можете добавить его в тот же .proto:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
Комбинирование сообщений приводит к раздуванию Хотя несколько типов сообщений (таких как message, enum и service) могут быть определены в одном файле .proto, это также может привести к раздуванию зависимостей, когда большое количество сообщений с различными зависимостями определяется в одном файле. Рекомендуется включать как можно меньше типов сообщений на файл .proto.
Добавление комментариев
Чтобы добавить комментарии в ваши файлы .proto:
- Предпочитайте комментарии в стиле C/C++/Java '//' в строке перед элементом кода .proto
- Встроенные/многострочные комментарии в стиле C
/* ... */также принимаются.- При использовании многострочных комментариев предпочтительна строка отступа '*'.
/**
* SearchRequest представляет поисковый запрос с опциями пагинации,
* чтобы указать, какие результаты включить в ответ.
*/
message SearchRequest {
string query = 1;
// Какой номер страницы нам нужен?
int32 page_number = 2;
// Количество результатов, возвращаемых на страницу.
int32 results_per_page = 3;
}
Удаление полей
Удаление полей может вызвать серьезные проблемы, если не делать это правильно.
Когда вам больше не нужно поле, и все ссылки на него были удалены из клиентского кода, вы можете удалить определение поля из сообщения. Однако вы должны зарезервировать удаленный номер поля. Если вы не зарезервируете номер поля, возможно, что в будущем разработчик повторно использует этот номер.
Вам также следует зарезервировать имя поля, чтобы кодировки JSON и TextFormat вашего сообщения продолжали разбираться.
Зарезервированные номера полей
Если вы обновляете тип сообщения, полностью удаляя поле или закомментируя его, будущие разработчики могут повторно использовать номер поля при внесении своих собственных обновлений в тип. Это может вызвать серьезные проблемы, как описано в Последствия повторного использования номеров полей. Чтобы этого не произошло, добавьте удаленный номер поля в список reserved.
Компилятор protoc будет генерировать сообщения об ошибках, если какие-либо будущие разработчики попытаются использовать эти зарезервированные номера полей.
message Foo {
reserved 2, 15, 9 to 11;
}
Диапазоны зарезервированных номеров полей включительны (9 to 11 это то же самое, что 9, 10, 11).
Зарезервированные имена полей
Повторное использование старого имени поля позже обычно безопасно, за исключением случаев использования кодировок TextProto или JSON, где имя поля сериализуется. Чтобы избежать этого риска, вы можете добавить удаленное имя поля в список reserved.
Зарезервированные имена влияют только на поведение компилятора protoc, а не на поведение во время выполнения, за одним исключением: реализации TextProto могут отбрасывать неизвестные поля (без вызова ошибки, как с другими неизвестными полями) с зарезервированными именами во время разбора (только реализации C++ и Go делают это сегодня). Парсинг JSON во время выполнения не затрагивается зарезервированными именами.
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
Обратите внимание, что вы не можете смешивать имена полей и числовые значения в одном операторе reserved.
Что генерируется из вашего .proto?
Когда вы запускаете компилятор буферов протокола на .proto файле, компилятор генерирует код на выбранном вами языке, который вам понадобится для работы с типами сообщений, описанными в файле, включая получение и установку значений полей, сериализацию ваших сообщений в выходной поток и разбор ваших сообщений из входного потока.
- Для C++ компилятор генерирует файлы
.hи.ccиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для Java компилятор генерирует файл
.javaс классом для каждого типа сообщения, а также специальный классBuilderдля создания экземпляров классов сообщений. - Для Kotlin в дополнение к сгенерированному Java коду компилятор генерирует файл
.ktдля каждого типа сообщения с улучшенным Kotlin API. Это включает DSL, который упрощает создание экземпляров сообщений, accessor nullable полей и функцию копирования. - Python немного отличается – компилятор Python генерирует модуль со статическим дескриптором каждого типа сообщения в вашем
.proto, который затем используется с метаклассом для создания необходимого класса доступа к данным Python во время выполнения. - Для Go компилятор генерирует файл
.pb.goс типом для каждого типа сообщения в вашем файле. - Для Ruby компилятор генерирует файл
.rbс модулем Ruby, содержащим ваши типы сообщений. - Для Objective-C компилятор генерирует файлы
pbobjc.hиpbobjc.mиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для C# компилятор генерирует файл
.csиз каждого.proto, с классом для каждого типа сообщения, описанного в вашем файле. - Для PHP компилятор генерирует файл сообщения
.phpдля каждого типа сообщения, описанного в вашем файле, и файл метаданных.phpдля каждого компилируемого файла.proto. Файл метаданных используется для загрузки допустимых типов сообщений в пул дескрипторов. - Для Dart компилятор генерирует файл
.pb.dartс классом для каждого типа сообщения в вашем файле.
Вы можете узнать больше об использовании API для каждого языка, следуя руководству для выбранного языка. Для получения более подробной информации об API см. соответствующую справочную информацию по API.
Скалярные типы значений
Скалярное поле сообщения может иметь один из следующих типов – в таблице показан тип, указанный в файле .proto, и соответствующий тип в автоматически сгенерированном классе:
| Тип Proto | Примечания |
|---|---|
| double | Использует формат двойной точности IEEE 754. |
| float | Использует формат одинарной точности IEEE 754. |
| int32 | Использует переменную длину кодирования. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте вместо этого sint32. |
| int64 | Использует переменную длину кодирования. Неэффективно для кодирования отрицательных чисел – если ваше поле, вероятно, будет иметь отрицательные значения, используйте вместо этого sint64. |
| uint32 | Использует переменную длину кодирования. |
| uint64 | Использует переменную длину кодирования. |
| sint32 | Использует переменную длину кодирования. Значение со знаком. Более эффективно кодирует отрицательные числа, чем обычные int32. |
| sint64 | Использует переменную длину кодирования. Значение со знаком. Более эффективно кодирует отрицательные числа, чем обычные int64. |
| fixed32 | Всегда четыре байта. Более эффективно, чем uint32, если значения часто больше 228. |
| fixed64 | Всегда восемь байта. Более эффективно, чем uint64, если значения часто больше 256. |
| sfixed32 | Всегда четыре байта. |
| sfixed64 | Всегда восемь байта. |
| bool | |
| string | Строка должна всегда содержать текст в кодировке UTF-8 или 7-битный ASCII и не может быть длиннее 232. |
| bytes | Может содержать любую произвольную последовательность байт не длиннее 232. |
| Тип Proto | Тип C++ | Тип Java/Kotlin[1] | Тип Python[3] | Тип Go | Тип Ruby | Тип C# | Тип PHP | Тип Dart | Тип Rust |
|---|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | Float | double | float | double | f64 |
| float | float | float | float | float32 | Float | float | float | double | f32 |
| int32 | int32_t | int | int | int32 | Fixnum или Bignum (по мере необходимости) | int | integer | int | i32 |
| int64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| uint32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum или Bignum (по мере необходимости) | uint | integer | int | u32 |
| uint64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sint32 | int32_t | int | int | int32 | Fixnum или Bignum (по мере необходимости) | int | integer | int | i32 |
| sint64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| fixed32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum или Bignum (по мере необходимости) | uint | integer | int | u32 |
| fixed64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
| sfixed32 | int32_t | int | int | int32 | Fixnum или Bignum (по мере необходимости) | int | integer | int | i32 |
| sfixed64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
| bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | bool |
| string | std::string | String | str/unicode[5] | string | String (UTF-8) | string | string | String | ProtoString |
| bytes | std::string | ByteString | str (Python 2), bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string | List |
ProtoBytes |
[1] Kotlin использует соответствующие типы из Java, даже для беззнаковых типов, чтобы обеспечить совместимость в смешанных Java/Kotlin кодовых базах.
[2] В Java беззнаковые 32-битные и 64-битные целые числа представлены с использованием их знаковых аналогов, причем старший бит просто хранится в знаковом бите.
[3] Во всех случаях установка значений в поле выполняет проверку типа, чтобы убедиться, что оно допустимо.
[4] 64-битные или беззнаковые 32-битные целые числа всегда представлены как long при декодировании, но могут быть int, если при установке поля задан int. Во всех случаях значение должно помещаться в тип, представленный при установке. См. [2].
[5] Строки Python представлены как unicode при декодировании, но могут быть str, если задана строка ASCII (это может измениться).
[6] Integer используется на 64-битных машинах, а string используется на 32-битных машинах.
Вы можете узнать больше о том, как эти типы кодируются при сериализации вашего сообщения, в Кодировании Protocol Buffer.
Значения полей по умолчанию
Когда сообщение разбирается, если закодированные байты сообщения не содержат определенное поле, доступ к этому полю в разобранном объекте возвращает значение по умолчанию для этого поля. Значения по умолчанию зависят от типа:
- Для строк значением по умолчанию является пустая строка.
- Для bytes значением по умолчанию являются пустые байты.
- Для bools значением по умолчанию является false.
- Для числовых типов значением по умолчанию является ноль.
- Для полей сообщения поле не установлено. Его точное значение зависит от языка. Подробности см. в руководстве по сгенерированному коду.
- Для перечислений значением по умолчанию является первое определенное значение перечисления, которое должно быть 0. См. Значение перечисления по умолчанию.
Значением по умолчанию для повторяющихся полей является пустое (обычно пустой список на соответствующем языке).
Значением по умолчанию для полей map является пустое (обычно пустая карта на соответствующем языке).
Обратите внимание, что для скалярных полей с неявным присутствием после разбора сообщения нет возможности определить, было ли это поле явно установлено в значение по умолчанию (например, было ли логическое значение установлено в false) или просто не установлено вовсе: вы должны иметь это в виду при определении типов ваших сообщений. Например, не используйте логическое значение, которое включает какое-либо поведение при установке в false, если вы не хотите, чтобы это поведение также происходило по умолчанию. Также обратите внимание, что если скалярное поле сообщения установлено в значение по умолчанию, значение не будет сериализовано в провод. Если значение float или double установлено в +0, оно не будет сериализовано, но -0 считается отличным и будет сериализовано.
См. руководство по сгенерированному коду для вашего выбранного языка для получения более подробной информации о том, как работают значения по умолчанию в сгенерированном коде.
Перечисления
Когда вы определяете тип сообщения, вам может понадобиться, чтобы одно из его полей имело только одно значение из предопределенного списка. Например, допустим, вы хотите добавить поле corpus для каждого SearchRequest, где corpus может быть UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS или VIDEO. Вы можете сделать это очень просто, добавив enum в ваше определение сообщения с константой для каждого возможного значения.
В следующем примере мы добавили enum с именем Corpus со всеми возможными значениями и поле типа Corpus:
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
Значение перечисления по умолчанию
Значением по умолчанию для поля SearchRequest.corpus является CORPUS_UNSPECIFIED, потому что это первое значение, определенное в перечислении.
В proto3 первое значение, определенное в определении перечисления, должно иметь значение ноль и должно иметь имя ENUM_TYPE_NAME_UNSPECIFIED или ENUM_TYPE_NAME_UNKNOWN. Это потому что:
- Должно быть нулевое значение, чтобы мы могли использовать 0 как числовое значение по умолчанию.
- Нулевое значение должно быть первым элементом для совместимости с семантикой proto2, где первое значение перечисления является значением по умолчанию, если явно не указано другое значение.
Также рекомендуется, чтобы это первое, значение по умолчанию, не имело семантического значения, кроме «это значение не было указано».
Псевдонимы значений перечисления
Вы можете определить псевдонимы, присваивая одно и то же значение разным константам перечисления. Для этого вам нужно установить опцию allow_alias в true. В противном случае компилятор буферов протокола сгенерирует предупреждение, когда будут найдены псевдонимы. Хотя все значения псевдонимов действительны для сериализации, при десериализации используется только первое значение.
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Раскомментирование этой строки вызовет предупреждение.
ENAA_FINISHED = 2;
}
Константы перечислителя должны находиться в диапазоне 32-битного целого числа. Поскольку значения enum используют varint кодирование на проводе, отрицательные значения неэффективны и поэтому не рекомендуются. Вы можете определять enum внутри определения сообщения, как в предыдущем примере, или снаружи – эти enum могут быть повторно использованы в любом определении сообщения в вашем файле .proto. Вы также можете использовать тип enum, объявленный в одном сообщении, в качестве типа поля в другом сообщении, используя синтаксис _MessageType_._EnumType_.
Когда вы запускаете компилятор буферов протокола на .proto, который использует enum, сгенерированный код будет иметь соответствующий enum для Java, Kotlin или C++ или специальный класс EnumDescriptor для Python, который используется для создания набора символьных констант с целочисленными значениями в классе, сгенерированном во время выполнения.
{{% alert title="Важно" color="warning" %}} Сгенерированный код может быть подвержен ограничениям, специфичным для языка, на количество перечислителей (несколько тысяч для одного языка). Ознакомьтесь с ограничениями для языков, которые вы планируете использовать. {{% /alert %}}
Во время десериализации нераспознанные значения enum будут сохранены в сообщении, хотя то, как это представлено при десериализации сообщения, зависит от языка. В языках, которые поддерживают открытые типы enum со значениями вне диапазона указанных символов, таких как C++ и Go, неизвестное значение enum просто хранится как его базовое целочисленное представление. В языках с закрытыми типами enum, таких как Java, случай в enum используется для представления нераспознанного значения, и к базовому целому числу можно получить доступ с помощью специальных аксессоров. В любом случае, если сообщение сериализовано, нераспознанное значение все равно будет сериализовано с сообщением.
{{% alert title="Важно" color="warning" %}} Для получения информации о том, как должны работать enum, в сравнении с тем, как они работают в настоящее время на разных языках, см. Поведение Enum. {{% /alert %}}
Для получения дополнительной информации о том, как работать с enum сообщений в ваших приложениях, см. руководство по сгенерированному коду для вашего выбранного языка.
Зарезервированные значения
Если вы обновляете тип enum, полностью удаляя запись enum или комментируя ее, будущие пользователи могут повторно использовать числовое значение при внесении своих собственных обновлений в тип. Это может вызвать серьезные проблемы, если они позже загрузят старые экземпляры того же .proto, включая повреждение данных, ошибки конфиденциальности и т. д. Один из способов убедиться, что этого не произойдет, – указать, что числовые значения (и/или имена, которые также могут вызывать проблемы для сериализации JSON) ваших удаленных записей reserved. Компилятор буферов протокола будет жаловаться, если какие-либо будущие пользователи попытаются использовать эти идентификаторы. Вы можете указать, что ваш зарезервированный числовой диапазон значений доходит до максимально возможного значения, используя ключевое слово max.
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
Обратите внимание, что вы не можете смешивать имена полей и числовые значения в одном операторе reserved.
Использование других типов сообщений
Вы можете использовать другие типы сообщений в качестве типов полей. Например, допустим, вы хотите включить сообщения Result в каждое сообщение SearchResponse – чтобы сделать это, вы можете определить тип сообщения Result в том же .proto и затем указать поле типа Result в SearchResponse:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
Импорт определений
В предыдущем примере тип сообщения Result определен в том же файле, что и SearchResponse – что, если тип сообщения, который вы хотите использовать в качестве типа поля, уже определен в другом файле .proto?
Вы можете использовать определения из других файлов .proto, импортируя их. Чтобы импортировать определения другого .proto, вы добавляете оператор импорта в начало вашего файла:
import "myproject/other_protos.proto";
Компилятор protobuf ищет импортированные файлы в наборе каталогов, указанных с помощью флага -I/--proto_path. Путь, указанный в операторе import, разрешается относительно этих каталогов. Для получения дополнительной информации об использовании компилятора см. Генерация ваших классов.
Например, рассмотрим следующую структуру каталогов:
my_project/
├── protos/
│ ├── main.proto
│ └── common/
│ └── timestamp.proto
Чтобы использовать определения из timestamp.proto внутри main.proto, вы должны запустить компилятор из каталога my_project и установить --proto_path=protos. Тогда оператор import в main.proto будет:
// Находится в my_project/protos/main.proto
import "common/timestamp.proto";
Как правило, вы должны установить флаг --proto_path в каталог самого высокого уровня, который содержит proto. Часто это корень проекта, но в этом примере он находится в отдельном каталоге /protos.
По умолчанию вы можете использовать определения только из непосредственно импортированных файлов .proto. Однако иногда вам может понадобиться переместить файл .proto в новое место. Вместо того чтобы перемещать файл .proto напрямую и обновлять все места вызова одним изменением, вы можете поместить файл-заполнитель .proto в старое местоположение, чтобы перенаправить все импорты в новое местоположение, используя понятие import public.
Примечание: Функциональность публичного импорта, доступная в Java, наиболее эффективна при перемещении всего файла .proto или при использовании java_multiple_files = true. В этих случаях сгенерированные имена остаются стабильными, что позволяет избежать необходимости обновлять ссылки в вашем коде. Хотя технически функциональна при перемещении подмножества файла .proto без java_multiple_files = true, это требует одновременного обновления многих ссылок, поэтому может не значительно облегчить миграцию. Функциональность недоступна в Kotlin, TypeScript, JavaScript, GCL или с целями C++, которые используют статическое отражение protobuf.
Зависимости import public могут быть транзитивно использованы любым кодом, импортирующим proto, содержащий оператор import public. Например:
// new.proto
// Все определения перемещены сюда
// old.proto
// Это proto, которое импортируют все клиенты.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// Вы используете определения из old.proto и new.proto, но не other.proto
Использование типов сообщений proto2
Можно импортировать типы сообщений proto2 и использовать их в ваших proto3 сообщениях, и наоборот. Однако перечисления proto2 нельзя использовать напрямую в синтаксисе proto3 (это нормально, если импортированное сообщение proto2 использует их).
Вложенные типы
Вы можете определять и использовать типы сообщений внутри других типов сообщений, как в следующем примере – здесь сообщение Result определено внутри сообщения SearchResponse:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
Если вы хотите повторно использовать этот тип сообщения вне его родительского типа сообщения, вы ссылаетесь на него как _Parent_._Type_:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
Вы можете вкладывать сообщения настолько глубоко, насколько хотите. В примере ниже обратите внимание, что два вложенных типа с именем Inner полностью независимы, поскольку они определены в разных сообщениях:
message Outer { // Уровень 0
message MiddleAA { // Уровень 1
message Inner { // Уровень 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Уровень 1
message Inner { // Уровень 2
int32 ival = 1;
bool booly = 2;
}
}
}
Обновление типа сообщения
Если существующий тип сообщения больше не удовлетворяет всем вашим потребностям – например, вы хотите, чтобы формат сообщения имел дополнительное поле – но вы все еще хотите использовать код, созданный со старым форматом, не волнуйтесь! Очень просто обновлять типы сообщений, не нарушая любой из вашего существующего кода, когда вы используете бинарный формат провода.
{{% alert title="Примечание" color="note" %}} Если вы используете ProtoJSON или текстовый формат proto для хранения ваших сообщений буфера протокола, изменения, которые вы можете внести в ваше определение proto, отличаются. Безопасные изменения формата провода ProtoJSON описаны здесь. {{% /alert %}}
Проверьте Лучшие практики Proto и следующие правила:
Двоичные небезопасные для провода изменения
Небезопасные для провода изменения – это изменения схемы, которые приведут к поломке, если вы используете разбор данных, которые были сериализованы с использованием старой схемы, с парсером, который использует новую схему (или наоборот). Вносите небезопасные для провода изменения только если вы знаете, что все сериализаторы и десериализаторы данных используют новую схему.
- Изменение номеров полей для любого существующего поля не безопасно.
- Изменение номера поля эквивалентно удалению поля и добавлению нового поля с тем же типом. Если вы хотите перенумеровать поле, см. инструкции для удаления поля.
- Перемещение полей в существующий
oneofне безопасно.
Двоичные безопасные для провода изменения
Безопасные для провода изменения – это те, при которых полностью безопасно развивать схему таким образом без риска потери данных или новых сбоев при разборе.
Обратите внимание, что любые безопасные для провода изменения могут быть критическим изменением для кода приложения на данном языке. Например, добавление значения в предсуществующее enum будет критическим изменением компиляции для любого кода с исчерпывающим switch по этому enum. По этой причине Google может избегать внесения некоторых из этих типов изменений в публичные сообщения: AIP содержат рекомендации о том, какие из этих изменений безопасно вносить там.
- Добавление новых полей безопасно.
- Если вы добавляете новые поля, любые сообщения, сериализованные кодом, использующим ваш «старый» формат сообщения, все еще могут быть разобраны вашим новым сгенерированным кодом. Вы должны иметь в виду значения по умолчанию для этих элементов, чтобы новый код мог правильно взаимодействовать с сообщениями, сгенерированными старым кодом. Аналогично, сообщения, созданные вашим новым кодом, могут быть разобраны вашим старым кодом: старые двоичные файлы просто игнорируют новое поле при разборе. См. раздел Неизвестные поля для подробностей.
- Удаление полей безопасно.
- Тот же номер поля не должен использоваться снова в вашем обновленном типе сообщения. Вы можете вместо этого переименовать поле, возможно, добавив префикс "OBSOLETE_", или сделать номер поля зарезервированным, чтобы будущие пользователи вашего
.protoне могли случайно повторно использовать номер.
- Тот же номер поля не должен использоваться снова в вашем обновленном типе сообщения. Вы можете вместо этого переименовать поле, возможно, добавив префикс "OBSOLETE_", или сделать номер поля зарезервированным, чтобы будущие пользователи вашего
- Добавление дополнительных значений в enum безопасно.
- Изменение одного поля с явным присутствием или расширения на член нового
oneofбезопасно. - Изменение
oneof, который содержит только одно поле, на поле с явным присутствием безопасно. - Изменение поля на расширение с тем же номером и типом безопасно.
Двоичные совместимые с проводом изменения (Условно безопасные)
В отличие от безопасных для провода изменений, совместимые с проводом означают, что одни и те же данные могут быть разобраны как до, так и после данного изменения. Однако разбор данных может быть потерейным при такой форме изменения. Например, изменение int32 на int64 является совместимым изменением, но если значение больше INT32_MAX записано, клиент, который читает его как int32, отбросит старшие биты числа.
Вы можете вносить совместимые изменения в вашу схему только если вы тщательно управляете развертыванием в вашей системе. Например, вы можете изменить int32 на int64, но обеспечить, чтобы вы продолжали записывать только допустимые значения int32 до тех пор, пока новая схема не будет развернута на всех конечных точках, и затем subsequently начать записывать большие значения после этого.
Если ваша схема опубликована за пределами вашей организации, вы обычно не должны вносить совместимые с проводом изменения, так как вы не можете управлять развертыванием новой схемы, чтобы знать, когда различные диапазоны значений могут быть безопасны для использования.
int32,uint32,int64,uint64иboolвсе совместимы.- Если число разобрано из провода, которое не помещается в соответствующий тип, вы получите тот же эффект, как если бы вы привели число к этому типу в C++ (например, если 64-битное число читается как int32, оно будет усечено до 32 бит).
sint32иsint64совместимы друг с другом, но не совместимы с другими целочисленными типами.- Если записанное значение было между INT_MIN и INT_MAX включительно, оно будет разобрано как то же значение с любым типом. Если значение sint64 было записано вне этого диапазона и разобрано как sint32, varint усекается до 32 бит, а затем происходит zigzag декодирование (что вызовет наблюдение другого значения).
stringиbytesсовместимы, пока байты являются действительным UTF-8.- Встроенные сообщения совместимы с
bytes, если байты содержат закодированный экземпляр сообщения. fixed32совместим сsfixed32, иfixed64сsfixed64.- Для
string,bytesи полей сообщения, одиночное совместимо сrepeated.- При получении сериализованных данных повторяющегося поля в качестве входных данных, клиенты, которые ожидают, что это поле будет одиночным, примут последнее входное значение, если это поле примитивного типа, или объединят все входные элементы, если это поле типа сообщения. Обратите внимание, что это не вообще безопасно для числовых типов, включая bools и enums. Повторяющиеся поля числовых типов сериализуются в упакованном формате по умолчанию, который не будет правильно разобран, когда ожидается одиночное поле.
enumсовместим сint32,uint32,int64иuint64- Имейте в виду, что клиентский код может обрабатывать их по-разному, когда сообщение десериализуется: например, нераспознанные значения proto3
enumбудут сохранены в сообщении, но то, как это представлено, когда сообщение десериализуется, зависит от языка.
- Имейте в виду, что клиентский код может обрабатывать их по-разному, когда сообщение десериализуется: например, нераспознанные значения proto3
- Изменение поля между
map<K, V>и соответствующим полемrepeatedсообщения является двоично совместимым (см. Карты, ниже, для макета сообщения и других ограничений).- Однако безопасность изменения зависит от приложения: при десериализации и повторной сериализации сообщения клиенты, использующие определение поля
repeated, произведут семантически идентичный результат; однако клиенты, использующие определение поляmap, могут переупорядочивать записи и отбрасывать записи с дублирующимися ключами.
- Однако безопасность изменения зависит от приложения: при десериализации и повторной сериализации сообщения клиенты, использующие определение поля
Неизвестные поля
Неизвестные поля – это правильно сформированные сериализованные данные буфера протокола, представляющие поля, которые парсер не распознает. Например, когда старый двоичный файл разбирает данные, отправленные новым двоичным файлом с новыми полями, эти новые поля становятся неизвестными полями в старом двоичном файле.
Сообщения Proto3 сохраняют неизвестные поля и включают их во время разбора и в сериализованном выводе, что соответствует поведению proto2.
Сохранение неизвестных полей
Некоторые действия могут привести к потере неизвестных полей. Например, если вы сделаете одно из следующих действий, неизвестные поля будут потеряны:
- Сериализовать proto в JSON.
- Перебрать все поля в сообщении, чтобы заполнить новое сообщение.
Чтобы избежать потери неизвестных полей, сделайте следующее:
- Используйте двоичный формат; избегайте использования текстовых форматов для обмена данными.
- Используйте API, ориентированные на сообщения, такие как
CopyFrom()иMergeFrom(), для копирования данных, а не копирование поле за полем.
TextFormat является особым случаем. Сериализация в TextFormat печатает неизвестные поля, используя их номера полей. Но разбор данных TextFormat обратно в двоичный proto завершится неудачей, если есть записи, которые используют номера полей.
Any
Тип сообщения Any позволяет вам использовать сообщения как встроенные типы без наличия их определения .proto. Any содержит произвольное сериализованное сообение как bytes, вместе с URL, который действует как глобально уникальный идентификатор и разрешается к типу этого сообщения. Чтобы использовать тип Any, вам нужно импортировать google/protobuf/any.proto.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
URL типа по умолчанию для данного типа сообщения: type.googleapis.com/_packagename_._messagename_.
Различные реализации языков будут поддерживать вспомогательные библиотеки времени выполнения для упаковки и распаковки значений Any типобезопасным образом – например, в Java тип Any будет иметь специальные методы доступа pack() и unpack(), в то время как в C++ есть методы PackFrom() и UnpackTo():
// Хранение произвольного типа сообщения в Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Чтение произвольного сообщения из Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... обработка network_error ...
}
}
Oneof
Если у вас есть сообщение со многими одиночными полями и где одновременно будет установлено не более одного поля, вы можете обеспечить это поведение и сэкономить память, используя функцию oneof.
Поля oneof похожи на опциональные поля, за исключением того, что все поля в oneof разделяют память, и одновременно может быть установлено не более одного поля. Установка любого члена oneof автоматически очищает всех других членов. Вы можете проверить, какое значение в oneof установлено (если есть), используя специальный метод case() или WhichOneof(), в зависимости от выбранного вами языка.
Обратите внимание, что если установлено несколько значений, последнее установленное значение, определенное порядком в proto, перезапишет все предыдущие.
Номера полей для полей oneof должны быть уникальными в пределах заключающего сообщения.
Использование Oneof
Чтобы определить oneof в вашем .proto, вы используете ключевое слово oneof, за которым следует ваше имя oneof, в этом случае test_oneof:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Затем вы добавляете ваши поля oneof в определение oneof. Вы можете добавлять поля любого типа, кроме map полей и repeated полей. Если вам нужно добавить повторяющееся поле в oneof, вы можете использовать сообщение, содержащее повторяющееся поле.
В вашем сгенерированном коде поля oneof имеют те же геттеры и сеттеры, что и обычные поля. Вы также получаете специальный метод для проверки, какое значение (если есть) в oneof установлено. Вы можете узнать больше об API oneof для вашего выбранного языка в соответствующей справочной информации по API.
Особенности Oneof
-
Установка поля oneof автоматически очищает всех других членов oneof. Так что если вы установите несколько полей oneof, только последнее поле, которое вы установили, все еще будет иметь значение.
SampleMessage message; message.set_name("name"); CHECK_EQ(message.name(), "name"); // Вызов mutable_sub_message() очистит поле name и установит // sub_message в новый экземпляр SubMessage без установки каких-либо его полей. message.mutable_sub_message(); CHECK(message.name().empty()); -
Если парсер встречает несколько членов одного и того же oneof на проводе, в разобранном сообщении используется только последний увиденный член. При разборе данных на проводе, начиная с начала байтов, оцените следующее значение и примените следующие правила разбора:
-
Сначала проверьте, установлено ли в настоящее время другое поле в том же oneof, и если да, очистите его.
-
Затем примените содержимое, как если бы поле не было в oneof:
- Примитив перезапишет любое уже установленное значение
- Сообщение объединится с любым уже установленным значением
-
-
Oneof не может быть
repeated. -
Reflection API работают для полей oneof.
-
Если вы установите поле oneof в значение по умолчанию (например, установите поле oneof int32 в 0), «case» этого поля oneof будет установлен, и значение будет сериализовано в провод.
-
Если вы используете C++, убедитесь, что ваш код не вызывает сбоев памяти. Следующий пример кода приведет к сбою, потому что
sub_messageуже был удален вызовом методаset_name().SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Удалит sub_message sub_message->set_... // Сбой здесь -
Снова в C++, если вы
Swap()два сообщения с oneof, каждое сообщение окажется с case oneof другого: в примере нижеmsg1будет иметьsub_message, аmsg2будет иметьname.SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK_EQ(msg2.name(), "name");
Проблемы обратной совместимости
Будьте осторожны при добавлении или удалении полей oneof. Если проверка значения oneof возвращает None/NOT_SET, это может означать, что oneof не был установлен, или он был установлен в поле в другой версии oneof. Нет возможности сказать разницу, поскольку нет способа узнать, является ли неизвестное поле на проводе членом oneof.
Проблемы повторного использования тегов
- Перемещение одиночных полей в oneof или из oneof: Вы можете потерять некоторую информацию (некоторые поля будут очищены) после сериализации и разбора сообщения. Однако вы можете безопасно переместить одно поле в новый oneof и, возможно, сможете переместить несколько полей, если известно, что только одно из них когда-либо установлено. См. Обновление типа сообщения для дальнейших подробностей.
- Удаление поля oneof и его возвращение: Это может очистить ваше текущее установленное поле oneof после сериализации и разбора сообщения.
- Разделение или объединение oneof: Это имеет проблемы, аналогичные перемещению одиночных полей.
Карты
Если вы хотите создать ассоциативную карту как часть вашего определения данных, буферы протокола предоставляют удобный сокращенный синтаксис:
map<key_type, value_type> map_field = N;
...где key_type может быть любым целочисленным или строковым типом (так что любой скалярный тип, кроме типов с плавающей точкой и bytes). Обратите внимание, что ни enum, ни сообщения proto не допустимы для key_type. value_type может быть любым типом, кроме другой карты.
Итак, например, если вы хотите создать карту проектов, где каждое сообщение Project ассоциировано со строковым ключом, вы можете определить это так:
map<string, Project> projects = 3;
Особенности Карт
- Поля карт не могут быть
repeated. - Порядок в формате провода и порядок итерации карты значений карты не определен, поэтому вы не можете полагаться на то, что элементы вашей карты находятся в определенном порядке.
- При генерации текстового формата для
.protoкарты сортируются по ключу. Числовые ключи сортируются численно. - При разборе из провода или при слиянии, если есть дублирующиеся ключи карты, используется последний увиденный ключ. При разборе карты из текстового формата разбор может завершиться неудачей, если есть дублирующиеся ключи.
- Если вы предоставляете ключ, но не значение для поля карты, поведение при сериализации поля зависит от языка. В C++, Java, Kotlin и Python сериализуется значение по умолчанию для типа, в то время как в других языках ничего не сериализуется.
- Ни один символ
FooEntryне может существовать в той же области видимости, что и картаfoo, потому чтоFooEntryуже используется реализацией карты.
Сгенерированный API карты в настоящее время доступен для всех поддерживаемых языков. Вы можете узнать больше об API карты для вашего выбранного языка в соответствующей справочной информации по API.
Обратная совместимость
Синтаксис карты эквивалентен следующему на проводе, так что реализации буферов протокола, которые не поддерживают карты, все еще могут обрабатывать ваши данные:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Любая реализация буферов протокола, которая поддерживает карты, должна как производить, так и принимать данные, которые могут быть приняты более ранним определением.
Пакеты
Вы можете добавить необязательный спецификатор package в файл .proto, чтобы предотвратить конфликты имен между типами сообщений буфера протокола.
package foo.bar;
message Open { ... }
Затем вы можете использовать спецификатор пакета при определении полей вашего типа сообщения:
message Foo {
...
foo.bar.Open open = 1;
...
}
То, как спецификатор пакета влияет на сгенерированный код, зависит от вашего выбранного языка:
- В C++ сгенерированные классы обернуты внутри пространства имен C++. Например,
Openбудет в пространстве именfoo::bar. - В Java и Kotlin пакет используется как пакет Java, если вы явно не предоставите
option java_packageв вашем файле.proto. - В Python директива
packageигнорируется, поскольку модули Python организованы в соответствии с их расположением в файловой системе. - В Go директива
packageигнорируется, и сгенерированный файл.pb.goнаходится в пакете, названном в соответствии с соответствующим правилом Bazelgo_proto_library. Для проектов с открытым исходным кодом вы должны предоставить либо опциюgo_package, либо установить флаг Bazel-M. - В Ruby сгенерированные классы обернуты внутри вложенных пространств имен Ruby, преобразованных в требуемый стиль капитализации Ruby (первая буква заглавная; если первый символ не буква, добавляется
PB_). Например,Openбудет в пространстве именFoo::Bar. - В PHP пакет используется как пространство имен после преобразования в PascalCase, если вы явно не предоставите
option php_namespaceв вашем файле.proto. Например,Openбудет в пространстве именFoo\Bar. - В C# пакет используется как пространство имен после преобразования в PascalCase, если вы явно не предоставите
option csharp_namespaceв вашем файле.proto. Например,Openбудет в пространстве именFoo.Bar.
Обратите внимание, что даже когда директива package не влияет напрямую на сгенерированный код, например в Python, все равно настоятельно рекомендуется указывать пакет для файла .proto, так как в противном случае это может привести к конфликтам имен в дескрипторах и сделать proto непереносимым для других языков.
Пакеты и разрешение имен
Разрешение имен типов в языке буфера протокола работает как в C++: сначала ищется самая внутренняя область, затем следующая за ней внутренняя и так далее, причем каждый пакет считается «внутренним» по отношению к своему родительскому пакету. Начальная '.' (например, .foo.bar.Baz) означает начать с самой внешней области вместо этого.
Компилятор буфера протокола разрешает все имена типов, разбирая импортированные файлы .proto. Генератор кода для каждого языка знает, как ссылаться на каждый тип на этом языке, даже если он имеет разные правила области видимости.
Определение сервисов
Если вы хотите использовать ваши типы сообщений с системой RPC (Удаленный вызов процедур), вы можете определить интерфейс RPC сервиса в файле .proto, и компилятор буфера протокола сгенерирует код интерфейса сервиса и заглушки на вашем выбранном языке. Так, например, если вы хотите определить RPC сервис с методом, который принимает ваш SearchRequest и возвращает SearchResponse, вы можете определить это в вашем файле .proto следующим образом:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
Наиболее straightforward RPC система для использования с буферами протокола – это gRPC: нейтральная к языку и платформе система RPC с открытым исходным кодом, разработанная в Google. gRPC особенно хорошо работает с буферами протокола и позволяет вам генерировать соответствующий RPC код непосредственно из ваших файлов .proto с использованием специального плагина компилятора буфера протокола.
Если вы не хотите использовать gRPC, также возможно использовать буферы протокола с вашей собственной реализацией RPC. Вы можете узнать больше об этом в Руководстве по языку Proto2.
Также существует ряд текущих сторонних проектов по разработке реализаций RPC для Protocol Buffers. Для списка ссылок на проекты, о которых мы знаем, см. вики-страницу сторонних дополнений.
Отображение JSON
Стандартный двоичный формат провода protobuf является предпочтительным форматом сериализации для общения между двумя системами, которые используют protobuf. Для общения с системами, которые используют JSON, а не формат провода protobuf, Protobuf поддерживает каноническое кодирование в JSON.
Опции
Отдельные объявления в файле .proto могут быть аннотированы рядом опций. Опции не меняют общее значение объявления, но могут влиять на то, как оно обрабатывается в определенном контексте. Полный список доступных опций определен в /google/protobuf/descriptor.proto.
Некоторые опции являются опциями уровня файла, то есть они должны быть записаны на верхнем уровне области видимости, а не внутри любого сообщения, перечисления или определения сервиса. Некоторые опции являются опциями уровня сообщения, то есть они должны быть записаны внутри определений сообщений. Некоторые опции являются опциями уровня поля, то есть они должны быть записаны внутри определений полей. Опции также могут быть записаны на типах перечислений, значениях перечислений, полях oneof, типах сервисов и методах сервисов; однако в настоящее время нет полезных опций для любого из этих.
Вот несколько наиболее часто используемых опций:
-
java_package(опция файла): Пакет, который вы хотите использовать для ваших сгенерированных Java/Kotlin классов. Если в файле.protoне задана явная опцияjava_package, то по умолчанию будет использоваться пакет proto (указанный с помощью ключевого слова "package" в файле.proto). Однако пакеты proto обычно не являются хорошими пакетами Java, поскольку пакеты proto не ожидаются начинаться с обратных доменных имен. Если код Java или Kotlin не генерируется, эта опция не имеет эффекта.option java_package = "com.example.foo"; -
java_outer_classname(опция файла): Имя класса (и, следовательно, имя файла) для класса-обертки Java, который вы хотите сгенерировать. Если в файле.protoне указан явныйjava_outer_classname, имя класса будет сконструировано путем преобразования имени файла.protoв camel-case (такfoo_bar.protoстановитсяFooBar.java). Если опцияjava_multiple_filesотключена, то все другие классы/перечисления/и т.д., сгенерированные для файла.proto, будут сгенерированы внутри этого внешнего класса-обертки Java как вложенные классы/перечисления/и т.д. Если код Java не генерируется, эта опция не имеет эффекта.option java_outer_classname = "Ponycopter"; -
java_multiple_files(опция файла): Если false, для этого файла.protoбудет сгенерирован только один файл.java, и все Java классы/перечисления/и т.д., сгенерированные для сообщений верхнего уровня, сервисов и перечислений, будут вложены внутри внешнего класса (см.java_outer_classname). Если true, отдельные файлы.javaбудут сгенерированы для каждого из Java классов/перечислений/и т.д., сгенерированных для сообщений верхнего уровня, сервисов и перечислений, и класс-обертка Java, сгенерированный для этого файла.proto, не будет содержать никаких вложенных классов/перечислений/и т.д. Это логическая опция, которая по умолчаниюfalse. Если код Java не генерируется, эта опция не имеет эффекта.option java_multiple_files = true; -
optimize_for(опция файла): Может быть установлена вSPEED,CODE_SIZEилиLITE_RUNTIME. Это влияет на генераторы кода C++ и Java (и, возможно, сторонние генераторы) следующим образом:SPEED(по умолчанию): Компилятор буфера протокола сгенерирует код для сериализации, разбора и выполнения других общих операций над вашими типами сообщений. Этот код высоко оптимизирован.CODE_SIZE: Компилятор буфера протокола сгенерирует минимальные классы и будет полагаться на общий, основанный на отражении код для реализации сериализации, разбора и различных других операций. Сгенерированный код будет thus намного меньше, чем сSPEED, но операции будут медленнее. Классы все равно будут реализовывать точно такой же публичный API, как и в режимеSPEED. Этот режим наиболее полезен в приложениях, которые содержат очень большое количество файлов.protoи не нуждаются в том, чтобы все они были blindingly быстрыми.LITE_RUNTIME: Компилятор буфера протокола сгенерирует классы, которые зависят только от «облегченной» библиотеки времени выполнения (libprotobuf-liteвместоlibprotobuf). Облегченная среда выполнения намного меньше полной библиотеки (примерно на порядок меньше), но опускает определенные функции, такие как дескрипторы и отражение. Это особенно полезно для приложений, работающих на ограниченных платформах, таких как мобильные телефоны. Компилятор все равно будет генерировать быстрые реализации всех методов, как в режимеSPEED. Сгенерированные классы будут реализовывать только интерфейсMessageLiteна каждом языке, который предоставляет только подмножество методов полного интерфейсаMessage.
option optimize_for = CODE_SIZE; -
cc_generic_services,java_generic_services,py_generic_services(опции файла): Общие сервисы устарели. Должен ли компилятор буфера протокола генерировать абстрактный сервисный код на основе определений сервисов в C++, Java и Python, соответственно. По legacy причинам они по умолчаниюtrue. Однако, начиная с версии 2.3.0 (январь 2010), считается предпочтительным, чтобы реализации RPC предоставляли плагины генератора кода для генерации кода, более специфичного для каждой системы, а не полагаться на «абстрактные» сервисы.// Этот файл полагается на плагины для генерации сервисного кода. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false; -
cc_enable_arenas(опция файла): Включает выделение арены для сгенерированного кода C++. -
objc_class_prefix(опция файла): Устанавливает префикс класса Objective-C, который добавляется ко всем сгенерированным классам и перечислениям Objective-C из этого .proto. Значения по умолчанию нет. Вы должны использовать префиксы длиной от 3 до 5 заглавных символов, как рекомендовано Apple. Обратите внимание, что все 2-буквенные префиксы зарезервированы Apple. -
packed(опция поля): По умолчаниюtrueдля повторяющегося поля базового числового типа, вызывая использование более компактного кодирования. Чтобы использовать неупакованный wireformat, можно установитьfalse. Это обеспечивает совместимость с парсерами до версии 2.3.0 (редко нужно), как показано в следующем примере:repeated int32 samples = 4 [packed = false]; -
deprecated(опция поля): Если установленоtrue, указывает, что поле устарело и не должно использоваться новым кодом. В большинстве языков это не имеет реального эффекта. В Java это становится аннотацией@Deprecated. Для C++ clang-tidy будет генерировать предупреждения всякий раз, когда используются устаревшие поля. В будущем другие языково-специфичные генераторы кода могут генерировать аннотации устаревания на аксессорах поля, что, в свою очередь, вызовет предупреждение при компиляции кода, который пытается использовать поле. Если поле никем не используется и вы хотите предотвратить его использование новыми пользователями, рассмотрите возможность замены объявления поля на зарезервированное утверждение.int32 old_field = 6 [deprecated = true];
Опции значений перечисления
Опции значений перечисления поддерживаются. Вы можете использовать опцию deprecated, чтобы указать, что значение больше не должно использоваться. Вы также можете создавать пользовательские опции, используя расширения.
Следующий пример показывает синтаксис для добавления этих опций:
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Data {
DATA_UNSPECIFIED = 0;
DATA_SEARCH = 1 [deprecated = true];
DATA_DISPLAY = 2 [
(string_name) = "display_value"
];
}
Код C++ для чтения опции string_name может выглядеть примерно так:
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
См. Пользовательские опции, чтобы узнать, как применять пользовательские опции к значениям перечислений и к полям.
Пользовательские опции
Protocol Buffers также позволяет вам определять и использовать ваши собственные опции. Обратите внимание, что это продвинутая функция, которая большинству людей не нужна. Если вы все же думаете, что вам нужно создать свои собственные опции, см. Руководство по языку Proto2 для подробностей. Обратите внимание, что создание пользовательских опций использует расширения, которые разрешены только для пользовательских опций в proto3.
Удержание опций
Опции имеют понятие удержания (retention), которое контролирует, сохраняется ли опция в сгенерированном коде. Опции имеют удержание времени выполнения по умолчанию, что означает, что они сохраняются в сгенерированном коде и, таким образом, видны во время выполнения в сгенерированном пуле дескрипторов. Однако вы можете установить retention = RETENTION_SOURCE, чтобы указать, что опция (или поле внутри опции) не должна сохраняться во время выполнения. Это называется удержанием исходного кода.
Удержание опций – это продвинутая функция, о которой большинству пользователей не нужно беспокоиться, но она может быть полезна, если вы хотите использовать определенные опции без оплаты стоимости размера кода за их сохранение в ваших двоичных файлах. Опции с удержанием исходного кода все еще видны protoc и плагинам protoc, так что генераторы кода могут использовать их для настройки своего поведения.
Удержание может быть установлено непосредственно на опции, вот так:
extend google.protobuf.FileOptions {
optional int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
Оно также может быть установлено на простом поле, и в этом случае оно вступает в силу только тогда, когда это поле появляется внутри опции:
message OptionsMessage {
int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
Вы можете установить retention = RETENTION_RUNTIME, если хотите, но это не имеет эффекта, поскольку это поведение по умолчанию. Когда поле сообщения помечено RETENTION_SOURCE, все его содержимое отбрасывается; поля внутри него не могут переопределить это, пытаясь установить RETENTION_RUNTIME.
{{% alert title="Примечание" color="note" %}} По состоянию на Protocol Buffers 22.0, поддержка удержания опций все еще находится в разработке, и только C++ и Java поддерживаются. Go имеет поддержку, начиная с 1.29.0. Поддержка Python завершена, но еще не попала в релиз. {{% /alert %}}
Цели опций
Поля имеют опцию targets, которая контролирует типы сущностей, к которым поле может применяться при использовании в качестве опции. Например, если поле имеет targets = TARGET_TYPE_MESSAGE, то это поле не может быть установлено в пользовательской опции на перечислении (или любой другой не-сообщенной сущности). Protoc обеспечивает это и будет выдавать ошибку, если есть нарушение ограничений целей.
На первый взгляд, эта функция может показаться ненужной, учитывая, что каждая пользовательская опция является расширением сообщения опций для конкретной сущности, что уже ограничивает опцию этой одной сущностью. Однако цели опций полезны в случае, когда у вас есть общее сообщение опций, применяемое к нескольким типам сущностей, и вы хотите контролировать использование отдельных полей в этом сообщении. Например:
message MyOptions {
string file_only_option = 1 [targets = TARGET_TYPE_FILE];
int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
optional MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
optional MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
optional MyOptions enum_options = 50000;
}
// OK: это поле разрешено в опциях файла
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: это поле разрешено в опциях сообщения и перечисления
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Ошибка: file_only_option не может быть установлено на перечислении.
option (enum_options).file_only_option = "xyz";
}
Генерация ваших классов
Чтобы сгенерировать код Java, Kotlin, Python, C++, Go, Ruby, Objective-C или C#, который вам нужен для работы с типами сообщений, определенными в файле .proto, вам нужно запустить компилятор буфера протокола protoc на файле .proto. Если вы не установили компилятор, загрузите пакет и следуйте инструкциям в README. Для Go вам также нужно установить специальный плагин генератора кода для компилятора; вы можете найти его и инструкции по установке в репозитории golang/protobuf на GitHub.
Компилятор protobuf вызывается следующим образом:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATHуказывает каталог, в котором нужно искать файлы.protoпри разрешении директивimport. Если опущено, используется текущий каталог. Несколько каталогов импорта могут быть указаны путем передачи опции--proto_pathнесколько раз.-I=_IMPORT_PATH_может быть использован как короткая форма--proto_path.
Примечание: Пути к файлам относительно их proto_path должны быть глобально уникальными в данном двоичном файле. Например, если у вас есть proto/lib1/data.proto и proto/lib2/data.proto, эти два файла не могут быть использованы вместе с -I=proto/lib1 -I=proto/lib2, потому что будет неоднозначно, какой файл означает import "data.proto". Вместо этого следует использовать -Iproto/, и глобальные имена будут lib1/data.proto и lib2/data.proto.
Если вы публикуете библиотеку и другие пользователи могут использовать ваши сообщения напрямую, вы должны включить уникальное имя библиотеки в путь, под которым они ожидаются к использованию, чтобы избежать конфликтов имен файлов. Если у вас несколько каталогов в одном проекте, лучшей практикой является предпочтение установки одного -I в каталог верхнего уровня проекта.
-
Вы можете предоставить одну или несколько выходных директив:
--cpp_outгенерирует код C++ вDST_DIR. См. Справочник по сгенерированному коду C++ для получения дополнительной информации.--java_outгенерирует код Java вDST_DIR. См. Справочник по сгенерированному коду Java для получения дополнительной информации.--kotlin_outгенерирует дополнительный код Kotlin вDST_DIR. См. Справочник по сгенерированному коду Kotlin для получения дополнительной информации.--python_outгенерирует код Python вDST_DIR. См. Справочник по сгенерированному коду Python для получения дополнительной информации.--go_outгенерирует код Go вDST_DIR. См. Справочник по сгенерированному коду Go для получения дополнительной информации.--ruby_outгенерирует код Ruby вDST_DIR. См. Справочник по сгенерированному коду Ruby для получения дополнительной информации.--objc_outгенерирует код Objective-C вDST_DIR. См. Справочник по сгенерированному коду Objective-C для получения дополнительной информации.--csharp_outгенерирует код C# вDST_DIR. См. Справочник по сгенерированному коду C# для получения дополнительной информации.--php_outгенерирует код PHP вDST_DIR. См. Справочник по сгенерированному коду PHP для получения дополнительной информации.
В качестве дополнительного удобства, если
DST_DIRзаканчивается на.zipили.jar, компилятор запишет вывод в один файл архива формата ZIP с данным именем. Выходы.jarтакже получат файл манифеста, как требуется спецификацией Java JAR. Обратите внимание, что если выходной архив уже существует, он будет перезаписан. -
Вы должны предоставить один или несколько файлов
.protoв качестве входных данных. Несколько файлов.protoмогут быть указаны сразу. Хотя файлы названы относительно текущего каталога, каждый файл должен находиться в одном изIMPORT_PATH, чтобы компилятор мог определить его каноническое имя.
Расположение файла
Предпочитайте не помещать файлы .proto в тот же каталог, что и другие языковые исходники. Рассмотрите возможность создания подпакета proto для файлов .proto под корневым пакетом вашего проекта.
Расположение должно быть независимым от языка
При работе с кодом Java удобно помещать связанные файлы .proto в тот же каталог, что и исходный код Java. Однако, если какой-либо не-Java код когда-либо использует те же protos, префикс пути больше не будет иметь смысла. Поэтому в общем случае помещайте protos в связанный независимый от языка каталог, такой как //myteam/mypackage.
Исключением из этого правила является случай, когда ясно, что protos будут использоваться только в контексте Java, например, для тестирования.
Поддерживаемые платформы
Для получения информации о:
- операционных системах, компиляторах, системах сборки и версиях C++, которые поддерживаются, см. Политика поддержки фундаментального C++.
- версиях PHP, которые поддерживаются, см. Поддерживаемые версии PHP.
Ограничения Proto
Охватывает ограничения на количество поддерживаемых элементов в схемах proto.
В этой теме документированы ограничения на количество поддерживаемых элементов (полей, значений перечислений и т.д.) в схемах proto.
Эта информация представляет собой сборник обнаруженных ограничений многими инженерами, но не является исчерпывающей и может быть неверной/устаревшей в некоторых областях. По мере того как вы обнаруживаете ограничения в своей работе, вносите их в этот документ, чтобы помочь другим.
Количество полей
Все сообщения ограничены 65 535 полями.
Сообщение только с одиночными полями proto (такими как Boolean):
- ~2100 полей (proto2)
- ~3100 (proto3 без использования опциональных полей)
Пустое сообщение, расширенное одиночными полями (такими как Boolean):
- ~4100 полей (proto2)
Расширения не поддерживаются в proto3.
Чтобы проверить это ограничение, создайте proto-сообщение с количеством полей, превышающим верхнюю границу, и скомпилируйте его, используя правило Java proto. Ограничение исходит из спецификаций JVM.
Количество значений в перечислении
Самый низкий предел составляет ~1700 значений, в Java. Другие языки имеют разные ограничения.
Общий размер сообщения
Любой proto в сериализованной форме должен быть <2 GiB, так как это максимальный размер, поддерживаемый всеми реализациями. Рекомендуется ограничивать размеры запросов и ответов.
Ограничение глубины для демаршалинга Proto
- Java: 100
- C++: 100
- Go: 10000 (есть план уменьшить это до 100)
Если вы попытаетесь демаршалить сообщение, вложенность которого превышает лимит глубины, демаршалинг завершится неудачей.
Руководство по Стилю
Предоставляет рекомендации по наилучшей структуре ваших proto-определений.
Этот документ предоставляет руководство по стилю для файлов .proto. Следуя этим соглашениям, вы сделаете свои определения сообщений буфера протокола и соответствующие классы последовательными и удобочитаемыми.
Применение следующих стилистических рекомендаций контролируется через enforce_naming_style.
Стандартное форматирование файлов
- Длина строки должна быть не более 80 символов.
- Используйте отступ в 2 пробела.
- Предпочитайте использование двойных кавычек для строк.
Структура файла
Файлы должны называться lower_snake_case.proto.
Все файлы должны быть упорядочены следующим образом:
- Заголовок лицензии (если применимо)
- Обзор файла
- Синтаксис
- Пакет
- Импорты (отсортированные)
- Опции файла
- Все остальное
Стили именования идентификаторов
Идентификаторы Protobuf используют один из следующих стилей именования:
- TitleCase (ВерблюжийРегистр)
- Содержит заглавные буквы, строчные буквы и цифры
- Первый символ - заглавная буква
- Первая буква каждого слова заглавная
- lower_snake_case (нижний_змеиный_регистр)
- Содержит строчные буквы, подчеркивания и цифры
- Слова разделяются одним подчеркиванием
- UPPER_SNAKE_CASE (ВЕРХНИЙ_ЗМЕИНЫЙ_РЕГИСТР)
- Содержит заглавные буквы, подчеркивания и цифры
- Слова разделяются одним подчеркиванием
- camelCase (верблюжийРегистр)
- Содержит заглавные буквы, строчные буквы и цифры
- Первый символ - строчная буква
- Первая буква каждого последующего слова заглавная
- Примечание: Руководство по стилю ниже не использует camelCase для каких-либо идентификаторов в файлах .proto; терминология уточняется здесь только потому, что сгенерированный код некоторых языков может преобразовывать идентификаторы в этот стиль.
Во всех случаях рассматривайте аббревиатуры как отдельные слова: используйте GetDnsRequest вместо GetDNSRequest, dns_request вместо d_n_s_request.
Подчеркивания в идентификаторах
Не используйте подчеркивания в качестве начального или конечного символа имени. Любое подчеркивание всегда должно следовать за буквой (а не цифрой или вторым подчеркиванием).
Мотивация этого правила в том, что каждая реализация языка protobuf может преобразовывать идентификаторы в локальный стиль языка: имя song_id в файле .proto может в итоге иметь аксессоры для поля, которые капитализируются как SongId, songId или song_id в зависимости от языка.
Используя подчеркивания только перед буквами, мы избегаем ситуаций, когда имена могут различаться в одном стиле, но конфликтовать после преобразования в другой стиль.
Например, и DNS2, и DNS_2 преобразуются в TitleCase как Dns2. Разрешение любого из этих имен может привести к болезненным ситуациям, когда сообщение используется только в некоторых языках, где сгенерированный код сохраняет исходный стиль UPPER_SNAKE_CASE, широко устанавливается, и затем используется только позже в языке, где имена преобразуются в TitleCase, где они конфликтуют.
При применении этого правила стиля вы должны использовать XYZ2 или XYZ_V2 вместо XYZ_2 или XYZ_2V.
Пакеты
Используйте имена пакетов в lower_snake_case, разделенные точками.
Многословные имена пакетов могут быть в lower_snake_case или dot.delimited (имена пакетов, разделенные точками, создаются как вложенные пакеты/пространства имен в большинстве языков).
Имена пакетов должны пытаться быть короткими, но уникальными именами, основанными на имени проекта. Имена пакетов не должны быть пакетами Java (com.x.y); вместо этого используйте x.y как пакет и используйте опцию java_package по мере необходимости.
Имена сообщений
Используйте TitleCase для имен сообщений.
message SongRequest {
}
Имена полей
Используйте snake_case для имен полей, включая расширения.
Используйте имена во множественном числе для повторяющихся полей.
string song_name = 1;
repeated Song songs = 2;
Имена oneof
Используйте lower_snake_case для имен oneof.
oneof song_id {
string song_human_readable_id = 1;
int64 song_machine_id = 2;
}
Перечисления
Используйте TitleCase для имен типов перечислений.
Используйте UPPER_SNAKE_CASE для имен значений перечислений.
enum FooBar {
FOO_BAR_UNSPECIFIED = 0;
FOO_BAR_FIRST_VALUE = 1;
FOO_BAR_SECOND_VALUE = 2;
}
Первое указанное значение должно быть нулевым значением перечисления и иметь суффикс _UNSPECIFIED или _UNKNOWN. Это значение может использоваться как неизвестное/значение по умолчанию и должно отличаться от любых семантических значений, которые вы ожидаете явно установить. Для получения дополнительной информации о неуказанном значении перечисления см. страницу Лучших практик Proto.
Префиксы значений перечисления
Значения перечисления семантически считаются не ограниченными областью видимости их содержащего имени перечисления, поэтому одно и то же имя в двух родственных перечислениях не допускается. Например, следующее будет отклонено protoc, поскольку значение SET, определенное в двух перечислениях, считается находящимся в одной области видимости:
enum CollectionType {
COLLECTION_TYPE_UNSPECIFIED = 0;
SET = 1;
MAP = 2;
ARRAY = 3;
}
// Не скомпилируется - имя enum `SET` будет конфликтовать
// с определенным в enum `CollectionType`.
enum TennisVictoryType {
TENNIS_VICTORY_TYPE_UNSPECIFIED = 0;
GAME = 1;
SET = 2;
MATCH = 3;
}
Риск конфликтов имен высок, когда перечисления определены на верхнем уровне файла (не вложены в определение сообщения); в этом случае родственные элементы включают перечисления, определенные в других файлах, которые устанавливают тот же пакет, где protoc может быть не в состоянии обнаружить конфликт во время генерации кода.
Чтобы избежать этих рисков, настоятельно рекомендуется сделать одно из следующего:
- Добавлять префикс каждого значения с именем перечисления (преобразованным в UPPER_SNAKE_CASE)
- Вложить перечисление внутрь содержащего сообщения
Любой вариант достаточен для снижения рисков конфликтов, но предпочитайте перечисления верхнего уровня с префиксными значениями над созданием сообщения просто для смягчения проблемы. Поскольку некоторые языки не поддерживают определение перечисления внутри типа "struct", предпочтение префиксных значений обеспечивает последовательный подход во всех языках привязок.
Сервисы
Используйте TitleCase для имен сервисов и методов.
service FooService {
rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse);
rpc ListSomething(ListSomethingRequest) returns (ListSomethingResponse);
}
Чего следует избегать
Обязательные поля
Обязательные поля - это способ обеспечить, чтобы данное поле было установлено при разборе байтов провода, и в противном случае отказаться от разбора сообщения. Инвариант required обычно не применяется к сообщениям, созданным в памяти. Обязательные поля были удалены в proto3. Поля required proto2, которые были мигрированы в редакции 2023, могут использовать функцию field_presence, установленную в LEGACY_REQUIRED, чтобы приспособиться.
Хотя принудительное применение обязательных полей на уровне схемы интуитивно желательно, одной из основных целей проектирования protobuf является поддержка долгосрочной эволюции схемы. Независимо от того, насколько очевидно обязательным кажется данное поле сегодня, существует правдоподобное будущее, в котором поле больше не должно устанавливаться (например, int64 user_id может потребоваться мигрировать на UserId user_id в будущем).
Особенно в случае промежуточных серверов, которые могут пересылать сообщения, которые им на самом деле не нужно обрабатывать, семантика required оказалась слишком вредной для этих долгосрочных целей эволюции, и поэтому теперь очень настоятельно не рекомендуется.
См. Required является строго устаревшим.
Группы
Группы - это альтернативный синтаксис и формат провода для вложенных сообщений. Группы считаются устаревшими в proto2, были удалены из proto3 и преобразуются в разделяемое представление в редакции 2023. Вы можете использовать определение вложенного сообщения и поле этого типа вместо использования синтаксиса группы, используя функцию message_encoding для совместимости с проводом.
См. groups.
Поведение Enum
Объясняет, как enums работают в Protocol Buffers в настоящее время и как они должны работать.
Перечисления (enums) ведут себя по-разному в различных языковых библиотеках. Эта тема охватывает различные поведения, а также планы по переходу protobufs к состоянию, где они будут согласованы во всех языках. Если вы ищете информацию об использовании enums в целом, см. соответствующие разделы в руководствах по языку proto2, proto3 и редакциях 2023.
Определения
Enums имеют два различных варианта (открытые и закрытые). Они ведут себя идентично, за исключением обработки неизвестных значений. Практически это означает, что простые случаи работают одинаково, но некоторые крайние случаи имеют интересные последствия.
Для целей объяснения предположим, что у нас есть следующий файл .proto (мы намеренно не указываем, является ли это файлом syntax = "proto2", syntax = "proto3" или edition = "2023" прямо сейчас):
enum Enum {
A = 0;
B = 1;
}
message Msg {
optional Enum enum = 1;
}
Различие между открытыми и закрытыми можно encapsulate одним вопросом:
Что происходит, когда программа разбирает двоичные данные, содержащие поле 1 со значением
2?
- Открытые enums разберут значение
2и сохранят его непосредственно в поле. Аксессоры сообщат, что поле установлено, и вернут что-то, представляющее2. - Закрытые enums разберут значение
2и сохранят его в наборе неизвестных полей сообщения. Аксессоры сообщат, что поле не установлено, и вернут значение enum по умолчанию.
Последствия Закрытых Enums
Поведение закрытых enums имеет неожиданные последствия при разборе повторяющегося поля. Когда поле repeated Enum разбирается, все неизвестные значения будут помещены в набор неизвестных полей. При сериализации эти неизвестные значения будут записаны снова, но не на своем исходном месте в списке. Например, для файла .proto:
enum Enum {
A = 0;
B = 1;
}
message Msg {
repeated Enum r = 1;
}
Формат провода, содержащий значения [0, 2, 1, 2] для поля 1, будет разобран так, что повторяющееся поле содержит [0, 1], а значение [2, 2] окажется сохраненным как неизвестное поле. После повторной сериализации сообщения формат провода будет соответствовать [0, 1, 2, 2].
Аналогично, карты с закрытыми enums в качестве их значения будут помещать целые записи (ключ и значение) в неизвестные поля, когда значение неизвестно.
История
До введения syntax = "proto3" все enums были закрытыми. Proto3 и редакции используют открытые enums именно из-за неожиданного поведения, которое вызывают закрытые enums. Вы можете использовать features.enum_type, чтобы явно установить enums в редакциях как открытые, если это необходимо.
Спецификация
Следующее определяет поведение соответствующих спецификации реализаций для protobuf. Поскольку это тонкий момент, многие реализации не соответствуют спецификации. См. Известные проблемы для подробностей о том, как ведут себя различные реализации.
- Когда файл
proto2импортирует enum, определенный в файлеproto2, этот enum должен рассматриваться как закрытый. - Когда файл
proto3импортирует enum, определенный в файлеproto3, этот enum должен рассматриваться как открытый. - Когда файл
proto3импортирует enum, определенный в файлеproto2, компиляторprotocвыдаст ошибку. - Когда файл
proto2импортирует enum, определенный в файлеproto3, этот enum должен рассматриваться как открытый.
Редакции соблюдают то поведение, которое enum имел в файле, из которого происходит импорт. Enums proto2 всегда рассматриваются как закрытые, enums proto3 всегда рассматриваются как открытые, а при импорте из другого файла редакций используется настройка функции.
Известные проблемы
C++
Все известные выпуски C++ не соответствуют спецификации. Когда файл proto2 импортирует enum, определенный в файле proto3, C++ рассматривает это поле как закрытый enum. В редакциях это поведение представлено устаревшей функцией поля features.(pb.cpp).legacy_closed_enum. Есть два варианта перехода к соответствующему поведению:
- Удалить функцию поля. Это рекомендуемый подход, но он может вызвать изменения поведения во время выполнения. Без функции нераспознанные целые числа окажутся сохраненными в поле, приведенном к типу enum, вместо того чтобы быть помещенными в набор неизвестных полей.
- Изменить enum на закрытый. Это не рекомендуется и может вызвать изменения поведения во время выполнения, если кто-либо еще использует этот enum. Нераспознанные целые числа окажутся в наборе неизвестных полей вместо этих полей.
C#
Все известные выпуски C# не соответствуют спецификации. C# рассматривает все enums как открытые.
Java
Все известные выпуски Java не соответствуют спецификации. Когда файл proto2 импортирует enum, определенный в файле proto3, Java рассматривает это поле как закрытый enum.
В редакциях это поведение представлено устаревшей функцией поля features.(pb.java).legacy_closed_enum). Есть два варианта перехода к соответствующему поведению:
- Удалить функцию поля. Это может вызвать изменения поведения во время выполнения. Без функции нераспознанные целые числа окажутся сохраненными в поле, и метод getter enum вернет значение
UNRECOGNIZED. До этого эти значения помещались в набор неизвестных полей. - Изменить enum на закрытый. Если кто-либо еще использует его, они могут увидеть изменения поведения во время выполнения. Нераспознанные целые числа окажутся в наборе неизвестных полей вместо этих полей.
ПРИМЕЧАНИЕ: Обработка открытых enums в Java имеет удивительные крайние случаи. Для следующих определений:
syntax = "proto3"; enum Enum { A = 0; B = 1; } message Msg { repeated Enum name = 1; }Java сгенерирует методы
Enum getName()иint getNameValue(). МетодgetNameбудет возвращатьEnum.UNRECOGNIZEDдля значений вне известного набора (таких как2), тогда какgetNameValueвернет2.Аналогично, Java сгенерирует методы
Builder setName(Enum value)иBuilder setNameValue(int value). МетодsetNameбудет выбрасывать исключение при передачеEnum.UNRECOGNIZED, тогда какsetNameValueпримет2.
Kotlin
Все известные выпуски Kotlin не соответствуют спецификации. Когда файл proto2 импортирует enum, определенный в файле proto3, Kotlin рассматривает это поле как закрытый enum.
Kotlin построен на Java и разделяет все ее особенности.
Go
Все известные выпуски Go не соответствуют спецификации. Go рассматривает все enums как открытые.
JSPB
Все известные выпуски JSPB не соответствуют спецификации. JSPB рассматривает все enums как открытые.
PHP
PHP соответствует спецификации.
Python
Python соответствует спецификации в версиях выше 4.22.0 (выпущен в 2023 Q1).
Старые версии, которые больше не поддерживаются, не соответствуют спецификации. Когда файл proto2 импортирует enum, определенный в файле proto3,
Ruby
Все известные выпуски Ruby не соответствуют спецификации. Ruby рассматривает все enums как открытые.
Objective-C
Objective-C соответствует спецификации в версиях выше 3.22.0 (выпущен в 2023 Q1).
Старые версии, которые больше не поддерживаются, не соответствуют спецификации. Когда файл proto2 импортирует enum, определенный в файле proto3, несоответствующие версии ObjC рассматривают это поле как закрытый enum.
Swift
Swift соответствует спецификации.
Dart
Dart рассматривает все enums как закрытые.
Encoding "Кодирование"
Объясняет, как Protocol Buffers кодирует данные в файлы или для передачи по сети.
Этот документ описывает wire format (формат передачи) protocol buffer, который определяет детали того, как ваше сообщение отправляется по сети и сколько места оно занимает на диске. Вероятно, вам не нужно понимать это для использования protocol buffers в вашем приложении, но это полезная информация для оптимизаций.
Если вы уже знакомы с концепциями, но хотите справочник, перейдите к разделу Сокращенная шпаргалка.
Protoscope - это очень простой язык для описания фрагментов низкоуровневого формата передачи, который мы будем использовать для визуальной справки по кодированию различных сообщений. Синтаксис Protoscope состоит из последовательности токенов, каждый из которых кодируется в определенную последовательность байтов.
Например, обратные апострофы обозначают необработанный шестнадцатеричный литерал, например `70726f746f6275660a`. Это кодируется в точные байты, обозначенные шестнадцатеричным значением в литерале. Кавычки обозначают строки UTF-8, например "Hello, Protobuf!". Этот литерал синонимичен `48656c6c6f2c2050726f746f62756621` (который, если присмотреться, состоит из байтов ASCII). Мы представим больше языка Protoscope по мере обсуждения аспектов формата передачи.
Инструмент Protoscope также может выводить закодированные protocol buffers в виде текста. См. https://github.com/protocolbuffers/protoscope/tree/main/testdata для примеров.
Все примеры в этой теме предполагают, что вы используете Редакцию 2023 или новее.
Простое сообщение
Допустим, у вас есть следующее очень простое определение сообщения:
message Test1 {
int32 a = 1;
}
В приложении вы создаете сообщение Test1 и устанавливаете a в 150. Затем вы сериализуете сообщение в выходной поток. Если бы вы могли исследовать закодированное сообщение, вы бы увидели три байта:
08 96 01
Пока все мало и численно - но что это значит? Если вы используете инструмент Protoscope для вывода этих байтов, вы получите что-то вроде 1: 150. Как он узнал, что это содержимое сообщения?
Base 128 Varints
Целые числа с переменной шириной, или varints, являются основой формата передачи. Они позволяют кодировать 64-битные целые числа без знака, используя от одного до десяти байтов, причем маленькие значения используют меньше байтов.
Каждый байт в varint имеет бит продолжения, который указывает, является ли следующий за ним байт частью varint. Это старший бит (MSB) байта (иногда также называемый знаковым битом). Младшие 7 бит являются полезной нагрузкой; результирующее целое число строится путем объединения 7-битных полезных нагрузок его составных байтов.
Итак, например, вот число 1, закодированное как `01` - это один байт, поэтому MSB не установлен:
0000 0001
^ msb
А вот 150, закодированное как `9601` - это немного сложнее:
10010110 00000001
^ msb ^ msb
Как понять, что это 150? Сначала вы отбрасываете MSB из каждого байта, так как он просто говорит нам, достигли ли мы конца числа (как вы можете видеть, он установлен в первом байте, так как в varint больше одного байта). Эти 7-битные полезные нагрузки находятся в порядке little-endian. Преобразуйте в порядок big-endian, объедините и интерпретируйте как 64-битное целое число без знака:
10010110 00000001 // Исходные входные данные.
0010110 0000001 // Убрать биты продолжения.
0000001 0010110 // Преобразовать в big-endian.
00000010010110 // Объединить.
128 + 16 + 4 + 2 = 150 // Интерпретировать как 64-битное целое число без знака.
Поскольку varints так важны для protocol buffers, в синтаксисе protoscope мы ссылаемся на них как на простые целые числа. 150 это то же самое, что `9601`.
Структура сообщения
Сообщение protocol buffer - это серия пар ключ-значение. Двоичная версия сообщения использует номер поля как ключ - имя и объявленный тип для каждого поля могут быть определены только на стороне декодирования путем ссылки на определение типа сообщения (т.е. файл .proto). Protoscope не имеет доступа к этой информации, поэтому он может предоставить только номера полей.
Когда сообщение кодируется, каждая пара ключ-значение превращается в запись, состоящую из номера поля, типа провода и полезной нагрузки. Тип провода сообщает парсеру, насколько велика полезная нагрузка после него. Это позволяет старым парсерам пропускать новые поля, которые они не понимают. Этот тип схемы иногда называют Tag-Length-Value (TLV).
Существует шесть типов провода: VARINT, I64, LEN, SGROUP, EGROUP и I32
| ID | Имя | Используется для |
|---|---|---|
| 0 | VARINT | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
| 1 | I64 | fixed64, sfixed64, double |
| 2 | LEN | string, bytes, вложенные сообщения, упакованные повторяющиеся поля |
| 3 | SGROUP | начало группы (устарело) |
| 4 | EGROUP | конец группы (устарело) |
| 5 | I32 | fixed32, sfixed32, float |
"Тег" записи кодируется как varint, образованный из номера поля и типа провода по формуле (field_number << 3) | wire_type. Другими словами, после декодирования varint, представляющего поле, младшие 3 бита говорят нам тип провода, а остальная часть целого числа говорит нам номер поля.
Теперь давайте снова посмотрим на наш простой пример. Теперь вы знаете, что первое число в потоке всегда является varint ключом, и здесь это `08`, или (отбрасывая MSB):
000 1000
Вы берете последние три бита, чтобы получить тип провода (0), а затем сдвигаете вправо на три, чтобы получить номер поля (1). Protoscope представляет тег как целое число, за которым следует двоеточие и тип провода, поэтому мы можем записать вышеуказанные байты как 1:VARINT.
Поскольку тип провода равен 0, или VARINT, мы знаем, что нам нужно декодировать varint, чтобы получить полезную нагрузку. Как мы видели выше, байты `9601` декодируются в varint как 150, что дает нам нашу запись. Мы можем записать это в Protoscope как 1:VARINT 150.
Protoscope может вывести тип для тега, если после : есть пробел. Он делает это, заглядывая вперед на следующий токен и угадывая, что вы имели в виду (правила подробно описаны в language.txt Protoscope). Например, в 1: 150 сразу после нетипизированного тега идет varint, поэтому Protoscope выводит его тип как VARINT. Если бы вы написали 2: {}, он увидел бы { и угадал LEN; если бы вы написали 3: 5i32, он угадал бы I32, и так далее.
Больше целочисленных типов
Логические значения и перечисления
Логические значения и перечисления кодируются так, как если бы они были int32. Логические значения, в частности, всегда кодируются как `00` или `01`. В Protoscope false и true являются псевдонимами для этих строк байтов.
Целые числа со знаком
Как вы видели в предыдущем разделе, все типы protocol buffer, связанные с типом провода 0, кодируются как varints. Однако varints беззнаковые, поэтому различные знаковые типы, sint32 и sint64 против int32 или int64, кодируют отрицательные целые числа по-разному.
Типы intN кодируют отрицательные числа как дополнение до двух, что означает, что, как беззнаковые 64-битные целые числа, у них установлен старший бит. В результате это означает, что все десять байтов должны быть использованы. Например, -2 преобразуется protoscope в
11111110 11111111 11111111 11111111 11111111
11111111 11111111 11111111 11111111 00000001
Это дополнение до двух от 2, определенное в беззнаковой арифметике как ~0 - 2 + 1, где ~0 - это все-единицы 64-битное целое число. Полезно понять, почему это производит так много единиц.
sintN использует кодировку "ZigZag" вместо дополнения до двух для кодирования отрицательных целых чисел. Положительные целые числа p кодируются как 2 * p (четные числа), в то время как отрицательные целые числа n кодируются как 2 * |n| - 1 (нечетные числа). Таким образом, кодировка "зигзагообразно" переходит между положительными и отрицательными числами. Например:
| Исходное со знаком | Закодировано как |
|---|---|
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| ... | ... |
| 0x7fffffff | 0xfffffffe |
| -0x80000000 | 0xffffffff |
Другими словами, каждое значение n кодируется с использованием
(n << 1) ^ (n >> 31)
для sint32, или
(n << 1) ^ (n >> 63)
для 64-битной версии.
Когда sint32 или sint64 разбирается, его значение декодируется обратно в исходную, знаковую версию.
В protoscope добавление суффикса z к целому числу заставит его кодироваться как ZigZag. Например, -500z это то же самое, что varint 999.
Не-varint числа
Не-varint числовые типы просты. double и fixed64 имеют тип провода I64, который говорит парсеру ожидать фиксированный восьмибайтовый блок данных. Значения double кодируются в формате IEEE 754 двойной точности. Мы можем указать запись double, написав 5: 25.4, или запись fixed64 с 6: 200i64.
Аналогично float и fixed32 имеют тип провода I32, который говорит ему ожидать четыре байта вместо этого. Значения float кодируются в формате IEEE 754 одинарной точности. Синтаксис для них состоит в добавлении суффикса i32. 25.4i32 выдаст четыре байта, как и 200i32. Типы тегов выводятся как I32.
Записи с указанием длины
Префиксы длины - еще одна основная концепция в формате провода. Тип провода LEN имеет динамическую длину, заданную varint сразу после тега, за которым следует полезная нагрузка, как обычно.
Рассмотрим эту схему сообщения:
message Test2 {
string b = 2;
}
Запись для поля b является строкой, и строки кодируются с помощью LEN. Если мы установим b в "testing", мы закодируем как запись LEN с номером поля 2, содержащую строку ASCII "testing". Результат - `120774657374696e67`. Разбивая байты,
12 07 [74 65 73 74 69 6e 67]
мы видим, что тег, `12`, это 00010 010, или 2:LEN. Байт, который следует, - это int32 varint 7, и следующие семь байтов - это кодировка UTF-8 для "testing". Int32 varint означает, что максимальная длина строки составляет 2GB.
В Protoscope это записывается как 2:LEN 7 "testing". Однако может быть неудобно повторять длину строки (которая в тексте Protoscope уже ограничена кавычками). Обертывание содержимого Protoscope в фигурные скобки сгенерирует для него префикс длины: {"testing"} является сокращением для 7 "testing". {} всегда выводится полями как запись LEN, поэтому мы можем записать эту запись просто как 2: {"testing"}.
Поля bytes кодируются таким же образом.
Подсообщения
Поля подсообщений также используют тип провода LEN. Вот определение сообщения с вложенным сообщением нашего исходного примера сообщения, Test1:
message Test3 {
Test1 c = 3;
}
Если поле a Test1 (т.е. поле c.a Test3) установлено в 150, мы получаем `1a03089601`. Разбивая это:
1a 03 [08 96 01]
Последние три байта (в []) точно такие же, как из нашего самого первого примера. Эти байты предшествуют тегу типа LEN и длине 3, точно так же, как кодируются строки.
В Protoscope подсообщения довольно кратки. `1a03089601` можно записать как 3: {1: 150}.
Отсутствующие элементы
Отсутствующие поля легко закодировать: мы просто опускаем запись, если она отсутствует. Это означает, что "огромные" protos с установленными только несколькими полями довольно разрежены.
Повторяющиеся элементы
Начиная с Редакции 2023, repeated поля примитивного типа (любой скалярный тип, который не string или bytes) по умолчанию "упакованы".
Упакованные repeated поля, вместо того чтобы кодироваться как одна запись на элемент, кодируются как одна запись LEN, которая содержит каждый элемент, объединенный вместе. Для декодирования элементы декодируются из записи LEN один за другим, пока полезная нагрузка не исчерпана. Начало следующего элемента определяется длиной предыдущего, которая сама зависит от типа поля. Таким образом, если у нас есть:
message Test4 {
string d = 4;
repeated int32 e = 6;
}
и мы конструируем сообщение Test4 с d, установленным в "hello", и e, установленным в 1, 2 и 3, это может быть закодировано как `3206038e029ea705`, или записано как Protoscope,
4: {"hello"}
6: {3 270 86942}
Однако, если повторяющееся поле установлено в расширенное (переопределяя состояние по умолчанию packed) или не упаковывается (строки и сообщения), то кодируется запись для каждого отдельного значения. Также записи для e не должны появляться последовательно и могут перемежаться с другими полями; сохраняется только порядок записей для одного и того же поля относительно друг друга. Таким образом, это может выглядеть следующим образом:
6: 1
6: 2
4: {"hello"}
6: 3
Только повторяющиеся поля примитивных числовых типов могут быть объявлены "упакованными". Это типы, которые обычно используют типы провода VARINT, I32 или I64.
Обратите внимание, что хотя обычно нет причин кодировать более одной пары ключ-значение для упакованного повторяющегося поля, парсеры должны быть готовы принять несколько пар ключ-значение. В этом случае полезные нагрузки должны быть объединены. Каждая пара должна содержать целое число элементов. Следующее является допустимым кодированием того же сообщения выше, которое парсеры должны принимать:
6: {3 270}
6: {86942}
Парсеры protocol buffer должны быть способны разбирать повторяющиеся поля, которые были скомпилированы как packed, как если бы они не были упакованы, и наоборот. Это позволяет добавлять [packed=true] к существующим полям совместимым вперед и назад образом.
Oneofs
Поля Oneof кодируются так же, как если бы поля не были в oneof. Правила, применяемые к oneofs, не зависят от того, как они представлены на проводе.
Последний побеждает
Обычно закодированное сообщение никогда не будет иметь более одного экземпляра не-repeated поля. Однако ожидается, что парсеры обработают случай, когда они есть. Для числовых типов и строк, если одно и то же поле появляется несколько раз, парсер принимает последнее значение, которое он видит. Для вложенных полей сообщений парсер объединяет несколько экземпляров одного и того же поля, как с методом Message::MergeFrom - то есть все одиночные скалярные поля в последнем экземпляре заменяют те в первом, одиночные вложенные сообщения объединяются, а repeated поля объединяются. Эффект этих правил заключается в том, что разбор конкатенации двух закодированных сообщений дает точно такой же результат, как если бы вы разобрали два сообщения отдельно и объединили результирующие объекты. То есть это:
MyMessage message;
message.ParseFromString(str1 + str2);
эквивалентно этому:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
Это свойство иногда полезно, так как позволяет объединить два сообщения (путем конкатенации), даже если вы не знаете их типы.
Карты
Поля карт - это просто сокращение для специального вида повторяющегося поля. Если у нас есть
message Test6 {
map<string, int32> g = 7;
}
это фактически то же самое, что
message Test6 {
message g_Entry {
string key = 1;
int32 value = 2;
}
repeated g_Entry g = 7;
}
Таким образом, карты кодируются почти точно как поле repeated сообщения: как последовательность записей типа LEN с двумя полями каждое. Исключение состоит в том, что порядок не гарантируется сохраненным при сериализации карт.
Группы
Группы - это устаревшая функция, которую не следует использовать, но они остаются в формате провода и заслуживают упоминания.
Группа немного похожа на подсообщение, но она ограничена специальными тегами, а не префиксом LEN. Каждая группа в сообщении имеет номер поля, который используется на этих специальных тегах.
Группа с номером поля 8 начинается с тега 8:SGROUP. Записи SGROUP имеют пустую полезную нагрузку, так что все это просто обозначает начало группы. После того как все поля в группе перечислены, соответствующий тег 8:EGROUP обозначает ее конец. Записи EGROUP также не имеют полезной нагрузки, так что 8:EGROUP - это вся запись. Номера полей групп должны совпадать. Если мы встречаем 7:EGROUP, где ожидаем 8:EGROUP, сообщение неправильно сформировано.
Protoscope предоставляет удобный синтаксис для записи групп. Вместо записи
8:SGROUP
1: 2
3: {"foo"}
8:EGROUP
Protoscope позволяет
8: !{
1: 2
3: {"foo"}
}
Это сгенерирует соответствующие маркеры начала и конца группы. Синтаксис !{} может встречаться только сразу после нетипизированного выражения тега, такого как 8:.
Порядок полей
Номера полей могут быть объявлены в любом порядке в файле .proto. Выбранный порядок не влияет на то, как сообщения сериализуются.
Когда сообщение сериализуется, нет гарантированного порядка, в котором его известные или неизвестные поля будут записаны. Порядок сериализации - это деталь реализации, и детали любой конкретной реализации могут измениться в будущем. Поэтому парсеры protocol buffer должны быть способны разбирать поля в любом порядке.
Последствия
- Не предполагайте, что байтовый вывод сериализованного сообщения стабилен. Это особенно верно для сообщений с транзитивными полями bytes, представляющими другие сериализованные сообщения protocol buffer.
- По умолчанию повторные вызовы методов сериализации на одном и том же экземпляре сообщения protocol buffer могут не производить один и тот же байтовый вывод. То есть, сериализация по умолчанию не является детерминированной.
- Детерминированная сериализация гарантирует только тот же байтовый вывод для конкретного бинарника. Байтовый вывод может меняться между разными версиями бинарника.
- Следующие проверки могут не сработать для экземпляра сообщения protocol buffer
foo:foo.SerializeAsString() == foo.SerializeAsString()Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())
- Вот несколько примеров сценариев, где логически эквивалентные сообщения protocol buffer
fooиbarмогут сериализоваться в разные байтовые выводы:barсериализован старым сервером, который рассматривает некоторые поля как неизвестные.barсериализован сервером, реализованным на другом языке программирования и сериализующим поля в другом порядке.barимеет поле, которое сериализуется недетерминированным образом.barимеет поле, которое хранит сериализованный байтовый вывод сообщения protocol buffer, которое сериализовано по-другому.barсериализован новым сервером, который сериализует поля в другом порядке из-за изменения реализации.fooиbarявляются конкатенациями одних и тех же отдельных сообщений в разном порядке.
Ограничения размера закодированного Proto
Protos должны быть меньше 2 GiB при сериализации. Многие реализации proto откажутся сериализовать или разбирать сообщения, превышающие этот предел.
Сокращенная шпаргалка
Следующее предоставляет наиболее заметные части формата провода в удобном для справки формате.
message := (tag value)*
tag := (field << 3) bit-or wire_type;
encoded as uint32 varint
value := varint for wire_type == VARINT,
i32 for wire_type == I32,
i64 for wire_type == I64,
len-prefix for wire_type == LEN,
<empty> for wire_type == SGROUP or EGROUP
varint := int32 | int64 | uint32 | uint64 | bool | enum | sint32 | sint64;
encoded as varints (sintN are ZigZag-encoded first)
i32 := sfixed32 | fixed32 | float;
encoded as 4-byte little-endian (float is IEEE 754
single-precision); memcpy of the equivalent C types (u?int32_t,
float)
i64 := sfixed64 | fixed64 | double;
encoded as 8-byte little-endian (double is IEEE 754
double-precision); memcpy of the equivalent C types (u?int64_t,
double)
len-prefix := size (message | string | bytes | packed);
size encoded as int32 varint
string := valid UTF-8 string (e.g. ASCII);
max 2GB of bytes
bytes := any sequence of 8-bit bytes;
max 2GB of bytes
packed := varint* | i32* | i64*,
consecutive values of the type specified in `.proto`
См. также Справку по языку Protoscope.
Ключ
message := (tag value)*
: Сообщение кодируется как последовательность из нуля или более пар тегов и значений.
tag := (field << 3) bit-or wire_type
: Тег - это комбинация wire_type, хранящегося в трех младших битах, и номера поля, определенного в файле .proto.
value := varint for wire_type == VARINT, ...
: Значение хранится по-разному в зависимости от wire_type, указанного в теге.
varint := int32 | int64 | uint32 | uint64 | bool | enum | sint32 | sint64
: Вы можете использовать varint для хранения любого из перечисленных типов данных.
i32 := sfixed32 | fixed32 | float
: Вы можете использовать fixed32 для хранения любого из перечисленных типов данных.
i64 := sfixed64 | fixed64 | double
: Вы можете использовать fixed64 для хранения любого из перечисленных типов данных.
len-prefix := size (message | string | bytes | packed)
: Значение с префиксом длины хранится как длина (закодированная как varint), а затем один из перечисленных типов данных.
string := valid UTF-8 string (e.g. ASCII)
: Как описано, строка должна использовать кодировку символов UTF-8. Строка не может превышать 2GB.
bytes := any sequence of 8-bit bytes
: Как описано, bytes могут хранить пользовательские типы данных размером до 2GB.
packed := varint* | i32* | i64*
: Используйте тип данных packed, когда вы храните последовательные значения типа, описанного в определении протокола. Тег опускается для значений после первого, что амортизирует затраты на теги до одного на поле, а не на элемент.
ProtoJSON
Охватывает, как использовать утилиты преобразования Protobuf в JSON.
Protobuf поддерживает каноническое кодирование в JSON, что упрощает обмен данными с системами, которые не поддерживают стандартный двоичный формат передачи protobuf.
На этой странице указан формат, но ряд дополнительных крайних случаев, которые определяют соответствующий парсер ProtoJSON, охвачены в Наборе тестов на соответствие Protobuf и не подробно описаны здесь.
Нецели формата
Не может представлять некоторые схемы JSON
Формат ProtoJSON предназначен для представления JSON схем, которые выразимы на языке схем Protobuf.
Может быть возможно представить многие предсуществующие схемы JSON как схему Protobuf и разобрать ее с помощью ProtoJSON, но он не предназначен для возможности представления произвольных схем JSON.
Например, нет способа выразить в схеме Protobuf типы, которые могут быть распространены в схемах JSON, такие как number[][] или number|string.
Возможно использовать типы google.protobuf.Struct и google.protobuf.Value, чтобы разрешить произвольный JSON быть разобранным в схему Protobuf, но они только позволяют вам захватывать значения как неупорядоченные карты ключ-значение без схемы.
Не так эффективен, как двоичный формат передачи
Формат ProtoJSON не так эффективен, как двоичный формат передачи, и никогда не будет.
Конвертер использует больше CPU для кодирования и декодирования сообщений и (за исключением редких случаев) закодированные сообщения потребляют больше места.
Не имеет таких хороших гарантий эволюции схемы, как двоичный формат передачи
Формат ProtoJSON не поддерживает неизвестные поля и помещает имена полей и значений перечислений в закодированные сообщения, что делает гораздо более сложным изменение этих имен позже. Удаление полей является критическим изменением, которое вызовет ошибку разбора.
См. Безопасность JSON Wire ниже для более подробной информации.
Описание формата
Представление каждого типа
Следующая таблица показывает, как данные представлены в файлах JSON.
| Protobuf | JSON | Пример JSON | Примечания |
|---|---|---|---|
| message | object | {"fooBar": v, "g": null, ...} |
Генерирует объекты JSON. Имена полей сообщения преобразуются в
lowerCamelCase и становятся ключами объектов JSON. Если указана опция поля
json_name, указанное значение
будет использоваться как ключ вместо этого. Парсеры принимают как имя lowerCamelCase
(или указанное опцией json_name), так и
исходное имя поля proto. null является допустимым значением
для всех типов полей и оставляет поле неустановленным. \0 (nul) не может
использоваться внутри значения json_name. Подробнее о причинах см.
Более строгая проверка для json_name.
|
| enum | string | "FOO_BAR" |
Используется имя значения перечисления, указанное в proto. Парсеры принимают как имена перечислений, так и целочисленные значения. |
| map<K,V> | object | {"k": v, ...} |
Все ключи преобразуются в строки (ключи в спецификации JSON могут быть только строками). |
| repeated V | array | [v, ...] |
null принимается как пустой список []. |
| bool | true, false | true, false |
|
| string | string | "Hello World!" |
|
| bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
Значением JSON будут данные, закодированные как строка с использованием стандартного base64 кодирования с заполнением. Принимаются как стандартное, так и URL-безопасное base64 кодирование с/без заполнения. |
| int32, fixed32, uint32 | number | 1, -10, 0 |
Значением JSON будет десятичное число. Принимаются как числа, так и строки. Пустые строки недопустимы. Экспоненциальная нотация (например, `1e2`) принимается как в кавычках, так и без них. |
| int64, fixed64, uint64 | string | "1", "-10" |
Значением JSON будет десятичная строка. Принимаются как числа, так и строки. Пустые строки недопустимы. Экспоненциальная нотация (например, `1e2`) принимается как в кавычках, так и без них. |
| float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" |
Значением JSON будет число или одно из специальных строковых значений "NaN", "Infinity" и "-Infinity". Принимаются как числа, так и строки. Пустые строки недопустимы. Экспоненциальная нотация также принимается. |
| Any | object |
{"@type": "url", "f": v, ... } |
Если Any содержит известный тип, который имеет специальное
отображение JSON в этой таблице (например, google.protobuf.Duration),
он будет преобразован следующим образом: {"@type": xxx, "value":
yyy}. В противном случае значение будет преобразовано в объект JSON
как обычно, и будет вставлено дополнительное поле "@type"
со значением URL, указывающим тип сообщения.
|
| Timestamp | string | "1972-01-01T10:00:20.021Z" |
Использует RFC 3339 (см. уточнение), где сгенерированный вывод всегда будет Z-нормализован и использует 0, 3, 6 или 9 дробных цифр. Смещения, отличные от "Z", также принимаются. |
| Duration | string | "1.000340012s", "1s" |
Сгенерированный вывод всегда содержит 0, 3, 6 или 9 дробных цифр, в зависимости от требуемой точности, с последующим суффиксом "s". Принимаются любые дробные цифры (также отсутствующие), если они помещаются в точность наносекунд и требуется суффикс "s". Это *не* формат 'duration' RFC 3339 (см. Durations для уточнения). |
| Struct | object |
{ ... } |
Любой объект JSON. См. struct.proto. |
| Wrapper types | various types | 2, "2", "foo", true, "true", null, 0, ... |
Обертки используют то же представление в JSON, что и обернутый примитивный
тип, за исключением того, что null разрешен и сохраняется во время
преобразования и передачи данных.
|
| FieldMask | string | "f.fooBar,h" |
См. field_mask.proto. |
| ListValue | array | [foo, bar, ...] |
|
| Value | value | Любое значение JSON. Проверьте google.protobuf.Value для деталей. | |
| NullValue | null | JSON null. Особый случай [поведения разбора null](#null-values). | |
| Empty | object | {} |
Пустой объект JSON |
Присутствие и значения по умолчанию
При генерации вывода в формате JSON из protocol buffer, если поле поддерживает присутствие, сериализаторы должны выдавать значение поля тогда и только тогда, когда соответствующий hasser вернул бы true.
Если поле не поддерживает присутствие поля и имеет значение по умолчанию (например, любое пустое повторяющееся поле), сериализаторы должны опускать его из вывода. Реализация может предоставлять опции для включения полей со значениями по умолчанию в вывод.
Null значения
Сериализаторы не должны выдавать значения null.
Парсеры должны принимать null как допустимое значение для любого поля, с поведением:
- Любая проверка допустимости ключа все равно должна происходить (запрет неизвестных полей)
- Поле должно оставаться неустановленным, как если бы его не было во входных данных вообще (hassers все равно должны возвращать false, где применимо).
Значения null не допускаются внутри повторяющихся полей.
google.protobuf.NullValue является особым исключением из этого поведения: null обрабатывается как сторожевой значение присутствия для этого типа, и поэтому поле этого типа должно обрабатываться сериализаторами и парсерами в соответствии со стандартным поведением присутствия. Это поведение соответственно позволяет google.protobuf.Struct и google.protobuf.Value без потерь кругло обрабатывать произвольный JSON.
Дублирующиеся значения
Сериализаторы никогда не должны сериализовать одно и то же поле несколько раз, ни несколько разных случаев в одном oneof в одном объекте JSON.
Парсеры должны принимать дублирование одного и того же поля, и должно сохраняться последнее предоставленное значение. Это также применяется к "альтернативным написаниям" одного и того же имени поля.
Если реализации не могут поддерживать необходимую информацию о порядке полей, предпочтительнее отклонять входные данные с дублирующимися ключами, чем иметь произвольное значение победителем. В некоторых реализациях поддержание порядка полей объектов может быть непрактичным или неосуществимым, поэтому настоятельно рекомендуется, чтобы системы избегали reliance на конкретное поведение для дублирующихся полей в ProtoJSON, где это возможно.
Числовые значения вне диапазона
При разборе числового значения, если число, которое разбирается из провода, не помещается в соответствующий тип, парсер должен принудительно преобразовать значение к соответствующему типу. Это имеет то же поведение, что и простое приведение в C++ или Java (например, если число больше 2^32 читается для поля int32, оно будет усечено до 32 бит).
Безопасность ProtoJSON Wire
При использовании ProtoJSON только некоторые изменения схемы безопасно делать в распределенной системе. Это контрастирует с теми же концепциями, примененными к двоичному формату передачи.
Небезопасные для JSON Wire изменения
Небезопасные для провода изменения - это изменения схемы, которые сломаются, если вы разбираете данные, которые были сериализованы с использованием старой схемы, с парсером, который использует новую схему (или наоборот). Вы почти никогда не должны делать изменения схемы такой формы.
- Изменение поля на расширение или с расширения того же номера и типа не безопасно.
- Изменение поля между
stringиbytesне безопасно. - Изменение поля между типом сообщения и
bytesне безопасно. - Изменение любого поля с
optionalнаrepeatedне безопасно. - Изменение поля между
map<K, V>и соответствующим полемrepeatedсообщения не безопасно. - Перемещение полей в существующий
oneofне безопасно.
Безопасные для JSON Wire изменения
Безопасные для провода изменения - это те, при которых полностью безопасно развивать схему таким образом без риска потери данных или новых сбоев при разборе.
Обратите внимание, что почти все безопасные для провода изменения могут быть критическим изменением для кода приложения. Например, добавление значения в предсуществующее enum будет критическим изменением компиляции для любого кода с исчерпывающим switch по этому enum. По этой причине Google может избегать внесения некоторых из этих типов изменений в публичные сообщения. AIP содержат рекомендации о том, какие из этих изменений безопасно вносить там.
- Изменение одного поля
optionalна член новогоoneofбезопасно. - Изменение
oneof, который содержит только одно поле, на полеoptionalбезопасно. - Изменение поля между любыми из
int32,sint32,sfixed32,fixed32безопасно. - Изменение поля между любыми из
int64,sint64,sfixed64,fixed64безопасно. - Изменение номера поля безопасно (поскольку номера полей не используются в формате ProtoJSON), но все же настоятельно не рекомендуется, поскольку это очень небезопасно в двоичном формате передачи.
- Добавление значений в enum безопасно, если "Выдавать значения enum как целые числа" установлено на всех relevant клиентах (см. опции)
Совместимые с JSON Wire изменения (Условно безопасные)
В отличие от безопасных для провода изменений, совместимые с проводом означают, что одни и те же данные могут быть разобраны как до, так и после данного изменения. Однако клиент, который читает его, получит потерю данных при такой форме изменения. Например, изменение int32 на int64 является совместимым изменением, но если значение больше INT32_MAX записано, клиент, который читает его как int32, отбросит старшие биты.
Вы можете вносить совместимые изменения в вашу схему только если вы тщательно управляете развертыванием в вашей системе. Например, вы можете изменить int32 на int64, но обеспечить, чтобы вы продолжали записывать только допустимые значения int32 до тех пор, пока новая схема не будет развернута на всех конечных точках, и затем начать записывать большие значения после этого.
Совместимые, но с проблемами обработки неизвестных полей
В отличие от двоичного формата передачи, реализации ProtoJSON обычно не распространяют неизвестные поля. Это означает, что добавление к схемам обычно совместимо, но приведет к сбоям разбора, если клиент, использующий старую схему, наблюдает новое содержимое.
Это означает, что вы можете добавлять к вашей схеме, но вы не можете безопасно начать записывать их, пока не узнаете, что схема была развернута на relevant клиенте или сервере (или что relevant клиенты установили флаг Игнорировать Неизвестные Поля, обсуждаемый ниже).
- Добавление и удаление полей считается совместимым с этой оговоркой.
- Удаление значений enum считается совместимым с этой оговоркой.
Совместимые, но потенциально с потерей
- Изменение между любыми из 32-битных целых чисел (
int32,uint32,sint32,sfixed32,fixed32) и любыми из 64-битных целых чисел (int64,uint64,sint64,sfixed32) является совместимым изменением.- Если число разобрано из провода, которое не помещается в соответствующий тип, вы получите тот же эффект, как если бы вы привели число к этому типу в C++ (например, если 64-битное число читается как int32, оно будет усечено до 32 бит).
- В отличие от двоичного формата передачи,
boolне совместим с целыми числами. - Обратите внимание, что типы int64 по умолчанию заключаются в кавычки, чтобы избежать потери точности при обработке как double или число JavaScript, а 32-битные типы по умолчанию без кавычек. Соответствующие реализации будут принимать оба случая для всех целочисленных типов, но несоответствующие реализации могут неправильно обработать этот случай и не обработать int32 в кавычках или int64 без кавычек, что может сломаться при этом изменении.
enumможет быть условно совместим сstring- Если любой клиент использует флаг "enums-as-ints", то enums будут вместо этого совместимы с целочисленными типами.
Уточнения RFC 3339
Метки времени
Метки времени ProtoJSON используют формат метки времени RFC 3339. К сожалению, некоторая неоднозначность в спецификации RFC 3339 создала несколько крайних случаев, где различные другие реализации RFC 3339 не согласны, является ли формат законным или нет.
RFC 3339 намеревается объявить строгое подмножество формата ISO-8601, и некоторая дополнительная неоднозначность была создана, поскольку RFC 3339 был опубликован в 2002 году, а затем ISO-8601 был пересмотрен без соответствующих пересмотров RFC 3339.
Наиболее заметно, ISO-8601-1988 содержит эту заметку:
В представлениях даты и времени строчные символы могут использоваться, когда заглавные символы недоступны.
Неоднозначно, предполагает ли эта заметка, что парсеры должны принимать строчные буквы в целом, или она только предполагает, что строчные буквы могут использоваться как замена в средах, где заглавные не могут быть технически использованы. RFC 3339 содержит заметку, которая намеревается прояснить интерпретацию, что строчные буквы должны приниматься в целом.
ISO-8601-2019 не содержит соответствующей заметки и однозначно, что строчные буквы не разрешены.
Это создало некоторую путаницу для всех библиотек, которые заявляют, что они поддерживают RFC 3339: сегодня RFC 3339 заявляет, что это профиль ISO-8601, но содержит уточняющую заметку, ссылающуюся на текст, который отсутствует в последней спецификации ISO-8601.
Спецификация ProtoJSON принимает решение, что формат метки времени - это более строгое определение "RFC 3339 как профиль ISO-8601-2019". Некоторые реализации Protobuf могут не соответствовать, используя реализацию разбора метки времени, которая реализована как "RFC 3339 как профиль ISO-8601-1988", которая будет принимать несколько дополнительных крайних случаев.
Для последовательной интероперабельности парсеры должны принимать только более строгое подмножество формата, где это возможно. При использовании несоответствующей реализации, которая принимает более слабое определение, настоятельно избегайте reliance на дополнительные крайние случаи, которые принимаются.
Длительности
RFC 3339 также определяет формат длительности, но, к сожалению, формат длительности RFC 3339 не имеет никакого способа выразить разрешение меньше секунды.
Кодировка длительности ProtoJSON напрямую вдохновлена представлением dur-seconds RFC 3339, но она способна кодировать наносекундную точность. Для целого числа секунд два представления могут совпадать (как 10s), но длительности ProtoJSON принимают дробные значения, и соответствующие реализации должны точно представлять наносекундную точность (как 10.500000001s).
Опции JSON
Соответствующая реализация protobuf JSON может предоставлять следующие опции:
-
Всегда выдавать поля без присутствия: Поля, которые не поддерживают присутствие и которые имеют свое значение по умолчанию, опускаются по умолчанию в выводе JSON (например, целое число с неявным присутствием со значением 0, поля строк с неявным присутствием, которые являются пустыми строками, и пустые повторяющиеся и карт поля). Реализация может предоставлять опцию для переопределения этого поведения и вывода полей с их значениями по умолчанию.
Начиная с v25.x, реализации C++, Java и Python не соответствуют, так как этот флаг влияет на поля
optionalproto2, но не на поляoptionalproto3. Исправление запланировано для будущего релиза. -
Игнорировать неизвестные поля: Парсер protobuf JSON должен отклонять неизвестные поля по умолчанию, но может предоставлять опцию для игнорирования неизвестных полей при разборе.
-
Использовать имя поля proto вместо имени lowerCamelCase: По умолчанию принтер protobuf JSON должен преобразовывать имя поля в lowerCamelCase и использовать его как имя JSON. Реализация может предоставлять опцию использовать имя поля proto как имя JSON вместо этого. Парсеры protobuf JSON обязаны принимать как преобразованное имя lowerCamelCase, так и имя поля proto.
-
Выдавать значения enum как целые числа вместо строк: Имя значения enum используется по умолчанию в выводе JSON. Может быть предоставлена опция использовать числовое значение значения enum вместо этого.
Методики
Описывает некоторые часто используемые шаблоны проектирования для работы с Protocol Buffers.
Вы также можете отправлять вопросы по проектированию и использованию в группу обсуждения Protocol Buffers.
Общие суффиксы имен файлов
Довольно часто сообщения записываются в файлы в нескольких различных форматах. Мы рекомендуем использовать следующие расширения файлов для этих файлов.
| Содержимое | Расширение |
|---|---|
| Текстовый формат | .txtpb |
| Формат передачи (Wire) | .binpb |
| Формат JSON | .json |
Для Текстового формата конкретно .textproto также довольно распространено, но мы
рекомендуем .txtpb за его краткость.
Потоковая передача нескольких сообщений
Если вы хотите записать несколько сообщений в один файл или поток, вам нужно
самостоятельно отслеживать, где заканчивается одно сообщение и начинается следующее. Формат передачи
Protocol Buffer не является самодостотаточным (self-delimiting), поэтому парсеры protocol buffer
не могут определить, где сообщение заканчивается, самостоятельно. Самый простой способ решить эту
проблему - записать размер каждого сообщения перед записью самого
сообщения. Когда вы читаете сообщения обратно, вы читаете размер, затем читаете байты
в отдельный буфер, затем разбираете из этого буфера. (Если вы хотите избежать
копирования байтов в отдельный буфер, ознакомьтесь с классом CodedInputStream (в
C++ и Java), которому можно указать ограничить чтение определенным количеством
байтов.)
Большие наборы данных
Protocol Buffers не предназначены для обработки больших сообщений. Как общее практическое правило, если вы имеете дело с сообщениями размером более мегабайта каждое, возможно, пора рассмотреть альтернативную стратегию.
Тем не менее, Protocol Buffers отлично подходят для обработки отдельных сообщений внутри большого набора данных. Обычно большие наборы данных представляют собой коллекцию небольших частей, где каждая маленькая часть - это структурированные данные. Хотя Protocol Buffers не могут обработать весь набор сразу, использование Protocol Buffers для кодирования каждой части значительно упрощает вашу проблему: теперь все, что вам нужно, - это обработать набор байтовых строк, а не набор структур.
Protocol Buffers не включают никакой встроенной поддержки для больших наборов данных, потому что разные ситуации требуют разных решений. Иногда простого списка записей будет достаточно, в то время как в других случаях вам нужно что-то более похожее на базу данных. Каждое решение должно разрабатываться как отдельная библиотека, чтобы только те, кому это нужно, несли затраты.
Самоописывающиеся сообщения
Protocol Buffers не содержат описаний своих собственных типов. Таким образом, имея только
сырое сообщение без соответствующего файла .proto, определяющего его тип,
трудно извлечь какие-либо полезные данные.
Однако содержимое файла .proto само может быть представлено с использованием protocol
buffers. Файл src/google/protobuf/descriptor.proto в пакете исходного кода
определяет involved типы сообщений. protoc может вывести
FileDescriptorSet— который представляет набор файлов .proto — используя
опцию --descriptor_set_out. С этим вы можете определить самоописывающееся
сообщение protocol следующим образом:
syntax = "proto3";
import "google/protobuf/any.proto";
import "google/protobuf/descriptor.proto";
message SelfDescribingMessage {
// Набор FileDescriptorProtos, которые описывают тип и его зависимости.
google.protobuf.FileDescriptorSet descriptor_set = 1;
// Сообщение и его тип, закодированные как сообщение Any.
google.protobuf.Any message = 2;
}
Используя такие классы, как DynamicMessage (доступный в C++ и Java), вы можете затем
писать инструменты, которые могут манипулировать SelfDescribingMessage.
При всем этом, причина, по которой эта функциональность не включена в библиотеку Protocol Buffer, заключается в том, что у нас внутри Google никогда не было необходимости в ней.
Эта техника требует поддержки динамических сообщений с использованием дескрипторов. Проверьте, поддерживают ли ваши платформы эту функцию, прежде чем использовать самоописывающиеся сообщения.
Add ons
Сторонние дополнения
Ссылки на множество проектов с открытым исходным кодом, которые добавляют полезную функциональность поверх Protocol Buffers.
Многие проекты с открытым исходным кодом добавляют полезную функциональность поверх Protocol Buffers. Для просмотра списка ссылок на известные нам проекты обратитесь к странице сторонних дополнений в вики.
Объявления расширений
Подробно описывает, что такое объявления расширений, зачем они нужны и как их использовать.
Введение
На этой странице подробно описано, что такое объявления расширений, зачем они нужны, и как их использовать.
{{% alert title="Примечание" color="note" %}} Proto3 не поддерживает расширения (за исключением объявления пользовательских опций). Расширения полностью поддерживаются в proto2 и редакциях хотя.{{% /alert %}}
Если вам нужно введение в расширения, прочитайте это руководство по расширениям
Мотивация
Объявления расширений направлены на нахождение золотой середины между обычными полями и расширениями. Как и расширения, они избегают создания зависимости от типа сообщения поля, что, следовательно, приводит к более простому графу сборки и меньшим бинарным файлам в средах, где неиспользуемые сообщения трудно или невозможно отсечь. Как и обычные поля, имя/номер поля появляются в содержащем сообщении, что упрощает избежание конфликтов и позволяет увидеть удобный список того, какие поля объявлены.
Перечисление занятых номеров расширений с помощью объявлений расширений упрощает пользователям выбор доступного номера расширения и избежание конфликтов.
Использование
Объявления расширений являются опцией диапазонов расширений. Как предварительные
объявления в C++, вы можете объявить тип поля, имя поля и мощность
(одиночное или повторяющееся) поля расширения без импорта файла .proto,
содержащего полное определение расширения:
edition = "2023";
message Foo {
extensions 4 to 1000 [
declaration = {
number: 4,
full_name: ".my.package.event_annotations",
type: ".logs.proto.ValidationAnnotations",
repeated: true },
declaration = {
number: 999,
full_name: ".foo.package.bar",
type: "int32"}];
}
Этот синтаксис имеет следующую семантику:
- Несколько
declarationс различными номерами расширений могут быть определены в одном диапазоне расширений, если размер диапазона позволяет. - Если есть какое-либо объявление для диапазона расширений, все расширения диапазона также должны быть объявлены. Это предотвращает добавление необъявленных расширений и обеспечивает использование объявлений для любого нового расширения в этом диапазоне.
- Данный тип сообщения (
.logs.proto.ValidationAnnotations) не нуждается в предварительном определении или импорте. Мы проверяем только, что это допустимое имя, которое потенциально может быть определено в другом файле.proto. - Когда этот или другой файл
.protoопределяет расширение этого сообщения (Foo) с этим именем или номером, мы обеспечиваем, чтобы номер, тип и полное имя расширения совпадали с тем, что предварительно объявлено здесь.
{{% alert title="Предупреждение" color="warning" %}}
Избегайте использования объявлений для групп диапазонов расширений, таких как extensions 4, 999.
Неясно, к какому диапазону расширений применяются объявления, и в настоящее время
это не поддерживается.{{% /alert %}}
Объявления расширений ожидают два поля расширения с разными пакетами:
package my.package;
extend Foo {
repeated logs.proto.ValidationAnnotations event_annotations = 4;
}
package foo.package;
extend Foo {
optional int32 bar = 999;
}
Зарезервированные объявления
Объявление расширения может быть помечено reserved: true, чтобы указать, что оно
больше не активно используется и определение расширения было удалено. Не
удаляйте объявление расширения и не редактируйте его значения type или full_name.
Этот тег reserved отделен от зарезервированного ключевого слова для обычных полей и
не требует разбиения диапазона расширений.
edition = "2023";
message Foo {
extensions 4 to 1000 [
declaration = {
number: 500,
full_name: ".my.package.event_annotations",
type: ".logs.proto.ValidationAnnotations",
reserved: true }];
}
Определение поля расширения, использующее номер, который reserved в
объявлении, не скомпилируется.
Представление в descriptor.proto
Объявление расширения представлено в descriptor.proto как поля в
proto2.ExtensionRangeOptions:
message ExtensionRangeOptions {
message Declaration {
optional int32 number = 1;
optional string full_name = 2;
optional string type = 3;
optional bool reserved = 5;
optional bool repeated = 6;
}
repeated Declaration declaration = 2;
}
Поиск полей через рефлексию
Объявления расширений не возвращаются из обычных функций поиска полей
таких как Descriptor::FindFieldByName() или Descriptor::FindFieldByNumber(). Как и
расширения, они обнаруживаются процедурами поиска расширений, такими как
DescriptorPool::FindExtensionByName(). Это явный выбор, который
отражает тот факт, что объявления не являются определениями и не имеют достаточной
информации для возврата полного FieldDescriptor.
Объявленные расширения все еще ведут себя как обычные расширения с точки зрения TextFormat и JSON. Это также означает, что миграция существующего поля на объявленное расширение потребует сначала миграции любого рефлексивного использования этого поля.
Используйте объявления расширений для выделения номеров
Расширения используют номера полей так же, как это делают обычные поля, поэтому важно чтобы каждому расширению был назначен номер, уникальный в пределах родительского сообщения. Мы рекомендуем использовать объявления расширений для объявления номера поля и типа для каждого расширения в родительском сообщении. Объявления расширений служат реестром всех расширений родительского сообщения, и protoc будет обеспечивать отсутствие конфликтов номеров полей. Когда вы добавляете новое расширение, выберите следующий доступный номер, обычно просто увеличив на единицу предыдущий добавленный номер расширения.
СОВЕТ: Существует особое руководство для
MessageSet, которое предоставляет скрипт для помощи в выборе
следующего доступного номера.
Всякий раз, когда вы удаляете расширение, убедитесь, что пометили номер поля как reserved,
чтобы устранить риск случайного повторного использования
его.
Эта конвенция является лишь рекомендацией - команда protobuf не имеет возможности или желания заставлять кого-либо придерживаться ее для каждого расширяемого сообщения. Если вы, как владелец расширяемого proto, не хотите координировать номера расширений через объявления расширений, вы можете выбрать координацию другими средствами. Однако будьте очень осторожны, потому что случайное повторное использование номера расширения может вызвать серьезные проблемы.
Один из способов обойти проблему - полностью избегать расширений и использовать
google.protobuf.Any
вместо этого. Это может быть хорошим выбором для API, которые работают с хранилищем, или для
сквозных систем, где клиент заботится о содержимом proto, но
система, получающая его, - нет.
Последствия повторного использования номера расширения
Расширение - это поле, определенное вне содержащего сообщения; обычно в отдельном файле .proto. Такое распределение определений делает легким для двух разработчиков случайно создать разные определения для одного и того же расширения номера поля.
Последствия изменения определения расширения одинаковы для расширений и стандартных полей. Повторное использование номера поля вводит неоднозначность в то, как proto должен декодироваться из формата провода. Формат провода protobuf является lean и не предоставляет хорошего способа обнаружения полей, закодированных с использованием одного определения и декодированных с использованием другого.
Эта неоднозначность может проявиться в короткие сроки, например, когда клиент использует одно определение расширения, а сервер использует другое при общении .
Эта неоднозначность также может проявиться в течение более длительного периода времени, например, при хранении данных закодированных с использованием одного определения расширения и последующем извлечении и декодировании с использованием второго определения расширения. Этот долгосрочный случай может быть трудным для диагностики, если первое определение расширения было удалено после того, как данные были закодированы и сохранены.
Результатом этого может быть:
- Ошибка разбора (лучший сценарий).
- Утечка PII / SPII – если PII или SPII записываются с использованием одного определения расширения и читаются с использованием другого определения расширения.
- Повреждение данных – если данные читаются с использованием «неправильного» определения, изменяются и перезаписываются.
Неоднозначность определения данных почти наверняка будет стоить кому-то времени на отладку как минимум. Это также может вызвать утечку или повреждение данных, на устранение которых уйдут месяцы.
Советы по использованию
Никогда не удаляйте объявление расширения
Удаление объявления расширения открывает дверь для случайного повторного использования в будущем. Если расширение больше не обрабатывается и определение удалено, объявление расширения может быть помечено как зарезервированное.
Никогда не используйте имя поля или номер из списка reserved для нового объявления расширения
Зарезервированные номера могли использоваться для полей или других расширений в прошлом.
Использование full_name зарезервированного поля
не рекомендуется
из-за
возможности неоднозначности при использовании textproto.
Никогда не меняйте тип существующего объявления расширения
Изменение типа поля расширения может привести к повреждению данных.
Если поле расширения имеет тип enum или message, и этот enum или message переименовывается, обновление имени объявления требуется и безопасно. Чтобы избежать поломок, обновление типа, определения поля расширения и объявления расширения должно произходить в одном коммите.
Будьте осторожны при переименовании поля расширения
Хотя переименование поля расширения допустимо для формата провода, это может сломать парсинг JSON и TextFormat.
Присутствие полей
Объясняет различные дисциплины отслеживания присутствия для полей protobuf. Также объясняется поведение явного отслеживания присутствия для сингулярных полей proto3 с базовыми типами.
Предпосылки
Присутствие поля — это концепция, определяющая, имеет ли поле protobuf значение. Существует два различных проявления присутствия для protobuf: неявное присутствие, когда сгенерированный API сообщения хранит только значения полей, и явное присутствие, когда API также хранит информацию о том, было ли поле установлено.
{{% alert title="Примечание" color="note" %}} Мы
рекомендуем всегда добавлять метку optional для базовых типов proto3. Это
обеспечивает более плавный переход к редакциям (editions), которые по умолчанию используют явное присутствие.{{% /alert %}}
Дисциплины присутствия
Дисциплины присутствия определяют семантику преобразования между представлением API и сериализованным представлением. Дисциплина неявного присутствия полагается на само значение поля для принятия решений во время (де)сериализации, в то время как дисциплина явного присутствия полагается на состояние явного отслеживания.
Присутствие в Потоке "тег-значение" (Бинарный формат) сериализации
Бинарный формат (wire format) — это поток тегированных, саморазделяющихся значений. По определению, бинарный формат представляет последовательность присутствующих значений. Другими словами, каждое значение, найденное в сериализации, представляет присутствующее поле; более того, сериализация не содержит информации об отсутствующих значениях.
Сгенерированный API для proto-сообщения включает определения (де)сериализации, которые преобразуют между типами API и потоком по определению присутствующих пар (тег, значение). Это преобразование разработано для обеспечения прямой и обратной совместимости при изменениях в определении сообщения; однако эта совместимость вносит некоторые (возможно, удивительные) особенности при десериализации сообщений в бинарном формате:
- При сериализации поля с неявным присутствием не сериализуются, если они
содержат значение по умолчанию.
- Для числовых типов значением по умолчанию является 0.
- Для перечислений (enum) значением по умолчанию является элемент с нулевым значением.
- Для строк, байтов и повторяющихся полей значением по умолчанию является значение нулевой длины.
- "Пустые" значения с длинной-разделителем (например, пустые строки) могут быть корректно представлены в сериализованных значениях: поле "присутствует" в том смысле, что оно появляется в бинарном формате. Однако, если сгенерированный API не отслеживает присутствие, то эти значения могут не быть повторно сериализованы; т.е. пустое поле может стать "отсутствующим" после цикла сериализации-десериализации.
- При десериализации повторяющиеся значения полей могут обрабатываться по-разному
в зависимости от определения поля.
- Дубликаты
repeatedполей обычно добавляются к представлению поля в API. (Обратите внимание, что сериализация упакованного повторяющегося поля производит только одно значение с длинной-разделителем в потоке тегов.) - Дубликаты
optionalполей следуют правилу "последнее wins".
- Дубликаты
- Поля
oneofобеспечивают инвариант на уровне API, что только одно поле установлено в один момент времени. Однако бинарный формат может включать несколько пар (тег, значение), которые концептуально принадлежатoneof. Аналогичноoptionalполям, сгенерированный API следует правилу "последнее wins". - Значения вне допустимого диапазона не возвращаются для полей-перечислений в сгенерированных API proto2. Однако значения вне диапазона могут быть сохранены как неизвестные поля в API, даже если тег в бинарном формате был распознан.
Присутствие в форматах Сопоставления по имени поля
Protobuf может быть представлен в удобочитаемых, текстовых форматах. Два известных
формата — это TextFormat (формат вывода, производимый сгенерированными методами DebugString сообщений)
и JSON.
Эти форматы имеют свои собственные требования к корректности и, как правило, строже, чем форматы потока тегированных значений. Однако TextFormat более близко повторяет семантику бинарного формата и в определенных случаях предоставляет аналогичную семантику (например, добавление повторяющихся пар имя-значение к повторяющемуся полю). В частности, аналогично бинарному формату, TextFormat включает только присутствующие поля.
JSON — гораздо более строгий формат, и он не может корректно представить некоторые семантики бинарного формата или TextFormat.
- В частности, элементы JSON семантически неупорядочены, и каждый элемент должен иметь уникальное имя. Это отличается от правил TextFormat для повторяющихся полей.
- JSON может включать поля, которые "отсутствуют", в отличие от дисциплины
неявного присутствия для других форматов:
- JSON определяет значение
null, которое может использоваться для представления определенного, но отсутствующего поля. - Значения повторяющихся полей могут быть включены в форматированный вывод, даже если они равны значению по умолчанию (пустой список).
- JSON определяет значение
- Поскольку элементы JSON неупорядочены, нет способа однозначно
интерпретировать правило "последнее wins".
- В большинстве случаев это нормально: элементы JSON должны иметь уникальные имена: повторяющиеся значения полей не являются допустимым JSON, поэтому их не нужно разрешать, как в TextFormat.
- Однако это означает, что может быть невозможно однозначно интерпретировать поля
oneof: если присутствуют несколько вариантов, они неупорядочены.
Теоретически JSON может представлять присутствие семантически сохраняющим образом. На практике, однако, корректность представления присутствия может варьироваться в зависимости от выбора реализации, особенно если JSON был выбран как средство взаимодействия с клиентами, не использующими protobuf.
Присутствие в API Proto2
В этой таблице приведено, отслеживается ли присутствие для полей в API proto2 (как для сгенерированных API, так и с использованием динамической рефлексии):
| Тип поля | Явное присутствие |
|---|---|
| Сингулярное числовое (целое или с плавающей точкой) | ✔️ |
| Сингулярное перечисление (enum) | ✔️ |
| Сингулярная строка или bytes | ✔️ |
| Сингулярное сообщение (message) | ✔️ |
| Повторяющееся (repeated) | |
| Oneofs | ✔️ |
| Карты (maps) |
Сингулярные поля (всех типов) явно отслеживают присутствие в сгенерированном API.
Сгенерированный интерфейс сообщения включает методы для запроса присутствия полей.
Например, для поля foo существует соответствующий метод has_foo. (Конкретное
имя следует тому же языково-специфичному соглашению об именовании, что и методы доступа к полям.)
Эти методы иногда называются "hazzers" внутри реализации protobuf.
Аналогично сингулярным полям, поля oneof явно отслеживают, какой из членов, если любой, содержит значение. Например, рассмотрим этот пример oneof:
oneof foo {
int32 a = 1;
float b = 2;
}
В зависимости от целевого языка, сгенерированный API обычно включал бы несколько методов:
- Hazzer для oneof:
has_foo - Метод варианта oneof:
foo_case(или подобный) - Hazzers для членов:
has_a,has_b - Геттеры для членов:
a,b
Повторяющиеся поля и карты не отслеживают присутствие: нет различия между пустым и отсутствующим повторяющимся полем.
Присутствие в API Proto3
В этой таблице приведено, отслеживается ли присутствие для полей в API proto3 (как для сгенерированных API, так и с использованием динамической рефлексии):
| Тип поля | optional | Явное присутствие |
|---|---|---|
| Сингулярное числовое (целое или с плавающей точкой) | Нет | |
| Сингулярное числовое (целое или с плавающей точкой) | Да | ✔️ |
| Сингулярное перечисление (enum) | Нет | |
| Сингулярное перечисление (enum) | Да | ✔️ |
| Сингулярная строка или bytes | Нет | |
| Сингулярная строка или bytes | Да | ✔️ |
| Сингулярное сообщение (message) | Нет | ✔️ |
| Сингулярное сообщение (message) | Да | ✔️ |
| Повторяющееся (repeated) | Н/П | |
| Oneofs | Н/П | ✔️ |
| Карты (maps) | Н/П |
Аналогично API proto2, API proto3 не отслеживает присутствие явно для повторяющихся
полей. Без метки optional API proto3 также не отслеживает присутствие для
базовых типов (числовые, строки, байты и перечисления). Поля Oneof
явно раскрывают присутствие, хотя тот же набор методов hazzer может не
генерироваться, как в API proto2.
Это поведение по умолчанию (отсутствие отслеживания присутствия без метки optional)
отличается от поведения proto2. Мы рекомендуем использовать метку optional в
proto3, если у вас нет конкретной причины не делать этого.
В рамках дисциплины неявного присутствия значение по умолчанию является синонимом "отсутствует" для целей сериализации. Чтобы "очистить" поле (чтобы оно не сериализовалось), пользователь API устанавливает для него значение по умолчанию.
Значение по умолчанию для полей типа enum в рамках неявного присутствия — это
соответствующий элемент перечисления со значением 0. Согласно правилам синтаксиса proto3, все типы перечислений
обязаны иметь элемент перечисления, который соответствует 0. По соглашению, это
UNKNOWN или элемент с похожим именем. Если нулевое значение концептуально находится вне
области допустимых значений для приложения, это поведение можно рассматривать как
эквивалентное явному присутствию.
Присутствие в API Редакций (Editions)
В этой таблице приведено, отслеживается ли присутствие для полей в API редакций (editions) (как для сгенерированных API, так и с использованием динамической рефлексии):
| Тип поля | Явное присутствие |
|---|---|
| Сингулярное числовое (целое или с плавающей точкой) | ✔️ |
| Сингулярное перечисление (enum) | ✔️ |
| Сингулярная строка или bytes | ✔️ |
| Сингулярное сообщение† (message) | ✔️ |
| Повторяющееся (repeated) | |
| Oneofs† | ✔️ |
| Карты (maps) |
† Сообщения и oneof никогда не имели неявного присутствия, и редакции
не позволяют установить field_presence = IMPLICIT.
API на основе редакций отслеживают присутствие полей явно, аналогично proto2, если
только для features.field_presence не установлено значение IMPLICIT. Аналогично API proto2,
API на основе редакций не отслеживают присутствие явно для повторяющихся полей.
Семантические различия
Дисциплина сериализации неявного присутствия приводит к видимым различиям от дисциплины явного присутствия, когда установлено значение по умолчанию. Для сингулярного поля с числовым, enum или строковым типом:
- Дисциплина неявного присутствия:
- Значения по умолчанию не сериализуются.
- Значения по умолчанию не объединяются-from (при слиянии).
- Чтобы "очистить" поле, ему устанавливается значение по умолчанию.
- Значение по умолчанию может означать:
- поле было явно установлено в значение по умолчанию, которое является допустимым в предметной области значений приложения;
- поле было концептуально "очищено" установкой значения по умолчанию; или
- поле никогда не устанавливалось.
- Методы
has_не генерируются (но см. примечание после этого списка)
- Дисциплина явного присутствия:
- Явно установленные значения всегда сериализуются, включая значения по умолчанию.
- Неустановленные поля никогда не объединяются-from.
- Явно установленные поля — включая значения по умолчанию — объединяются-from.
- Сгенерированный метод
has_fooуказывает, было ли полеfooустановлено (и не очищено). - Для очистки (т.е. сброса) значения должен использоваться сгенерированный метод
clear_foo.
{{% alert title="Примечание" color="note" %}}
Методы Has_ в большинстве случаев не генерируются для полей с неявным присутствием. Исключением
из этого поведения является Dart, который генерирует методы has_ для файлов схемы proto3.{{% /alert %}}
Соображения по слиянию
В соответствии с правилами неявного присутствия практически невозможно для целевого поля объединить-from его значение по умолчанию (с использованием функций слияния API protobuf). Это происходит потому, что значения по умолчанию пропускаются, аналогично дисциплине сериализации неявного присутствия. Слияние обновляет только целевое (merged-to) сообщение, используя не пропущенные значения из сообщения-обновления (merged-from).
Разница в поведении при слиянии имеет дальнейшие последствия для протоколов, которые полагаются на частичные обновления "patch". Если присутствие поля не отслеживается, то сам по себе патч обновления не может представить обновление до значения по умолчанию, потому что только не-значения по умолчанию объединяются-from.
Обновление для установки значения по умолчанию в этом случае требует некоторого внешнего механизма,
такого как FieldMask. Однако, если присутствие отслеживается, то все явно установленные
значения — даже значения по умолчанию — будут объединены в цель.
Соображения по совместимости изменений
Изменение поля между явным присутствием и неявным присутствием является бинарно-совместимым изменением для сериализованных значений в бинарном формате. Однако, сериализованное представление сообщения может отличаться в зависимости от того, какая версия определения сообщения использовалась для сериализации. В частности, когда "отправитель" явно устанавливает поле в его значение по умолчанию:
- Сериализованное значение, следующее дисциплине неявного присутствия, не содержит значения по умолчанию, даже если оно было явно установлено.
- Сериализованное значение, следующее дисциплине явного присутствия, содержит каждое "присутствующее" поле, даже если оно содержит значение по умолчанию.
Это изменение может быть безопасным или нет, в зависимости от семантики приложения. Например, рассмотрим двух клиентов с разными версиями определения сообщения.
Клиент A использует это определение сообщения, которое следует дисциплине явного присутствия
для поля foo:
syntax = "proto3";
message Msg {
optional int32 foo = 1;
}
Клиент B использует определение того же сообщения, за исключением того, что оно следует дисциплине отсутствия присутствия:
syntax = "proto3";
message Msg {
int32 foo = 1;
}
Теперь рассмотрим сценарий, в котором клиент A наблюдает за присутствием foo, когда клиенты
многократно обмениваются "одним и тем же" сообщением путем десериализации и повторной сериализации:
// Клиент A:
Msg m_a;
m_a.set_foo(1); // не-значение по умолчанию
assert(m_a.has_foo()); // OK
Send(m_a.SerializeAsString()); // клиенту B
// Клиент B:
Msg m_b;
m_b.ParseFromString(Receive()); // от клиента A
assert(m_b.foo() == 1); // OK
Send(m_b.SerializeAsString()); // клиенту A
// Клиент A:
m_a.ParseFromString(Receive()); // от клиента B
assert(m_a.foo() == 1); // OK
assert(m_a.has_foo()); // OK
m_a.set_foo(0); // значение по умолчанию
Send(m_a.SerializeAsString()); // клиенту B
// Клиент B:
Msg m_b;
m_b.ParseFromString(Receive()); // от клиента A
assert(m_b.foo() == 0); // OK
Send(m_b.SerializeAsString()); // клиенту A
// Клиент A:
m_a.ParseFromString(Receive()); // от клиента B
assert(m_a.foo() == 0); // OK
assert(m_a.has_foo()); // FAIL (СБОЙ)
Если клиент A зависит от явного присутствия для foo, то "цикл обхода"
через клиента B будет потерянным с точки зрения клиента A. В примере
это небезопасное изменение: клиент A требует (через assert), чтобы поле
присутствовало; даже без каких-либо изменений через API это требование терпит неудачу
в зависящем от значения и пира случае.
Как включить Явное присутствие в Proto3
Это общие шаги для использования поддержки отслеживания присутствия полей в proto3:
- Добавьте поле
optionalв файл.proto. - Запустите
protoc(как минимум v3.15, или v3.12 с использованием флага--experimental_allow_proto3_optional). - Используйте сгенерированные методы "hazzer" и "clear" в коде приложения, вместо сравнения или установки значений по умолчанию.
Изменения в файле .proto
Это пример сообщения proto3 с полями, которые следуют как семантике отсутствия присутствия, так и явного присутствия:
syntax = "proto3";
package example;
message MyMessage {
// неявное присутствие:
int32 not_tracked = 1;
// Явное присутствие:
optional int32 tracked = 2;
}
Вызов protoc
Отслеживание присутствия для сообщений proto3 включено по умолчанию
начиная с v3.15.0,
ранее, до
v3.12.0,
требовался флаг --experimental_allow_proto3_optional при использовании отслеживания присутствия с protoc.
Использование сгенерированного кода
Сгенерированный код для полей proto3 с явным присутствием (метка optional)
будет таким же, как если бы он был в файле proto2.
Это определение, используемое в примерах "неявного присутствия" ниже:
syntax = "proto3";
package example;
message Msg {
int32 foo = 1;
}
Это определение, используемое в примерах "явного присутствия" ниже:
syntax = "proto3";
package example;
message Msg {
optional int32 foo = 1;
}
В примерах функция GetProto создает и возвращает сообщение типа
Msg с неуказанным содержимым.
Пример на C++
Неявное присутствие:
Msg m = GetProto();
if (m.foo() != 0) {
// "Очистить" поле:
m.set_foo(0);
} else {
// Значение по умолчанию: поле могло отсутствовать.
m.set_foo(1);
}
Явное присутствие:
Msg m = GetProto();
if (m.has_foo()) {
// Очистить поле:
m.clear_foo();
} else {
// Поле отсутствует, поэтому установить его.
m.set_foo(1);
}
Пример на C#
Неявное присутствие:
var m = GetProto();
if (m.Foo != 0) {
// "Очистить" поле:
m.Foo = 0;
} else {
// Значение по умолчанию: поле могло отсутствовать.
m.Foo = 1;
}
Явное присутствие:
var m = GetProto();
if (m.HasFoo) {
// Очистить поле:
m.ClearFoo();
} else {
// Поле отсутствует, поэтому установить его.
m.Foo = 1;
}
Пример на Go
Неявное присутствие:
m := GetProto()
if m.Foo != 0 {
// "Очистить" поле:
m.Foo = 0
} else {
// Значение по умолчанию: поле могло отсутствовать.
m.Foo = 1
}
Явное присутствие:
m := GetProto()
if m.Foo != nil {
// Очистить поле:
m.Foo = nil
} else {
// Поле отсутствует, поэтому установить его.
m.Foo = proto.Int32(1)
}
Пример на Java
Эти примеры используют Builder для демонстрации очистки. Простая проверка присутствия
и получение значений из Builder следует тому же API, что и тип сообщения.
Неявное присутствие:
Msg.Builder m = GetProto().toBuilder();
if (m.getFoo() != 0) {
// "Очистить" поле:
m.setFoo(0);
} else {
// Значение по умолчанию: поле могло отсутствовать.
m.setFoo(1);
}
Явное присутствие:
Msg.Builder m = GetProto().toBuilder();
if (m.hasFoo()) {
// Очистить поле:
m.clearFoo()
} else {
// Поле отсутствует, поэтому установить его.
m.setFoo(1);
}
Пример на Python
Неявное присутствие:
m = example.Msg()
if m.foo != 0:
# "Очистить" поле:
m.foo = 0
else:
# Значение по умолчанию: поле могло отсутствовать.
m.foo = 1
Явное присутствие:
m = example.Msg()
if m.HasField('foo'):
# Очистить поле:
m.ClearField('foo')
else:
# Поле отсутствует, поэтому установить его.
m.foo = 1
Пример на Ruby
Неявное присутствие:
m = Msg.new
if m.foo != 0
# "Очистить" поле:
m.foo = 0
else
# Значение по умолчанию: поле могло отсутствовать.
m.foo = 1
end
Явное присутствие:
m = Msg.new
if m.has_foo?
# Очистить поле:
m.clear_foo
else
# Поле отсутствует, поэтому установить его.
m.foo = 1
end
Пример на Javascript
Неявное присутствие:
var m = new Msg();
if (m.getFoo() != 0) {
// "Очистить" поле:
m.setFoo(0);
} else {
// Значение по умолчанию: поле могло отсутствовать.
m.setFoo(1);
}
Явное присутствие:
var m = new Msg();
if (m.hasFoo()) {
// Очистить поле:
m.clearFoo()
} else {
// Поле отсутствует, поэтому установить его.
m.setFoo(1);
}
Пример на Objective-C
Неявное присутствие:
Msg *m = [Msg message];
if (m.foo != 0) {
// "Очистить" поле:
m.foo = 0;
} else {
// Значение по умолчанию: поле могло отсутствовать.
m.foo = 1;
}
Явное присутствие:
Msg *m = [Msg message];
if ([m hasFoo]) {
// Очистить поле:
[m clearFoo];
} else {
// Поле отсутствует, поэтому установить его.
m.foo = 1;
}
Шпаргалка
Proto2:
Отслеживается ли присутствие поля?
| Тип поля | Отслеживается? |
|---|---|
| Сингулярное поле | да |
| Сингулярное поле сообщения | да |
| Поле в oneof | да |
| Повторяющееся поле & карта | нет |
Proto3:
Отслеживается ли присутствие поля?
| Тип поля | Отслеживается? |
|---|---|
| Другое сингулярное поле | если определено как optional |
| Сингулярное поле сообщения | да |
| Поле в oneof | да |
| Повторяющееся поле & карта | нет |
Редакция 2023:
Отслеживается ли присутствие поля?
| Тип поля (в порядке убывания приоритета) | Отслеживается? |
|---|---|
| Повторяющееся поле & карта | нет |
| Поля сообщений и Oneof | да |
Другие сингулярные поля, если features.field_presence установлено в IMPLICIT | нет |
| Все остальные поля | да |
Сериализация Proto не является канонической
Объясняет, как работает сериализация и почему она не является канонической.
Многие хотят, чтобы сериализованный proto канонически представлял содержимое этого proto. Варианты использования включают:
- использование сериализованного proto в качестве ключа в хэш-таблице
- снятие отпечатка (fingerprint) или контрольной суммы сериализованного proto
- сравнение сериализованных полезных нагрузок как способ проверки равенства сообщений
К сожалению, сериализация protobuf не является (и не может быть) канонической. Существует несколько заметных исключений, таких как MapReduce, но в целом вам следует воспринимать сериализацию proto как нестабильную. Эта страница объясняет, почему.
Детерминированность — это не каноничность
Детерминированная сериализация не является канонической. Сериализатор может генерировать разный вывод по многим причинам, включая, но не ограничиваясь, следующими вариациями:
- Схема protobuf изменяется любым способом.
- Приложение, которое собирается, изменяется любым способом.
- Бинарный файл собирается с разными флагами (например, opt vs. debug).
- Библиотека protobuf обновляется.
Это означает, что хэши сериализованных protobuf хрупки и нестабильны во времени или пространстве.
Существует много причин, по которым сериализованный вывод может измениться. Вышеуказанный список не является исчерпывающим. Некоторые из них — это inherent difficulties (внутренние сложности) в проблемном пространстве, которые сделали бы гарантирование канонической сериализации неэффективным или невозможным, даже если бы мы этого захотели. Другие — это вещи, которые мы намеренно оставляем неопределенными, чтобы предоставить возможности для оптимизации.
Внутренние барьеры для стабильной сериализации
Объекты Protobuf сохраняют неизвестные поля для обеспечения прямой и обратной совместимости. Обработка неизвестных полей является основным препятствием для канонической сериализации.
В бинарном формате (wire format) поля bytes и вложенные подсообщения используют один и тот же тип в бинарном формате (wire type). Эта неоднозначность делает невозможным корректную канонизацию сообщений, хранящихся в наборе неизвестных полей. Поскольку одни и те же содержимые могут быть либо тем, либо другим, невозможно узнать, следует ли обрабатывать это как сообщение и рекурсивно спускаться вниз или нет.
Для эффективности реализации обычно сериализуют неизвестные поля после известных полей. Однако каноническая сериализация потребовала бы перемежения неизвестных полей с известными полями в соответствии с номерами полей. Это наложило бы значительные затраты на эффективность и размер кода на всех пользователей, даже на тех, которые не требуют этой функции.
Вещи, намеренно оставленные неопределенными
Даже если бы каноническая сериализация была осуществима (то есть, если бы мы могли решить проблему неизвестных полей), мы намеренно оставляем порядок сериализации неопределенным, чтобы позволить больше возможностей для оптимизации:
- Если мы можем доказать, что поле никогда не используется в бинарном файле, мы можем полностью удалить его из схемы и обрабатывать его как неизвестное поле. Это экономит значительный размер кода и такты процессора.
- Могут существовать возможности для оптимизации путем сериализации векторов одного и того же поля вместе, даже если это нарушит порядок номеров полей.
Чтобы оставить место для подобных оптимизаций, мы хотим намеренно перемешивать порядок полей в некоторых конфигурациях, чтобы приложения не стали неадекватно зависеть от порядка полей.
Десериализация представлений Proto для отладки
Как логировать отладочную информацию в Protocol Buffers.
Начиная с версии 30.x, Protobuf API DebugString (Message::DebugString,
Message::ShortDebugString, Message::Utf8DebugString), дополнительные Protobuf
API (proto2::ShortFormat, proto2::Utf8Format), строковые функции Abseil
(такие как absl::StrCat, absl::StrFormat, absl::StrAppend и
absl::Substitute) и API логирования Abseil начнут автоматически преобразовывать
proto-аргументы в новый формат отладки
. Смотрите связанное объявление
здесь.
В отличие от формата вывода Protobuf DebugString, новый формат отладки автоматически редактирует конфиденциальные поля, заменяя их значения строкой "[REDACTED]" (без кавычек). Кроме того, чтобы гарантировать, что этот новый формат вывода не может быть десериализован парсерами Protobuf TextFormat, независимо от того, содержит ли базовый proto поля SPII (конфиденциальной информации), мы добавляем набор рандомизированных ссылок, ведущих на эту статью, и последовательность пробелов случайной длины. Новый формат отладки выглядит следующим образом:
goo.gle/debugstr
spii_field: [REDACTED]
normal_field: "value"
Обратите внимание, что новый формат отладки отличается от формата вывода DebugString только двумя способами:
- Префикс URL
- Значения полей SPII заменяются на "[REDACTED]" (без кавычек)
Новый формат отладки никогда не удаляет имена полей; он только заменяет значение на "[REDACTED]", если поле считается конфиденциальным. Если вы не видите определенные поля в выводе, это потому, что эти поля не установлены в proto.
Совет: Если вы видите только URL и больше ничего, ваш proto пуст!
Почему здесь этот URL?
Мы хотим убедиться, что никто не десериализует человеко-читаемые представления
сообщения protobuf, предназначенные для отладки системы человеком. Исторически
сложилось, что .DebugString() и TextFormat были взаимозаменяемыми, и существующие системы используют
DebugString для передачи и хранения данных.
Мы хотим убедиться, что конфиденциальные данные не попадают в логи случайно. Поэтому мы прозрачно редактируем некоторые значения полей из сообщений protobuf перед преобразованием их в строку ("[REDACTED]"). Это снижает риски безопасности и конфиденциальности при случайном логировании, но рискует потерей данных, если другие системы десериализуют ваше сообщение. Чтобы устранить этот риск, мы намеренно разделяем машиночитаемый TextFormat и человеко-читаемый формат отладки, который следует использовать в сообщениях логов.
Почему в моей веб-странице есть ссылки? Почему мой код выдает это новое "представление для отладки"?
Это сделано намеренно, чтобы сделать "представление для отладки" ваших protobuf (создаваемое, например, при логировании) несовместимым с TextFormat. Мы хотим помешать кому-либо зависеть от механизмов отладки для передачи данных между программами. Исторически формат отладки (генерируемый API DebugString) и TextFormat некорректно использовались взаимозаменяемо. Мы надеемся, что это преднамеренное изменение предотвратит это в будущем.
Мы намеренно выбрали ссылку вместо менее заметных изменений формата, чтобы
получить возможность предоставить контекст. Это может выделяться в пользовательских интерфейсах, например, если вы
отображаете статусную информацию в таблице на веб-странице. Вы можете использовать
TextFormat::PrintToString вместо этого, который не будет редактировать какую-либо информацию и
сохранит форматирование. Однако используйте этот API с осторожностью — встроенных защит нет.
Как правило, если вы записываете данные в отладочные логи или
генерируете статусные сообщения, вам следует продолжать использовать Формат отладки со
ссылкой. Даже если вы в настоящее время не обрабатываете конфиденциальные данные, помните, что
системы могут меняться, а код повторно используется.
Я попытался преобразовать это сообщение в TextFormat, но заметил, что формат меняется каждый раз при перезапуске процесса.
Это сделано намеренно. Не пытайтесь разобрать вывод этого формата отладки. Мы оставляем за собой право изменять синтаксис без предварительного уведомления. Синтаксис формата отладки случайным образом меняется для каждого процесса, чтобы предотвратить непреднамеренные зависимости. Если синтаксическое изменение в формате отладки сломает вашу систему, скорее всего, вам следует использовать API TextFormat, а не представление proto для отладки.
Часто задаваемые вопросы
Могу ли я просто использовать TextFormat везде?
Не используйте TextFormat для создания сообщений логов. Это обойдет все встроенные защиты, и вы рискуете случайно залогировать конфиденциальную информацию. Даже если ваши системы в настоящее время не обрабатывают никаких конфиденциальных данных, это может измениться в будущем.
Различайте логи и информацию, предназначенную для дальнейшей обработки другими системами, используя либо представление для отладки, либо TextFormat, в зависимости от ситуации.
Я хочу писать файлы конфигурации, которые должны быть и человеко-читаемыми, и машино-читаемыми
Для этого случая использования вы можете явно использовать TextFormat. Вы несете ответственность за то, чтобы ваши файлы конфигурации не содержали никакой PII (персонально идентифицируемой информации).
Я пишу модульный тест и хочу сравнить DebugString в утверждении теста
Если вы хотите сравнить значения protobuf, используйте MessageDifferencer, как в
следующем примере:
using google::protobuf::util::MessageDifferencer;
...
MessageDifferencer diff;
...
diff.Compare(foo, bar);
Помимо игнорирования различий в форматировании и порядке полей, вы также получите лучшие сообщения об ошибках.
Editions
Обзор редакций Protobuf
Редакции Protobuf заменяют обозначения proto2 и proto3, которые мы использовали
для Protocol Buffers. Вместо добавления syntax = "proto2" или syntax = "proto3" в начале файлов определений proto вы используете номер редакции, например
edition = "2024", чтобы указать поведение по умолчанию для вашего файла.
Редакции позволяют языку развиваться постепенно с течением времени.
Вместо жестко заданного поведения, которое было в старых версиях, редакции представляют собой набор функций (features) со значением (поведением) по умолчанию для каждой функции. Функции — это опции на уровне файла, сообщения, поля, перечисления и т.д., которые определяют поведение protoc, генераторов кода и сред выполнения protobuf. Вы можете явно переопределить поведение на этих различных уровнях (файл, сообщение, поле, ...), когда ваши потребности не совпадают с поведением по умолчанию для выбранной вами редакции. Вы также можете переопределять свои собственные переопределения. Раздел ниже в этой теме о лексической области видимости более подробно освещает этот вопрос.
ПРИМЕЧАНИЕ: Последняя выпущенная редакция — 2024.
Жизненный цикл функции
Редакции предоставляют фундаментальные этапы для жизненного цикла функции. У функций есть ожидаемый жизненный цикл: введение, изменение поведения по умолчанию, устаревание и последующее удаление. Например:
-
Редакция 2031 создает
feature.amazing_new_featureсо значением по умолчаниюfalse. Это значение сохраняет то же поведение, что и все предыдущие редакции. То есть по умолчанию воздействия нет. Не все новые функции будут по умолчанию иметь no-op опцию, но для этого примераamazing_new_featureимеет. -
Разработчики обновляют свои .proto файлы до
edition = "2031". -
Более поздняя редакция, например редакция 2033, меняет значение по умолчанию для
feature.amazing_new_featureсfalseнаtrue. Это желаемое поведение для всех protobuf, и причина, по которой команда protobuf создала эту функцию.Использование инструмента Prototiller для миграции более ранних версий proto-файлов в редакцию 2033 добавляет явные записи
feature.amazing_new_feature = falseпо мере необходимости, чтобы продолжить сохранять предыдущее поведение. Разработчики удаляют эти вновь добавленные настройки, когда хотят, чтобы новое поведение применялось к их .proto файлам.
-
В какой-то момент
feature.amazing_new_featureпомечается как устаревшая в одной редакции и удаляется в более поздней.Когда функция удаляется, генераторы кода для этого поведения и библиотеки времени выполнения, которые его поддерживают, также могут быть удалены. Сроки будут щедрыми, однако. Следуя примеру на предыдущих этапах жизненного цикла, устаревание может произойти в редакции 2034, но удаление не произойдет до редакции 2036, примерно двумя годами позже. Удаление функции всегда будет инициировать увеличение мажорной версии.
У вас будет полное время миграции Google плюс период устаревания для обновления вашего кода.
Предыдущий пример жизненного цикла использовал булевы значения для функций, но
функции также могут использовать перечисления. Например, features.field_presence имеет значения
LEGACY_REQUIRED, EXPLICIT и IMPLICIT.
Миграция на редакции Protobuf
Редакции не нарушат работу существующих бинарных файлов и не изменят бинарный, текстовый или JSON формат сериализации сообщения. Редакция 2023 была настолько минимально разрушительной, насколько это возможно. Она установила базовый уровень и объединила определения proto2 и proto3 в новый единый формат определений.
По мере выпуска новых редакций поведение по умолчанию для функций может изменяться. Вы можете поручить Prototiller выполнить no-op преобразование вашего .proto файла или вы можете выбрать принятие некоторых или всех новых поведений. Планируется, что редакции будут выпускаться примерно раз в год.
С Proto2 на редакции
В этом разделе показан файл proto2 и то, как он может выглядеть после запуска инструмента Prototiller для изменения файлов определений на использование синтаксиса редакций Protobuf.
Синтаксис Proto2
// файл proto2
syntax = "proto2";
package com.example;
message Player {
// в proto2 optional поля имеют явное присутствие
optional string name = 1 [default = "N/A"];
// proto2 все еще поддерживает проблемное правило поля "required"
required int32 id = 2;
// в proto2 по умолчанию это не упаковано (packed)
repeated int32 scores = 3;
enum Handed {
HANDED_UNSPECIFIED = 0;
HANDED_LEFT = 1;
HANDED_RIGHT = 2;
HANDED_AMBIDEXTROUS = 3;
}
// в proto2 перечисления закрыты (closed)
optional Handed handed = 4;
reserved "gender";
}
Синтаксис редакций
// Версия файла proto2 в редакциях
edition = "2024";
package com.example;
option features.utf8_validation = NONE;
option features.enforce_naming_style = STYLE_LEGACY;
option features.default_symbol_visibility = EXPORT_ALL;
// Устанавливает поведение по умолчанию для строк C++
option features.(pb.cpp).string_type = STRING;
message Player {
// поля имеют явное присутствие, поэтому явная настройка не нужна
string name = 1 [default = "N/A"];
// чтобы соответствовать поведению proto2, LEGACY_REQUIRED установлен на уровне поля
int32 id = 2 [features.field_presence = LEGACY_REQUIRED];
// чтобы соответствовать поведению proto2, EXPANDED установлен на уровне поля
repeated int32 scores = 3 [features.repeated_field_encoding = EXPANDED];
export enum Handed {
// это переопределяет поведение редакций по умолчанию, которое OPEN
option features.enum_type = CLOSED;
HANDED_UNSPECIFIED = 0;
HANDED_LEFT = 1;
HANDED_RIGHT = 2;
HANDED_AMBIDEXTROUS = 3;
}
Handed handed = 4;
reserved gender;
}
С Proto3 на редакции
В этом разделе показан файл proto3 и то, как он может выглядеть после запуска инструмента Prototiller для изменения файлов определений на использование синтаксиса редакций Protobuf.
Синтаксис Proto3
// файл proto3
syntax = "proto3";
package com.example;
message Player {
// в proto3 optional поля имеют явное присутствие
optional string name = 1;
// в proto3 правило поля не указано, по умолчанию неявное присутствие
int32 id = 2;
// в proto3 по умолчанию это упаковано (packed)
repeated int32 scores = 3;
enum Handed {
HANDED_UNSPECIFIED = 0;
HANDED_LEFT = 1;
HANDED_RIGHT = 2;
HANDED_AMBIDEXTROUS = 3;
}
// в proto3 перечисления открыты (open)
optional Handed handed = 4;
reserved "gender";
}
Синтаксис редакций
// Версия файла proto3 в редакциях
edition = "2024";
package com.example;
option features.utf8_validation = NONE;
option features.enforce_naming_style = STYLE_LEGACY;
option features.default_symbol_visibility = EXPORT_ALL;
// Устанавливает поведение по умолчанию для строк C++
option features.(pb.cpp).string_type = STRING;
message Player {
// поля имеют явное присутствие, поэтому явная настройка не нужна
string name = 1 [default = "N/A"];
// чтобы соответствовать поведению proto3, IMPLICIT установлен на уровне поля
int32 id = 2 [features.field_presence = IMPLICIT];
// PACKED является состоянием по умолчанию и приведено только для иллюстрации
repeated int32 scores = 3 [features.repeated_field_encoding = PACKED];
export enum Handed {
HANDED_UNSPECIFIED = 0;
HANDED_LEFT = 1;
HANDED_RIGHT = 2;
HANDED_AMBIDEXTROUS = 3;
}
Handed handed = 4;
reserved gender;
}
Лексическая область видимости
Синтаксис редакций поддерживает лексическую область видимости с разрешенным для каждой функции списком целей. Например, в Редакции 2023 функции могут быть указаны только на уровне файла или на самом низком уровне детализации. Реализация лексической области видимости позволяет вам установить поведение по умолчанию для функции во всем файле, а затем переопределить это поведение на уровне сообщения, поля, перечисления, значения перечисления, oneof, сервиса или метода. Настройки, сделанные на более высоком уровне (файл, сообщение), применяются, когда никакая настройка не сделана в той же области видимости (поле, значение перечисления). Любые функции, не установленные явно, соответствуют поведению, определенному в версии редакции, используемой для .proto файла.
Следующий пример кода показывает некоторые функции, установленные на уровне файла, поля и перечисления.
edition = "2024";
option features.enum_type = CLOSED;
message Person {
string name = 1;
int32 id = 2 [features.field_presence = IMPLICIT];
enum Pay_Type {
PAY_TYPE_UNSPECIFIED = 1;
PAY_TYPE_SALARY = 2;
PAY_TYPE_HOURLY = 3;
}
enum Employment {
option features.enum_type = OPEN;
EMPLOYMENT_UNSPECIFIED = 0;
EMPLOYMENT_FULLTIME = 1;
EMPLOYMENT_PARTTIME = 2;
}
Employment employment = 4;
}
В предыдущем примере функция присутствия установлена в IMPLICIT; она бы
по умолчанию была EXPLICIT, если бы не была установлена. enum Pay_Type будет CLOSED,
так как он применяет настройку на уровне файла. Однако enum Employment будет
OPEN, так как он установлен внутри перечисления.
Prototiller
Когда инструмент Prototiller будет запущен, мы предоставим как руководство по миграции, так и инструменты миграции для облегчения перехода на редакции и между ними. Инструмент позволит вам:
- конвертировать файлы определений proto2 и proto3 в новый синтаксис редакций, в больших масштабах
- мигрировать файлы с одной редакции на другую
- манипулировать proto-файлами другими способами
Обратная совместимость
Мы создаем редакции Protobuf максимально ненарушающими. Например, вы можете импортировать определения proto2 и proto3 в файлы определений на основе редакций, и наоборот:
// файл myproject/foo.proto
syntax = "proto2";
enum Employment {
EMPLOYMENT_UNSPECIFIED = 0;
EMPLOYMENT_FULLTIME = 1;
EMPLOYMENT_PARTTIME = 2;
}
// файл myproject/edition.proto
edition = "2024";
import "myproject/foo.proto";
Хотя сгенерированный код изменяется при переходе с proto2 или proto3 на редакции, бинарный формат (wire format) не меняется. Вы все равно сможете получать доступ к файлам данных proto2 и proto3 или потокам файлов, используя ваши proto-определения с синтаксисом редакций.
Изменения грамматики
В редакциях по сравнению с proto2 и proto3 есть некоторые изменения грамматики.
Описание синтаксиса
Вместо элемента syntax вы используете элемент edition:
syntax = "proto2";
syntax = "proto3";
edition = "2028";
Зарезервированные имена
Вам больше не нужно заключать имена полей и значений перечислений в кавычки при их резервировании:
reserved foo, bar;
Синтаксис Group
Синтаксис Group, доступный в proto2, удален в редакциях. Специальный
бинарный формат, который использовали groups, все еще доступен через использование кодировки сообщений DELIMITED.
Метка Required
Метка required, доступная только в proto2, недоступна в редакциях. Базовая
функциональность все еще доступна
через использование features.field_presence=LEGACY_REQUIRED.
import option
Редакция 2024 добавила поддержку импорта опций с использованием синтаксиса import option.
Импорты опций должны следовать после любых других операторов import.
В отличие от обычных операторов import, import option импортирует только пользовательские опции,
определенные в .proto файле, без импорта других символов.
Это означает, что сообщения и перечисления исключены из импорта опций. В
следующем примере сообщение Bar не может быть использовано как тип поля в
foo.proto, но опции с типом Bar все еще могут быть установлены.
// bar.proto
edition = "2024";
import "google/protobuf/descriptor.proto";
message Bar {
bool bar = 1;
}
extend proto2.FileOptions {
bool file_opt1 = 5000;
Bar file_opt2 = 5001;
}
// foo.proto:
edition = "2024";
import option "bar.proto";
option (file_opt1) = true;
option (file_opt2) = {bar: true};
message Foo {
// Bar bar = 1; // Это не разрешено
}
Импорты опций не требуют сгенерированного кода для своих символов и, следовательно, должны
указываться как option_deps в proto_library вместо deps. Это позволяет избежать
генерации недостижимого кода.
proto_library(
name = "foo",
srcs = ["foo.proto"],
option_deps = [":custom_option_proto"]
)
Импорты опций и option_deps настоятельно рекомендуются при импорте
функций языка protobuf и других пользовательских опций, чтобы избежать генерации
ненужного кода.
Это заменяет import weak, который был удален в Редакции 2024.
Ключевые слова export / local
Ключевые слова export и local были добавлены в Редакции 2024 в качестве модификаторов
видимости символов, которые можно импортировать, вместо поведения по умолчанию, заданного
features.default_symbol_visibility.
Это управляет тем, какие символы могут быть импортированы из других proto-файлов, но не влияет на генерацию кода.
В Редакции 2024 они могут быть установлены для всех символов message и enum
по умолчанию. Однако некоторые значения функции default_symbol_visibility дополнительно
ограничивают, какие символы можно экспортировать.
Пример:
// Символы верхнего уровня экспортируются по умолчанию в Редакции 2024
message LocalMessage {
int32 baz = 1;
// Вложенные символы являются локальными по умолчанию в Редакции 2024; применение ключевого слова `export`
// переопределяет это
export enum ExportedNestedEnum {
UNKNOWN_EXPORTED_NESTED_ENUM_VALUE = 0;
}
}
// Ключевое слово `local` переопределяет поведение по умолчанию для экспорта сообщений
local message AnotherMessage {
int32 foo = 1;
}
import weak и опция поля Weak
Начиная с Редакции 2024, слабый импорт (weak imports) больше не разрешен.
Если вы ранее полагались на import weak для объявления "слабой
зависимости"—чтобы импортировать пользовательские опции без сгенерированного кода для C++ и
Go—вам следует вместо этого перейти на использование import option.
См. подробности в разделе import option.
Опция поля ctype
Начиная с Редакции 2024, опция поля ctype больше не разрешена. Используйте вместо нее
функцию string_type.
См. подробности
features.(pb.cpp).string_type.
Опция файла java_multiple_files
Начиная с Редакции 2024, опция файла java_multiple_files больше недоступна.
Вместо нее используйте функцию Java
features.(pb.java).nest_in_file_class.
Настройки функций для редакций
Функции редакций Protobuf и как они влияют на поведение protobuf.
В этой теме представлен обзор функций, включенных в выпущенные версии редакций. Функции последующих редакций будут добавлены в эту тему. Мы анонсируем новые редакции в разделе Новости.
Прежде чем настраивать параметры функций в вашем новом содержимом определения схемы, убедитесь, что вы понимаете, зачем вы их используете. Избегайте карго-культа (cargo-culting) с функциями.
Prototiller
Prototiller — это инструмент командной строки, который обновляет файлы конфигурации схемы proto между версиями синтаксиса и редакциями. Он еще не выпущен, но на него ссылаются throughout этой теме.
Функции
В следующих разделах приведены все поведения, которые можно настроить с помощью функций в редакциях. Сохранение поведения Proto2 или Proto3 показывает, как переопределить поведения по умолчанию, чтобы ваши файлы определений proto вели себя как файлы proto2 или proto3. Для получения дополнительной информации о том, как редакции и функции вместе работают для установки поведения, см. Обзор редакций Protobuf.
Настройки функций применяются на разных уровнях:
На уровне файла: Эти настройки применяются ко всем элементам (сообщениям, полям, перечислениям и т.д.), которые не имеют переопределяющей настройки.
Не вложенные: Сообщения, перечисления и сервисы могут переопределять настройки, сделанные на уровне файла. Они применяются ко всему внутри них (полям сообщений, значениям перечислений), что не переопределено, но не применяются к другим параллельным сообщениям и перечислениям.
Вложенные: Oneof, сообщения и перечисления могут переопределять настройки из сообщения, в которое они вложены.
Самый низкий уровень: Поля, расширения, значения перечислений, диапазоны расширений и методы — это самый низкий уровень, на котором вы можете переопределять настройки.
Каждый из следующих разделов содержит комментарий, в котором указано, к какой области видимости функция может быть применена. Следующий пример показывает mock-функцию, примененную к каждой области видимости:
edition = "2024";
// Определение на уровне файла
option features.bar = BAZ;
enum Foo {
// Определение на уровне перечисления (не вложенная область)
option features.bar = QUX;
A = 1;
B = 2;
}
message Corge {
// Определение на уровне сообщения (не вложенная область)
option features.bar = QUUX;
message Garply {
// Определение на уровне сообщения (вложенная область)
option features.bar = WALDO;
string id = 1;
}
// Определение на уровне поля (самый низкий уровень)
Foo A = 1 [features.bar = GRAULT];
}
В этом примере настройка "GRAULT" в определении функции на самом низком уровне
переопределяет настройку "QUUX" в не вложенной области. И внутри
сообщения Garply "WALDO" переопределяет "QUUX".
features.default_symbol_visibility
Эта функция позволяет устанавливать видимость по умолчанию для сообщений и перечислений, делая их доступными или недоступными при импорте другими protobuf. Использование этой функции уменьшит количество мертвых символов, чтобы создавать меньшие бинарные файлы.
В дополнение к установке значений по умолчанию для всего файла, вы можете использовать ключевые слова local
и export для установки поведения для каждого поля. Подробнее об этом читайте в разделе
Ключевые слова export / local.
Доступные значения:
EXPORT_ALL: Это значение по умолчанию до Редакции 2024. Все сообщения и перечисления экспортируются по умолчанию.EXPORT_TOP_LEVEL: Все символы верхнего уровня по умолчанию экспортируются; вложенные по умолчанию являются локальными.LOCAL_ALL: Все символы по умолчанию являются локальными.STRICT: Все символы локальны по умолчанию. Вложенные типы не могут быть экспортированы, за исключением особого случая дляmessage { enum {} reserved 0 to max; }. Это станет значением по умолчанию в будущей редакции.
Применимо к следующей области видимости: Enum, Message
Добавлено в: Редакция 2024
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | EXPORT_TOP_LEVEL |
| 2023 | EXPORT_ALL |
| proto3 | EXPORT_ALL |
| proto2 | EXPORT_ALL |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример показывает, как вы можете применить функцию к элементам в ваших файлах определений схемы proto:
// foo.proto
edition = "2024";
// Видимость символов по умолчанию EXPORT_TOP_LEVEL. Установка
// default_symbol_visibility переопределяет эти значения по умолчанию
option features.default_symbol_visibility = LOCAL_ALL;
// Символы верхнего уровня экспортируются по умолчанию в Редакции 2024; применение ключевого слова local
// переопределяет это
export message LocalMessage {
int32 baz = 1;
// Вложенные символы являются локальными по умолчанию в Редакции 2024; применение ключевого слова export
// переопределяет это
enum ExportedNestedEnum {
UNKNOWN_EXPORTED_NESTED_ENUM_VALUE = 0;
}
}
// bar.proto
edition = "2024";
import "foo.proto";
message ImportedMessage {
// Следующее допустимо, потому что импортированное сообщение явно переопределяет
// настройку видимости в foo.proto
LocalMessage bar = 1;
// Следующее недопустимо, потому что default_symbol_visibility установлен в
// `LOCAL_ALL`
// LocalMessage.ExportedNestedEnum qux = 2;
}
features.enforce_naming_style
Введена в Редакции 2024, эта функция включает строгое соблюдение стиля именования, как определено в руководстве по стилю, чтобы гарантировать возможность кругового обмена (round-trippable) protobuf по умолчанию, с возможностью отказаться от этого с помощью значения функции.
Доступные значения:
STYLE2024: Обеспечивает строгое соблюдение руководства по стилю для именования.STYLE_LEGACY: Применяет уровень соблюдения руководства по стилю, действовавший до Редакции 2024.
Применимо к следующей области видимости: File
Добавлено в: Редакция 2024
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | STYLE2024 |
| 2023 | STYLE_LEGACY |
| proto3 | STYLE_LEGACY |
| proto2 | STYLE_LEGACY |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл Редакции 2023:
Редакция 2023 по умолчанию использует STYLE_LEGACY, поэтому некорректное имя поля допустимо:
edition = "2023";
message Foo {
// Некорректное имя поля не является проблемой
int64 bar_1 = 1;
}
Редакция 2024 по умолчанию использует STYLE2024, поэтому требуется переопределение, чтобы сохранить
некорректное имя поля:
edition = "2024";
// Чтобы сохранить некорректное имя поля, переопределите настройку STYLE2024
option features.enforce_naming_style = STYLE_LEGACY;
message Foo {
int64 bar_1 = 1;
}
features.enum_type
Эта функция устанавливает поведение для обработки значений перечислений, которые не содержатся в определенном наборе. См. Поведение перечислений для получения дополнительной информации об открытых и закрытых перечислениях.
Эта функция не влияет на файлы proto3, поэтому в этом разделе нет примеров "до" и "после" для файла proto3.
Доступные значения:
CLOSED:Закрытые перечисления сохраняют значения перечислений, находящиеся вне диапазона, в наборе неизвестных полей.OPEN:Открытые перечисления разбирают значения вне диапазона непосредственно в их поля.
Применимо к следующим областям видимости: File, Enum
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | OPEN |
| 2023 | OPEN |
| proto3 | OPEN |
| proto2 | CLOSED |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
enum Foo {
A = 2;
B = 4;
C = 6;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
enum Foo {
// Установка функции enum_type переопределяет перечисление OPEN по умолчанию
option features.enum_type = CLOSED;
A = 2;
B = 4;
C = 6;
}
features.field_presence
Эта функция устанавливает поведение для отслеживания присутствия поля, то есть концепции имеет ли поле protobuf значение.
Доступные значения:
LEGACY_REQUIRED: Поле является обязательным для разбора и сериализации. Любое явно установленное значение сериализуется в бинарный формат (даже если оно совпадает со значением по умолчанию).EXPLICIT: Поле имеет явное отслеживание присутствия. Любое явно установленное значение сериализуется в бинарный формат (даже если оно совпадает со значением по умолчанию). Для сингулярных примитивных полей генерируются функцииhas_*для полей, установленных вEXPLICIT.IMPLICIT: Поле не имеет отслеживания присутствия. Значение по умолчанию не сериализуется в бинарный формат (даже если оно явно установлено). Функцииhas_*не генерируются для полей, установленных вIMPLICIT.
Применимо к следующим областям видимости: File, Field
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | EXPLICIT |
| 2023 | EXPLICIT |
| proto3 | IMPLICIT* |
| proto2 | EXPLICIT |
* proto3 использует IMPLICIT, если только у поля нет метки optional, в этом случае
оно ведет себя как EXPLICIT. См.
Присутствие в API Proto3
для получения дополнительной информации.
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
message Foo {
required int32 x = 1;
optional int32 y = 2;
repeated int32 z = 3;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
message Foo {
// Установка функции field_presence сохраняет поведение required из proto2
int32 x = 1 [features.field_presence = LEGACY_REQUIRED];
int32 y = 2;
repeated int32 z = 3;
}
Следующий пример показывает файл proto3:
syntax = "proto3";
message Bar {
int32 x = 1;
optional int32 y = 2;
repeated int32 z = 3;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
// Установка функции field_presence на уровне файла соответствует неявному значению по умолчанию proto3
option features.field_presence = IMPLICIT;
message Bar {
int32 x = 1;
// Установка field_presence здесь сохраняет явное состояние, которое поле proto3
// имеет из-за синтаксиса optional
int32 y = 2 [features.field_presence = EXPLICIT];
repeated int32 z = 3;
}
Обратите внимание, что метки required и optional больше не существуют в Редакциях, так как
соответствующее поведение устанавливается явно с помощью функции field_presence.
features.json_format
Эта функция устанавливает поведение для разбора и сериализации JSON.
Эта функция не влияет на файлы proto3, поэтому в этом разделе нет примеров "до" и "после" для файла proto3. Поведение редакций соответствует поведению в proto3.
Доступные значения:
ALLOW: Среда выполнения должна разрешать разбор и сериализацию JSON. Проверки применяются на уровне proto, чтобы убедиться, что существует четко определенное отображение в JSON.LEGACY_BEST_EFFORT: Среда выполнения делает все возможное для разбора и сериализации JSON. Разрешены определенные protobuf, которые могут привести к неопределенному поведению во время выполнения (например, отображения многие:1 или 1:многие).
Применимо к следующим областям видимости: File, Message, Enum
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | ALLOW |
| 2023 | ALLOW |
| proto3 | ALLOW |
| proto2 | LEGACY_BEST_EFFORT |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
message Foo {
// Только предупреждение
string bar = 1;
string bar_ = 2;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
option features.json_format = LEGACY_BEST_EFFORT;
message Foo {
string bar = 1;
string bar_ = 2;
}
features.message_encoding
Эта функция устанавливает поведение для кодирования полей при сериализации.
Эта функция не влияет на файлы proto3, поэтому в этом разделе нет примеров "до" и "после" для файла proto3.
В зависимости от языка, поля, которые являются "group-подобными", могут иметь неожиданное использование заглавных букв в сгенерированном коде и в текстовом формате, чтобы обеспечить обратную совместимость с proto2. Поля сообщений являются "group-подобными", если выполняются все следующие условия:
- Указано кодирование сообщения
DELIMITED - Тип сообщения определен в той же области видимости, что и поле
- Имя поля точно соответствует имени типа в нижнем регистре
Доступные значения:
LENGTH_PREFIXED: Поля кодируются с использованием типа бинарного формата LEN, описанного в Структура сообщения.DELIMITED: Поля типа message кодируются как groups.
Применимо к следующим областям видимости: File, Field
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | LENGTH_PREFIXED |
| 2023 | LENGTH_PREFIXED |
| proto3 | LENGTH_PREFIXED |
| proto2 | LENGTH_PREFIXED |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
message Foo {
group Bar = 1 {
optional int32 x = 1;
repeated int32 y = 2;
}
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
message Foo {
message Bar {
int32 x = 1;
repeated int32 y = 2;
}
Bar bar = 1 [features.message_encoding = DELIMITED];
}
features.repeated_field_encoding
Эта функция — это то, во что была мигрирована опция
packed
для repeated полей в proto2/proto3 в Редакциях.
Доступные значения:
PACKED:Repeatedполя примитивного типа кодируются как одна запись LEN, которая содержит каждый элемент, объединенный вместе.EXPANDED:Repeatedполя кодируются с номером поля для каждого значения.
Применимо к следующим областям видимости: File, Field
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | PACKED |
| 2023 | PACKED |
| proto3 | PACKED |
| proto2 | EXPANDED |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
message Foo {
repeated int32 bar = 6 [packed=true];
repeated int32 baz = 7;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
option features.repeated_field_encoding = EXPANDED;
message Foo {
repeated int32 bar = 6 [features.repeated_field_encoding=PACKED];
repeated int32 baz = 7;
}
Следующий пример показывает файл proto3:
syntax = "proto3";
message Foo {
repeated int32 bar = 6;
repeated int32 baz = 7 [packed=false];
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
message Foo {
repeated int32 bar = 6;
repeated int32 baz = 7 [features.repeated_field_encoding=EXPANDED];
}
features.utf8_validation
Эта функция устанавливает, как проверяются строки. Она применяется ко всем языкам, кроме
тех, для которых есть специфичная для языка функция utf8_validation, которая переопределяет ее.
См. features.(pb.java).utf8_validation для
специфичной для Java функции.
Эта функция не влияет на файлы proto3, поэтому в этом разделе нет примеров "до" и "после" для файла proto3.
Доступные значения:
VERIFY: Среда выполнения должна проверять UTF-8. Это поведение по умолчанию для proto3.NONE: Поле ведет себя как непроверенное полеbytesв бинарном формате. Парсеры могут обрабатывать этот тип поля непредсказуемым образом, например, заменяя недопустимые символы. Это поведение по умолчанию для proto2.
Применимо к следующим областям видимости: File, Field
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | VERIFY |
| 2023 | VERIFY |
| proto3 | VERIFY |
| proto2 | NONE |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
message MyMessage {
string foo = 1;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
message MyMessage {
string foo = 1 [features.utf8_validation = NONE];
}
Специфичные для языка функции
Некоторые функции применяются к определенным языкам и не применяются к тем же protobuf на других языках. Использование этих функций требует от вас импорта соответствующего файла *_features.proto из среды выполнения языка. Примеры в следующих разделах показывают эти импорты.
features.(pb.go).api_level
Языки: Go
Функция api_level позволяет выбрать, для какой версии API плагин Go protobuf
должен генерировать код. Opaque API — это последняя версия реализации
Protocol Buffers для языка программирования Go. Предыдущая
версия теперь называется Open Struct API. См.
Go Protobuf: выпуск Opaque API
(блог пост) для введения.
Доступные значения:
API_OPEN: Open Struct API генерирует типы структур, которые открыты для прямого доступа.API_HYBRID: Hybrid — это шаг между Open и Opaque: Hybrid API также включает методы доступа (так что вы можете обновить свой код), но все еще экспортирует поля структуры как раньше. Нет разницы в производительности; этот уровень API только помогает с миграцией.API_OPAQUE: С Opaque API поля структуры скрыты и больше не могут быть доступны напрямую. Вместо этого новые методы доступа позволяют получать, устанавливать или очищать поле.
Применимо к следующим областям видимости: Message, File
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2023 | API_OPEN |
| 2024 | API_OPAQUE |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Вы можете установить функцию api_level, начиная с редакции 2023:
edition = "2023";
import "google/protobuf/go_features.proto";
// Удалите эту строку после миграции кода на Opaque API.
option features.(pb.go).api_level = API_HYBRID;
См. также: Opaque API: Миграция
features.(pb.cpp).enum_name_uses_string_view
Языки: C++
До Редакции 2024 все сгенерированные типы перечислений предоставляли следующую функцию для
получения метки из значения перечисления, что создавало некоторые накладные расходы на создание
экземпляров std::string во время выполнения:
const std::string& Foo_Name(int);
Значение функции по умолчанию в Редакции 2024 изменяет эту сигнатуру на возврат
absl::string_view, чтобы позволить лучшее разделение хранилища и потенциальную
экономию памяти/ЦП. Если вы еще не готовы к миграции, вы можете переопределить это,
чтобы вернуть предыдущее поведение. См.
Тип возвращаемого значения string_view
в руководстве по миграции для получения дополнительной информации по этой теме.
Доступные значения:
true: Перечисление используетstring_viewдля своих значений.false: Перечисление используетstd::stringдля своих значений.
Применимо к следующим областям видимости: Enum, File
Добавлено в: Редакция 2024
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | true |
| 2023 | false |
| proto3 | false |
| proto2 | false |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
features.(pb.java).large_enum
Языки: Java
Эта специфичная для языка функция позволяет использовать новую функциональность, которая обрабатывает большие перечисления в Java, не вызывая ошибок компилятора. Обратите внимание, что эта функция воспроизводит поведение, подобное перечислению, но имеет некоторые заметные различия. Например, операторы switch не поддерживаются.
Доступные значения:
true: Java перечисления будут использовать новую функциональность.false: Java перечисления будут продолжать использовать Java перечисления.
Применимо к следующим областям видимости: Enum
Добавлено в: Редакция 2024
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | false |
| 2023 | false |
| proto3 | false |
| proto2 | false |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
features.(pb.cpp/pb.java).legacy_closed_enum
Языки: C++, Java
Эта функция определяет, должно ли поле с открытым типом перечисления вести себя так, как если бы оно было закрытым перечислением. Это позволяет редакциям воспроизводить несоответствующее поведение в Java и C++ из proto2 и proto3.
Эта функция не влияет на файлы proto3, и поэтому в этом разделе нет примеров "до" и "после" для файла proto3.
Доступные значения:
true: Рассматривает перечисление как закрытое независимо отenum_type.false: Уважает то, что установлено вenum_type.
Применимо к следующим областям видимости: File, Field
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | false |
| 2023 | false |
| proto3 | false |
| proto2 | true |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
import "myproject/proto3file.proto";
message Msg {
myproject.proto3file.Proto3Enum name = 1;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
import "myproject/proto3file.proto";
import "google/protobuf/cpp_features.proto";
import "google/protobuf/java_features.proto";
message Msg {
myproject.proto3file.Proto3Enum name = 1 [
features.(pb.cpp).legacy_closed_enum = true,
features.(pb.java).legacy_closed_enum = true
];
}
features.(pb.java).nest_in_file_class
Языки: Java
Эта функция управляет тем, будет ли генератор Java вкладывать сгенерированный класс
в класс сгенерированного файла Java. Установка этой опции в NO эквивалентна
установке java_multiple_files = true в proto2/proto3/редакции 2023.
Имя внешнего класса по умолчанию также обновлено, чтобы всегда быть именем файла .proto в верблюжьем регистре
с суффиксом Proto по умолчанию (например, foo/bar_baz.proto
становится BarBazProto). Вы все еще можете переопределить это, используя опцию файла
java_outer_classname и заменить значение по умолчанию до Редакции 2024, которое было
BarBaz или BarBazOuterClass в зависимости от наличия конфликтов.
Доступные значения:
NO: Не вкладывать сгенерированный класс в класс файла.YES: Вкладывать сгенерированный класс в класс файла.- Legacy: Внутреннее значение, используемое когда установлена опция
java_multiple_files.
Применимо к следующим областям видимости: Message, Enum, Service
Добавлено в: Редакция 2024
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | NO |
| 2023 | LEGACY |
| proto3 | LEGACY |
| proto2 | LEGACY |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
features.(pb.cpp).string_type
Языки: C++
Эта функция определяет, как сгенерированный код должен обращаться со строковыми полями. Это
заменяет опцию ctype из proto2 и proto3 и предлагает новую
функцию string_type. В Редакции 2023 вы можете указать либо ctype, либо
string_type для поля, но не обе одновременно. В Редакции 2024 опция ctype
удалена.
Доступные значения:
VIEW: Генерирует методы доступаstring_viewдля поля.CORD: Генерирует методы доступаCordдля поля. Не поддерживается для полей расширений.STRING: Генерирует методы доступаstringдля поля.
Применимо к следующим областям видимости: File, Field
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | VIEW |
| 2023 | STRING |
| proto3 | STRING |
| proto2 | STRING |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
message Foo {
optional string bar = 6;
optional string baz = 7 [ctype = CORD];
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
import "google/protobuf/cpp_features.proto";
message Foo {
string bar = 6 [features.(pb.cpp).string_type = STRING];
string baz = 7 [features.(pb.cpp).string_type = CORD];
}
Следующий пример показывает файл proto3:
syntax = "proto3"
message Foo {
string bar = 6;
string baz = 7 [ctype = CORD];
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
import "google/protobuf/cpp_features.proto";
message Foo {
string bar = 6 [features.(pb.cpp).string_type = STRING];
string baz = 7 [features.(pb.cpp).string_type = CORD];
}
features.(pb.java).utf8_validation
Языки: Java
Эта специфичная для языка функция позволяет переопределить настройки на уровне файла на уровне поля только для Java.
Эта функция не влияет на файлы proto3, и поэтому в этом разделе нет примеров "до" и "после" для файла proto3.
Доступные значения:
DEFAULT: Поведение соответствует установленномуfeatures.utf8_validation.VERIFY: Переопределяет настройкуfeatures.utf8_validationна уровне файла, чтобы принудительно установить ее вVERIFYтолько для Java.
Применимо к следующим областям видимости: Field, File
Добавлено в: Редакция 2023
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | DEFAULT |
| 2023 | DEFAULT |
| proto3 | DEFAULT |
| proto2 | DEFAULT |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Следующий пример кода показывает файл proto2:
syntax = "proto2";
option java_string_check_utf8=true;
message MyMessage {
string foo = 1;
string bar = 2;
}
После запуска Prototiller эквивалентный код может выглядеть так:
edition = "2024";
import "google/protobuf/java_features.proto";
option features.utf8_validation = NONE;
option features.(pb.java).utf8_validation = VERIFY;
message MyMessage {
string foo = 1;
string bar = 2;
}
features.(pb.go).strip_enum_prefix
Языки: Go
Значения перечислений не ограничены областью видимости их содержащего имени перечисления, поэтому рекомендуется добавлять префикс каждого значения именем перечисления:
edition = "2024";
enum Strip {
STRIP_ZERO = 0;
STRIP_ONE = 1;
}
Однако сгенерированный код Go теперь будет содержать два префикса!
type Strip int32
const (
Strip_STRIP_ZERO Strip = 0
Strip_STRIP_ONE Strip = 1
)
Специфичная для языка функция strip_enum_prefix определяет, будет ли генератор кода Go
удалять повторяющийся префикс или нет.
Доступные значения:
STRIP_ENUM_PREFIX_KEEP: Сохранять имя как есть, даже если оно повторяющееся.STRIP_ENUM_PREFIX_GENERATE_BOTH: Генерировать оба, полное имя и сокращенное имя (чтобы помочь с миграцией вашего кода Go).STRIP_ENUM_PREFIX_STRIP: Удалять префикс имени перечисления из имен значений перечисления.
Применимо к следующим областям видимости: Enum, File
Добавлено в: Редакция 2024
Поведение по умолчанию для синтаксиса/редакции:
| Синтаксис/редакция | По умолчанию |
|---|---|
| 2024 | STRIP_ENUM_PREFIX_KEEP |
Примечание: Настройки функций на разных элементах схемы имеют разные области видимости.
Вы можете установить функцию strip_enum_prefix в файлах .proto редакции 2024 (или новее):
edition = "2024";
import "google/protobuf/go_features.proto";
option features.(pb.go).strip_enum_prefix = STRIP_ENUM_PREFIX_STRIP;
enum Strip {
STRIP_ZERO = 0;
STRIP_ONE = 1;
}
Сгенерированный код Go теперь будет удалять префикс STRIP:
type Strip int32
const (
Strip_ZERO Strip = 0
Strip_ONE Strip = 1
)
Сохранение поведения Proto2 или Proto3 в Редакции 2023
Вы можете захотеть перейти на формат редакций, но еще не разбираться с обновлениями в способе поведения сгенерированного кода. Этот раздел показывает изменения, которые инструмент Prototiller вносит в ваши .proto файлы, чтобы заставить protobuf на основе редакции 2023 вести себя как файл proto2 или proto3.
После того как эти изменения сделаны на уровне файла, вы получаете значения по умолчанию proto2 или proto3. Вы можете переопределить на более низких уровнях (уровень сообщения, уровень поля), чтобы учесть дополнительные различия в поведении (такие как required, proto3 optional) или если вы хотите, чтобы ваше определение было почти как proto2 или proto3.
Мы рекомендуем использовать Prototiller, если у вас нет конкретной причины не делать этого. Чтобы вручную применить все это вместо использования Prototiller, добавьте содержимое из следующих разделов в начало вашего .proto файла.
Поведение Proto2
Следующее показывает настройки для воспроизведения поведения proto2 с Редакцией 2023.
edition = "2023";
import "google/protobuf/cpp_features.proto";
import "google/protobuf/java_features.proto";
option features.field_presence = EXPLICIT;
option features.enum_type = CLOSED;
option features.repeated_field_encoding = EXPANDED;
option features.json_format = LEGACY_BEST_EFFORT;
option features.utf8_validation = NONE;
option features.(pb.cpp).legacy_closed_enum = true;
option features.(pb.java).legacy_closed_enum = true;
Поведение Proto3
Следующее показывает настройки для воспроизведения поведения proto3 с Редакцией 2023.
edition = "2023";
import "google/protobuf/cpp_features.proto";
import "google/protobuf/java_features.proto";
option features.field_presence = IMPLICIT;
option features.enum_type = OPEN;
// `packed=false` необходимо преобразовать в функции repeated_field_encoding на уровне поля
// в синтаксисе Редакций
option features.json_format = ALLOW;
option features.utf8_validation = VERIFY;
option features.(pb.cpp).legacy_closed_enum = false;
option features.(pb.java).legacy_closed_enum = false;
Редакция 2023 к 2024
Следующее показывает настройки для воспроизведения поведения Редакции 2023 с Редакцией 2024.
// foo/bar_baz.proto
edition = "2024";
import option "third_party/protobuf/cpp_features.proto";
import option "third_party/java/protobuf/java_features.proto";
import option "third_party/golang/protobuf/v2/src/google/protobuf/go_features.proto";
// Если ранее полагались на значение по умолчанию java_outer_classname редакции 2023.
option java_outer_classname = "BarBaz" // или BarBazOuterClass
option features.(pb.cpp).string_type = STRING;
option features.enforce_naming_style = STYLE_LEGACY;
option features.default_symbol_visibility = EXPORT_ALL;
option features.(pb.cpp).enum_name_uses_string_view = false;
option features.(pb.go).api_level = API_OPEN;
message MyMessage {
option features.(pb.java).nest_in_file_class = YES;
}
Предостережения и исключения
Этот раздел показывает изменения, которые вам нужно будет сделать вручную, если вы решите не использовать Prototiller.
Установка значений по умолчанию на уровне файла, показанных в предыдущем разделе, устанавливает поведение по умолчанию в большинстве случаев, но есть несколько исключений.
Редакция 2023 и позже
optional: Удалите все экземпляры меткиoptionalи изменитеfeatures.field_presenceнаEXPLICIT, если значение по умолчанию для файла —IMPLICIT.required: Удалите все экземпляры меткиrequiredи добавьте опциюfeatures.field_presence=LEGACY_REQUIREDна уровне поля.groups: Развернитеgroupsв отдельное сообщение и добавьте опциюfeatures.message_encoding = DELIMITEDна уровне поля. См.features.message_encodingдля получения дополнительной информации об этом.java_string_check_utf8: Удалите эту опцию файла и замените ее наfeatures.(pb.java).utf8_validation. Вам нужно будет импортировать функции Java, как описано в Специфичные для языка функции.packed: Для файлов proto2, преобразованных в формат редакций, удалите опцию поляpackedи добавьте[features.repeated_field_encoding=PACKED]на уровне поля, когда вы не хотите поведенияEXPANDED, которое вы установили в Поведение Proto2. Для файлов proto3, преобразованных в формат редакций, добавьте[features.repeated_field_encoding=EXPANDED]на уровне поля, когда вы не хотите поведения по умолчанию для proto3.
Редакция 2024 и позже
- (C++)
ctype: Удалите все экземпляры опцииctypeи установите значениеfeatures.(pb.cpp).string_type. - (C++ и Go)
weak: Удалите слабый импорт. Используйтеimport optionвместо этого. - (Java)
java_multiple_files: Удалитеjava_multiple_filesи используйтеfeatures.(pb.java).nest_in_file_classвместо этого.
Реализация поддержки редакций
Инструкции по реализации поддержки редакций в средах выполнения и плагинах.
В этой теме объясняется, как реализовать редакции в новых средах выполнения и генераторах.
Обзор
Редакция 2023
Первой выпущенной редакцией является Редакция 2023, которая предназначена для объединения синтаксисов proto2 и proto3. Функции, которые мы добавили для покрытия различий в поведении, подробно описаны в разделе Настройки функций для редакций.
Определение функции
В дополнение к поддержке редакций и глобальных функций, которые мы определили, вам
может понадобиться определить свои собственные функции, чтобы использовать инфраструктуру. Это
позволит вам определять произвольные функции, которые могут использоваться вашими генераторами и
средами выполнения для управления новыми поведениями. Первый шаг — запросить номер расширения
для сообщения FeatureSet в descriptor.proto выше 9999. Вы можете отправить
pull-request нам в GitHub, и он будет включен в наш следующий релиз (см.,
например, #15439).
Как только у вас будет номер расширения, вы можете создать свой features proto (аналогично cpp_features.proto). Они обычно выглядят примерно так:
edition = "2023";
package foo;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FeatureSet {
MyFeatures features = <номер расширения>;
}
message MyFeatures {
enum FeatureValue {
FEATURE_VALUE_UNKNOWN = 0;
VALUE1 = 1;
VALUE2 = 2;
}
FeatureValue feature_value = 1 [
targets = TARGET_TYPE_FIELD,
targets = TARGET_TYPE_FILE,
feature_support = {
edition_introduced: EDITION_2023,
edition_deprecated: EDITION_2024,
deprecation_warning: "Функция будет удалена в 2025",
edition_removed: EDITION_2025,
},
edition_defaults = { edition: EDITION_LEGACY, value: "VALUE1" },
edition_defaults = { edition: EDITION_2024, value: "VALUE2" }
];
}
Здесь мы определили новую функцию перечисления foo.feature_value (в настоящее время поддерживаются только
логические и перечислимые типы). В дополнение к определению значений, которые она может принимать, вам также нужно указать, как ее можно использовать:
- Цели (Targets) - указывает типы дескрипторов proto, к которым может быть прикреплена эта функция. Это контролирует, где пользователи могут явно указать функцию. Каждый тип должен быть явно перечислен.
- Поддержка функции (Feature support) - указывает срок жизни этой функции относительно редакции. Вы должны указать редакцию, в которой она была введена, и она не будет разрешена до этого. Вы можете дополнительно устареть или удалить функцию в более поздних редакциях.
- Значения по умолчанию для редакций (Edition defaults) - указывает любые изменения значения по умолчанию для
функции. Это должно охватывать каждую поддерживаемую редакцию, но вы можете опустить любую
редакцию, где значение по умолчанию не изменилось. Обратите внимание, что
EDITION_PROTO2иEDITION_PROTO3могут быть указаны здесь для предоставления значений по умолчанию для "устаревших" редакций (см. Устаревшие редакции).
Что такое функция?
Функции предназначены для предоставления механизма постепенного отказа от плохого поведения со временем, на границах редакций. Хотя сроки фактического удаления функции могут составлять годы (или десятилетия) в будущем, желаемой целью любой функции должно быть eventual removal (окончательное удаление). Когда идентифицируется плохое поведение, вы можете ввести новую функцию, которая защищает исправление. В следующей редакции (или, возможно, позже) вы переключили бы значение по умолчанию, все еще позволяя пользователям сохранять свое старое поведение при обновлении. В какой-то момент в будущем вы пометили бы функцию как устаревшую, что вызвало бы пользовательское предупреждение для любых пользователей, переопределяющих ее. В более поздней редакции вы затем пометили бы ее как удаленную, предотвращая дальнейшее переопределение пользователями (но значение по умолчанию все равно будет применяться). До тех пор, пока поддержка этой последней редакции не будет прекращена в breaking release, функция останется usable (используемой) для protobuf, застрявших на старых редакциях, давая им время на миграцию.
Флаги, управляющие опциональными поведениями, которые вы не собираетесь удалять, лучше реализовывать как пользовательские опции. Это связано с причиной, по которой мы ограничили функции либо логическими, либо перечислимыми типами. Любое поведение, управляемое (относительно) неограниченным количеством значений, вероятно, не очень подходит для framework редакций, поскольку нереалистично eventually turn down (со временем отключить) так много различных поведений.
Одно предостережение к этому — поведения, связанные с wire boundaries (границами бинарного формата). Использование
специфичных для языка функций для управления поведением сериализации или разбора может быть
опасным, поскольку с другой стороны может быть любой другой язык. Изменения
wire-format (бинарного формата) всегда должны контролироваться глобальными функциями в descriptor.proto,
которые могут единообразно соблюдаться каждой средой выполнения.
Генераторы
Генераторы, написанные на C++, получают многое бесплатно, потому что они используют среду выполнения C++.
Им не нужно самим обрабатывать Разрешение функций,
и если им нужны какие-либо расширения функций, они могут зарегистрировать их в
GetFeatureExtensions в своем CodeGenerator. Они обычно могут использовать
GetResolvedSourceFeatures для доступа к разрешенным функциям для дескриптора в
codegen и GetUnresolvedSourceFeatures для доступа к их собственным неразрешенным функциям.
Плагины, написанные на том же языке, что и среда выполнения, для которой они генерируют код, могут требовать некоторой пользовательской bootstrapping (начальной загрузки) для их определений функций.
Явная поддержка
Генераторы должны точно указывать, какие редакции они поддерживают. Это позволяет вам
безопасно добавлять поддержку редакции после ее выпуска, по вашему собственному
расписанию. Protoc будет отклонять любые protobuf редакций, отправленные генераторам, которые не
включают FEATURE_SUPPORTS_EDITIONS в поле supported_features их
CodeGeneratorResponse. Кроме того, у нас есть поля minimum_edition и
maximum_edition для указания вашего точного окна поддержки. После того как вы
определили весь код и изменения функций для новой редакции, вы можете увеличить
maximum_edition, чтобы сообщить об этой поддержке.
Тесты кодогенерации
У нас есть набор тестов кодогенерации, которые можно использовать для фиксации того, что Редакция 2023 не производит непредвиденных функциональных изменений. Они были очень полезны в языках, таких как C++ и Java, где значительная часть функциональности находится в gencode. С другой стороны, в языках, таких как Python, где gencode — это в основном просто коллекция сериализованных дескрипторов, они не так полезны.
Эта инфраструктура еще не является переиспользуемой, но планируется быть таковой в будущем релизе. В тот момент вы сможете использовать их для проверки того, что миграция на редакции не имеет никаких непредвиденных изменений кодогенерации.
Среды выполнения
Среды выполнения без рефлексии или динамических сообщений не должны нуждаться в каких-либо действиях для реализации редакций. Вся эта логика должна обрабатываться генератором кода.
Языки с рефлексией, но без динамических сообщений нуждаются в разрешенных функциях, но могут дополнительно выбрать обработку только в их генераторе. Это может быть сделано путем передачи как разрешенных, так и неразрешенных наборов функций в среду выполнения во время кодогенерации. Это позволяет избежать повторной реализации Разрешения функций в среде выполнения с основным недостатком эффективности, поскольку это создаст уникальный набор функций для каждого дескриптора.
Языки с динамическими сообщениями должны полностью реализовывать редакции, потому что им нужно иметь возможность строить дескрипторы во время выполнения.
Рефлексия синтаксиса
Первый шаг в реализации редакций в среде выполнения с рефлексией — это
удалить все прямые проверки ключевого слова syntax. Все они должны быть перемещены
в более детальные вспомогательные функции, которые могут продолжать использовать syntax, если
это необходимо.
Следующие вспомогательные функции должны быть реализованы для дескрипторов, с соответствующими языку названиями:
FieldDescriptor::has_presence- Имеет ли поле явное присутствие- Повторяющиеся поля никогда не имеют присутствия
- Поля сообщений, расширений и oneof всегда имеют явное присутствие
- Все остальное имеет присутствие тогда и только тогда, когда
field_presenceнеIMPLICIT
FieldDescriptor::is_required- Является ли поле обязательнымFieldDescriptor::requires_utf8_validation- Должно ли поле проверяться на валидность utf8FieldDescriptor::is_packed- Имеет ли повторяющееся поле упакованное (packed) кодированиеFieldDescriptor::is_delimited- Имеет ли поле сообщения разделенное (delimited) кодированиеEnumDescriptor::is_closed- Является ли перечисление закрытым
{{% alert title="Примечание" color="note" %}} В
большинстве языков функция кодирования сообщения все еще сигнализируется с помощью
TYPE_GROUP, а обязательные поля все еще имеют установленный LABEL_REQUIRED. Это не
идеально и было сделано, чтобы облегчить миграции downstream. В конечном итоге эти
следует перенести на соответствующие вспомогательные функции и
TYPE_MESSAGE/LABEL_OPTIONAL.{{% /alert %}}
Пользователи downstream должны перейти на эти новые вспомогательные функции вместо использования синтаксиса напрямую. Следующий класс существующих API дескрипторов в идеале следует устареть и в конечном итоге удалить, поскольку они раскрывают информацию о синтаксисе:
FileDescriptorsyntax- API proto3 optional
FieldDescriptor::has_optional_keywordOneofDescriptor::is_syntheticDescriptor::*real_oneof*- следует переименовать в просто "oneof", а существующие вспомогательные функции "oneof" следует удалить, поскольку они раскрывают информацию о синтетических oneof (которых не существует в редакциях).
- Тип Group
- Значение перечисления
TYPE_GROUPследует удалить, заменив его на вспомогательную функциюis_delimited.
- Значение перечисления
- Метка Required
- Значение перечисления
LABEL_REQUIREDследует удалить, заменив его на вспомогательную функциюis_required.
- Значение перечисления
Существует много классов пользовательского кода, где эти проверки существуют, но не
являются враждебными к редакциям. Например, код, который должен обрабатывать proto3 optional
особо из-за его синтетической реализации oneof, не будет враждебен к
редакциям, пока полярность выглядит как syntax == "proto3" (а не
syntax != "proto2").
Если невозможно полностью удалить эти API, их следует устареть и не поощрять.
Видимость функций
Как обсуждалось в
editions-feature-visibility,
feature protos должны оставаться внутренней деталью любой реализации Protobuf.
Поведения, которые они контролируют, должны быть доступны через методы дескриптора, но сами
protos не должны. Примечательно, что это означает, что любые опции, которые
предоставляются пользователям, должны иметь свои поля features удалены.
Единственный случай, когда мы разрешаем функциям просачиваться наружу, — это при сериализации дескрипторов. Полученные descriptor protos должны быть точным представлением исходных proto файлов и должны содержать неразрешенные функции внутри опций.
Устаревшие редакции
Как более подробно обсуждается в legacy-syntax-editions, отличный способ получить ранний охват вашей реализации редакций — это унифицировать proto2, proto3 и редакции. Это эффективно мигрирует proto2 и proto3 в редакции под капотом и заставляет все вспомогательные функции, реализованные в Рефлексии синтаксиса, использовать функции исключительно (вместо ветвления на синтаксисе). Это можно сделать, вставив фазу feature inference (вывода функций) в Разрешение функций, где различные аспекты proto файла могут информировать о том, какие функции уместны. Эти функции затем могут быть объединены с функциями родителя, чтобы получить разрешенный набор функций.
Хотя мы предоставляем разумные значения по умолчанию для proto2/proto3 уже для редакции 2023, требуются следующие дополнительные выводы:
- required - мы выводим
LEGACY_REQUIREDприсутствие, когда поле имеетLABEL_REQUIRED - groups - мы выводим
DELIMITEDкодирование сообщения, когда поле имеетTYPE_GROUP - packed - мы выводим
PACKEDкодирование, когда опцияpackedистинна - expanded - мы выводим
EXPANDEDкодирование, когда поле proto3 имеетpackedявно установленным в false
Тесты на соответствие
Были добавлены специфичные для редакций тесты на соответствие, но для них нужно явно opted-in (дать согласие).
Флаг --maximum_edition 2023 может быть передан runner (исполнителю), чтобы включить их. Вам
нужно будет настроить ваш testee binary (тестируемый бинарный файл) для обработки следующих новых типов сообщений:
protobuf_test_messages.editions.proto2.TestAllTypesProto2- Идентично старому proto2 сообщению, но преобразованному в редакцию 2023protobuf_test_messages.editions.proto3.TestAllTypesProto3- Идентично старому proto3 сообщению, но преобразованному в редакцию 2023protobuf_test_messages.editions.TestAllTypesEdition2023- Используется для покрытия специфичных для редакции-2023 тестовых случаев
Разрешение функций
Редакции используют лексическую область видимости для определения функций, что означает, что любой не-C++ код,
которому необходимо реализовать поддержку редакций, должен будет повторно реализовать наш алгоритм разрешения функций.
Однако основная часть работы выполняется самим protoc,
который можно настроить на вывод промежуточного сообщения FeatureSetDefaults.
Это сообщение содержит "компиляцию" набора файлов определений функций,
излагая значения функций по умолчанию в каждой редакции.
Например, определение функции выше скомпилировалось бы в следующие значения по умолчанию между proto2 и редакцией 2025 (в нотации text-format):
defaults {
edition: EDITION_PROTO2
overridable_features { [foo.features] {} }
fixed_features {
// Глобальные значения функций по умолчанию…
[foo.features] { feature_value: VALUE1 }
}
}
defaults {
edition: EDITION_PROTO3
overridable_features { [foo.features] {} }
fixed_features {
// Глобальные значения функций по умолчанию…
[foo.features] { feature_value: VALUE1 }
}
}
defaults {
edition: EDITION_2023
overridable_features {
// Глобальные значения функций по умолчанию…
[foo.features] { feature_value: VALUE1 }
}
}
defaults {
edition: EDITION_2024
overridable_features {
// Глобальные значения функций по умолчанию…
[foo.features] { feature_value: VALUE2 }
}
}
defaults {
edition: EDITION_2025
overridable_features {
// Глобальные значения функций по умолчанию…
}
fixed_features { [foo.features] { feature_value: VALUE2 } }
}
minimum_edition: EDITION_PROTO2
maximum_edition: EDITION_2025
Глобальные значения функций по умолчанию опущены для компактности, но они также были бы присутствуют. Этот объект содержит упорядоченный список каждой редакции с уникальным набором значений по умолчанию (некоторые редакции могут в итоге отсутствовать) в указанном диапазоне. Каждый набор значений по умолчанию разделен на переопределяемые и фиксированные функции. Первые — это поддерживаемые функции для редакции, которые могут быть свободно переопределены пользователями. Фиксированные функции — это те, которые еще не были введены или были удалены, и не могут быть переопределены пользователями.
Мы предоставляем правило Bazel для компиляции этих промежуточных объектов:
load("@com_google_protobuf//editions:defaults.bzl", "compile_edition_defaults")
compile_edition_defaults(
name = "my_defaults",
srcs = ["//some/path:lang_features_proto"],
maximum_edition = "PROTO2",
minimum_edition = "2024",
)
Выходной FeatureSetDefaults может быть встроен в строковый литерал в виде сырых данных (raw string literal) на
том языке, в котором вам нужно выполнить разрешение функций. Мы также предоставляем
макрос embed_edition_defaults для этого:
embed_edition_defaults(
name = "embed_my_defaults",
defaults = ":my_defaults",
output = "my_defaults.h",
placeholder = "DEFAULTS_DATA",
template = "my_defaults.h.template",
)
В качестве альтернативы, вы можете вызвать protoc напрямую (вне Bazel), чтобы сгенерировать эти данные:
protoc --edition_defaults_out=defaults.binpb --edition_defaults_minimum=PROTO2 --edition_defaults_maximum=2023 <файлы функций...>
Как только сообщение defaults подключено и разобрано вашим кодом, разрешение функций для дескриптора файла в данной редакции следует простому алгоритму:
- Проверить, что редакция находится в соответствующем диапазоне [
minimum_edition,maximum_edition] - Выполнить бинарный поиск по упорядоченному полю
defaultsдля самой высокой записи, меньшей или равной редакции - Объединить
overridable_featuresвfixed_featuresиз выбранных значений по умолчанию - Объединить любые явные функции, установленные в дескрипторе (поле
featuresв опциях файла)
Оттуда вы можете рекурсивно разрешать функции для всех других дескрипторов:
- Инициализировать набором функций родительского дескриптора
- Объединить любые явные функции, установленные в дескрипторе (поле
featuresв опциях)
Для определения "родительского" дескриптора вы можете обратиться к нашей реализации на C++. В большинстве случаев это straightforward (просто), но расширения немного удивительны, потому что их родителем является enclosing scope (охватывающая область), а не extendee. Oneof также нужно рассматривать как родителя их полей.
Тесты на соответствие
В будущем релизе мы планируем добавить тесты на соответствие для проверки разрешения функций кросс-языково. До тех пор наши регулярные тесты на соответствие дают частичное покрытие, и наши примеры модульных тестов на наследование могут быть портированы для предоставления более комплексного покрытия.
Примеры
Ниже приведены некоторые реальные примеры того, как мы реализовали поддержку редакций в наших средах выполнения и плагинах.
Java
- #14138 - Начальная загрузка компилятора с C++ gencode для Java features proto
- #14377 - Использование функций в генераторах кода Java, Kotlin и Java Lite, включая тесты кодогенерации
- #15210 - Использование функций в полных средах выполнения Java, охватывающее начальную загрузку функций Java, разрешение функций и устаревшие редакции, вместе с модульными тестами и тестированием на соответствие
Чистый Python
- #14546 - Настройка тестов кодогенерации заранее
- #14547 - Полностью реализует редакции за один раз, вместе с модульными тестами и тестированием на соответствие
𝛍pb
- #14638 - Первый проход реализации редакций, охватывающий разрешение функций и устаревшие редакции
- #14667 - Добавлено более полная обработка метки/типа поля, поддержка code generator (генератора кода) upb и некоторые тесты
- #14678 - Подключает upb к среде выполнения Python, с большим количеством модульных тестов и тестов на соответствие
Ruby
- #16132 - Подключение upb/Java ко всем четырем средам выполнения Ruby для полной поддержки редакций
Руководства по языкам
Основы Protocol Buffer: C++
Базовое введение в работу с protocol buffers для программистов на C++.
Это руководство предоставляет базовое введение для программистов на C++ в работу с protocol buffers. На примере создания простого приложения оно показывает, как:
- Определять форматы сообщений в файле
.proto. - Использовать компилятор protocol buffer.
- Использовать C++ API protocol buffer для записи и чтения сообщений.
Это не исчерпывающее руководство по использованию protocol buffers в C++. Для получения более подробной справочной информации см. Руководство по языку Protocol Buffer, Справочник по C++ API, Руководство по сгенерированному коду на C++ и Справочник по кодированию.
Проблемная область
Пример, который мы будем использовать, — это очень простое приложение "адресная книга", которое может читать и записывать контактные данные людей в файл и из файла. Каждый человек в адресной книге имеет имя, ID, адрес электронной почты и контактный номер телефона.
Как сериализовать и извлекать такие структурированные данные? Есть несколько способов решить эту проблему:
- Необработанные структуры данных в памяти могут быть отправлены/сохранены в бинарной форме. Со временем это хрупкий подход, поскольку принимающий/читающий код должен быть скомпилирован с точно такой же раскладкой памяти, порядком байтов и т.д. Также, по мере накопления данных в сыром формате и распространения копий программ, настроенных на этот формат, становится очень трудно расширить формат.
- Вы можете придумать специальный способ кодирования элементов данных в единую строку — например, кодируя 4 целых числа как "12:3:-23:67". Это простой и гибкий подход, хотя он требует написания одноразового кода кодирования и разбора, и разбор накладывает небольшие затраты времени выполнения. Это лучше всего работает для кодирования очень простых данных.
- Сериализовать данные в XML. Этот подход может быть очень привлекательным, поскольку XML (вроде как) читаем человеком и есть библиотеки привязок для множества языков. Это может быть хорошим выбором, если вы хотите делиться данными с другими приложениями/проектами. Однако XML печально известен своей прожорливостью к месту, и кодирование/декодирование может наложить огромные штрафы на производительность приложений. Кроме того, навигация по DOM-дереву XML значительно сложнее, чем навигация по простым полям в классе в обычных условиях.
Вместо этих вариантов вы можете использовать protocol buffers. Protocol buffers — это
гибкое, эффективное, автоматизированное решение, созданное именно для этой проблемы. С
protocol buffers вы пишете .proto описание структуры данных, которую
хотите сохранить. На основе этого компилятор protocol buffer создает класс,
который реализует автоматическое кодирование и разбор данных protocol buffer в
эффективном бинарном формате. Сгенерированный класс предоставляет геттеры и сеттеры для
полей, составляющих protocol buffer, и заботится о деталях
чтения и записи protocol buffer как единого целого. Важно, что формат protocol buffer
поддерживает идею расширения формата со временем таким образом, что
код все еще может читать данные, закодированные в старом формате.
Где найти пример кода
Пример кода включен в пакет с исходным кодом, в директории "examples".
Определение вашего формата протокола
Чтобы создать ваше приложение "адресная книга", вам нужно начать с файла .proto.
Определения в файле .proto просты: вы добавляете сообщение для
каждой структуры данных, которую хотите сериализовать, затем указываете имя и тип для
каждого поля в сообщении. Вот файл .proto, который определяет ваши сообщения,
addressbook.proto.
edition = "2023";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
Как вы можете видеть, синтаксис похож на C++ или Java. Давайте пройдемся по каждой части файла и посмотрим, что она делает.
Файл .proto начинается с объявления edition. Редакции заменяют
старые объявления syntax = "proto2" и syntax = "proto3" и предоставляют
более гибкий способ развития языка с течением времени.
Далее следует объявление пакета, которое помогает предотвратить конфликты имен между разными проектами. В C++ ваши сгенерированные классы будут помещены в пространство имен, соответствующее имени пакета.
После объявления пакета идут ваши определения сообщений. Сообщение —
это просто агрегат, содержащий набор типизированных полей. Многие стандартные простые типы данных
доступны в качестве типов полей, включая bool, int32, float,
double и string. Вы также можете добавить дальнейшую структуру в ваши сообщения,
используя другие типы сообщений в качестве типов полей — в приведенном выше примере сообщение Person
содержит сообщения PhoneNumber, а сообщение AddressBook
содержит сообщения Person. Вы даже можете определять типы сообщений, вложенные внутрь
других сообщений — как вы можете видеть, тип PhoneNumber определен внутри
Person. Вы также можете определить типы перечислений, если хотите, чтобы одно из ваших полей имело
одно из предопределенного списка значений — здесь вы хотите указать, что номер телефона
может быть одного из нескольких типов.
Маркеры " = 1", " = 2" на каждом элементе идентифицируют уникальный номер поля, который поле использует в бинарном кодировании. Номера полей 1-15 требуют на один байт меньше для кодирования, чем более высокие номера, поэтому в качестве оптимизации вы можете решить использовать эти номера для часто используемых или повторяющихся элементов, оставляя номера полей 16 и выше для реже используемых элементов.
Поля могут быть одним из следующих:
-
singular (сингулярные): По умолчанию поля являются необязательными (optional), что означает, что поле может быть установлено, а может и не быть. Если сингулярное поле не установлено, используется значение по умолчанию, зависящее от типа: ноль для числовых типов, пустая строка для строк, false для bool, и первое определенное значение перечисления для перечислений (которое должно быть 0). Обратите внимание, что вы не можете явно установить поле в
singular. Это описание неповторяющегося поля. -
repeated(повторяющиеся): Поле может повторяться любое количество раз (включая ноль). Порядок повторяющихся значений будет сохранен. Думайте о повторяющихся полях как о динамически sized arrays (массивах переменного размера).
В старых версиях protobuf существовало ключевое слово required, но оно оказалось
хрупким и не поддерживается в современных protobuf (хотя в редакциях
есть функция, которую вы можете использовать, чтобы включить его, для обратной совместимости).
Вы найдете полное руководство по написанию файлов .proto — включая все
возможные типы полей — в
Руководстве по языку Protocol Buffer.
Однако не ищите возможности, похожие на наследование классов — protocol
buffers этого не делают.
Компиляция ваших Protocol Buffers
Теперь, когда у вас есть .proto, следующее, что нужно сделать, — это сгенерировать
классы, которые вам понадобятся для чтения и записи сообщений AddressBook
(и, следовательно, Person и PhoneNumber). Для этого вам нужно запустить компилятор
protocol buffer protoc на вашем .proto:
-
Если вы не установили компилятор, следуйте инструкциям в Установка компилятора Protocol Buffer.
-
Теперь запустите компилятор, указав исходную директорию (где находится исходный код вашего приложения — используется текущая директория, если вы не предоставили значение), целевую директорию (куда вы хотите, чтобы сгенерированный код попал; часто та же, что и
$SRC_DIR) и путь к вашему.proto. В этом случае:protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.protoПоскольку вы хотите классы C++, вы используете опцию
--cpp_out— похожие опции предоставляются для других поддерживаемых языков.
Это генерирует следующие файлы в указанной вами целевой директории:
addressbook.pb.h, заголовочный файл, который объявляет ваши сгенерированные классы.addressbook.pb.cc, который содержит реализацию ваших классов.
API Protocol Buffer
Давайте посмотрим на некоторый сгенерированный код и увидим, какие классы и функции
компилятор создал для вас. Если вы посмотрите в addressbook.pb.h, вы можете увидеть,
что у вас есть класс для каждого сообщения, которое вы указали в addressbook.proto.
Присмотревшись к классу Person, вы можете увидеть, что компилятор
сгенерировал методы доступа для каждого поля. Например, для полей name, id, email
и phones у вас есть эти методы:
// name
bool has_name() const; // Только для явного присутствия
void clear_name();
const ::std::string& name() const;
void set_name(const ::std::string& value);
::std::string* mutable_name();
// id
bool has_id() const;
void clear_id();
int32_t id() const;
void set_id(int32_t value);
// email
bool has_email() const;
void clear_email();
const ::std::string& email() const;
void set_email(const ::std::string& value);
::std::string* mutable_email();
// phones
int phones_size() const;
void clear_phones();
const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
const ::tutorial::Person_PhoneNumber& phones(int index) const;
::tutorial::Person_PhoneNumber* mutable_phones(int index);
::tutorial::Person_PhoneNumber* add_phones();
Как вы можете видеть, геттеры имеют точно такое же имя, как поле в нижнем регистре, и
сеттерные методы начинаются с set_. Также есть методы has_ для сингулярных
полей, которые имеют явное отслеживание присутствия; они возвращают true, если это поле было
установлено. Наконец, каждое поле имеет метод clear_, который сбрасывает поле обратно
в его состояние по умолчанию.
В то время как числовое поле id имеет только базовый набор методов доступа, описанный выше,
поля name и email имеют пару дополнительных методов, потому что они являются
строками — mutable_ геттер, который позволяет вам получить прямой указатель на строку,
и дополнительный сеттер. Обратите внимание, что вы можете вызвать mutable_email(), даже если email
еще не установлен; он будет автоматически инициализирован пустой строкой. Если бы
у вас было повторяющееся поле сообщения в этом примере, оно также имело бы метод mutable_,
но не имело бы метода set_.
Повторяющиеся поля также имеют некоторые специальные методы — если вы посмотрите на методы для
повторяющегося поля phones, вы увидите, что вы можете:
- проверить
_size(размер) повторяющегося поля (другими словами, сколько номеров телефонов связано с этимPerson). - получить указанный номер телефона, используя его индекс.
- обновить существующий номер телефона по указанному индексу.
- добавить другой номер телефона в сообщение, который вы затем можете редактировать (повторяющиеся
скалярные типы имеют
add_, который просто позволяет вам передать новое значение).
Для получения дополнительной информации о том, какие именно члены компилятор протоколов генерирует для любого конкретного определения поля, см. Справочник по сгенерированному коду на C++.
Перечисления и вложенные классы
Сгенерированный код включает перечисление PhoneType, которое соответствует вашему .proto
перечислению. Вы можете ссылаться на этот тип как Person::PhoneType и его значения как
Person::PHONE_TYPE_MOBILE, Person::PHONE_TYPE_HOME и
Person::PHONE_TYPE_WORK (детали реализации немного более
сложные, но вам не нужно понимать их, чтобы использовать перечисление).
Компилятор также сгенерировал для вас вложенный класс с именем
Person::PhoneNumber. Если вы посмотрите на код, вы увидите, что "настоящий"
класс на самом деле называется Person_PhoneNumber, но typedef, определенный внутри
Person, позволяет вам обращаться с ним так, как если бы это был вложенный класс. Единственный случай,
когда это имеет значение, — это если вы хотите сделать forward-declaration (предварительное объявление) класса в
другом файле — вы не можете forward-declare вложенные типы в C++, но вы
можете forward-declare Person_PhoneNumber.
Стандартные методы сообщений
Каждый класс сообщений также содержит ряд других методов, которые позволяют вам проверять или манипулировать всем сообщением, включая:
bool IsInitialized() const;: проверяет, установлены ли все обязательные поля.string DebugString() const;: возвращает удобочитаемое представление сообщения, особенно полезное для отладки.void CopyFrom(const Person& from);: перезаписывает сообщение значениями данного сообщения.void Clear();: очищает все элементы обратно в пустое состояние.
Эти и методы ввода-вывода, описанные в следующем разделе, реализуют
интерфейс Message, общий для всех классов C++ protocol buffer. Для получения дополнительной информации
см.
полную документацию API для Message.
Разбор и сериализация
Наконец, каждый класс protocol buffer имеет методы для записи и чтения сообщений вашего выбранного типа, используя бинарный формат protocol buffer. К ним относятся:
bool SerializeToString(string* output) const;: сериализует сообщение и сохраняет байты в данной строке. Обратите внимание, что байты являются бинарными, а не текстовыми; мы используем классstringтолько как удобный контейнер.bool ParseFromString(const string& data);: разбирает сообщение из данной строки.bool SerializeToOstream(ostream* output) const;: записывает сообщение в данный C++ostream.bool ParseFromIstream(istream* input);: разбирает сообщение из данного C++istream.
Это лишь пара из предоставленных вариантов для разбора и сериализации.
См.
справочник по API Message
для получения полного списка.
{{% alert title="Важно" color="warning" %}} Protocol Buffers и объектно-ориентированный дизайн Классы Protocol buffer — это, по сути, держатели данных (как структуры в C), которые не предоставляют дополнительной функциональности; они не являются хорошими полноправными гражданами в объектной модели. Если вы хотите добавить более богатое поведение в сгенерированный класс, лучший способ сделать это — обернуть сгенерированный класс protocol buffer в специфичный для приложения класс. Обертывание protocol buffers также является хорошей идеей, если вы не контролируете дизайн файла .proto (если, скажем, вы используете его из другого проекта). В этом случае вы можете использовать класс-обертку для создания интерфейса, лучше подходящего для уникальной среды вашего приложения: скрывая некоторые данные и методы, предоставляя удобные функции и т.д. Вы не можете добавить поведение к сгенерированным классам, наследуясь от них, так как они являются final (запечатанными). Это предотвращает нарушение внутренних механизмов и, в любом случае, не является хорошей объектно-ориентированной практикой.
{{% /alert %}}
Написание сообщения
Теперь давайте попробуем использовать ваши классы protocol buffer. Первое, что вы хотите, чтобы ваше приложение "адресная книга" могло делать, — это записывать личные данные в ваш файл адресной книги. Для этого вам нужно создать и заполнить экземпляры ваших классов protocol buffer, а затем записать их в выходной поток.
Вот программа, которая читает AddressBook из файла, добавляет одного нового Person
в него на основе пользовательского ввода и записывает новый AddressBook обратно в файл
снова. Части, которые напрямую вызывают или ссылаются на код, сгенерированный компилятором протокола,
выделены.
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Эта функция заполняет сообщение Person на основе пользовательского ввода.
void PromptForAddress(tutorial::Person& person) {
cout << "Введите ID человека: ";
int id;
cin >> id;
person.set_id(id);
cin.ignore(256, '\n');
cout << "Введите имя: ";
getline(cin, *person.mutable_name());
cout << "Введите адрес электронной почты (пусто для отсутствия): ";
string email;
getline(cin, email);
if (!email.empty()) {
person.set_email(email);
}
while (true) {
cout << "Введите номер телефона (или оставьте пустым для завершения): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
tutorial::Person::PhoneNumber* phone_number = person.add_phones();
phone_number->set_number(number);
cout << "Это мобильный, домашний или рабочий телефон? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_WORK);
} else {
cout << "Неизвестный тип телефона. Используется значение по умолчанию." << endl;
}
}
}
// Главная функция: Читает всю адресную книгу из файла,
// добавляет одного человека на основе пользовательского ввода, затем записывает её обратно в тот же
// файл.
int main(int argc, char* argv[]) {
// Проверяем, что версия библиотеки, с которой мы слинковались,
// совместима с версией заголовков, с которой мы компилировали.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Использование: " << argv[0] << " ФАЙЛ_АДРЕСНОЙ_КНИГИ" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Читаем существующую адресную книгу.
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": Файл не найден. Создается новый файл." << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Не удалось разобрать адресную книгу." << endl;
return -1;
}
}
// Добавляем адрес.
PromptForAddress(*address_book.add_people());
{
// Записываем новую адресную книгу обратно на диск.
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Не удалось записать адресную книгу." << endl;
return -1;
}
}
// Опционально: Удаляем все глобальные объекты, выделенные libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
Обратите внимание на макрос GOOGLE_PROTOBUF_VERIFY_VERSION. Это хорошая практика — хотя
и не строго необходимая — выполнять этот макрос перед использованием библиотеки C++ Protocol
Buffer. Он проверяет, что вы не случайно слинковались с
версией библиотеки, которая несовместима с версией заголовков, с которой вы
компилировали. Если обнаружено несоответствие версий, программа завершится. Обратите внимание,
что каждый файл .pb.cc автоматически вызывает этот макрос при запуске.
Также обратите внимание на вызов ShutdownProtobufLibrary() в конце программы.
Все, что он делает, — это удаляет любые глобальные объекты, которые были выделены библиотекой Protocol
Buffer. Это необязательно для большинства программ, поскольку процесс просто
завершится, и ОС позаботится о возврате всей его памяти.
Однако, если вы используете проверку утечек памяти, которая требует, чтобы каждый последний объект
был освобожден, или если вы пишете библиотеку, которая может быть загружена и выгружена
несколько раз одним процессом, то вы, возможно, захотите заставить Protocol Buffers
очистить всё.
Чтение сообщения
Конечно, адресная книга была бы не очень полезна, если бы вы не могли получить из нее никакой информации! Этот пример читает файл, созданный приведенным выше примером, и печатает всю информацию в нем.
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Перебирает всех людей в AddressBook и печатает информацию о них.
void ListPeople(const tutorial::AddressBook& address_book) {
for (const tutorial::Person& person : address_book.people()) {
cout << "ID человека: " << person.id() << endl;
cout << " Имя: " << person.name() << endl;
if (person.has_email()) { // Исправлено: проверяем has_email(), а не !has_email()
cout << " Адрес электронной почты: " << person.email() << endl;
}
for (const tutorial::Person::PhoneNumber& phone_number : person.phones()) {
switch (phone_number.type()) {
case tutorial::Person::PHONE_TYPE_MOBILE:
cout << " Мобильный телефон #: ";
break;
case tutorial::Person::PHONE_TYPE_HOME:
cout << " Домашний телефон #: ";
break;
case tutorial::Person::PHONE_TYPE_WORK:
cout << " Рабочий телефон #: ";
break;
case tutorial::Person::PHONE_TYPE_UNSPECIFIED:
default:
cout << " Телефон #: ";
break;
}
cout << phone_number.number() << endl;
}
}
}
// Главная функция: Читает всю адресную книгу из файла и печатает всю
// информацию внутри.
int main(int argc, char* argv[]) {
// Проверяем, что версия библиотеки, с которой мы слинковались,
// совместима с версией заголовков, с которой мы компилировали.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Использование: " << argv[0] << " ФАЙЛ_АДРЕСНОЙ_КНИГИ" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Читаем существующую адресную книгу.
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Не удалось разобрать адресную книгу." << endl;
return -1;
}
}
ListPeople(address_book);
// Опционально: Удаляем все глобальные объекты, выделенные libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
Расширение Protocol Buffer
Рано или поздно после того, как вы выпустите код, использующий ваш protocol buffer, вы несомненно захотите "улучшить" определение protocol buffer. Если вы хотите, чтобы ваши новые буферы были обратно совместимы, а ваши старые буферы были вперед совместимы — а вы почти certainly хотите этого — тогда есть некоторые правила, которым вы должны следовать. В новой версии protocol buffer:
- вы не должны изменять номера полей любых существующих полей.
- вы можете удалять сингулярные или повторяющиеся поля.
- вы можете добавлять новые сингулярные или повторяющиеся поля, но вы должны использовать новые номера полей (то есть номера полей, которые никогда не использовались в этом protocol buffer, даже удаленными полями).
(Есть некоторые исключения из этих правил, но они редко используются.)
Если вы следуете этим правилам, старый код будет happily (с радостью) читать новые сообщения и просто игнорировать любые новые поля. Для старого code (кода) поля, которые были удалены, просто будут иметь свое значение по умолчанию, а удаленные повторяющиеся поля будут пусты. Новый код также будет прозрачно читать старые сообщения. Однако имейте в виду, что новые поля не будут присутствовать в старых сообщениях, поэтому вам нужно будет проверить их присутствие, проверив, имеют ли они значение по умолчанию (например, пустую строку) перед использованием.
Советы по оптимизации
Библиотека C++ Protocol Buffers чрезвычайно сильно оптимизирована. Однако правильное использование может улучшить производительность еще больше. Вот несколько советов, как выжать каждую последнюю каплю скорости из библиотеки:
-
Используйте Арены (Arenas) для выделения памяти. Когда вы создаете много сообщений protocol buffer в короткоживущей операции (например, разбор одного запроса), распределитель памяти системы может стать узким местом. Арены предназначены для смягчения этого. Используя арену, вы можете выполнять множество выделений с низкими накладными расходами и одно освобождение для всех них сразу. Это может значительно улучшить производительность в приложениях, насыщенных сообщениями.
Чтобы использовать арены, вы выделяете сообщения на объекте
google::protobuf::Arena:google::protobuf::Arena arena; tutorial::Person* person = google::protobuf::Arena::Create<tutorial::Person>(&arena); // ... заполняем person ...Когда объект арены уничтожается, все сообщения, выделенные на нем, освобождаются. Для получения более подробной информации см. Руководство по Аренам.
-
Повторно используйте объекты сообщений, не находящиеся в арене, когда это возможно. Сообщения пытаются сохранять любую память, которую они выделяют, для повторного использования, даже когда они очищаются. Таким образом, если вы обрабатываете много сообщений одного и того же типа и схожей структуры последовательно, рекомендуется повторно использовать один и тот же объект сообщения каждый раз, чтобы снять нагрузку с распределителя памяти. Однако объекты могут стать раздутыми со временем, особенно если ваши сообщения различаются по "форме" или если вы иногда конструируете сообщение, которое намного больше обычного. Вы должны отслеживать размеры ваших объектов сообщений, вызывая метод
SpaceUsed, и удалять их, как только они станут слишком большими.Повторное использование сообщений в аренах может привести к неограниченному росту памяти. Повторное использование сообщений в куче безопаснее. Однако даже с сообщениями в куче вы все равно можете столкнуться с проблемами high water mark (максимального уровня) полей. Например, если вы видите сообщения:
a: [1, 2, 3, 4] b: [1]и
a: [1] b: [1, 2, 3, 4]и повторно используете сообщения, то оба поля будут иметь достаточно памяти для самого большого размера, который они видели. Так что если каждый вход имел только 5 элементов, повторно использованное сообщение будет иметь память для 8.
-
Распределитель памяти вашей системы может быть плохо оптимизирован для выделения множества маленьких объектов из нескольких потоков. Попробуйте использовать Google's TCMalloc вместо него.
Продвинутое использование
Protocol buffers имеют применения, выходящие за рамки простых методов доступа и сериализации. Непременно исследуйте Справочник по C++ API, чтобы увидеть, что еще вы можете сделать с ними.
Одной ключевой особенностью, предоставляемой классами сообщений протокола, является reflection (рефлексия). Вы можете перебирать поля сообщения и манипулировать их значениями без написания вашего кода против какого-либо конкретного типа сообщения. Очень полезный способ использования рефлексии — это преобразование сообщений протокола в другие кодировки и обратно, такие как XML или JSON. Более продвинутое использование рефлексии может заключаться в нахождении разниц между двумя сообщениями одного типа или в разработке своего рода "регулярных выражений для сообщений протокола", в которых вы можете писать выражения, соответствующие определенному содержимому сообщения. Если вы используете свое воображение, возможно применять Protocol Buffers к гораздо более широкому кругу проблем, чем вы могли изначально ожидать!
Рефлексия предоставляется интерфейсом
Message::Reflection.
Основы Protocol Buffer: Go
Базовое введение в работу с protocol buffers для программистов на Go.
Это руководство предоставляет базовое введение для программистов на Go в работу с protocol buffers, используя proto3 версию языка protocol buffers. На примере создания простого приложения оно показывает, как:
- Определять форматы сообщений в файле
.proto. - Использовать компилятор protocol buffer.
- Использовать Go API protocol buffer для записи и чтения сообщений.
Это не исчерпывающее руководство по использованию protocol buffers в Go. Для получения более подробной справочной информации см. Руководство по языку Protocol Buffer, Справочник по Go API, Руководство по сгенерированному коду на Go и Справочник по кодированию.
Проблемная область
Пример, который мы будем использовать, — это очень простое приложение "адресная книга", которое может читать и записывать контактные данные людей в файл и из файла. Каждый человек в адресной книге имеет имя, ID, адрес электронной почты и контактный номер телефона.
Как сериализовать и извлекать такие структурированные данные? Есть несколько способов решить эту проблему:
- Использовать gobs для сериализации структур данных Go. Это хорошее решение в специфичной для Go среде, но оно не работает хорошо, если вам нужно делиться данными с приложениями, написанными для других платформ.
- Вы можете придумать специальный способ кодирования элементов данных в единую строку — например, кодируя 4 целых числа как "12:3:-23:67". Это простой и гибкий подход, хотя он требует написания одноразового кода кодирования и разбора, и разбор накладывает небольшие затраты времени выполнения. Это лучше всего работает для кодирования очень простых данных.
- Сериализовать данные в XML. Этот подход может быть очень привлекательным, поскольку XML (вроде как) читаем человеком и есть библиотеки привязок для множества языков. Это может быть хорошим выбором, если вы хотите делиться данными с другими приложениями/проектами. Однако XML печально известен своей прожорливостью к месту, и кодирование/декодирование может наложить огромные штрафы на производительность приложений. Кроме того, навигация по DOM-дереву XML значительно сложнее, чем навигация по простым полям в структуре в обычных условиях.
Protocol buffers — это гибкое, эффективное, автоматизированное решение для
решения именно этой проблемы. С protocol buffers вы пишете .proto описание
структуры данных, которую хотите сохранить. На основе этого компилятор protocol buffer
создает класс, который реализует автоматическое кодирование и разбор данных protocol buffer
в эффективном бинарном формате. Сгенерированный класс предоставляет
геттеры и сеттеры для полей, составляющих protocol buffer, и заботится
о деталях чтения и записи protocol buffer как единого целого.
Важно, что формат protocol buffer поддерживает идею расширения формата со временем таким образом,
что код все еще может читать данные, закодированные в старом формате.
Где найти пример кода
Наш пример — это набор приложений командной строки для управления файлом данных
адресной книги, закодированным с использованием protocol buffers. Команда add_person_go добавляет
новую запись в файл данных. Команда list_people_go разбирает файл данных
и печатает данные в консоль.
Вы можете найти полный пример в директории examples репозитория GitHub.
Определение вашего формата протокола
Чтобы создать ваше приложение "адресная книга", вам нужно начать с файла .proto.
Определения в файле .proto просты: вы добавляете сообщение для
каждой структуры данных, которую хотите сериализовать, затем указываете имя и тип для
каждого поля в сообщении. В нашем примере файл .proto, который определяет
сообщения, это
addressbook.proto.
Файл .proto начинается с объявления пакета, которое помогает предотвратить
конфликты имен между разными проектами.
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
Опция go_package определяет путь импорта пакета, который
будет содержать весь сгенерированный код для этого файла. Имя пакета Go будет
последним компонентом пути импорта. Например, наш пример будет использовать
имя пакета "tutorialpb".
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
Далее идут ваши определения сообщений. Сообщение — это просто агрегат,
содержащий набор типизированных полей. Многие стандартные простые типы данных
доступны в качестве типов полей, включая bool, int32, float, double и string. Вы
также можете добавить дальнейшую структуру в ваши сообщения, используя другие типы сообщений в качестве
типов полей.
message Person {
string name = 1;
int32 id = 2; // Уникальный ID номер для этого человека.
string email = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
// Наш файл адресной книги - это просто один из таких.
message AddressBook {
repeated Person people = 1;
}
В приведенном выше примере сообщение Person содержит сообщения PhoneNumber,
в то время как сообщение AddressBook содержит сообщения Person. Вы даже можете определять
типы сообщений, вложенные внутрь других сообщений — как вы можете видеть, тип PhoneNumber
определен внутри Person. Вы также можете определить типы enum, если хотите,
чтобы одно из ваших полей имело одно из предопределенного списка значений — здесь вы хотите
указать, что номер телефона может быть одним из PHONE_TYPE_MOBILE,
PHONE_TYPE_HOME или PHONE_TYPE_WORK.
Маркеры " = 1", " = 2" на каждом элементе идентифицируют уникальный "тег", который поле использует в бинарном кодировании. Номера тегов 1-15 требуют на один байт меньше для кодирования, чем более высокие номера, поэтому в качестве оптимизации вы можете решить использовать эти теги для часто используемых или повторяющихся элементов, оставляя теги 16 и выше для реже используемых необязательных элементов. Каждый элемент в повторяющемся поле требует перекодировки номера тега, поэтому повторяющиеся поля являются особенно хорошими кандидатами для этой оптимизации.
Если значение поля не установлено, используется значение по умолчанию: ноль для числовых типов, пустая строка для строк, false для bool. Для встроенных сообщений значением по умолчанию всегда является "экземпляр по умолчанию" или "прототип" сообщения, у которого none (ни одно) из его полей не установлено. Вызов метода доступа для получения значения поля, которое не было явно установлено, всегда возвращает значение по умолчанию для этого поля.
Если поле repeated (повторяющееся), поле может повторяться любое количество раз
(включая ноль). Порядок повторяющихся значений будет сохранен в
protocol buffer. Думайте о повторяющихся полях как о динамически sized arrays (массивах переменного размера).
Вы найдете полное руководство по написанию файлов .proto — включая все
возможные типы полей — в
Руководстве по языку Protocol Buffer.
Однако не ищите возможности, похожие на наследование классов — protocol
buffers этого не делают.
Компиляция ваших Protocol Buffers
Теперь, когда у вас есть .proto, следующее, что нужно сделать, — это сгенерировать
классы, которые вам понадобятся для чтения и записи сообщений AddressBook
(и, следовательно, Person и PhoneNumber). Для этого вам нужно запустить компилятор
protocol buffer protoc на вашем .proto:
-
Если вы не установили компилятор, скачайте пакет и следуйте инструкциям в README.
-
Выполните следующую команду, чтобы установить плагин Go protocol buffers:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latestПлагин компилятора
protoc-gen-goбудет установлен в$GOBIN, по умолчанию в$GOPATH/bin. Он должен быть в вашем$PATH, чтобы компилятор протоколаprotocмог его найти. -
Теперь запустите компилятор, указав исходную директорию (где находится исходный код вашего приложения — используется текущая директория, если вы не предоставили значение), целевую директорию (куда вы хотите, чтобы сгенерированный код попал; часто та же, что и
$SRC_DIR) и путь к вашему.proto. В этом случае вы бы вызвали:protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.protoПоскольку вы хотите код Go, вы используете опцию
--go_out— похожие опции предоставляются для других поддерживаемых языков.
Это генерирует
github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go
в указанной вами целевой директории.
API Protocol Buffer
Генерация addressbook.pb.go дает вам следующие полезные типы:
- Структуру
AddressBookс полемPeople. - Структуру
Personс полями дляName,Id,EmailиPhones. - Структуру
Person_PhoneNumberс полями дляNumberиType. - Тип
Person_PhoneTypeи значение, определенное для каждого значения в перечисленииPerson.PhoneType.
Вы можете прочитать больше о деталях того, что именно генерируется, в Руководстве по сгенерированному коду на Go, но в основном вы можете рассматривать их как совершенно обычные типы Go.
Вот пример из
модульных тестов команды list_people
того, как вы можете создать экземпляр Person:
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
},
}
Написание сообщения
Вся цель использования protocol buffers — сериализовать ваши данные так, чтобы их
можно было разобрать в другом месте. В Go вы используете функцию Marshal
из библиотеки proto
(Marshal)
для сериализации ваших данных protocol buffer. Указатель на struct сообщения protocol buffer
реализует интерфейс proto.Message. Вызов
proto.Marshal возвращает protocol buffer, закодированный в его wire format (бинарном формате).
Например, мы используем эту функцию в
команде add_person:
book := &pb.AddressBook{}
// ...
// Записываем новую адресную книгу обратно на диск.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
Чтение сообщения
Чтобы разобрать закодированное сообщение, вы используете функцию Unmarshal
из библиотеки proto
(Unmarshal).
Вызов этого разбирает данные в in как protocol buffer и помещает
результат в book. Таким образом, чтобы разобрать файл в
команде list_people,
мы используем:
// Читаем существующую адресную книгу.
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
Расширение Protocol Buffer
Рано или поздно после того, как вы выпустите код, использующий ваш protocol buffer, вы несомненно захотите "улучшить" определение protocol buffer. Если вы хотите, чтобы ваши новые буферы были обратно совместимы, а ваши старые буферы были вперед совместимы — а вы почти certainly (несомненно) хотите этого — тогда есть некоторые правила, которым вы должны следовать. В новой версии protocol buffer:
- вы не должны изменять номера тегов любых существующих полей.
- вы можете удалять поля.
- вы можете добавлять новые поля, но вы должны использовать новые номера тегов (т.е. номера тегов, которые никогда не использовались в этом protocol buffer, даже удаленными полями).
(Есть некоторые исключения из этих правил, но они редко используются.)
Если вы следуете этим правилам, старый код будет happily (с радостью) читать новые сообщения и просто игнорировать любые новые поля. Для старого кода, сингулярные поля, которые были удалены, будут просто иметь свое значение по умолчанию, а удаленные повторяющиеся поля будут пусты. Новый код также будет прозрачно читать старые сообщения.
Однако имейте в виду, что новые поля не будут присутствовать в старых сообщениях, поэтому вам нужно будет сделать что-то разумное со значением по умолчанию. Используется зависящее от типа значение по умолчанию: для строк значением по умолчанию является пустая строка. Для булевых значений значением по умолчанию является false. Для числовых типов значением по умолчанию является ноль.
Основы Protocol Buffer: Dart
Базовое введение в работу с protocol buffers для программистов на Dart.
Данное руководство представляет собой базовое введение в работу с protocol buffers для программистов на Dart, используя версию языка protocol buffers proto3. На примере создания простого приложения показано, как:
- Определять форматы сообщений в файле
.proto. - Использовать компилятор protocol buffer.
- Использовать Dart API protocol buffer для записи и чтения сообщений.
Это не исчерпывающее руководство по использованию protocol buffers в Dart. Для получения более подробной справочной информации смотрите Руководство по языку Protocol Buffer, Обзор языка Dart, Справочник по Dart API, Руководство по сгенерированному коду для Dart и Справочник по кодированию.
Предметная область
Пример, который мы будем использовать, — это очень простое приложение "адресная книга", которое может читать и записывать контактные данные людей в файл и из файла. У каждого человека в адресной книге есть имя, идентификатор, адрес электронной почты и номер контактного телефона.
Как сериализовать и извлекать такие структурированные данные? Есть несколько способов решить эту проблему:
- Можно придумать собственный способ кодирования элементов данных в единую строку — например, кодирование 4 целых чисел как "12:3:-23:67". Это простой и гибкий подход, хотя он требует написания специального кода для кодирования и парсинга, а парсинг несет небольшие затраты времени выполнения. Это лучше всего подходит для кодирования очень простых данных.
- Сериализовать данные в XML. Этот подход может быть очень привлекательным, поскольку XML (в некоторой степени) читаем человеком, и для многих языков есть библиотеки привязок. Это может быть хорошим выбором, если вы хотите обмениваться данными с другими приложениями/проектами. Однако XML известен своей требовательностью к пространству, а его кодирование/декодирование может привести к огромным потерям производительности приложений. Кроме того, навигация по дереву XML DOM значительно сложнее, чем навигация по простым полям в классе.
Protocol buffers — это гибкое, эффективное, автоматизированное решение именно для этой проблемы. С protocol buffers вы пишете описание .proto структуры данных, которую хотите сохранить. На его основе компилятор protocol buffer создает класс, который реализует автоматическое кодирование и парсинг данных protocol buffer в эффективном бинарном формате. Сгенерированный класс предоставляет геттеры и сеттеры для полей, составляющих protocol buffer, и заботится о деталях чтения и записи protocol buffer как единого целого. Важно, что формат protocol buffer поддерживает идею расширения формата с течением времени таким образом, что код может читать данные, закодированные в старом формате.
Где найти код примера
Наш пример — это набор консольных приложений для управления файлом данных адресной книги, закодированным с помощью protocol buffers. Команда dart add_person.dart добавляет новую запись в файл данных. Команда dart list_people.dart разбирает файл данных и выводит данные в консоль.
Полный пример можно найти в директории examples репозитория GitHub.
Определение формата протокола
Чтобы создать приложение "адресная книга", нужно начать с файла .proto. Определения в файле .proto просты: вы добавляете сообщение для каждой структуры данных, которую хотите сериализовать, а затем указываете имя и тип для каждого поля в сообщении. В нашем примере файл .proto, определяющий сообщения, это addressbook.proto.
Файл .proto начинается с объявления пакета, который помогает предотвратить конфликты имен между разными проектами.
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
Далее идут определения сообщений. Сообщение — это просто агрегат, содержащий набор типизированных полей. Многие стандартные простые типы данных доступны в качестве типов полей, включая bool, int32, float, double и string. Вы также можете добавить дополнительную структуру в свои сообщения, используя другие типы сообщений в качестве типов полей.
message Person {
string name = 1;
int32 id = 2; // Уникальный идентификационный номер для этого человека.
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
// Наш файл адресной книги - это просто один из таких экземпляров.
message AddressBook {
repeated Person people = 1;
}
В приведенном выше примере сообщение Person содержит сообщения PhoneNumber, а сообщение AddressBook содержит сообщения Person. Вы можете даже определять типы сообщений, вложенные в другие сообщения — как видно, тип PhoneNumber определен внутри Person. Вы также можете определять типы enum, если хотите, чтобы одно из ваших полей имело одно из предопределенного списка значений — здесь вы хотите указать, что номер телефона может быть одним из PHONE_TYPE_MOBILE, PHONE_TYPE_HOME или PHONE_TYPE_WORK.
Маркеры " = 1", " = 2" на каждом элементе идентифицируют уникальный "тег", который это поле использует в бинарном кодировании. Для номеров тегов 1-15 требуется на один байт меньше для кодирования, чем для старших номеров, поэтому в качестве оптимизации вы можете решить использовать эти теги для часто используемых или повторяющихся элементов, оставляя теги 16 и выше для реже используемых необязательных элементов. Каждый элемент в повторяющемся поле требует перекодировки номера тега, поэтому повторяющиеся поля являются особенно хорошими кандидатами для этой оптимизации.
Если значение поля не установлено, используется значение по умолчанию: ноль для числовых типов, пустая строка для строк, false для булевых значений. Для встроенных сообщений значением по умолчанию всегда является "экземпляр по умолчанию" или "прототип" сообщения, у которого не установлены никакие поля. Вызов аксессора для получения значения поля, которое не было явно установлено, всегда возвращает значение по умолчанию для этого поля.
Если поле repeated, поле может повторяться любое количество раз (включая ноль). Порядок повторяющихся значений сохраняется в protocol buffer. Consider repeated fields as dynamically sized arrays.
Вы найдете полное руководство по написанию файлов .proto — включая все возможные типы полей — в Руководстве по языку Protocol Buffer. Не ищите средства, подобные наследованию классов — protocol buffers этого не делают.
Компиляция ваших Protocol Buffers
Теперь, когда у вас есть .proto, следующее, что нужно сделать, — сгенерировать классы, которые понадобятся для чтения и записи сообщений AddressBook (и, следовательно, Person и PhoneNumber). Для этого нужно запустить компилятор protocol buffer protoc для вашего .proto:
-
Если вы не установили компилятор, загрузите пакет и следуйте инструкциям в README.
-
Установите плагин Dart Protocol Buffer, как описано в его README. Исполняемый файл
bin/protoc-gen-dartдолжен быть в вашемPATH, чтобыprotocмог его найти. -
Теперь запустите компилятор, указав исходную директорию (где находится исходный код вашего приложения — используется текущая директория, если значение не указано), целевую директорию (куда вы хотите поместить сгенерированный код; часто совпадает с
$SRC_DIR) и путь к вашему.proto. В этом случае вы должны вызвать:protoc -I=$SRC_DIR --dart_out=$DST_DIR $SRC_DIR/addressbook.protoПоскольку вам нужен код на Dart, вы используете опцию
--dart_out— аналогичные опции предоставлены для других поддерживаемых языков.
Это генерирует addressbook.pb.dart в указанной вами целевой директории.
API Protocol Buffer
Генерация addressbook.pb.dart дает вам следующие полезные типы:
- Класс
AddressBookс геттеромList<Person> get people. - Класс
Personс методами доступа дляname,id,emailиphones. - Класс
Person_PhoneNumberс методами доступа дляnumberиtype. - Класс
Person_PhoneTypeсо статическими полями для каждого значения в перечисленииPerson.PhoneType.
Вы можете подробнее прочитать о деталях того, что именно генерируется, в Руководстве по сгенерированному коду для Dart.
Запись сообщения
Теперь давайте попробуем использовать ваши классы protocol buffer. Первое, что вы хотите, чтобы ваше приложение "адресная книга" умело делать, — это записывать личные данные в файл адресной книги. Для этого нужно создать и заполнить экземпляры ваших классов protocol buffer, а затем записать их в выходной поток.
Вот программа, которая читает AddressBook из файла, добавляет одного нового Person в него на основе пользовательского ввода и записывает новый AddressBook обратно в файл. Части, которые напрямую вызывают или ссылаются на код, сгенерированный компилятором protocol, выделены.
import 'dart:io';
import 'dart_tutorial/addressbook.pb.dart';
// Эта функция заполняет сообщение Person на основе пользовательского ввода.
Person promptForAddress() {
Person person = Person();
print('Enter person ID: ');
String input = stdin.readLineSync();
person.id = int.parse(input);
print('Enter name');
person.name = stdin.readLineSync();
print('Enter email address (blank for none) : ');
String email = stdin.readLineSync();
if (email.isNotEmpty) {
person.email = email;
}
while (true) {
print('Enter a phone number (or leave blank to finish): ');
String number = stdin.readLineSync();
if (number.isEmpty) break;
Person_PhoneNumber phoneNumber = Person_PhoneNumber();
phoneNumber.number = number;
print('Is this a mobile, home, or work phone? ');
String type = stdin.readLineSync();
switch (type) {
case 'mobile':
phoneNumber.type = Person_PhoneType.PHONE_TYPE_MOBILE;
break;
case 'home':
phoneNumber.type = Person_PhoneType.PHONE_TYPE_HOME;
break;
case 'work':
phoneNumber.type = Person_PhoneType.PHONE_TYPE_WORK;
break;
default:
print('Unknown phone type. Using default.');
}
person.phones.add(phoneNumber);
}
return person;
}
// Читает всю адресную книгу из файла, добавляет одного человека на основе
// пользовательского ввода, затем записывает её обратно в тот же файл.
main(List arguments) {
if (arguments.length != 1) {
print('Usage: add_person ADDRESS_BOOK_FILE');
exit(-1);
}
File file = File(arguments.first);
AddressBook addressBook;
if (!file.existsSync()) {
print('File not found. Creating new file.');
addressBook = AddressBook();
} else {
addressBook = AddressBook.fromBuffer(file.readAsBytesSync());
}
addressBook.people.add(promptForAddress());
file.writeAsBytes(addressBook.writeToBuffer());
}
Чтение сообщения
Конечно, адресная книга была бы не очень полезна, если бы вы не могли извлечь из нее информацию! Этот пример читает файл, созданный приведенным выше примером, и печатает всю информацию в нем.
import 'dart:io';
import 'dart_tutorial/addressbook.pb.dart';
import 'dart_tutorial/addressbook.pbenum.dart';
// Итерируется по всем людям в AddressBook и печатает информацию о них.
void printAddressBook(AddressBook addressBook) {
for (Person person in addressBook.people) {
print('Person ID: ${ person.id}');
print(' Name: ${ person.name}');
if (person.hasEmail()) {
print(' E-mail address:${ person.email}');
}
for (Person_PhoneNumber phoneNumber in person.phones) {
switch (phoneNumber.type) {
case Person_PhoneType.PHONE_TYPE_MOBILE:
print(' Mobile phone #: ');
break;
case Person_PhoneType.PHONE_TYPE_HOME:
print(' Home phone #: ');
break;
case Person_PhoneType.PHONE_TYPE_WORK:
print(' Work phone #: ');
break;
default:
print(' Unknown phone #: ');
break;
}
print(phoneNumber.number);
}
}
}
// Читает всю адресную книгу из файла и печатает всю
// информацию внутри.
main(List arguments) {
if (arguments.length != 1) {
print('Usage: list_person ADDRESS_BOOK_FILE');
exit(-1);
}
// Читает существующую адресную книгу.
File file = File(arguments.first);
AddressBook addressBook = AddressBook.fromBuffer(file.readAsBytesSync());
printAddressBook(addressBook);
}
Расширение Protocol Buffer
Рано или поздно после выпуска кода, использующего ваш protocol buffer, вы, несомненно, захотите "улучшить" его определение. Если вы хотите, чтобы ваши новые буферы были обратно совместимыми, а ваши старые буферы были向前совместимыми — а вы почти certainly этого хотите — тогда есть некоторые правила, которым вы должны следовать. В новой версии protocol buffer:
- вы не должны изменять номера тегов любых существующих полей.
- вы можете удалять поля.
- вы можете добавлять новые поля, но вы должны использовать новые номера тегов (т.е. номера тегов, которые никогда не использовались в этом protocol buffer, даже удаленными полями).
(Есть некоторые исключения из этих правил, но они редко используются.)
Если вы будете следовать этим правилам, старый код будет успешно читать новые сообщения и просто игнорировать любые новые поля. Для старого кода удаленные единичные поля будут просто иметь свое значение по умолчанию, а удаленные повторяющиеся поля будут пусты. Новый код также будет прозрачно читать старые сообщения.
Однако помните, что новые поля не будут присутствовать в старых сообщениях, поэтому вам нужно будет сделать что-то разумное со значением по умолчанию. Используется значение по умолчанию, зависящее от типа: для строк значением по умолчанию является пустая строка. Для булевых значений — false. Для числовых типов — ноль.
Основы Protocol Buffer: Python
Базовое введение в работу с protocol buffers для программистов на Python.
Это руководство предоставляет базовое введение для программистов на Python в работу с protocol buffers. На примере создания простого приложения оно показывает, как:
- Определять форматы сообщений в файле
.proto. - Использовать компилятор protocol buffer.
- Использовать Python API protocol buffer для записи и чтения сообщений.
Это не исчерпывающее руководство по использованию protocol buffers в Python. Для получения более подробной справочной информации см. Руководство по языку Protocol Buffer (proto2), Руководство по языку Protocol Buffer (proto3), Справочник по Python API, Руководство по сгенерированному коду на Python и Справочник по кодированию.
Проблемная область
Пример, который мы будем использовать, — это очень простое приложение "адресная книга", которое может читать и записывать контактные данные людей в файл и из файла. Каждый человек в адресной книге имеет имя, ID, адрес электронной почты и контактный номер телефона.
Как сериализовать и извлекать такие структурированные данные? Есть несколько способов решить эту проблему:
- Использовать Python pickling. Это подход по умолчанию, так как он встроен в язык, но он плохо справляется с эволюцией схемы, а также не работает очень хорошо, если вам нужно делиться данными с приложениями, написанными на C++ или Java.
- Вы можете придумать специальный способ кодирования элементов данных в единую строку — например, кодируя 4 целых числа как "12:3:-23:67". Это простой и гибкий подход, хотя он требует написания одноразового кода кодирования и разбора, и разбор накладывает небольшие затраты времени выполнения. Это лучше всего работает для кодирования очень простых данных.
- Сериализовать данные в XML. Этот подход может быть очень привлекательным, поскольку XML (вроде как) читаем человеком и есть библиотеки привязок для множества языков. Это может быть хорошим выбором, если вы хотите делиться данными с другими приложениями/проектами. Однако XML печально известен своей прожорливостью к месту, и кодирование/декодирование может наложить огромные штрафы на производительность приложений. Кроме того, навигация по DOM-дереву XML значительно сложнее, чем навигация по простым полям в классе в обычных условиях.
Вместо этих вариантов вы можете использовать protocol buffers. Protocol buffers — это
гибкое, эффективное, автоматизированное решение для решения именно этой проблемы. С
protocol buffers вы пишете .proto описание структуры данных, которую
хотите сохранить. На основе этого компилятор protocol buffer создает класс, который
реализует автоматическое кодирование и разбор данных protocol buffer в
эффективном бинарном формате. Сгенерированный класс предоставляет геттеры и сеттеры для
полей, составляющих protocol buffer, и заботится о деталях
чтения и записи protocol buffer как единого целого. Важно, что формат protocol buffer
поддерживает идею расширения формата со временем таким образом, что
код все еще может читать данные, закодированные в старом формате.
Где найти пример кода
Пример кода включен в пакет с исходным кодом, в директории "examples". Скачайте его здесь.
Определение вашего формата протокола
Чтобы создать ваше приложение "адресная книга", вам нужно начать с файла .proto.
Определения в файле .proto просты: вы добавляете сообщение для
каждой структуры данных, которую хотите сериализовать, затем указываете имя и тип для
каждого поля в сообщении. Вот файл .proto, который определяет ваши сообщения,
addressbook.proto.
edition = "2023";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2 [default = PHONE_TYPE_HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
Как вы можете видеть, синтаксис похож на C++ или Java. Давайте пройдемся по каждой части файла и посмотрим, что она делает.
Файл .proto начинается с объявления пакета, которое помогает предотвратить
конфликты имен между разными проектами. В Python пакеты обычно
определяются структурой директорий, поэтому package, который вы определяете в вашем .proto
файле, не окажет влияния на сгенерированный код. Однако вы все равно должны
объявить его, чтобы избежать коллизий имен в пространстве имен Protocol Buffers, а также
в неподдерживаемых языках.
Далее идут ваши определения сообщений. Сообщение — это просто агрегат,
содержащий набор типизированных полей. Многие стандартные простые типы данных
доступны в качестве типов полей, включая bool, int32, float, double и string. Вы
также можете добавить дальнейшую структуру в ваши сообщения, используя другие типы сообщений в качестве
типов полей — в приведенном выше примере сообщение Person содержит PhoneNumber
сообщения, в то время как сообщение AddressBook содержит Person сообщения. Вы можете
даже определять типы сообщений, вложенные внутрь других сообщений — как вы можете видеть,
тип PhoneNumber определен внутри Person. Вы также можете определить типы enum
если хотите, чтобы одно из ваших полей имело одно из предопределенного списка значений —
здесь вы хотите указать, что номер телефона может быть одним из следующих типов телефонов:
PHONE_TYPE_MOBILE, PHONE_TYPE_HOME или PHONE_TYPE_WORK.
Маркеры " = 1", " = 2" на каждом элементе идентифицируют уникальный "тег", который поле использует в бинарном кодировании. Номера тегов 1-15 требуют на один байт меньше для кодирования, чем более высокие номера, поэтому в качестве оптимизации вы можете решить использовать эти теги для часто используемых или повторяющихся элементов, оставляя теги 16 и выше для реже используемых необязательных элементов. Каждый элемент в повторяющемся поле требует перекодировки номера тега, поэтому повторяющиеся поля являются particularly good candidates (особенно хорошими кандидатами) для этой оптимизации.
Вы найдете полное руководство по написанию файлов .proto — включая все
возможные типы полей — в
Руководстве по языку Protocol Buffer.
Однако не ищите возможности, похожие на наследование классов — protocol
buffers этого не делают.
Компиляция ваших Protocol Buffers
Теперь, когда у вас есть .proto, следующее, что нужно сделать, — это сгенерировать
классы, которые вам понадобятся для чтения и записи сообщений AddressBook
(и, следовательно, Person и PhoneNumber). Для этого вам нужно запустить компилятор
protocol buffer protoc на вашем .proto:
-
Если вы не установили компилятор, скачайте пакет и следуйте инструкциям в README.
-
Теперь запустите компилятор, указав исходную директорию (где находится исходный код вашего приложения — используется текущая директория, если вы не предоставили значение), целевую директорию (куда вы хотите, чтобы сгенерированный код попал; часто та же, что и
$SRC_DIR) и путь к вашему.proto. В этом случае вы...:protoc --proto_path=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.protoПоскольку вы хотите классы Python, вы используете опцию
--python_out— похожие опции предоставляются для других поддерживаемых языков.Protoc также может генерировать Python stubs (заглушки) (
.pyi) с помощью--pyi_out.
Это генерирует addressbook_pb2.py (или addressbook_pb2.pyi) в указанной вами
целевой директории.
API Protocol Buffer
В отличие от случаев, когда вы генерируете код protocol buffer для Java и C++, компилятор Python protocol
buffer не генерирует ваш код доступа к данным напрямую. Вместо этого
(как вы увидите, если посмотрите на addressbook_pb2.py) он генерирует специальные
дескрипторы для всех ваших сообщений, перечислений и полей, и некоторые mysteriously
empty classes (таинственно пустые классы), по одному для каждого типа сообщения:
import google3
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
from google.protobuf.internal import builder as _builder
_runtime_version.ValidateProtobufRuntimeVersion(
_runtime_version.Domain.GOOGLE_INTERNAL,
0,
20240502,
0,
'',
'main.proto'
)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nmain.proto\x12\x08tutorial\"\xa3\x02\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\x05\x12\r\n\x05\x65mail\x18\x03 \x01(\t\x12,\n\x06phones\x18\x04 \x03(\x0b\x32\x1c.tutorial.Person.PhoneNumber\x1aX\n\x0bPhoneNumber\x12\x0e\n\x06number\x18\x01 \x01(\t\x12\x39\n\x04type\x18\x02 \x01(\x0e\x32\x1a.tutorial.Person.PhoneType:\x0fPHONE_TYPE_HOME\"h\n\tPhoneType\x12\x1a\n\x16PHONE_TYPE_UNSPECIFIED\x10\x00\x12\x15\n\x11PHONE_TYPE_MOBILE\x10\x01\x12\x13\n\x0fPHONE_TYPE_HOME\x10\x02\x12\x13\n\x0fPHONE_TYPE_WORK\x10\x03\"/\n\x0b\x41\x64\x64ressBook\x12 \n\x06people\x18\x01 \x03(\x0b\x32\x10.tutorial.Person')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google3.main_pb2', _globals)
if not _descriptor._USE_C_DESCRIPTORS:
DESCRIPTOR._loaded_options = None
_globals['_PERSON']._serialized_start=25
_globals['_PERSON']._serialized_end=316
_globals['_PERSON_PHONENUMBER']._serialized_start=122
_globals['_PERSON_PHONENUMBER']._serialized_end=210
_globals['_PERSON_PHONETYPE']._serialized_start=212
_globals['_PERSON_PHONETYPE']._serialized_end=316
_globals['_ADDRESSBOOK']._serialized_start=318
_globals['_ADDRESSBOOK']._serialized_end=365
# @@protoc_insertion_point(module_scope)
Важная строка в каждом классе — __metaclass__ = reflection.GeneratedProtocolMessageType. Хотя детали того, как работают Python
метаклассы, выходят за рамки этого руководства, вы можете думать о них как о
шаблоне для создания классов. Во время загрузки метакласс
GeneratedProtocolMessageType использует указанные дескрипторы для
создания всех методов Python, которые вам нужны для работы с каждым типом сообщения, и добавляет
их в соответствующие классы. Затем вы можете использовать полностью заполненные классы в
вашем коде.
Конечный эффект всего этого заключается в том, что вы можете использовать класс Person так, как если бы он
определял каждое поле базового класса Message как обычное поле. Например,
вы можете написать:
import addressbook_pb2
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
phone = person.phones.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.Person.PHONE_TYPE_HOME
Обратите внимание, что эти присваивания не просто добавляют произвольные новые поля к
generic Python object (общему объекту Python). Если вы попытаетесь присвоить полю значение, которое не определено
в файле .proto, будет вызвано AttributeError. Если вы присвоите полю
значение неправильного типа, будет вызвано TypeError. Также, чтение
значения поля до того, как оно было установлено, возвращает значение по умолчанию.
person.no_such_field = 1 # вызывает AttributeError
person.id = "1234" # вызывает TypeError
Для получения дополнительной информации о том, какие именно члены компилятор протоколов генерирует для любого конкретного определения поля, см. Справочник по сгенерированному коду на Python.
Перечисления
Перечисления расширяются метаклассом в набор символьных констант с
целочисленными значениями. Так, например, константа
addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK имеет значение 2.
Стандартные методы сообщений
Каждый класс сообщений также содержит ряд других методов, которые позволяют вам проверять или манипулировать всем сообщением, включая:
IsInitialized(): проверяет, установлены ли все обязательные поля.__str__(): возвращает удобочитаемое представление сообщения, особенно полезное для отладки. (Обычно вызывается какstr(message)илиprint message.)CopyFrom(other_msg): перезаписывает сообщение значениями данного сообщения.Clear(): очищает все элементы обратно в пустое состояние.
Эти методы реализуют интерфейс Message. Для получения дополнительной информации см.
полную документацию API для Message.
Разбор и сериализация
Наконец, каждый класс protocol buffer имеет методы для записи и чтения сообщений вашего выбранного типа, используя бинарный формат protocol buffer. К ним относятся:
SerializeToString(): сериализует сообщение и возвращает его в виде строки. Обратите внимание, что байты являются бинарными, а не текстовыми; мы используем типstrтолько как удобный контейнер.ParseFromString(data): разбирает сообщение из данной строки.
Это лишь пара из предоставленных вариантов для разбора и сериализации.
Снова см.
справочник по API Message
для получения полного списка.
Вы также можете легко сериализовать сообщения в JSON и из JSON. Модуль json_format
предоставляет помощники для этого:
MessageToJson(message): сериализует сообщение в строку JSON.Parse(json_string, message): разбирает строку JSON в данное сообщение.
Например:
from google.protobuf import json_format
import addressbook_pb2
person = addressbook_pb2.Person()
person.id = 1234
person.name = "John Doe"
person.email = "jdoe@example.com"
# Сериализовать в JSON
json_string = json_format.MessageToJson(person)
# Разобрать из JSON
new_person = addressbook_pb2.Person()
json_format.Parse(json_string, new_person)
{{% alert title="Важно" color="warning" %}} Protocol Buffers и объектно-ориентированный дизайн
Классы Protocol buffer — это, по сути, держатели данных (как структуры в C), которые
не предоставляют дополнительной функциональности; они не являются хорошими полноправными
гражданами в объектной модели. Если вы хотите добавить более богатое поведение в сгенерированный
класс, лучший способ сделать это — обернуть сгенерированный класс protocol buffer в
специфичный для приложения класс. Обертывание protocol buffers также является хорошей идеей, если
вы не контролируете дизайн файла .proto (если, скажем, вы
используете его из другого проекта). В этом случае вы можете использовать класс-обертку
для создания интерфейса, лучше подходящего для уникальной среды вашего
приложения: скрывая некоторые данные и методы, предоставляя удобные функции и т.д.
Вы никогда не должны добавлять поведение к сгенерированным классам, наследуясь от
них. Это нарушит внутренние механизмы и, в любом случае, не является хорошей объектно-ориентированной
практикой. {{% /alert %}}
Написание сообщения
Теперь давайте попробуем использовать ваши классы protocol buffer. Первое, что вы хотите, чтобы ваше приложение "адресная книга" могло делать, — это записывать личные данные в ваш файл адресной книги. Для этого вам нужно создать и заполнить экземпляры ваших классов protocol buffer, а затем записать их в выходной поток.
Вот программа, которая читает AddressBook из файла, добавляет одного нового
Person в него на основе пользовательского ввода и записывает новый AddressBook обратно в
файл снова. Части, которые напрямую вызывают или ссылаются на код, сгенерированный
компилятором протокола, выделены.
#!/usr/bin/env python3
import addressbook_pb2
import sys
# Эта функция заполняет сообщение Person на основе пользовательского ввода.
def PromptForAddress(person):
person.id = int(input("Введите ID человека: "))
person.name = input("Введите имя: ")
email = input("Введите адрес электронной почты (пусто для отсутствия): ")
if email != "":
person.email = email
while True:
number = input("Введите номер телефона (или оставьте пустым для завершения): ")
if number == "":
break
phone_number = person.phones.add()
phone_number.number = number
phone_type = input("Это мобильный, домашний или рабочий телефон? ")
if phone_type == "mobile":
phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_MOBILE
elif phone_type == "home":
phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME
elif phone_type == "work":
phone_number.type = addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK
else:
print("Неизвестный тип телефона; оставляем значение по умолчанию.")
# Главная процедура: Читает всю адресную книгу из файла,
# добавляет одного человека на основе пользовательского ввода, затем записывает её обратно в тот же
# файл.
if len(sys.argv) != 2:
print("Использование:", sys.argv[0], "ФАЙЛ_АДРЕСНОЙ_КНИГИ")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Читаем существующую адресную книгу.
try:
with open(sys.argv[1], "rb") as f:
address_book.ParseFromString(f.read())
except IOError:
print(sys.argv[1] + ": Не удалось открыть файл. Создается новый.")
# Добавляем адрес.
PromptForAddress(address_book.people.add())
# Записываем новую адресную книгу обратно на диск.
with open(sys.argv[1], "wb") as f:
f.write(address_book.SerializeToString())
Чтение сообщения
Конечно, адресная книга была бы не очень полезна, если бы вы не могли получить из нее никакой информации! Этот пример читает файл, созданный приведенным выше примером, и печатает всю информацию в нем.
#!/usr/bin/env python3
import addressbook_pb2
import sys
# Перебирает всех людей в AddressBook и печатает информацию о них.
def ListPeople(address_book):
for person in address_book.people:
print("ID человека:", person.id)
print(" Имя:", person.name)
if person.HasField('email'):
print(" Адрес электронной почты:", person.email)
for phone_number in person.phones:
if phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_MOBILE:
print(" Мобильный телефон #: ", end="")
elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_HOME:
print(" Домашний телефон #: ", end="")
elif phone_number.type == addressbook_pb2.Person.PhoneType.PHONE_TYPE_WORK:
print(" Рабочий телефон #: ", end="")
print(phone_number.number)
# Главная процедура: Читает всю адресную книгу из файла и печатает всю
# информацию внутри.
if len(sys.argv) != 2:
print("Использование:", sys.argv[0], "ФАЙЛ_АДРЕСНОЙ_КНИГИ")
sys.exit(-1)
address_book = addressbook_pb2.AddressBook()
# Читаем существующую адресную книгу.
with open(sys.argv[1], "rb") as f:
address_book.ParseFromString(f.read())
ListPeople(address_book)
Расширение Protocol Buffer
Рано или поздно после того, как вы выпустите код, использующий ваш protocol buffer, вы несомненно захотите "улучшить" определение protocol buffer. Если вы хотите, чтобы ваши новые буферы были обратно совместимы, а ваши старые буферы были вперед совместимы — а вы почти certainly (несомненно) хотите этого — тогда есть некоторые правила, которым вы должны следовать. В новой версии protocol buffer:
- вы не должны изменять номера тегов любых существующих полей.
- вы не должны добавлять или удалять любые обязательные поля.
- вы можете удалять необязательные или повторяющиеся поля.
- вы можете добавлять новые необязательные или повторяющиеся поля, но вы должны использовать новые номера тегов (то есть номера тегов, которые никогда не использовались в этом protocol buffer, даже удаленными полями).
(Есть некоторые исключения из этих правил, но они редко используются.)
Если вы следуете этим правилам, старый код будет happily (с радостью) читать новые сообщения и просто
игнорировать любые новые поля. Для старого кода, необязательные поля, которые были удалены,
будут просто иметь свое значение по умолчанию, а удаленные повторяющиеся поля будут пусты. Новый
код также будет прозрачно читать старые сообщения. Однако имейте в виду, что новые
необязательные поля не будут присутствовать в старых сообщениях, поэтому вам нужно будет либо
проверять явно, установлены ли они с помощью HasField('field_name'), либо предоставить
разумное значение по умолчанию в вашем .proto файле с [default = value] после
номера тега. Если значение по умолчанию не указано для необязательного элемента,
используется значение по умолчанию, зависящее от типа: для строк значением по умолчанию является
пустая строка. Для булевых значений значением по умолчанию является false. Для числовых типов
значением по умолчанию является ноль. Также обратите внимание, что если вы добавили новое повторяющееся поле,
ваш новый код не сможет определить, было ли оно оставлено пустым (новым кодом)
или никогда не устанавливалось вообще (старым кодом), поскольку для него нет проверки HasField.
Продвинутое использование
Protocol buffers имеют применения, выходящие за рамки простых методов доступа и сериализации. Непременно исследуйте Справочник по Python API, чтобы увидеть, что еще вы можете сделать с ними.
Одной ключевой особенностью, предоставляемой классами сообщений протокола, является reflection (рефлексия). Вы можете перебирать поля сообщения и манипулировать их значениями без написания вашего кода против какого-либо конкретного типа сообщения. Очень полезный способ использования рефлексии — это преобразование сообщений протокола в другие кодировки и обратно, такие как XML или JSON (см. Разбор и сериализация для примера). Более продвинутое использование рефлексии может заключаться в нахождении разниц между двумя сообщениями одного типа, или в разработке своего рода "регулярных выражений для сообщений протокола", в которых вы можете писать выражения, соответствующие определенному содержимому сообщения. Если вы используете свое воображение, возможно применять Protocol Buffers к гораздо более широкому кругу проблем, чем вы могли изначально ожидать!
Рефлексия предоставляется как часть
интерфейса Message.
RUST
Руководство по сгенерированному коду на Rust
Описывает API объектов сообщений, которые компилятор protocol buffer генерирует для любого данного определения протокола.
На этой странице описано, какой именно код Rust компилятор protocol buffer генерирует для любого данного определения протокола.
Этот документ охватывает, как компилятор protocol buffer генерирует код Rust для proto2, proto3 и редакций protobuf. Любые различия между сгенерированным кодом для proto2, proto3 и редакций выделены. Вам следует прочитать руководство по языку proto2, руководство по языку proto3 или руководство по редакциям перед чтением этого документа.
Protobuf Rust
Protobuf Rust — это реализация protocol buffers, разработанная для возможности находиться поверх других существующих реализаций protocol buffer, которые мы называем 'ядрами' (kernels).
Решение поддерживать несколько не-Rust ядер значительно повлияло на наш публичный API,
включая выбор использования пользовательских типов, таких как ProtoStr, вместо
стандартных типов Rust, таких как str. См.
Проектные решения Rust Proto
для получения дополнительной информации по этой теме.
Генерируемые имена файлов
Каждый rust_proto_library будет скомпилирован как один crate (пакет). Что наиболее важно, для
каждого файла .proto в srcs соответствующего proto_library создается один файл Rust,
и все эти файлы образуют единый crate.
Файлы, генерируемые компилятором, различаются между ядрами. В общем случае, имена
выходных файлов вычисляются путем взятия имени файла .proto и
замены расширения.
Генерируемые файлы:
- Ядро C++:
.c.pb.rs- сгенерированный код Rust.pb.thunks.cc- сгенерированные thunks C++ (код-клей, который вызывает код Rust, и который делегирует вызовы C++ Protobuf API).
- Ядро C++ Lite:
- <то же, что и для ядра C++>
- Ядро UPB
.u.pb.rs- сгенерированный код Rust.
Каждый proto_library также будет иметь файл generated.rs, который рассматривается как
точка входа для crate. Этот файл будет реэкспортировать символы из всех
остальных файлов Rust в crate.
Пакеты
В отличие от большинства других языков, объявления package в файлах .proto
не используются в кодогенерации Rust. Вместо этого, каждый целевой объект rust_proto_library(name = "some_rust_proto")
выпускает crate с именем some_rust_proto, который содержит
сгенерированный код для всех файлов .proto в целевом объекте.
Сообщения
Для объявления сообщения:
message Foo {}
Компилятор генерирует структуру с именем Foo. Структура Foo определяет
следующие ассоциированные функции и методы:
Ассоциированные функции
fn new() -> Self: Создает новый экземплярFoo.
Трейты
По ряду причин, включая размер gencode, проблемы с коллизиями имен и стабильность gencode, большая часть распространенной функциональности для сообщений реализована в виде трейтов, а не как inherent implementations (встроенные реализации).
Большинству пользователей следует импортировать наш prelude (прелюдию), которая включает только трейты и наш
макрос proto! и никакие другие типы (use protobuf::prelude::*). Если вы предпочитаете
избегать прелюдий, вы всегда можете импортировать конкретные трейты по мере необходимости (см.
документацию здесь для имен и определений трейтов, если вы хотите импортировать их напрямую).
fn parse(data: &[u8]) -> Result<Self, ParseError>: Разбирает новый экземпляр сообщения.fn parse_dont_enforce_required(data: &[u8]) -> Result<Self, ParseError>: То же, что иparse, но не завершается ошибкой при отсутствии proto2requiredполей.fn clear(&mut self): Очищает сообщение.fn clear_and_parse(&mut self, data: &[u8]) -> Result<(), ParseError>: Очистка и разбор в существующий экземпляр.fn clear_and_parse_dont_enforce_required(&mut self, data: &[u8]) -> Result<(), ParseError>: То же, что иparse, но не завершается ошибкой при отсутствии proto2requiredполей.fn serialize(&self) -> Result<Vec<u8>, SerializeError>: Сериализует сообщение в формат Protobuf wire format. Сериализация может завершиться неудачей, но редко. Причины неудачи включают в себя, если представление превышает максимальный размер закодированного сообщения (должен быть меньше 2 GiB), иrequiredполя (proto2), которые не установлены.fn take_from(&mut self, other): Перемещаетotherвself, отбрасывая любое предыдущее состояние, которое содержалself.fn copy_from(&mut self, other): Копируетotherвself, отбрасывая любое предыдущее состояние, которое содержалself.otherне изменяется.fn merge_from(&mut self, other): Объединяетotherвself.fn as_view(&self) -> FooView<'_>: Возвращает неизменяемый handle (представление, view) дляFoo. Это более подробно рассматривается в разделе о proxy types (типах-посредниках).fn as_mut(&mut self) -> FooMut<'_>: Возвращает изменяемый handle (mut) дляFoo. Это более подробно рассматривается в разделе о proxy types.
Foo дополнительно реализует следующие стандартные трейты:
std::fmt::Debugstd::default::Defaultstd::clone::Clonestd::marker::Sendstd::marker::Sync
Плавное создание новых экземпляров
Дизайн API сеттеров следует нашим установленным идиомам Protobuf, но
многословие при создании новых экземпляров является небольшой проблемой в некоторых других
языках. Чтобы смягчить это, мы предлагаем макрос proto!, который можно использовать для
более краткого/плавного создания новых экземпляров.
Например, вместо написания этого:
#![allow(unused)] fn main() { let mut msg = SomeMsg::new(); msg.set_x(1); msg.set_y("hello"); msg.some_submessage_mut().set_z(42); }
Этот макрос можно использовать, чтобы написать это следующим образом:
#![allow(unused)] fn main() { let msg = proto!(SomeMsg { x: 1, y: "hello", some_submsg: SomeSubmsg { z: 42 } }); }
Типы-посредники сообщений
По ряду технических причин мы решили избегать использования нативных ссылок Rust
(&T и &mut T) в определенных случаях. Вместо этого нам нужно выражать
эти концепции с помощью типов — View и Mut. Эти ситуации — это разделяемые и
изменяемые ссылки на:
- Сообщения
- Повторяющиеся поля
- Поля-карты
Например, компилятор создает структуры FooView<'a> и FooMut<'msg>
наряду с Foo. Эти типы используются вместо &Foo и &mut Foo, и
они ведут себя так же, как нативные ссылки Rust, с точки зрения поведения borrow checker.
Так же, как и нативные заимствования, View являются Copy, и borrow checker будет
обеспечивать, чтобы у вас могло быть либо любое количество View, либо не более одного Mut одновременно.
Для целей этой документации мы сосредотачиваемся на описании всех методов,
создаваемых для owned message type (типа сообщения, которым владеют) (Foo). Подмножество этих функций с
получателем &self также будет включено в FooView<'msg>. Подмножество этих
функций с получателем либо &self, либо &mut self также будет включено в
FooMut<'msg>.
Чтобы создать owned message type из типа View / Mut, вызовите to_owned(), что
создает глубокую копию.
См. соответствующий раздел в нашей документации по проектным решениям для более подробного обсуждения, почему был сделан этот выбор.
Вложенные типы
Для объявления сообщения:
message Foo {
message Bar {
enum Baz { ... }
}
}
В дополнение к структуре с именем Foo создается модуль с именем foo для
содержания структуры для Bar. И аналогично вложенный модуль с именем bar для
содержания глубоко вложенного перечисления Baz:
#![allow(unused)] fn main() { pub struct Foo {} pub mod foo { pub struct Bar {} pub mod bar { pub struct Baz { ... } } } }
Поля
В дополнение к методам, описанным в предыдущем разделе, компилятор protocol
buffer генерирует набор методов доступа для каждого поля, определенного
внутри сообщения в файле .proto.
В соответствии со стилем Rust, методы пишутся в нижнем регистре/snake-case, например
has_foo() и clear_foo(). Обратите внимание, что капитализация части имени поля
в методе доступа сохраняет стиль из исходного .proto файла, который,
в свою очередь, должен быть в нижнем регистре/snake-case согласно
руководству по стилю файлов .proto.
Поля с явным присутствием
Явное присутствие означает, что поле различает значение по умолчанию и
неустановленное значение. В proto2 поля optional имеют явное присутствие. В proto3
только поля сообщений и поля oneof или optional имеют явное присутствие.
Присутствие устанавливается с помощью опции
features.field_presence
в редакциях.
Числовые поля
Для этого определения поля:
int32 foo = 1;
Компилятор генерирует следующие методы доступа:
fn has_foo(&self) -> bool: Возвращаетtrue, если поле установлено.fn foo(&self) -> i32: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.fn foo_opt(&self) -> protobuf::Optional<i32>: Возвращает optional с вариантомSet(value), если поле установлено, илиUnset(default value), если оно не установлено.fn set_foo(&mut self, val: i32): Устанавливает значение поля. После вызова этогоhas_foo()будет возвращатьtrue, аfoo()будет возвращатьvalue.fn clear_foo(&mut self): Очищает значение поля. После вызова этогоhas_foo()будет возвращатьfalse, аfoo()будет возвращать значение по умолчанию.
Для других числовых типов полей (включая bool) int32 заменяется на
соответствующий тип Rust в соответствии с
таблицей скалярных типов значений.
Строковые поля и поля bytes
Для этих определений полей:
string foo = 1;
bytes foo = 1;
Компилятор генерирует следующие методы доступа:
fn has_foo(&self) -> bool: Возвращаетtrue, если поле установлено.fn foo(&self) -> &protobuf::ProtoStr: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.fn foo_opt(&self) -> protobuf::Optional<&ProtoStr>: Возвращает optional с вариантомSet(value), если поле установлено, илиUnset(default value)если оно не установлено.fn set_foo(&mut self, val: impl IntoProxied<ProtoString>): Устанавливает значение поля.fn clear_foo(&mut self): Очищает значение поля. После вызова этогоhas_foo()будет возвращатьfalse, аfoo()будет возвращать значение по умолчанию.
Для полей типа bytes компилятор сгенерирует тип ProtoBytes
вместо этого.
Поля перечислений
Для этого определения перечисления в любой версии синтаксиса proto:
enum Bar {
BAR_UNSPECIFIED = 0;
BAR_VALUE = 1;
BAR_OTHER_VALUE = 2;
}
Компилятор генерирует структуру, где каждый вариант является ассоциированной константой:
#![allow(unused)] fn main() { #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct Bar(i32); impl Bar { pub const Unspecified: Bar = Bar(0); pub const Value: Bar = Bar(1); pub const OtherValue: Bar = Bar(2); } }
Для этого определения поля:
Bar foo = 1;
Компилятор генерирует следующие методы доступа:
fn has_foo(&self) -> bool: Возвращаетtrue, если поле установлено.fn foo(&self) -> Bar: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.fn foo_opt(&self) -> Optional<Bar>: Возвращает optional с вариантомSet(value), если поле установлено, илиUnset(default value), если оно не установлено.fn set_foo(&mut self, val: Bar): Устанавливает значение поля. После вызова этогоhas_foo()будет возвращатьtrue, аfoo()будет возвращатьvalue.fn clear_foo(&mut self): Очищает значение поля. После вызова этогоhas_foo()будет возвращать false, аfoo()будет возвращать значение по умолчанию.
Встроенные поля сообщений
Для типа сообщения Bar из любой версии синтаксиса proto:
message Bar {}
Для любого из этих определений полей:
message MyMessage {
Bar foo = 1;
}
Компилятор сгенерирует следующие методы доступа:
fn foo(&self) -> BarView<'_>: Возвращает view текущего значения поля. Если поле не установлено, возвращает пустое сообщение.fn foo_mut(&mut self) -> BarMut<'_>: Возвращает изменяемый handle к текущему значению поля. Устанавливает поле, если оно не установлено. После вызова этого методаhas_foo()возвращает true.fn foo_opt(&self) -> protobuf::Optional<BarView>: Если поле установлено, возвращает вариантSetс егоvalue. Иначе возвращает вариантUnsetсо значением по умолчанию.fn set_foo(&mut self, value: impl protobuf::IntoProxied<Bar>): Устанавливает поле вvalue. После вызова этого методаhas_foo()возвращаетtrue.fn has_foo(&self) -> bool: Возвращаетtrue, если поле установлено.fn clear_foo(&mut self): Очищает поле. После вызова этого методаhas_foo()возвращаетfalse.
Поля с неявным присутствием (proto3 и редакции)
Неявное присутствие означает, что поле не различает значение по умолчанию и
неустановленное значение. В proto3 поля по умолчанию имеют неявное присутствие. В
редакциях вы можете объявить поле с неявным присутствием, установив
функцию field_presence в IMPLICIT.
Числовые поля
Для этих определений полей:
// proto3
int32 foo = 1;
// редакции
message MyMessage {
int32 foo = 1 [features.field_presence = IMPLICIT];
}
Компилятор генерирует следующие методы доступа:
fn foo(&self) -> i32: Возвращает текущее значение поля. Если поле не установлено, возвращает0.fn set_foo(&mut self, val: i32): Устанавливает значение поля.
Для других числовых типов полей (включая bool) int32 заменяется на
соответствующий тип Rust в соответствии с
таблицей скалярных типов значений.
Строковые поля и поля bytes
Для этих определений полей:
// proto3
string foo = 1;
bytes foo = 1;
// редакции
string foo = 1 [features.field_presence = IMPLICIT];
bytes bar = 2 [features.field_presence = IMPLICIT];
Компилятор сгенерирует следующие методы доступа:
fn foo(&self) -> &ProtoStr: Возвращает текущее значение поля. Если поле не установлено, возвращает пустую строку/пустые байты.fn set_foo(&mut self, value: IntoProxied<ProtoString>): Устанавливает поле вvalue.
Для полей типа bytes компилятор сгенерирует тип ProtoBytes
вместо этого.
Сингулярные строковые поля и поля bytes с поддержкой Cord
[ctype = CORD] позволяет байтам и строкам храниться как
absl::Cord
в C++ Protobuf. absl::Cord в настоящее время не имеет эквивалентного типа в
Rust. Protobuf Rust использует перечисление для представления поля cord:
#![allow(unused)] fn main() { enum ProtoStringCow<'a> { Owned(ProtoString), Borrowed(&'a ProtoStr) } }
В обычном случае, для маленьких строк, absl::Cord хранит свои данные как
непрерывную строку. В этом случае методы доступа cord возвращают
ProtoStringCow::Borrowed. Если лежащий в основе absl::Cord не является непрерывным,
метод доступа копирует данные из cord в owned ProtoString и
возвращает ProtoStringCow::Owned. ProtoStringCow реализует
Deref<Target=ProtoStr>.
Для любого из этих определений полей:
optional string foo = 1 [ctype = CORD];
string foo = 1 [ctype = CORD];
optional bytes foo = 1 [ctype = CORD];
bytes foo = 1 [ctype = CORD];
Компилятор генерирует следующие методы доступа:
fn my_field(&self) -> ProtoStringCow<'_>: Возвращает текущее значение поля. Если поле не установлено, возвращает пустую строку/пустые байты.fn set_my_field(&mut self, value: IntoProxied<ProtoString>): Устанавливает поле вvalue. После вызова этой функцииfoo()возвращаетvalueиhas_foo()возвращаетtrue.fn has_foo(&self) -> bool: Возвращаетtrue, если поле установлено.fn clear_foo(&mut self): Очищает значение поля. После вызова этогоhas_foo()возвращаетfalse, аfoo()возвращает значение по умолчанию. Cords еще не реализованы.
Для полей типа bytes компилятор генерирует тип ProtoBytesCow
вместо этого.
Компилятор генерирует следующие методы доступа:
fn foo(&self) -> &ProtoStr: Возвращает текущее значение поля. Если поле не установлено, возвращает пустую строку/пустые байты.fn set_foo(&mut self, value: impl IntoProxied<ProtoString>): Устанавливает поле вvalue.
Поля перечислений
Для типа перечисления:
enum Bar {
BAR_UNSPECIFIED = 0;
BAR_VALUE = 1;
BAR_OTHER_VALUE = 2;
}
Компилятор генерирует структуру, где каждый вариант является ассоциированной константой:
#![allow(unused)] fn main() { #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct Bar(i32); impl Bar { pub const Unspecified: Bar = Bar(0); pub const Value: Bar = Bar(1); pub const OtherValue: Bar = Bar(2); } }
Для этих определений полей:
// proto3
Bar foo = 1;
// редакции
message MyMessage {
Bar foo = 1 [features.field_presence = IMPLICIT];
}
Компилятор сгенерирует следующие методы доступа:
fn foo(&self) -> Bar: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.fn set_foo(&mut self, value: Bar): Устанавливает значение поля. После вызова этогоhas_foo()будет возвращатьtrue, аfoo()будет возвращатьvalue.
Повторяющиеся поля
Для любого определения повторяющегося поля компилятор сгенерирует те же три метода доступа, которые отличаются только типом поля.
В редакциях вы можете управлять кодированием wire format повторяющихся примитивных
полей с помощью функции
repeated_field_encoding.
// proto2
repeated int32 foo = 1; // EXPANDED по умолчанию
// proto3
repeated int32 foo = 1; // PACKED по умолчанию
// редакции
repeated int32 foo = 1 [features.repeated_field_encoding = PACKED];
repeated int32 bar = 2 [features.repeated_field_encoding = EXPANDED];
Для любого из приведенных выше определений полей компилятор генерирует следующие методы доступа:
fn foo(&self) -> RepeatedView<'_, i32>: Возвращает view базового повторяющегося поля.fn foo_mut(&mut self) -> RepeatedMut<'_, i32>: Возвращает изменяемый handle к базовому повторяющемуся полю.fn set_foo(&mut self, src: impl IntoProxied<Repeated<i32>>): Устанавливает базовое повторяющееся поле в новое повторяющееся поле, предоставленное вsrc.
Для разных типов полей будут меняться только соответствующие generic-типы
RepeatedView, RepeatedMut и Repeated. Например,
для поля типа string метод доступа foo() возвращал бы
RepeatedView<'_, ProtoString>.
Поля-карты
Для этого определения поля-карты:
map<int32, int32> weight = 1;
Компилятор сгенерирует следующие 3 метода доступа:
fn weight(&self) -> protobuf::MapView<'_, i32, i32>: Возвращает неизменяемый view базовой карты.fn weight_mut(&mut self) -> protobuf::MapMut<'_, i32, i32>: Возвращает изменяемый handle к базовой карте.fn set_weight(&mut self, src: protobuf::IntoProxied<Map<i32, i32>>): Устанавливает базовую карту вsrc.
Для разных типов полей будут меняться только соответствующие generic-типы MapView,
MapMut и Map. Например, для поля типа
string метод доступа foo() возвращал бы MapView<'_, int32, ProtoString>.
Any
Any в настоящее время не обрабатывается особым образом в Rust Protobuf; он будет вести себя так, как если бы это было простое сообщение с этим определением:
message Any {
string type_url = 1;
bytes value = 2;
}
Oneof
Для определения oneof, подобного этому:
oneof example_name {
int32 foo_int = 4;
string foo_string = 9;
...
}
Компилятор сгенерирует методы доступа (геттеры, сеттеры, hazzer'ы) для каждого поля
так, как если бы то же поле было объявлено как optional поле вне oneof.
Таким образом, вы можете работать с полями oneof как с обычными полями, но установка одного из них
очистит другие поля в блоке oneof. Кроме того, для блока oneof
создаются следующие типы:
#![allow(unused)] fn main() { #[non_exhaustive] #[derive(Debug, Clone, Copy)] pub enum ExampleNameOneof<'msg> { FooInt(i32) = 4, FooString(&'msg protobuf::ProtoStr) = 9, not_set(std::marker::PhantomData<&'msg ()>) = 0 } }
#![allow(unused)] fn main() { #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ExampleNameCase { FooInt = 4, FooString = 9, not_set = 0 } }
Дополнительно, он сгенерирует два метода доступа:
fn example_name(&self) -> ExampleNameOneof<_>: Возвращает вариант перечисления, указывающий, какое поле установлено, и значение поля. Возвращаетnot_set, если никакое поле не установлено.fn example_name_case(&self) -> ExampleNameCase: Возвращает вариант перечисления, указывающий, какое поле установлено. Возвращаетnot_set, если никакое поле не установлено.
Перечисления
Для определения перечисления, подобного:
enum FooBar {
FOO_BAR_UNKNOWN = 0;
FOO_BAR_A = 1;
FOO_B = 5;
VALUE_C = 1234;
}
Компилятор сгенерирует:
#![allow(unused)] fn main() { #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct FooBar(i32); impl FooBar { pub const Unknown: FooBar = FooBar(0); pub const A: FooBar = FooBar(1); pub const FooB: FooBar = FooBar(5); pub const ValueC: FooBar = FooBar(1234); } }
Обратите внимание, что для значений с префиксом, совпадающим с именем перечисления, префикс будет
удален; это сделано для улучшения эргономики. Значения перечисления обычно имеют префикс
с именем перечисления, чтобы избежать коллизий имен между sibling enums (которые следуют
семантике перечислений C++, где значения не ограничены областью видимости их содержащего перечисления).
Поскольку сгенерированные константы Rust ограничены областью видимости внутри impl,
дополнительный префикс, который полезно добавлять в файлы .proto, был бы
избыточным в Rust.
Расширения (только proto2)
Rust API для расширений в настоящее время находится в разработке. Поля расширений будут сохраняться через parse/serialize, и в случае C++ interop любые установленные расширения будут сохранены, если к сообщению обращаются из Rust (и распространены в случае копирования сообщения или слияния).
Выделение в Arena
Rust API для сообщений, выделенных в arena, еще не реализован.
Внутренне, Protobuf Rust на ядре upb использует arenas, но на ядрах C++ он не использует. Однако ссылки (как константные, так и изменяемые) на сообщения, которые были выделены в arena в C++, могут быть безопасно переданы в Rust для доступа или изменения.
Сервисы
Rust API для сервисов еще не реализован.
Редактирование (редокция) в Rust
Описывает редактирование (редокцию) в Rust.
Используйте стандартный fmt::Debug ("{:?}" в строках формата) для сообщений Protobuf
для получения человеко-читаемых строк для логирования, сообщений об ошибках, исключений и аналогичных
случаев использования. Вывод этой отладочной информации не предназначен для машинного чтения
(в отличие от TextFormat и JSON, которые
не следует использовать для отладочного вывода).
Использование fmt::Debug позволяет редактировать (редоктировать) некоторые конфиденциальные поля.
Обратите внимание, что в ядре upb это редактирование еще не реализовано, но ожидается его добавление.
Сборка Rust Protos
Описывает, как собирать Rust protos с помощью Cargo или Bazel.
Cargo
См. пакет protobuf-example для примера настройки сборки.
Bazel
Процесс сборки Rust библиотеки для определения Protobuf аналогичен другим языкам программирования:
-
Используйте независимое от языка правило
proto_library:proto_library( name = "person_proto", srcs = ["person.proto"], ) -
Создайте Rust библиотеку:
load("//third_party/protobuf/rust:defs.bzl", "rust_proto_library") proto_library( name = "person_proto", srcs = ["person.proto"], ) rust_proto_library( name = "person_rust_proto", deps = [":person_proto"], ) -
Используйте библиотеку, включив ее в Rust бинарный файл:
load("//third_party/bazel_rules/rules_rust/rust:defs.bzl", "rust_binary") load("//third_party/protobuf/rust:defs.bzl", "rust_proto_library") proto_library( name = "person_proto", srcs = ["person.proto"], ) rust_proto_library( name = "person_rust_proto", deps = [":person_proto"], ) rust_binary( name = "greet", srcs = ["greet.rs"], deps = [ ":person_rust_proto", ], )
{{% alert title="Примечание" color="note" %}} Не используйте
rust_upb_proto_library или rust_cc_proto_library напрямую.
rust_proto_library проверяет глобальный флаг сборки, чтобы выбрать подходящий
бэкенд за вас. {{% /alert %}}
Проектные решения Rust Proto
Объясняет некоторые проектные выборы, которые делает реализация Rust Proto.
Как и в случае с любой библиотекой, Rust Protobuf разрабатывается с учетом потребностей как внутреннего использования Rust в Google, так и внешних пользователей. Выбор пути в этом проектом пространстве означает, что некоторые принятые решения не будут оптимальными для некоторых пользователей в некоторых случаях, даже если это правильный выбор для реализации в целом.
На этой странице рассматриваются некоторые более крупные проектные решения, которые принимает реализация Rust Protobuf, и соображения, которые привели к этим решениям.
Разработано для «поддержки» другими реализациями Protobuf, включая C++ Protobuf
Protobuf Rust — это не чистая Rust реализация protobuf, а безопасный Rust API, реализованный поверх существующих реализаций protobuf, или, как мы называем эти реализации: ядра (kernels).
Самым большим фактором, повлиявшим на это решение, была возможность нулевой стоимости добавления Rust в предсуществующий бинарный файл, который уже использует не-Rust Protobuf. Благодаря возможности реализации быть ABI-совместимой с сгенерированным кодом C++ Protobuf, можно делиться сообщениями Protobuf через языковую границу (FFI) как простыми указателями, избегая необходимости сериализовать на одном языке, передавать массив байт через границу и десериализовать на другом языке. Это также уменьшает размер бинарного файла для этих случаев использования, избегая избыточного встраивания информации о схеме в бинарный файл для одних и тех же сообщений для каждого языка.
Google рассматривает Rust как возможность постепенно получить memory safety (безопасность памяти) для ключевых частей предсуществующих brownfield (унаследованных) серверов на C++; стоимость сериализации на языковых границах предотвратила бы внедрение Rust для замены C++ во многих из этих важных и чувствительных к производительности случаев. Если бы мы создали greenfield (новую) Rust реализацию Protobuf, которая не имела бы этой поддержки, это в конечном итоге заблокировало бы внедрение Rust и потребовало бы, чтобы эти важные случаи оставались на C++.
Protobuf Rust в настоящее время поддерживает три ядра:
- Ядро C++ - сгенерированный код поддерживается C++ Protocol Buffers ( "полная" реализация, обычно используемая для серверов). Это ядро предлагает in-memory interoperability (совместимость в памяти) с кодом на C++, который использует среду выполнения C++. Это значение по умолчанию для серверов внутри Google.
- Ядро C++ Lite - сгенерированный код поддерживается C++ Lite Protocol Buffers (обычно используется для мобильных устройств). Это ядро предлагает in-memory interoperability с кодом на C++, который использует среду выполнения C++ Lite. Это значение по умолчанию для мобильных приложений внутри Google.
- Ядро upb - сгенерированный код поддерживается upb, высокопроизводительной и малой по размеру бинарника библиотекой Protobuf, написанной на C. upb предназначена для использования как деталь реализации средами выполнения Protobuf на других языках. Это значение по умолчанию в open source сборках, где мы ожидаем, что статическая линковка с кодом, уже использующим C++ Protobuf, будет более редкой.
Rust Protobuf разработан для поддержки нескольких альтернативных реализаций (включая несколько различных раскладок памяти), предоставляя при этом точно такой же API, что позволяет перекомпилировать тот же код приложения для работы на основе другой реализации. Это проектное ограничение значительно влияет на наши решения относительно публичного API, включая типы, используемые в геттерах (обсуждается далее в этом документе).
Нет чистого Rust ядра
Учитывая, что мы разработали API для возможности реализации несколькими базовыми реализациями, естественным вопросом является то, почему единственные поддерживаемые ядра написаны на небезопасных в отношении памяти языках C и C++ на сегодняшний день.
Хотя Rust, будучи memory-safe (безопасным в отношении памяти) языком, может значительно снизить подверженность критическим проблемам безопасности, ни один язык не immune (неуязвим) к проблемам безопасности. Реализации Protobuf, которые мы поддерживаем в качестве ядер, были тщательно изучены и протестированы fuzzing-ом до такой степени, что Google комфортно использует эти реализации для выполнения неизолированного (unsandboxed) разбора ненадежных входных данных в наших собственных серверах и приложениях.
Новый бинарный парсер, написанный на Rust в настоящее время, был бы понят как гораздо более вероятно содержащий критические уязвимости, чем наши предсуществующие C++ Protobuf или upb парсеры, которые были extensively (тщательно) протестированы fuzzing-ом, протестированы и проверены.
Существуют legitimate (обоснованные) аргументы в пользу поддержки реализации чистого Rust ядра в долгосрочной перспективе, включая возможность для разработчиков избежать необходимости иметь Clang доступным для компиляции кода C во время сборки.
Мы ожидаем, что Google будет поддерживать чистую Rust реализацию с тем же открытым API в какой-то более поздний срок, но у нас нет concrete roadmap (конкретного плана) для этого на данный момент. Вторая официальная Rust реализация Protobuf, которая имеет «лучший» API за счет избежания ограничений, возникающих из-за поддержки C++ Proto и upb, не планируется, поскольку мы не хотели бы fragment (дробить) собственное использование Protobuf в Google.
Типы-посредники View/Mut
API Rust Proto разработан с непрозрачными "Proxy" типами. Для файла .proto,
который определяет message SomeMsg {}, мы генерируем Rust типы SomeMsg,
SomeMsgView<'_> и SomeMsgMut<'_>. Простое правило заключается в том, что мы
ожидаем, что типы View и Mut будут заменять &SomeMsg и &mut SomeMsg во
всех использованиях по умолчанию, при этом все равно получая все проверки заимствований/Send/и т.д.
поведение, которое вы ожидаете от этих типов.
Другой взгляд для понимания этих типов
Чтобы лучше понять нюансы этих типов, может быть полезно думать о этих типах следующим образом:
#![allow(unused)] fn main() { struct SomeMsg(Box<cpp::SomeMsg>); struct SomeMsgView<'a>(&'a cpp::SomeMsg); struct SomeMsgMut<'a>(&'a mut cpp::SomeMsg); }
Под этим углом зрения вы можете видеть, что:
- Имея
&SomeMsg, можно получитьSomeMsgView(аналогично тому, как имея&Box<T>, вы можете получить&T) - Имея
SomeMsgView, не возможно получить&SomeMsg(аналогично тому, как имея&T, вы не могли бы получить&Box<T>).
Так же, как и в примере с &Box, это означает, что в аргументах функций
generally better (как правило, лучше) по умолчанию использовать SomeMsgView<'a>, а не &'a SomeMsg, так как это позволит superset (надмножеству) вызывающих сторон использовать функцию.
Почему
Есть две основные причины для этого дизайна: чтобы разблокировать возможные преимущества оптимизации и как неотъемлемый результат дизайна ядра.
Преимущество возможности оптимизации
Protobuf, будучи такой основной и широко распространенной технологией, необычно как склонен к тому, что все возможные наблюдаемые поведения зависят от кого-то, так и относительно небольшие оптимизации имеют необычно большое совокупное влияние в масштабе. Мы обнаружили, что большая непрозрачность типов дает необычно высокую пользу: они позволяют нам быть более deliberate (преднамеренными) в отношении того, какие именно поведения предоставляются, и дают нам больше возможностей для оптимизации реализации.
SomeMsgMut<'_> предоставляет те возможности, которые &mut SomeMsg не
предоставил бы: а именно, что мы можем создавать их лениво и с деталью реализации,
которая не совпадает с представлением owned message (сообщения, которым владеют). Это также по своей природе
позволяет нам контролировать определенные поведения, которые мы не смогли бы иначе ограничить или
контролировать: например, любой &mut можно использовать с std::mem::swap(), что является
поведением, которое накладывало бы строгие ограничения на то, какие инварианты вы можете
поддерживать между родительской и дочерней структурой, если &mut SomeChild предоставляется
вызывающим сторонам.
Присуще дизайну ядра
Другая причина использования proxy types — это скорее inherent limitation (присущее ограничение) нашего
дизайна ядра; когда у вас есть &T, должен существовать реальный Rust тип T в памяти
где-то.
Наш дизайн ядра C++ позволяет вам разобрать сообщение, которое содержит вложенные сообщения, и создать только небольшой Rust stack-allocated (выделенный на стеке) объект, представляющий корневое сообщение, при этом вся остальная память хранится в C++ Heap (куче). Когда вы позже обращаетесь к дочернему сообщению, не будет уже выделенного Rust объекта, который соответствует этому дочернему элементу, и поэтому нет экземпляра Rust для заимствования в этот момент.
Используя proxy types, мы можем по требованию создавать Rust proxy types, которые семантически действуют как заимствования, без какого-либо eagerly allocated (заранее выделенного) Rust памяти для этих экземпляров.
Не-Std типы
Простые типы, которые могут иметь непосредственно соответствующий Std тип
В некоторых случаях API Rust Protobuf может выбрать создание наших собственных типов, где
существует соответствующий std тип с тем же именем, при этом текущая
реализация может даже просто оборачивать std тип, например,
protobuf::UTF8Error.
Использование этих типов вместо std типов дает нам больше гибкости в оптимизации
реализации в будущем. Хотя наша текущая реализация использует валидацию UTF-8 из Rust std
сегодня, создавая наш собственный тип protobuf::Utf8Error, это
позволяет нам изменить реализацию на использование highly optimized (сильно оптимизированной) C++
реализации валидации UTF-8, которую мы используем из C++ Protobuf, которая быстрее,
чем валидация UTF-8 в Rust std.
ProtoString
Типы Rust str и std::string::String поддерживают strict invariant (строгий инвариант), что
они содержат только valid UTF-8 (валидный UTF-8), но тип C++ std::string не обеспечивает никаких
таких гарантий. Поля Protobuf с типом string предназначены для содержания только
валидного UTF-8, и C++ Protobuf действительно использует корректный и highly optimized (сильно оптимизированный) валидатор UTF8.
Однако, API поверхность C++ Protobuf не настроена на strict enforcement (строгое обеспечение)
как runtime invariant (инварианта времени выполнения) того, что его поля string всегда содержат валидный UTF-8,
вместо этого, в некоторых случаях он позволяет установку не-UTF8 данных в поле string
и валидация произойдет только позже, когда происходит сериализация.
Чтобы позволить интеграцию Rust в предсуществующие codebases (кодовые базы), которые используют C++ Protobuf,
при этом позволяя zero-cost boundary crossings (пересечения границ с нулевой стоимостью) без риска undefined behavior (неопределенного поведения) в Rust, мы,
к сожалению, должны избегать типов str/String для
геттеров полей string. Вместо этого используются типы ProtoStr и ProtoString,
которые являются эквивалентными типами, за исключением того, что они могут содержать невалидный UTF-8 в
редких ситуациях. Эти типы позволяют коду приложения выбрать, хочет ли он
выполнить валидацию по требованию, чтобы рассматривать поля как Result<&str>, или
работать с сырыми байтами, чтобы избежать какой-либо валидации во время выполнения. Все пути сеттеров
все еще разработаны так, чтобы позволять вам передавать типы &str или String.
Мы aware (осознаем), что vocabulary types (словарные типы), такие как str, очень важны для идиоматичного
использования, и намерены следить, является ли это решение правильным по мере развития деталей
использования Rust.
GO
Руководство по сгенерированному коду Go (Open)
Подробно описывает, какой именно код Go генерирует компилятор протоколов для любого заданного определения протокола.
Все различия между сгенерированным кодом для proto2, proto3 и editions выделены - обратите внимание, что эти различия относятся к сгенерированному коду, как описано в этом документе, а не к базовому API, который одинаков в обеих версиях. Вам следует прочитать руководство по языку proto2, руководство по языку proto3 или руководство по языку editions перед чтением этого документа.
warning
Вы смотрите документацию для старого API сгенерированного кода (Open Struct API).
Смотрите Сгенерированный код Go (Opaque) для соответствующей документации (нового) Opaque API.
Смотрите Go Protobuf: The new Opaque API для знакомства с Opaque API.
Вызов компилятора
Компилятору протоколов требуется плагин для генерации кода Go. Установите его с помощью Go 1.16 или выше, выполнив:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
Это установит бинарный файл protoc-gen-go в $GOBIN. Установите переменную окружения $GOBIN,
чтобы изменить местоположение установки. Она должна быть в вашем $PATH,
чтобы компилятор протоколов мог её найти.
Компилятор протоколов выводит код Go при вызове с флагом go_out.
Аргумент флага go_out — это каталог, куда вы хотите, чтобы компилятор
записывал ваш вывод Go. Компилятор создает один исходный файл для
каждого входного файла .proto. Имя выходного файла создается путем замены
расширения .proto на .pb.go.
То, где в выходном каталоге размещается сгенерированный файл .pb.go, зависит от
флагов компилятора. Существует несколько режимов вывода:
- Если указан флаг
paths=import, выходной файл помещается в каталог с именем, соответствующим пути импорта пакета Go (например, указанному параметромgo_packageв файле.proto). Например, входной файлprotos/buzz.protoс путем импорта Goexample.com/project/protos/fizzприводит к выходному файлу вexample.com/project/protos/fizz/buzz.pb.go. Это режим вывода по умолчанию, если флагpathsне указан. - Если указан флаг
module=$PREFIX, выходной файл помещается в каталог с именем, соответствующим пути импорта пакета Go (например, указанному параметромgo_packageв файле.proto), но с удалением указанного префикса каталога из имени выходного файла. Например, входной файлprotos/buzz.protoс путем импорта Goexample.com/project/protos/fizzиexample.com/project, указанным в качестве префиксаmodule, приводит к выходному файлу вprotos/fizz/buzz.pb.go. Генерация любых пакетов Go вне пути модуля приводит к ошибке. Этот режим полезен для вывода сгенерированных файлов непосредственно в модуль Go. - Если указан флаг
paths=source_relative, выходной файл помещается в тот же относительный каталог, что и входной файл. Например, входной файлprotos/buzz.protoприводит к выходному файлу вprotos/buzz.pb.go.
Флаги, специфичные для protoc-gen-go, предоставляются путем передачи флага go_opt при
вызове protoc. Можно передать несколько флагов go_opt. Например, при запуске:
protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
компилятор будет читать входные файлы foo.proto и bar/baz.proto из
каталога src и записывать выходные файлы foo.pb.go и bar/baz.pb.go в
каталог out. Компилятор автоматически создает вложенные выходные
подкаталоги, если необходимо, но не создает сам выходной каталог.
Пакеты
Для генерации кода Go путь импорта пакета Go должен быть предоставлен для
каждого файла .proto (включая те, от которых транзитивно зависят
генерируемые файлы .proto). Есть два способа указать путь импорта Go:
- объявив его внутри файла
.protoили - объявив его в командной строке при вызове
protoc.
Мы рекомендуем объявлять его внутри файла .proto, чтобы пакеты Go для
файлов .proto могли быть централизованно идентифицированы вместе с самими файлами .proto
и чтобы упростить набор флагов, передаваемых при вызове protoc. Если путь импорта Go для данного файла .proto предоставлен как в самом файле .proto, так и
в командной строке, то последний имеет приоритет над первым.
Путь импорта Go локально указывается в файле .proto путем объявления
опции go_package с полным путем импорта пакета Go. Пример использования:
option go_package = "example.com/project/protos/fizz";
Путь импорта Go может быть указан в командной строке при вызове
компилятора, путем передачи одного или нескольких флагов M${PROTO_FILE}=${GO_IMPORT_PATH}.
Пример использования:
protoc --proto_path=src \
--go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
--go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
protos/buzz.proto protos/bar.proto
Поскольку сопоставление всех файлов .proto с их путями импорта Go может быть довольно
большим, этот способ указания путей импорта Go обычно выполняется
некоторым инструментом сборки (например, Bazel), который
имеет контроль над всем деревом зависимостей. Если есть дублирующиеся записи для
данного файла .proto, то используется последняя указанная.
Как для опции go_package, так и для флага M значение может включать
явное имя пакета, отделенное от пути импорта точкой с запятой.
Например: "example.com/protos/foo;package_name". Такое использование не рекомендуется,
поскольку имя пакета по умолчанию будет выводиться из пути импорта разумным образом.
Путь импорта используется для определения того, какие операторы импорта должны быть сгенерированы,
когда один файл .proto импортирует другой файл .proto. Например, если a.proto
импортирует b.proto, то сгенерированному файлу a.pb.go нужно импортировать Go-пакет,
содержащий сгенерированный файл b.pb.go (если только оба файла не находятся в
одном пакете). Путь импорта также используется для построения выходных имен файлов.
Подробности см. в разделе «Вызов компилятора» выше.
Нет корреляции между путем импорта Go и
спецификатором package
в файле .proto. Последний относится только к пространству имен protobuf,
тогда как первый относится только к пространству имен Go. Также нет
корреляции между путем импорта Go и путем импорта .proto.
Уровень API
Сгенерированный код использует либо Open Struct API, либо Opaque API. Смотрите Go Protobuf: The new Opaque API (блогпост) для введения.
В зависимости от синтаксиса вашего файла .proto, вот какой API будет использоваться:
Синтаксис .proto | Уровень API |
|---|---|
| proto2 | Open Struct API |
| proto3 | Open Struct API |
| edition 2023 | Open Struct API |
| edition 2024+ | Opaque API |
Вы можете выбрать API, установив функцию editions api_level в вашем
файле .proto. Это можно установить для каждого файла или для каждого сообщения:
edition = "2023";
package log;
import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;
message LogEntry { … }
Для вашего удобства вы также можете переопределить уровень API по умолчанию с помощью
флага командной строки protoc:
protoc […] --go_opt=default_api_level=API_HYBRID
Чтобы переопределить уровень API по умолчанию для конкретного файла (вместо всех файлов),
используйте флаг сопоставления apilevelM (аналогично флагу M для путей импорта):
protoc […] --go_opt=apilevelMhello.proto=API_HYBRID
Флаги командной строки также работают для файлов .proto, все еще использующих синтаксис proto2 или proto3,
но если вы хотите выбрать уровень API из самого файла .proto,
вам нужно сначала перенести этот файл на editions.
Сообщения
Дано простое объявление сообщения:
message Artist {}
компилятор протоколов генерирует структуру с именем Artist. *Artist
реализует интерфейс
proto.Message.
Пакет
proto
предоставляет функции, которые работают с сообщениями, включая преобразование в бинарный формат и из него.
Интерфейс proto.Message определяет метод ProtoReflect. Этот метод
возвращает
protoreflect.Message,
который предоставляет представление сообщения на основе рефлексии.
Опция optimize_for не влияет на вывод генератора кода Go.
Когда несколько горутин одновременно обращаются к одному и тому же сообщению, применяются следующие правила:
- Доступ (чтение) к полям одновременно безопасен, за одним исключением:
- Первый доступ к ленивому полю является модификацией.
- Модификация разных полей в одном сообщении безопасна.
- Модификация поля одновременно не безопасна.
- Модификация сообщения любым способом одновременно с функциями
пакета
proto, такими какproto.Marshalилиproto.Size, не безопасна.
Вложенные типы
Сообщение может быть объявлено внутри другого сообщения. Например:
message Artist {
message Name {
}
}
В этом случае компилятор генерирует две структуры: Artist и Artist_Name.
Поля
Компилятор протоколов генерирует поле структуры для каждого поля, определенного в сообщении. Точная природа этого поля зависит от его типа и от того, является ли оно одиночным (singular), повторяющимся (repeated), отображением (map) или полем oneof.
Обратите внимание, что сгенерированные имена полей Go всегда используют верблюжью нотацию (camel-case),
даже если имя поля в файле .proto использует нижний регистр с подчеркиваниями
(как и должно).
Преобразование регистра работает следующим образом:
- Первая буква заглавная для экспорта. Если первый символ — подчеркивание, оно удаляется и добавляется заглавная X.
- Если за внутренним подчеркиванием следует буква в нижнем регистре, подчеркивание удаляется, а следующая буква заглавная.
Таким образом, поле proto birth_year становится BirthYear в Go, а
_birth_year_2 становится XBirthYear_2.
Скалярные поля с явным присутствием (Singular Explicit Presence)
Для определения поля:
int32 birth_year = 1;
компилятор генерирует структуру с полем *int32 с именем BirthYear и
методом доступа GetBirthYear(), который возвращает значение int32 в Artist или
значение по умолчанию, если поле не установлено. Если значение по умолчанию не задано явно,
используется нулевое значение этого
типа (0 для чисел, пустая строка для строк).
Для других типов скалярных полей (включая bool, bytes и string) *int32
заменяется соответствующим типом Go в соответствии с
таблицей скалярных типов значений.
Скалярные поля с неявным присутствием (Singular Implicit Presence)
Для этого определения поля:
int32 birth_year = 1;
Компилятор сгенерирует структуру с полем int32 с именем BirthYear и
методом доступа GetBirthYear(), который возвращает значение int32 в
birth_year или
нулевое значение этого типа,
если поле не установлено (0 для чисел, пустая строка для строк).
Поле структуры FirstActiveYear будет иметь тип *int32, потому что оно
помечено как optional.
Для других типов скалярных полей (включая bool, bytes и string) int32
заменяется соответствующим типом Go в соответствии с
таблицей скалярных типов значений.
Неустановленные значения в proto будут представлены как
нулевое значение этого типа (0 для
чисел, пустая строка для строк).
Одиночные поля сообщений (Singular Message Fields)
Дан тип сообщения:
message Band {}
Для сообщения с полем Band:
// proto2
message Concert {
optional Band headliner = 1;
// Сгенерированный код будет тем же, если используется required вместо optional.
}
// proto3
message Concert {
Band headliner = 1;
}
// editions
message Concert {
Band headliner = 1;
}
Компилятор сгенерирует структуру Go
type Concert struct {
Headliner *Band
}
Поля сообщений могут быть установлены в nil, что означает, что поле не установлено,
эффективно очищая поле. Это не эквивалентно установке значения в
«пустой» экземпляр структуры сообщения.
Компилятор также генерирует вспомогательную функцию func (m *Concert) GetHeadliner() *Band.
Эта функция возвращает nil *Band, если m равен nil или headliner
не установлен. Это делает возможным цепочку вызовов get без промежуточных
проверок на nil:
var m *Concert // по умолчанию nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())
Повторяющиеся поля (Repeated Fields)
Каждое повторяющееся поле генерирует в структуре Go поле-срез (slice) типа T, где
T — это тип элемента поля. Для этого сообщения с повторяющимся полем:
message Concert {
// Лучшая практика: используйте имена во множественном числе для повторяющихся полей:
// /programming-guides/style#repeated-fields
repeated Band support_acts = 1;
}
компилятор генерирует структуру Go:
type Concert struct {
SupportActs []*Band
}
Аналогично, для определения поля repeated bytes band_promo_images = 1;
компилятор сгенерирует структуру Go с полем [][]byte с именем
BandPromoImages. Для повторяющегося перечисления, например repeated MusicGenre genres = 2;, компилятор генерирует структуру с полем []MusicGenre с именем Genres.
Следующий пример показывает, как установить поле:
concert := &Concert{
SupportActs: []*Band{
{}, // Первый элемент.
{}, // Второй элемент.
},
}
Чтобы получить доступ к полю, вы можете сделать следующее:
support := concert.GetSupportActs() // тип support - []*Band.
b1 := support[0] // тип b1 - *Band, первый элемент в support_acts.
Поля-отображения (Map Fields)
Каждое поле-отображение генерирует в структуре поле типа map[TKey]TValue, где
TKey — это тип ключа поля, а TValue — тип значения поля. Для этого
сообщения с полем-отображением:
message MerchItem {}
message MerchBooth {
// items отображает название товара ("Signed T-Shirt") в
// сообщение MerchItem с дополнительной информацией о товаре.
map<string, MerchItem> items = 1;
}
компилятор генерирует структуру Go:
type MerchBooth struct {
Items map[string]*MerchItem
}
Поля Oneof
Для поля oneof компилятор protobuf генерирует одно поле с
интерфейсным типом isMessageName_MyField. Он также генерирует структуру для каждого из
одиночных полей внутри oneof. Все они
реализуют этот интерфейс isMessageName_MyField.
Для этого сообщения с полем oneof:
package account;
message Profile {
oneof avatar {
string image_url = 1;
bytes image_data = 2;
}
}
компилятор генерирует структуры:
type Profile struct {
// Типы, которые могут быть присвоены Avatar:
// *Profile_ImageUrl
// *Profile_ImageData
Avatar isProfile_Avatar `protobuf_oneof:"avatar"`
}
type Profile_ImageUrl struct {
ImageUrl string
}
type Profile_ImageData struct {
ImageData []byte
}
И *Profile_ImageUrl, и *Profile_ImageData реализуют isProfile_Avatar
посредством предоставления пустого метода isProfile_Avatar().
Следующий пример показывает, как установить поле:
p1 := &account.Profile{
Avatar: &account.Profile_ImageUrl{ImageUrl: "http://example.com/image.png"},
}
// imageData имеет тип []byte
imageData := getImageData()
p2 := &account.Profile{
Avatar: &account.Profile_ImageData{ImageData: imageData},
}
Чтобы получить доступ к полю, вы можете использовать type switch для значения, чтобы обработать различные типы сообщений.
switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
// Загрузить изображение профиля на основе URL
// используя x.ImageUrl
case *account.Profile_ImageData:
// Загрузить изображение профиля на основе байтов
// используя x.ImageData
case nil:
// Поле не установлено.
default:
return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}
Компилятор также генерирует методы получения func (m *Profile) GetImageUrl() string
и func (m *Profile) GetImageData() []byte. Каждая функция получения возвращает
значение для этого поля или нулевое значение, если оно не установлено.
Перечисления
Дано перечисление вида:
message Venue {
enum Kind {
KIND_UNSPECIFIED = 0;
KIND_CONCERT_HALL = 1;
KIND_STADIUM = 2;
KIND_BAR = 3;
KIND_OPEN_AIR_FESTIVAL = 4;
}
Kind kind = 1;
// ...
}
компилятор протоколов генерирует тип и серию констант с этим типом:
type Venue_Kind int32
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
Для перечислений внутри сообщения (как приведенное выше) имя типа начинается с имени сообщения:
type Venue_Kind int32
Для перечисления на уровне пакета:
enum Genre {
GENRE_UNSPECIFIED = 0;
GENRE_ROCK = 1;
GENRE_INDIE = 2;
GENRE_DRUM_AND_BASS = 3;
// ...
}
имя типа Go не изменяется относительно имени enum в proto:
type Genre int32
Этот тип имеет метод String(), который возвращает имя заданного значения.
Метод Enum() инициализирует вновь выделенную память заданным значением и
возвращает соответствующий указатель:
func (Genre) Enum() *Genre
Компилятор протоколов генерирует константу для каждого значения в перечислении. Для перечислений внутри сообщения константы начинаются с имени охватывающего сообщения:
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
Для перечисления на уровне пакета константы начинаются с имени перечисления:
const (
Genre_GENRE_UNSPECIFIED Genre = 0
Genre_GENRE_ROCK Genre = 1
Genre_GENRE_INDIE Genre = 2
Genre_GENRE_DRUM_AND_BASS Genre = 3
)
Компилятор protobuf также генерирует отображение целочисленных значений в строковые имена и отображение имен в значения:
var Genre_name = map[int32]string{
0: "GENRE_UNSPECIFIED",
1: "GENRE_ROCK",
2: "GENRE_INDIE",
3: "GENRE_DRUM_AND_BASS",
}
var Genre_value = map[string]int32{
"GENRE_UNSPECIFIED": 0,
"GENRE_ROCK": 1,
"GENRE_INDIE": 2,
"GENRE_DRUM_AND_BASS": 3,
}
Обратите внимание, что язык .proto позволяет нескольким символам перечисления иметь один и тот же
числовое значение. Символы с одинаковым числовым значением являются синонимами. Они
представлены в Go точно так же, с несколькими именами, соответствующими
одному числовому значению. Обратное отображение содержит единственную запись для
числового значения к имени, которое появляется первым в файле .proto.
Расширения
Дано определение расширения:
extend Concert {
int32 promo_id = 123;
}
Компилятор протоколов сгенерирует значение
protoreflect.ExtensionType
с именем E_Promo_id. Это значение может быть использовано с функциями
proto.GetExtension,
proto.SetExtension,
proto.HasExtension,
и
proto.ClearExtension
для доступа к расширению в сообщении. Функция GetExtension и
функция SetExtension возвращают и принимают значение interface{}
содержащее тип значения расширения.
Для одиночных скалярных полей расширения тип значения расширения является соответствующим типом Go из таблицы скалярных типов значений.
Для одиночных полей расширения встроенного сообщения тип значения расширения —
*M, где M — тип сообщения поля.
Для повторяющихся полей расширения тип значения расширения — это срез одиночного типа.
Например, дано следующее определение:
extend Concert {
int32 singular_int32 = 1;
repeated bytes repeated_strings = 2;
Band singular_message = 3;
}
Значения расширений могут быть доступны как:
m := &somepb.Concert{}
proto.SetExtension(m, extpb.E_SingularInt32, int32(1))
proto.SetExtension(m, extpb.E_RepeatedString, []string{"a", "b", "c"})
proto.SetExtension(m, extpb.E_SingularMessage, &extpb.Band{})
v1 := proto.GetExtension(m, extpb.E_SingularInt32).(int32)
v2 := proto.GetExtension(m, extpb.E_RepeatedString).([][]byte)
v3 := proto.GetExtension(m, extpb.E_SingularMessage).(*extpb.Band)
Расширения могут быть объявлены вложенными внутри другого типа. Например, распространенный шаблон — сделать что-то вроде этого:
message Promo {
extend Concert {
int32 promo_id = 124;
}
}
В этом случае значение ExtensionType называется E_Promo_Concert.
Сервисы
Генератор кода Go по умолчанию не выдает вывод для сервисов. Если вы включите плагин gRPC (см. руководство по быстрому старту gRPC Go), то код будет сгенерирован для поддержки gRPC.
Руководство по сгенерированному коду Go (Opaque)"
Подробно описывает, какой именно код Go генерирует компилятор протоколов для любого заданного определения протокола.
Все различия между сгенерированным кодом для proto2 и proto3 выделены - обратите внимание, что эти различия относятся к сгенерированному коду, как описано в этом документе, а не к базовому API, который одинаков в обеих версиях. Вам следует прочитать руководство по языку proto2 и/или руководство по языку proto3 перед чтением этого документа.
{{% alert title="Примечание" color="warning" %}}Вы смотрите документацию для Opaque API, который является текущей версией. Если вы работаете с файлами .proto, которые используют старый Open Struct API (вы можете определить это по настройке уровня API в соответствующих файлах .proto), смотрите Сгенерированный код Go (Open) для соответствующей документации. Смотрите Go Protobuf: The new Opaque API для знакомства с Opaque API. {{% /alert %}}
Вызов компилятора
Компилятору протоколов требуется плагин для генерации кода Go. Установите его с помощью Go 1.16 или выше, выполнив:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
Это установит бинарный файл protoc-gen-go в $GOBIN. Установите переменную окружения $GOBIN,
чтобы изменить местоположение установки. Она должна быть в вашем $PATH,
чтобы компилятор протоколов мог её найти.
Компилятор протоколов выводит код Go при вызове с флагом go_out.
Аргумент флага go_out — это каталог, куда вы хотите, чтобы компилятор
записывал ваш вывод Go. Компилятор создает один исходный файл для
каждого входного файла .proto. Имя выходного файла создается путем замены
расширения .proto на .pb.go.
То, где в выходном каталоге размещается сгенерированный файл .pb.go, зависит от
флагов компилятора. Существует несколько режимов вывода:
- Если указан флаг
paths=import, выходной файл помещается в каталог с именем, соответствующим пути импорта пакета Go (например, указанному параметромgo_packageв файле.proto). Например, входной файлprotos/buzz.protoс путем импорта Goexample.com/project/protos/fizzприводит к выходному файлу вexample.com/project/protos/fizz/buzz.pb.go. Это режим вывода по умолчанию, если флагpathsне указан. - Если указан флаг
module=$PREFIX, выходной файл помещается в каталог с именем, соответствующим пути импорта пакета Go (например, указанному параметромgo_packageв файле.proto), но с удалением указанного префикса каталога из имени выходного файла. Например, входной файлprotos/buzz.protoс путем импорта Goexample.com/project/protos/fizzиexample.com/project, указанным в качестве префиксаmodule, приводит к выходному файлу вprotos/fizz/buzz.pb.go. Генерация любых пакетов Go вне пути модуля приводит к ошибке. Этот режим полезен для вывода сгенерированных файлов непосредственно в модуль Go. - Если указан флаг
paths=source_relative, выходной файл помещается в тот же относительный каталог, что и входной файл. Например, входной файлprotos/buzz.protoприводит к выходному файлу вprotos/buzz.pb.go.
Флаги, специфичные для protoc-gen-go, предоставляются путем передачи флага go_opt при
вызове protoc. Можно передать несколько флагов go_opt. Например, при запуске:
protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
компилятор будет читать входные файлы foo.proto и bar/baz.proto из
каталога src и записывать выходные файлы foo.pb.go и bar/baz.pb.go в
каталог out. Компилятор автоматически создает вложенные выходные
подкаталоги, если необходимо, но не создает сам выходной каталог.
Пакеты
Для генерации кода Go путь импорта пакета Go должен быть предоставлен для
каждого файла .proto (включая те, от которых транзитивно зависят
генерируемые файлы .proto). Есть два способа указать путь импорта Go:
- объявив его внутри файла
.protoили - объявив его в командной строке при вызове
protoc.
Мы рекомендуем объявлять его внутри файла .proto, чтобы пакеты Go для
файлов .proto могли быть централизованно идентифицированы вместе с самими файлами .proto
и чтобы упростить набор флагов, передаваемых при вызове protoc. Если путь импорта Go для данного файла .proto предоставлен как в самом файле .proto, так и
в командной строке, то последний имеет приоритет над первым.
Путь импорта Go локально указывается в файле .proto путем объявления
опции go_package с полным путем импорта пакета Go. Пример использования:
option go_package = "example.com/project/protos/fizz";
Путь импорта Go может быть указан в командной строке при вызове
компилятора, путем передачи одного или нескольких флагов M${PROTO_FILE}=${GO_IMPORT_PATH}.
Пример использования:
protoc --proto_path=src \
--go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
--go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
protos/buzz.proto protos/bar.proto
Поскольку сопоставление всех файлов .proto с их путями импорта Go может быть довольно
большим, этот способ указания путей импорта Go обычно выполняется
некоторым инструментом сборки (например, Bazel), который
имеет контроль над всем деревом зависимостей. Если есть дублирующиеся записи для
данного файла .proto, то используется последняя указанная.
Как для опции go_package, так и для флага M значение может включать
явное имя пакета, отделенное от пути импорта точкой с запятой.
Например: "example.com/protos/foo;package_name". Такое использование не рекомендуется,
поскольку имя пакета по умолчанию будет выводиться из пути импорта разумным образом.
Путь импорта используется для определения того, какие операторы импорта должны быть сгенерированы,
когда один файл .proto импортирует другой файл .proto. Например, если a.proto
импортирует b.proto, то сгенерированному файлу a.pb.go нужно импортировать Go-пакет,
содержащий сгенерированный файл b.pb.go (если только оба файла не находятся в
одном пакете). Путь импорта также используется для построения выходных имен файлов.
Подробности см. в разделе «Вызов компилятора» выше.
Нет корреляции между путем импорта Go и
спецификатором package
в файле .proto. Последний относится только к пространству имен protobuf,
тогда как первый относится только к пространству имен Go. Также нет
корреляции между путем импорта Go и путем импорта .proto.
Уровень API
Сгенерированный код использует либо Open Struct API, либо Opaque API. Смотрите Go Protobuf: The new Opaque API (блогпост) для введения.
В зависимости от синтаксиса вашего файла .proto, вот какой API будет использоваться:
Синтаксис .proto | Уровень API |
|---|---|
| proto2 | Open Struct API |
| proto3 | Open Struct API |
| edition 2023 | Open Struct API |
| edition 2024+ | Opaque API |
Вы можете выбрать API, установив функцию editions api_level в вашем
файле .proto. Это можно установить для каждого файла или для каждого сообщения:
edition = "2023";
package log;
import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;
message LogEntry { … }
Для вашего удобства вы также можете переопределить уровень API по умолчанию с помощью
флага командной строки protoc:
protoc […] --go_opt=default_api_level=API_HYBRID
Чтобы переопределить уровень API по умолчанию для конкретного файла (вместо всех файлов),
используйте флаг сопоставления apilevelM (аналогично флагу M для путей импорта):
protoc […] --go_opt=apilevelMhello.proto=API_HYBRID
Флаги командной строки также работают для файлов .proto, все еще использующих синтаксис proto2 или proto3,
но если вы хотите выбрать уровень API из самого файла .proto,
вам нужно сначала перенести этот файл на editions.
Сообщения
Дано простое объявление сообщения:
message Artist {}
компилятор протоколов генерирует структуру с именем Artist. *Artist
реализует интерфейс
proto.Message.
Пакет
proto
предоставляет функции, которые работают с сообщениями, включая преобразование в бинарный формат и из него.
Интерфейс proto.Message определяет метод ProtoReflect. Этот метод
возвращает
protoreflect.Message,
который предоставляет представление сообщения на основе рефлексии.
Опция optimize_for не влияет на вывод генератора кода Go.
Когда несколько горутин одновременно обращаются к одному и тому же сообщению, применяются следующие правила:
- Доступ (чтение) к полям одновременно безопасен, за одним исключением:
- Первый доступ к ленивому полю является модификацией.
- Модификация разных полей в одном сообщении безопасна.
- Модификация поля одновременно не безопасна.
- Модификация сообщения любым способом одновременно с функциями
пакета
proto, такими какproto.Marshalилиproto.Size, не безопасна.
Вложенные типы
Сообщение может быть объявлено внутри другого сообщения. Например:
message Artist {
message Name {
}
}
В этом случае компилятор генерирует две структуры: Artist и Artist_Name.
Поля
Компилятор протоколов генерирует методы доступа (сеттеры и геттеры) для каждого поля, определенного в сообщении.
Обратите внимание, что сгенерированные методы доступа Go всегда используют верблюжью нотацию (camel-case),
даже если имя поля в файле .proto использует нижний регистр с подчеркиваниями
(как и должно).
Преобразование регистра работает следующим образом:
- Первая буква заглавная для экспорта. Если первый символ — подчеркивание, оно удаляется и добавляется заглавная X.
- Если за внутренним подчеркиванием следует буква в нижнем регистре, подчеркивание удаляется, а следующая буква заглавная.
Таким образом, вы можете получить доступ к полю proto birth_year с помощью метода GetBirthYear()
в Go, а _birth_year_2 с помощью GetXBirthYear_2().
Одиночные поля
Для этого определения поля:
// proto2 и proto3
message Artist {
optional int32 birth_year = 1;
}
// editions
message Artist {
int32 birth_year = 1 [features.field_presence = EXPLICIT];
}
компилятор генерирует структуру Go со следующими методами доступа:
func (m *Artist) GetBirthYear() int32
func (m *Artist) SetBirthYear(v int32)
При неявном присутствии (implicit presence) геттер возвращает значение int32 в birth_year или
нулевое значение этого
типа, если поле не установлено (0 для чисел, пустая строка для строк). При
явном присутствии (explicit presence) геттер возвращает значение int32 в birth_year или
значение по умолчанию, если поле не установлено. Если значение по умолчанию не задано явно,
используется нулевое значение.
Для других типов скалярных полей (включая bool, bytes и string) int32
заменяется соответствующим типом Go в соответствии с
таблицей скалярных типов значений.
В полях с явным присутствием вы также можете использовать эти методы:
func (m *Artist) HasBirthYear() bool
func (m *Artist) ClearBirthYear()
Одиночные поля сообщений (Singular Message Fields)
Дан тип сообщения:
message Band {}
Для сообщения с полем Band:
// proto2
message Concert {
optional Band headliner = 1;
// Сгенерированный код будет тем же, если используется required вместо optional.
}
// proto3 и editions
message Concert {
Band headliner = 1;
}
Компилятор сгенерирует структуру Go со следующими методами доступа:
type Concert struct { ... }
func (m *Concert) GetHeadliner() *Band { ... }
func (m *Concert) SetHeadliner(v *Band) { ... }
func (m *Concert) HasHeadliner() bool { ... }
func (m *Concert) ClearHeadliner() { ... }
Метод доступа GetHeadliner() безопасно вызывать, даже если m равен nil. Это
делает возможной цепочку вызовов get без промежуточных проверок на nil:
var m *Concert // по умолчанию nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())
Если поле не установлено, геттер вернет значение по умолчанию для поля. Для сообщений значение по умолчанию — это нулевой указатель.
В отличие от геттеров, сеттеры не выполняют проверки на nil за вас. Поэтому вы не можете безопасно вызывать сеттеры на возможно nil сообщениях.
Повторяющиеся поля (Repeated Fields)
Для повторяющихся полей методы доступа используют тип среза (slice). Для этого сообщения с повторяющимся полем:
message Concert {
// Лучшая практика: используйте имена во множественном числе для повторяющихся полей:
// /programming-guides/style#repeated-fields
repeated Band support_acts = 1;
}
компилятор генерирует структуру Go со следующими методами доступа:
type Concert struct { ... }
func (m *Concert) GetSupportActs() []*Band { ... }
func (m *Concert) SetSupportActs(v []*Band) { ... }
Аналогично, для определения поля repeated bytes band_promo_images = 1;
компилятор сгенерирует методы доступа, работающие с типом [][]byte. Для
повторяющегося перечисления repeated MusicGenre genres = 2;, компилятор
генерирует методы доступа, работающие с типом []MusicGenre.
Следующий пример показывает, как создать сообщение Concert с помощью
строителя (builder).
concert := Concert_builder{
SupportActs: []*Band{
{}, // Первый элемент.
{}, // Второй элемент.
},
}.Build()
Альтернативно, вы можете использовать сеттеры:
concert := &Concert{}
concert.SetSupportActs([]*Band{
{}, // Первый элемент.
{}, // Второй элемент.
})
Чтобы получить доступ к полю, вы можете сделать следующее:
support := concert.GetSupportActs() // тип support - []*Band.
b1 := support[0] // тип b1 - *Band, первый элемент в support_acts.
Поля-отображения (Map Fields)
Каждое поле-отображение генерирует методы доступа, работающие с типом map[TKey]TValue, где
TKey — это тип ключа поля, а TValue — тип значения поля. Для этого
сообщения с полем-отображением:
message MerchItem {}
message MerchBooth {
// items отображает название товара ("Signed T-Shirt") в
// сообщение MerchItem с дополнительной информацией о товаре.
map<string, MerchItem> items = 1;
}
компилятор генерирует структуру Go со следующими методами доступа:
type MerchBooth struct { ... }
func (m *MerchBooth) GetItems() map[string]*MerchItem { ... }
func (m *MerchBooth) SetItems(v map[string]*MerchItem) { ... }
Поля Oneof
Для поля oneof компилятор protobuf генерирует методы доступа для каждого из одиночных полей внутри oneof.
Для этого сообщения с полем oneof:
package account;
message Profile {
oneof avatar {
string image_url = 1;
bytes image_data = 2;
}
}
компилятор генерирует структуру Go со следующими методами доступа:
type Profile struct { ... }
func (m *Profile) WhichAvatar() case_Profile_Avatar { ... }
func (m *Profile) GetImageUrl() string { ... }
func (m *Profile) GetImageData() []byte { ... }
func (m *Profile) SetImageUrl(v string) { ... }
func (m *Profile) SetImageData(v []byte) { ... }
func (m *Profile) HasAvatar() bool { ... }
func (m *Profile) HasImageUrl() bool { ... }
func (m *Profile) HasImageData() bool { ... }
func (m *Profile) ClearAvatar() { ... }
func (m *Profile) ClearImageUrl() { ... }
func (m *Profile) ClearImageData() { ... }
Следующий пример показывает, как установить поле с помощью строителя (builder):
p1 := accountpb.Profile_builder{
ImageUrl: proto.String("https://example.com/image.png"),
}.Build()
...или, что то же самое, с помощью сеттера:
// imageData имеет тип []byte
imageData := getImageData()
p2 := &accountpb.Profile{}
p2.SetImageData(imageData)
Чтобы получить доступ к полю, вы можете использовать оператор switch по результату WhichAvatar():
switch m.WhichAvatar() {
case accountpb.Profile_ImageUrl_case:
// Загрузить изображение профиля на основе URL
// используя m.GetImageUrl()
case accountpb.Profile_ImageData_case:
// Загрузить изображение профиля на основе байтов
// используя m.GetImageData()
case accountpb.Profile_Avatar_not_set_case:
// Поле не установлено.
default:
return fmt.Errorf("Profile.Avatar имеет неожиданное новое поле oneof %v", x)
}
Строители (Builders)
Строители — это удобный способ конструирования и инициализации сообщения в рамках одного выражения, особенно при работе с вложенными сообщениями, как в модульных тестах.
В отличие от строителей в других языках (таких как Java), строители Go protobuf
не предназначены для передачи между функциями. Вместо этого немедленно вызывайте Build()
и передавайте полученное proto-сообщение, используя сеттеры для
модификации полей.
Перечисления
Дано перечисление вида:
message Venue {
enum Kind {
KIND_UNSPECIFIED = 0;
KIND_CONCERT_HALL = 1;
KIND_STADIUM = 2;
KIND_BAR = 3;
KIND_OPEN_AIR_FESTIVAL = 4;
}
Kind kind = 1;
// ...
}
компилятор протоколов генерирует тип и серию констант с этим типом:
type Venue_Kind int32
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
Для перечислений внутри сообщения (как приведенное выше) имя типа начинается с имени сообщения:
type Venue_Kind int32
Для перечисления на уровне пакета:
enum Genre {
GENRE_UNSPECIFIED = 0;
GENRE_ROCK = 1;
GENRE_INDIE = 2;
GENRE_DRUM_AND_BASS = 3;
// ...
}
имя типа Go не изменяется относительно имени enum в proto:
type Genre int32
Этот тип имеет метод String(), который возвращает имя заданного значения.
Метод Enum() инициализирует вновь выделенную память заданным значением и
возвращает соответствующий указатель:
func (Genre) Enum() *Genre
Компилятор протоколов генерирует константу для каждого значения в перечислении. Для перечислений внутри сообщения константы начинаются с имени охватывающего сообщения:
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
Для перечисления на уровне пакета константы начинаются с имени перечисления:
const (
Genre_GENRE_UNSPECIFIED Genre = 0
Genre_GENRE_ROCK Genre = 1
Genre_GENRE_INDIE Genre = 2
Genre_GENRE_DRUM_AND_BASS Genre = 3
)
Компилятор protobuf также генерирует отображение целочисленных значений в строковые имена и отображение имен в значения:
var Genre_name = map[int32]string{
0: "GENRE_UNSPECIFIED",
1: "GENRE_ROCK",
2: "GENRE_INDIE",
3: "GENRE_DRUM_AND_BASS",
}
var Genre_value = map[string]int32{
"GENRE_UNSPECIFIED": 0,
"GENRE_ROCK": 1,
"GENRE_INDIE": 2,
"GENRE_DRUM_AND_BASS": 3,
}
Обратите внимание, что язык .proto позволяет нескольким символам перечисления иметь один и тот же
числовое значение. Символы с одинаковым числовым значением являются синонимами. Они
представлены в Go точно так же, с несколькими именами, соответствующими
одному числовому значению. Обратное отображение содержит единственную запись для
числового значения к имени, которое появляется первым в файле .proto.
Расширения (proto2)
Дано определение расширения:
extend Concert {
optional int32 promo_id = 123;
}
Компилятор протоколов сгенерирует значение
protoreflect.ExtensionType
с именем E_Promo_id. Это значение может быть использовано с функциями
proto.GetExtension,
proto.SetExtension,
proto.HasExtension,
и
proto.ClearExtension
для доступа к расширению в сообщении. Функция GetExtension и
функция SetExtension возвращают и принимают значение interface{}
содержащее тип значения расширения.
Для одиночных скалярных полей расширения тип значения расширения является соответствующим типом Go из таблицы скалярных типов значений.
Для одиночных полей расширения встроенного сообщения тип значения расширения —
*M, где M — тип сообщения поля.
Для повторяющихся полей расширения тип значения расширения — это срез одиночного типа.
Например, дано следующее определение:
extend Concert {
optional int32 singular_int32 = 1;
repeated bytes repeated_strings = 2;
optional Band singular_message = 3;
}
Значения расширений могут быть доступны как:
m := &somepb.Concert{}
proto.SetExtension(m, extpb.E_SingularInt32, int32(1))
proto.SetExtension(m, extpb.E_RepeatedString, []string{"a", "b", "c"})
proto.SetExtension(m, extpb.E_SingularMessage, &extpb.Band{})
v1 := proto.GetExtension(m, extpb.E_SingularInt32).(int32)
v2 := proto.GetExtension(m, extpb.E_RepeatedString).([][]byte)
v3 := proto.GetExtension(m, extpb.E_SingularMessage).(*extpb.Band)
Расширения могут быть объявлены вложенными внутри другого типа. Например, распространенный шаблон — сделать что-то вроде этого:
message Promo {
extend Concert {
optional int32 promo_id = 124;
}
}
В этом случае значение ExtensionType называется E_Promo_Concert.
Сервисы
Генератор кода Go по умолчанию не выдает вывод для сервисов. Если вы включите плагин gRPC (см. руководство по быстрому старту gRPC Go), то код будет сгенерирован для поддержки gRPC.
Вопросы и ответы
Список часто задаваемых вопросов о реализации протокольных буферов в Go, с ответами на каждый.
Версии
В чем разница между github.com/golang/protobuf и google.golang.org/protobuf?
Модуль
github.com/golang/protobuf
— это оригинальный API протокольных буферов для Go.
Модуль
google.golang.org/protobuf
— это обновленная версия этого API, разработанная для простоты, удобства использования
и безопасности. Основные особенности обновленного API — поддержка рефлексии
и разделение пользовательского API от базовой реализации.
Мы рекомендуем использовать google.golang.org/protobuf в новом коде.
Версия v1.4.0 и выше github.com/golang/protobuf оборачивает новую
реализацию и позволяет программам постепенно переходить на новый API. Например,
известные типы, определенные в github.com/golang/protobuf/ptypes, являются
просто псевдонимами тех, что определены в новом модуле. Таким образом,
google.golang.org/protobuf/types/known/emptypb
и
github.com/golang/protobuf/ptypes/empty
могут использоваться взаимозаменяемо.
Что такое proto1, proto2, proto3 и editions?
Это редакции языка протокольных буферов. Это отличается от реализации protobuf на Go.
-
Editions — это новейший и рекомендуемый способ написания Protocol Buffers. Новые функции будут выпускаться как часть новых редакций. Для получения дополнительной информации см. Редакции Protocol Buffer.
-
proto3— это устаревшая версия языка. Мы рекомендуем новому коду использовать редакции. -
proto2— это устаревшая версия языка. Несмотря на то, что она была заменена proto3 и editions, proto2 все еще полностью поддерживается. -
proto1— это устаревшая версия языка. Она никогда не выпускалась как проект с открытым исходным кодом.
Существует несколько различных типов Message. Какой следует использовать?
-
"google.golang.org/protobuf/proto".Message— это интерфейсный тип, реализуемый всеми сообщениями, сгенерированными текущей версией компилятора протокольных буферов. Функции, которые работают с произвольными сообщениями, такие какproto.Marshalилиproto.Clone, принимают или возвращают этот тип. -
"google.golang.org/protobuf/reflect/protoreflect".Message— это интерфейсный тип, описывающий представление сообщения через рефлексию.Вызовите метод
ProtoReflectуproto.Message, чтобы получитьprotoreflect.Message. -
"google.golang.org/protobuf/reflect/protoreflect".ProtoMessage— это псевдоним"google.golang.org/protobuf/proto".Message. Эти два типа взаимозаменяемы. -
"github.com/golang/protobuf/proto".Message— это интерфейсный тип, определенный устаревшим API протокольных буферов для Go. Все сгенерированные типы сообщений реализуют этот интерфейс, но интерфейс не описывает поведение, ожидаемое от этих сообщений. Новому коду следует избегать использования этого типа.
Распространенные проблемы
"go install": working directory is not part of a module
В Go 1.15 и ниже вы установили переменную окружения GO111MODULE=on и
запускаете команду go install вне каталога модуля. Установите
GO111MODULE=auto или удалите переменную окружения.
В Go 1.16 и выше go install можно вызывать вне модуля,
указав явную версию: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
constant -1 overflows protoimpl.EnforceVersion
Вы используете сгенерированный файл .pb.go, который требует более новой версии
модуля "google.golang.org/protobuf".
Обновитесь до более новой версии с помощью:
go get -u google.golang.org/protobuf/proto
undefined: "github.com/golang/protobuf/proto".ProtoPackageIsVersion4
Вы используете сгенерированный файл .pb.go, который требует более новой версии
модуля "github.com/golang/protobuf".
Обновитесь до более новой версии с помощью:
go get -u github.com/golang/protobuf/proto
Что такое конфликт пространства имен протокольного буфера?
Все объявления протокольных буферов, связанные в бинарный файл Go, вставляются в глобальный реестр.
Каждое объявление protobuf (например, перечисления, значения перечислений или сообщения) имеет
абсолютное имя, которое представляет собой конкатенацию
имени пакета с
относительным именем объявления в исходном файле .proto (например,
my.proto.package.MyMessage.NestedMessage). Язык protobuf предполагает, что
все объявления универсально уникальны.
Если два объявления protobuf, связанные в бинарный файл Go, имеют одинаковое имя, то это приводит к конфликту пространства имен, и реестру невозможно правильно разрешить это объявление по имени. В зависимости от того, какая версия Go protobuf используется, это либо вызовет панику во время инициализации, либо тихо проигнорирует конфликт и приведет к потенциальной ошибке позже во время выполнения.
Как исправить конфликт пространства имен протокольного буфера?
Лучший способ исправить конфликт пространства имен зависит от причины, по которой он возникает.
Распространенные способы возникновения конфликтов пространств имен:
-
Вендорные .proto файлы. Когда один файл
.protoгенерируется в два или более пакетов Go и связывается в один бинарный файл Go, это вызывает конфликт для каждого объявления protobuf в сгенерированных пакетах Go. Обычно это происходит, когда файл.protoвендорится и из него генерируется пакет Go, или сам сгенерированный пакет Go вендорится. Пользователям следует избегать вендоринга и вместо этого зависеть от централизованного пакета Go для этого файла.proto.- Если файлом
.protoвладеет внешняя сторона и в нем отсутствует опцияgo_package, то вам следует согласовать с владельцем этого файла.protoуказание централизованного пакета Go, от которого множество пользователей могут зависеть.
- Если файлом
-
Отсутствующие или общие имена пакетов proto. Если файл
.protoне указывает имя пакета или использует слишком общее имя пакета (например, "my_service"), то высока вероятность того, что объявления внутри этого файла будут конфликтовать с другими объявлениями elsewhere во вселенной. Мы рекомендуем, чтобы каждый файл.protoимел имя пакета, которое намеренно выбрано быть универсально уникальным (например, с префиксом названия компании).
{{% alert title="Предупреждение" color="warning" %}}
Ретроспективное изменение имени пакета в файле .proto не является обратно
совместимым для типов, используемых в качестве полей расширений, хранящихся в google.protobuf.Any,
или для определений gRPC Service.
{{% /alert %}}
Начиная с v1.26.0 модуля google.golang.org/protobuf, будет сообщаться о жесткой ошибке
при запуске программы Go, в которую связано несколько конфликтующих
имен protobuf. Хотя предпочтительнее, чтобы источник
конфликта был исправлен, фатальную ошибку можно немедленно обойти одним из
двух способов:
-
Во время компиляции. Поведение по умолчанию для обработки конфликтов может быть указано во время компиляции с помощью переменной, инициализируемой компоновщиком:
go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn" -
Во время выполнения программы. Поведение для обработки конфликтов при выполнении конкретного бинарного файла Go может быть установлено с помощью переменной окружения:
GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn ./main
Как использовать редакции протокольных буферов?
Чтобы использовать редакцию protobuf, вы должны указать редакцию в вашем файле .proto.
Например, чтобы использовать редакцию 2023, добавьте следующее в начало вашего
файла .proto:
edition = "2023";
Компилятор протокольных буферов затем сгенерирует код Go, совместимый с
указанной редакцией. С редакциями вы также можете включать или отключать определенные
функции для вашего файла .proto. Для получения дополнительной информации см.
Редакции Protocol Buffer.
Как управлять поведением моего сгенерированного кода Go?
С редакциями вы можете управлять поведением сгенерированного кода Go, включая
или отключая определенные функции в вашем файле .proto. Например, чтобы установить
поведение API для вашей реализации, вы должны добавить следующее в ваш
файл .proto:
edition = "2023";
option features.(pb.go).api_level = API_OPAQUE;
Когда api_level установлен в API_OPAQUE, код Go, сгенерированный компилятором протокольных буферов,
скрывает поля структуры, так что к ним больше нельзя получить прямой доступ.
Вместо этого создаются новые методы доступа для получения, установки или очистки
поля.
Для полного списка доступных функций и их описаний см. Функции для редакций.
Почему reflect.DeepEqual ведет себя неожиданно с сообщениями protobuf?
Сгенерированные типы сообщений протокольных буферов включают внутреннее состояние, которое может различаться даже между эквивалентными сообщениями.
Кроме того, функция reflect.DeepEqual не знает о семантике
сообщений протокольных буферов и может сообщать о различиях, которых не существует. Например,
поле-отображение, содержащее nil-отображение, и содержащее не-nil отображение нулевой длины,
семантически эквивалентны, но будут reported как неравные
reflect.DeepEqual.
Используйте функцию
proto.Equal
для сравнения значений сообщений.
В тестах вы также можете использовать пакет
"github.com/google/go-cmp/cmp"
с опцией
protocmp.Transform().
Пакет cmp может сравнивать произвольные структуры данных, и
cmp.Diff выдает
читаемые человеком отчеты о различиях между значениями.
if diff := cmp.Diff(a, b, protocmp.Transform()); diff != "" {
t.Errorf("unexpected difference:\n%v", diff)
}
Закон Хайрума
Что такое Закон Хайрума и почему он в этом FAQ?
Закон Хайрума гласит:
При достаточном количестве пользователей API не имеет значения, что вы обещаете в контракте: все наблюдаемые поведения вашей системы будут использоваться кем-то.
Целью дизайна последней версии API протокольных буферов для Go является избежание, там где это возможно, предоставления наблюдаемых поведений, которые мы не можем обещать сохранять стабильными в будущем. Наша философия заключается в том, что преднамеренная нестабильность в областях, где мы не даем обещаний, лучше, чем создание иллюзии стабильности, только для того, чтобы это изменилось в будущем после того, как проект потенциально долго полагался на это ложное предположение.
Почему текст ошибок постоянно меняется?
Тесты, зависящие от точного текста ошибок, хрупки и часто ломаются, когда этот текст изменяется. Чтобы препятствовать небезопасному использованию текста ошибок в тестах, текст ошибок, производимых этим модулем, намеренно нестабилен.
Если вам нужно идентифицировать, произведена ли ошибка
protobuf модулем, мы
гарантируем, что все ошибки будут соответствовать
proto.Error
согласно errors.Is.
Почему вывод protojson постоянно меняется?
Мы не даем обещаний о долгосрочной стабильности реализации Go JSON формата для протокольных буферов. Спецификация определяет только то, что является допустимым JSON, но не предоставляет спецификации для канонического формата того, как маршалер должен форматировать данное сообщение. Чтобы избежать создания иллюзии, что вывод стабилен, мы намеренно вносим незначительные различия, чтобы сравнения побайтово скорее всего завершались неудачей.
Чтобы получить некоторую степень стабильности вывода, мы рекомендуем пропускать вывод через форматтер JSON.
Почему вывод prototext постоянно меняется?
Мы не даем обещаний о долгосрочной стабильности реализации Go
текстового формата. Не существует канонической спецификации текстового формата protobuf,
и мы хотим сохранить возможность вносить улучшения в
вывод пакета prototext в будущем. Поскольку мы не обещаем стабильности
вывода пакета, мы намеренно внесли нестабильность, чтобы препятствовать
пользователям зависеть от него.
Чтобы получить некоторую степень стабильности, мы рекомендуем пропускать вывод
prototext через программу
txtpbfmt. Форматтер
может быть напрямую вызван в Go с помощью
parser.Format.
Разное
Как использовать сообщение протокольного буфера в качестве хэш-ключа?
Вам нужна каноническая сериализация, где маршализованный вывод сообщения протокольного буфера гарантированно стабилен с течением времени. К сожалению, на данный момент не существует спецификации для канонической сериализации. Вам нужно будет написать свою собственную или найти способ обойтись без нее.
Могу ли я добавить новую функцию в реализацию протокольных буферов на Go?
Возможно. Мы всегда рады предложениям, но мы очень осторожны в добавлении новых вещей.
Реализация протокольных буферов на Go стремится быть последовательной с другими реализациями на языках. Как таковая, мы склонны избегать функций, которые чрезмерно специализированы только для Go. Специфичные для Go функции препятствуют цели протокольных буферов быть языково-нейтральным форматом обмена данными.
Если ваша идея не специфична для реализации на Go, вам следует присоединиться к группе обсуждения protobuf и предложить ее там.
Если у вас есть идея для реализации на Go, создайте issue в нашем трекере: https://github.com/golang/protobuf/issues
Могу ли я добавить опцию в Marshal или Unmarshal для их настройки?
Только если эта опция существует в других реализациях (например, C++, Java). Кодирование протокольных буферов (бинарная, JSON и текст) должно быть согласованным между реализациями, чтобы программа, написанная на одном языке, могла читать сообщения, написанные на другом.
Мы не будем добавлять какие-либо опции в реализацию на Go, которые влияют на данные, выводимые
функциями Marshal или читаемые функциями Unmarshal, если эквивалентная
опция не существует по крайней мере в одной другой поддерживаемой реализации.
Могу ли я настроить код, генерируемый protoc-gen-go?
В общем, нет. Protocol buffers предназначены быть языково-независимым форматом обмена данными, а специфичные для реализации настройки противоречат этому намерению.
Семантика размера в Go
Объясняет, как (не) использовать proto.Size
Функция proto.Size
возвращает размер в байтах wire-формата кодировки
proto.Message, проходя по всем его полям (включая подсообщения).
В частности, она возвращает размер того, как Go Protobuf закодирует сообщение.
Примечание о Protobuf Editions
С Protobuf Editions файлы .proto могут включать функции, которые изменяют
поведение сериализации. Это может повлиять на значение, возвращаемое proto.Size. Например,
установка features.field_presence = IMPLICIT приведет к тому, что скалярные поля,
установленные в значения по умолчанию, не будут сериализованы и, следовательно, не будут
влиять на размер сообщения.
Типичные случаи использования
Идентификация пустых сообщений
Проверка, возвращает ли
proto.Size
0, является распространенным способом проверки пустых сообщений:
if proto.Size(m) == 0 {
// Ни одно поле не установлено; пропустить обработку этого сообщения,
// или вернуть ошибку, или подобное.
}
Ограничение размера вывода программы
Допустим, вы пишете конвейер пакетной обработки, который производит рабочие задачи для другой системы, которую мы назовем «нисходящей системой» в этом примере. Нисходящая система настроена на обработку небольших и средних задач, но тестирование нагрузки показало, что система сталкивается с каскадным отказом, когда ей представляется рабочая задача размером более 500 МБ.
Лучшим решением является добавление защиты в нисходящую систему (см. https://cloud.google.com/blog/products/gcp/using-load-shedding-to-survive-a-success-disaster-cre-life-lessons), но когда реализация сброса нагрузки невозможна, вы можете решить добавить быстрое исправление в ваш конвейер:
func (*beamFn) ProcessElement(key string, value []byte, emit func(proto.Message)) {
task := produceWorkTask(value)
if proto.Size(task) > 500 * 1024 * 1024 {
// Пропускать каждую рабочую задачу более 500 МБ, чтобы не перегружать
// хрупкую нисходящую систему.
return
}
emit(task)
}
Неправильное использование: нет связи с Unmarshal
Поскольку proto.Size
возвращает количество байт для того, как Go Protobuf закодирует сообщение, это
небезопасно использовать proto.Size при демаршалинге (декодировании) потока входящих
сообщений Protobuf:
func bytesToSubscriptionList(data []byte) ([]*vpb.EventSubscription, error) {
subList := []*vpb.EventSubscription{}
for len(data) > 0 {
subscription := &vpb.EventSubscription{}
if err := proto.Unmarshal(data, subscription); err != nil {
return nil, err
}
subList = append(subList, subscription)
data = data[:len(data)-proto.Size(subscription)]
}
return subList, nil
}
Когда data содержит сообщение в неминимальном wire-формате,
proto.Size может вернуть размер, отличный от фактически демаршаленного,
что приводит к ошибке разбора (в лучшем случае) или неправильно разобранным данным в худшем
случае.
Следовательно, этот пример работает надежно только до тех пор, пока все входные сообщения сгенерированы (той же версией) Go Protobuf. Это неожиданно и, вероятно, не предполагалось.
Совет: Используйте
protodelim package
вместо этого для чтения/записи потоков сообщений Protobuf с указанием размера.
Продвинутое использование: предварительное определение размера буферов
Продвинутое использование
proto.Size — это
определение необходимого размера для буфера перед маршалингом:
opts := proto.MarshalOptions{
// Возможно избежать дополнительного вызова proto.Size в самом Marshal (см. документацию):
UseCachedSize: true,
}
// НЕ ОТПРАВЛЯЙТЕ код без реализации этой возможности оптимизации:
// вместо выделения памяти, возьмите буфер достаточного размера из пула.
// Знание размера буфера означает, что мы можем отбрасывать
// выбросы из пула, чтобы предотвратить неконтролируемый
// рост памяти в долгоживущих RPC-сервисах.
buf := make([]byte, 0, opts.Size(m))
var err error
buf, err = opts.MarshalAppend(buf, m) // не выделяет память
// Обратите внимание, что len(buf) может быть меньше cap(buf)! Читайте ниже:
Обратите внимание, что когда включено ленивое декодирование, proto.Size может вернуть больше байтов,
чем proto.Marshal (и варианты, такие как proto.MarshalAppend) запишет! Так
что когда вы помещаете закодированные байты в сеть (или на диск), обязательно работайте
с len(buf) и отбрасывайте любые предыдущие результаты proto.Size.
В частности, (под-)сообщение может «уменьшиться» между вызовами proto.Size и
proto.Marshal, когда:
- Включено ленивое декодирование
- и сообщение прибыло в неминимальном wire-формате
- и к сообщению не обращались до вызова
proto.Size, то есть оно еще не декодировано - и к сообщению обратились после
proto.Size(но доproto.Marshal), что вызывает его ленивое декодирование
Декодирование приводит к тому, что любые последующие вызовы proto.Marshal кодируют
сообщение (в отличие от простого копирования его wire-формата), что приводит к
неявной нормализации к тому, как Go кодирует сообщения, что в настоящее время выполняется в минимальном
wire-формате (но не полагайтесь на это!).
Как видите, сценарий довольно специфичен, но тем не менее лучшая
практика — рассматривать результаты proto.Size как верхнюю границу и никогда не предполагать, что
результат соответствует фактическому размеру закодированного сообщения.
Предыстория: Неминимальный wire-формат
При кодировании сообщений Protobuf существует один минимальный размер wire-формата и несколько больших неминимальных wire-форматов, которые декодируются в то же сообщение.
Неминимальный wire-формат (иногда также называемый «денормализованным wire-форматом») относится к сценариям, таким как появление неповторяющихся полей несколько раз, неоптимальное кодирование varint, упакованные повторяющиеся поля, которые появляются неупакованными в wire-формате, и другим.
Мы можем столкнуться с неминимальным wire-форматом в разных сценариях:
- Намеренно. Protobuf поддерживает конкатенацию сообщений путем конкатенации их wire-формата.
- Случайно. (Возможно, сторонний) кодировщик Protobuf кодирует не идеально (например, использует больше места, чем необходимо, при кодировании varint).
- Злонамеренно. Злоумышленник может специально создавать сообщения Protobuf, чтобы вызвать сбои по сети.
GO API
Миграция на Opaque API в Go
Описывает автоматизированную миграцию на Opaque API.
Opaque API — это последняя версия реализации Protocol Buffers для языка программирования Go. Старая версия теперь называется Open Struct API. Смотрите Go Protobuf: Releasing the Opaque API (блогпост) для введения.
Миграция на Opaque API происходит постепенно, для каждого proto-сообщения или
файла .proto, путем установки функции api_level в одно из ее
возможных значений:
API_OPENвыбирает Open Struct API. Это было портировано обратно в редакцию 2023, поэтому более старые версии плагина Go могут не учитывать это.API_HYBRID— это шаг между Open и Opaque: Hybrid API также включает методы доступа (так что вы можете обновить свой код), но все еще экспортирует поля структуры как и раньше. Нет разницы в производительности; этот уровень API только помогает с миграцией.API_OPAQUEвыбирает Opaque API; это значение по умолчанию для Edition 2024 и новее.
Чтобы переопределить значение по умолчанию для конкретного файла .proto, установите функцию api_level:
edition = "2024";
package log;
import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPEN;
message LogEntry { … }
Прежде чем вы сможете изменить api_level на API_OPAQUE для существующих файлов, все
существующие использования сгенерированного proto-кода должны быть обновлены. Инструмент
open2opaque помогает в этом.
Для вашего удобства вы также можете переопределить уровень API по умолчанию с помощью
флага командной строки protoc:
protoc […] --go_opt=default_api_level=API_OPEN
Чтобы переопределить уровень API по умолчанию для конкретного файла (вместо всех файлов),
используйте флаг сопоставления apilevelM (аналогично
флагу M для путей импорта):
protoc […] --go_opt=apilevelMhello.proto=API_OPEN
Автоматизированная миграция
Мы стараемся сделать миграцию существующих проектов на Opaque API максимально простой для вас: наш инструмент open2opaque выполняет большую часть работы!
Чтобы установить инструмент миграции, используйте:
go install google.golang.org/open2opaque@latest
go install golang.org/x/tools/cmd/goimports@latest
{{% alert title="Примечание" color="info" %}} Если вы столкнетесь с какими-либо проблемами при автоматизированной миграции, обратитесь к Opaque API: Руководство по ручной миграции. {{% /alert %}}
Подготовка проекта
Убедитесь, что ваша среда сборки и проект используют достаточно свежие версии Protocol Buffers и Go Protobuf:
-
Обновите компилятор protobuf (protoc) с страницы релизов protobuf до версии 29.0 или новее.
-
Обновите плагин Go для компилятора protobuf (protoc-gen-go) до версии 1.36.0 или новее:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest -
В каждом проекте обновите файл
go.mod, чтобы использовать модуль protobuf версии 1.36.0 или новее:go get google.golang.org/protobuf@latest{{% alert title="Примечание" color="note" %}} Если вы еще не импортируете
google.golang.org/protobuf, вы, возможно, все еще используете старый модуль. Смотрите анонсgoogle.golang.org/protobuf(от 2020) и мигрируйте ваш код, прежде чем вернуться на эту страницу. {{% /alert %}}
Шаг 1. Переход на Hybrid API
Используйте инструмент open2opaque, чтобы перевести ваши файлы .proto на Hybrid API:
open2opaque setapi -api HYBRID $(find . -name "*.proto")
Затем перекомпилируйте ваши протокольные буферы.
Ваш существующий код продолжит собираться. Hybrid API — это шаг между Open и Opaque API, который добавляет новые методы доступа, но оставляет поля структуры видимыми.
Шаг 2. open2opaque rewrite
Чтобы переписать ваш код Go для использования Opaque API, выполните команду open2opaque rewrite:
open2opaque rewrite -levels=red github.com/robustirc/robustirc/...
Вы можете указать один или несколько пакетов или шаблонов.
В качестве примера, если у вас был код наподобие этого:
logEntry := &logpb.LogEntry{}
if req.IPAddress != nil {
logEntry.IPAddress = redactIP(req.IPAddress)
}
logEntry.BackendServer = proto.String(host)
Инструмент перепишет его для использования методов доступа:
logEntry := &logpb.LogEntry{}
if req.HasIPAddress() {
logEntry.SetIPAddress(redactIP(req.GetIPAddress()))
}
logEntry.SetBackendServer(host)
Другой распространенный пример — инициализация сообщения protobuf литералом структуры:
return &logpb.LogEntry{
BackendServer: proto.String(host),
}
В Opaque API эквивалентом является использование Builder:
return logpb.LogEntry_builder{
BackendServer: proto.String(host),
}.Build()
Инструмент классифицирует доступные перезаписи по разным уровням. Аргумент
-levels=red включает все перезаписи, включая те, которые требуют проверки человеком.
Доступны следующие уровни:
- зеленый: Безопасные перезаписи (высокая уверенность). Включает большинство изменений, которые делает инструмент. Эти изменения не требуют пристального взгляда и могут быть даже отправлены автоматизацией, без какого-либо человеческого надзора.
- желтый: (разумная уверенность) Эти перезаписи требуют проверки человеком. Они должны быть правильными, но пожалуйста, проверьте их.
- красный: Потенциально опасные
перезаписи, изменяющие редкие и сложные шаблоны. Они требуют тщательной
проверки человеком. Например, когда существующая функция принимает параметр
*string, типичное исправление с использованиемproto.String(msg.GetFoo())не работает, если функция предназначалась для изменения значения поля путем записи в указатель (*foo = "value").
Многие программы могут быть полностью мигрированы только с помощью зеленых изменений. Прежде чем вы сможете мигрировать proto-сообщение или файл на Opaque API, вам нужно завершить все перезаписи всех уровней, после чего в вашем коде не останется прямого доступа к структуре.
Шаг 3. Миграция и проверка
Чтобы завершить миграцию, используйте инструмент open2opaque, чтобы перевести ваши файлы .proto
на Opaque API:
open2opaque setapi -api OPAQUE $(find . -name "*.proto")
Теперь любой оставшийся код, который еще не был переписан под Opaque API, больше не скомпилируется.
Запустите ваши модульные тесты, интеграционные тесты и другие шаги проверки, если они есть.
Вопросы? Проблемы?
Сначала ознакомьтесь с Opaque API FAQ. Если это не ответит на ваш вопрос или не решит вашу проблему, смотрите Где можно задать вопросы или сообщить о проблемах?
Opaque API: Ручная миграция
Описывает ручную миграцию на Opaque API
Opaque API — это последняя версия реализации Protocol Buffers для языка программирования Go. Старая версия теперь называется Open Struct API. Смотрите Go Protobuf: Releasing the Opaque API (блогпост) для введения.
Это руководство пользователя по миграции использования Go Protobuf со старого Open Struct API на новый Opaque API.
{{% alert title="Предупреждение" color="warning" %}} Вы
смотрите руководство по ручной миграции. Обычно лучше использовать
инструмент open2opaque для автоматизации миграции. Смотрите
Миграция на Opaque API
вместо этого. {{% /alert %}}
Руководство по сгенерированному коду предоставляет более подробную информацию. Это руководство сравнивает старый и новый API параллельно.
Создание сообщений
Предположим, есть сообщение protobuf, определенное следующим образом:
message Foo {
uint32 uint32 = 1;
bytes bytes = 2;
oneof union {
string string = 4;
MyMessage message = 5;
}
enum Kind { … };
Kind kind = 9;
}
Вот пример того, как создать это сообщение из литеральных значений:
| Open Struct API (старый) | Opaque API (новый) |
|
|
Как вы можете видеть, структуры builder позволяют осуществить почти 1:1 перевод между Open Struct API (старый) и Opaque API (новый).
В целом, предпочтительнее использовать builders для удобочитаемости. Только в редких случаях, например, при создании сообщений Protobuf в горячем внутреннем цикле, может быть предпочтительнее использовать сеттеры вместо builders. Смотрите Opaque API FAQ: Следует ли использовать builders или сеттеры? для более подробной информации.
Исключением из приведенного выше примера является работа с oneof: Open Struct API (старый) использует тип структуры-обертки для каждого случая oneof, тогда как Opaque API (новый) рассматривает поля oneof как обычные поля сообщения:
| Open Struct API (старый) | Opaque API (новый) |
|
|
Для набора полей структуры Go, связанных с объединением oneof, только одно поле может быть заполнено. Если заполнено несколько полей случаев oneof, побеждает последнее (в порядке объявления полей в вашем .proto файле).
Скалярные поля
Предположим, есть сообщение, определенное со скалярным полем:
message Artist {
int32 birth_year = 1;
}
Поля сообщений Protobuf, для которых Go использует скалярные типы (bool, int32, int64,
uint32, uint64, float32, float64, string, []byte и enum), будут иметь методы доступа Get и
Set. Поля с
явным присутствием
также будут иметь методы Has и Clear.
Для поля типа int32 с именем birth_year будут сгенерированы следующие методы доступа
для него:
func (m *Artist) GetBirthYear() int32
func (m *Artist) SetBirthYear(v int32)
func (m *Artist) HasBirthYear() bool
func (m *Artist) ClearBirthYear()
Get возвращает значение для поля. Если поле не установлено или получатель сообщения
равен nil, он возвращает значение по умолчанию. Значение по умолчанию —
нулевое значение, если явно не установлено с
помощью опции default.
Set сохраняет предоставленное значение в поле. Он вызывает панику при вызове на nil
получателе сообщения.
Для полей bytes вызов Set с nil []byte будет считаться установленным. Например,
вызов Has сразу после возвращает true. Вызов Get сразу
после вернет срез нулевой длины (может быть либо nil, либо пустым срезом). Пользователи
должны использовать Has для определения присутствия и не полагаться на то, возвращает ли Get
nil.
Has сообщает, заполнено ли поле. Он возвращает false при вызове на
nil получателе сообщения.
Clear очищает поле. Он вызывает панику при вызове на nil получателе сообщения.
Примеры фрагментов кода, использующих поле string в:
| Open Struct API (старый) | Opaque API (новый) |
|
|
Поля сообщений
Предположим, есть сообщение, определенное с полем типа сообщение:
message Band {}
message Concert {
Band headliner = 1;
}
Поля сообщений Protobuf типа message будут иметь методы Get, Set, Has и
Clear.
Для поля типа message с именем headliner будут сгенерированы следующие методы доступа
для него:
func (m *Concert) GetHeadliner() *Band
func (m *Concert) SetHeadliner(*Band)
func (m *Concert) HasHeadliner() bool
func (m *Concert) ClearHeadliner()
Get возвращает значение для поля. Он возвращает nil, если не установлено или при вызове на
nil получателе сообщения. Проверка, возвращает ли Get nil, эквивалентна проверке
возвращает ли Has false.
Set сохраняет предоставленное значение в поле. Он вызывает панику при вызове на nil
получателе сообщения. Вызов Set с nil указателем эквивалентен вызову
Clear.
Has сообщает, заполнено ли поле. Он возвращает false при вызове на
nil получателе сообщения.
Clear очищает поле. Он вызывает панику при вызове на nil получателе сообщения.
Примеры фрагментов кода
| Open Struct API (старый) | Opaque (новый) |
|
|
Повторяющиеся поля
Предположим, есть сообщение, определенное с повторяющимся полем типа сообщение:
message Concert {
repeated Band support_acts = 2;
}
Повторяющиеся поля будут иметь методы Get и Set.
Get возвращает значение для поля. Он возвращает nil, если поле не установлено или
получатель сообщения равен nil.
Set сохраняет предоставленное значение в поле. Он вызывает панику при вызове на nil
получателе сообщения. Set сохранит копию заголовка среза, который предоставлен.
Изменения в содержимом среза наблюдаемы в повторяющемся поле. Следовательно, если
Set вызывается с пустым срезом, вызов Get сразу после вернет
тот же срез. Для вывода wire или text маршалинга переданный nil срез
неотличим от пустого среза.
Для повторяющегося поля типа message с именем support_acts в сообщении Concert,
будут сгенерированы следующие методы доступа для него:
func (m *Concert) GetSupportActs() []*Band
func (m *Concert) SetSupportActs([]*Band)
Примеры фрагментов кода
| Open Struct API (старый) | Opaque API (новый) |
|
|
Отображения
Предположим, есть сообщение, определенное с полем типа отображение:
message MerchBooth {
map<string, MerchItems> items = 1;
}
Поля-отображения будут иметь методы Get и Set.
Get возвращает значение для поля. Он возвращает nil, если поле не установлено или
получатель сообщения равен nil.
Set сохраняет предоставленное значение в поле. Он вызывает панику при вызове на nil
получателе сообщения. Set сохранит копию предоставленной ссылки на отображение. Изменения
в предоставленном отображении наблюдаемы в поле отображения.
Для поля-отображения с именем items в сообщении MerchBooth будут сгенерированы следующие методы доступа
для него:
func (m *MerchBooth) GetItems() map[string]*MerchItem
func (m *MerchBooth) SetItems(map[string]*MerchItem)
Примеры фрагментов кода
| Open Struct API (старый) | Opaque API (новый) |
|
|
Oneofs
Для каждой группы объединения oneof будут методы Which, Has и Clear
в сообщении. Также будут методы Get, Set, Has и Clear для
каждого поля случая oneof в этом объединении.
Предположим, есть сообщение, определенное с полями oneof image_url и
image_data в oneof avatar следующим образом:
message Profile {
oneof avatar {
string image_url = 1;
bytes image_data = 2;
}
}
Сгенерированный Opaque API для этого oneof будет:
func (m *Profile) WhichAvatar() case_Profile_Avatar { … }
func (m *Profile) HasAvatar() bool { … }
func (m *Profile) ClearAvatar() { … }
type case_Profile_Avatar protoreflect.FieldNumber
const (
Profile_Avatar_not_set_case case_Profile_Avatar = 0
Profile_ImageUrl_case case_Profile_Avatar = 1
Profile_ImageData_case case_Profile_Avatar = 2
)
Which сообщает, какое поле случая установлено, возвращая номер поля. Он
возвращает 0, когда ни одно не установлено или при вызове на nil получателе сообщения.
Has сообщает, установлено ли любое из полей внутри oneof. Он возвращает
false при вызове на nil получателе сообщения.
Clear очищает текущее установленное поле случая в oneof. Он вызывает панику на nil
получателе сообщения.
Сгенерированный Opaque API для каждого поля случая oneof будет:
func (m *Profile) GetImageUrl() string { … }
func (m *Profile) GetImageData() []byte { … }
func (m *Profile) SetImageUrl(v string) { … }
func (m *Profile) SetImageData(v []byte) { … }
func (m *Profile) HasImageUrl() bool { … }
func (m *Profile) HasImageData() bool { … }
func (m *Profile) ClearImageUrl() { … }
func (m *Profile) ClearImageData() { … }
Get возвращает значение для поля случая. Он вернет нулевое значение, если
поле случая не установлено или при вызове на nil получателе сообщения.
Set сохраняет предоставленное значение в поле случая. Он также неявно очищает
поле случая, которое было ранее заполнено в объединении oneof. Вызов
Set на поле случая сообщения oneof с nil значением установит поле в
пустое сообщение. Он вызывает панику при вызове на nil получателе сообщения.
Has сообщает, установлено ли поле случая или нет. Он возвращает false при вызове
на nil получателе сообщения.
Clear очищает поле случая. Если оно было ранее установлено, объединение oneof также
очищается. Если объединение oneof установлено на другое поле, оно не очистит
объединение oneof. Он вызывает панику при вызове на nil получателе сообщения.
Примеры фрагментов кода
| Open Struct API (старый) | Opaque API (новый) |
|
|
Рефлексия
Код, который использует пакет Go reflect на типах сообщений proto для доступа к полям структуры
и тегам, больше не будет работать при миграции с Open Struct
API. Коду потребуется перейти на использование
protoreflect
Некоторые распространенные библиотеки используют Go reflect под капотом, примеры:
- encoding/json
- Используйте protobuf/encoding/protojson.
- pretty
- cmp
- Чтобы правильно использовать
cmp.Equalс сообщениями protobuf, используйте protocmp.Transform
- Чтобы правильно использовать
Go Opaque API FAQ
Список часто задаваемых вопросов о Opaque API.
Opaque API — это последняя версия реализации Protocol Buffers для языка программирования Go. Старая версия теперь называется Open Struct API. Смотрите Go Protobuf: The new Opaque API (блогпост) для введения.
Этот FAQ отвечает на распространенные вопросы о новом API и процессе миграции.
Какой API следует использовать при создании нового файла .proto?
Мы рекомендуем выбирать Opaque API для новой разработки.
Protobuf Edition 2024 (см. Обзор редакций Protobuf) сделал Opaque API значением по умолчанию.
Как включить новый Opaque API для моих сообщений?
Начиная с Protobuf Edition 2023, вы можете выбрать Opaque API, установив
функцию editions api_level в API_OPAQUE в вашем файле .proto. Это можно
установить для каждого файла или для каждого сообщения:
edition = "2023";
package log;
import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;
message LogEntry { … }
Protobuf Edition 2024 по умолчанию использует Opaque API, что означает, что вам не понадобятся дополнительные импорты или опции:
edition = "2024";
package log;
message LogEntry { … }
Для вашего удобства вы также можете переопределить уровень API по умолчанию с помощью
флага командной строки protoc:
protoc […] --go_opt=default_api_level=API_HYBRID
Чтобы переопределить уровень API по умолчанию для конкретного файла (вместо всех файлов),
используйте флаг сопоставления apilevelM (аналогично
флагу M для путей импорта):
protoc […] --go_opt=apilevelMhello.proto=API_HYBRID
Флаги командной строки также работают для файлов .proto, все еще использующих синтаксис proto2 или proto3,
но если вы хотите выбрать уровень API из самого файла .proto,
вам нужно сначала перенести этот файл на editions.
Как включить ленивое декодирование?
- Мигрируйте ваш код для использования opaque реализации.
- Установите опцию
[lazy = true]на поля подсообщений proto, которые должны быть лениво декодированы. - Запустите ваши модульные и интеграционные тесты, а затем разверните в промежуточной среде.
Игнорируются ли ошибки при ленивом декодировании?
Нет.
proto.Marshal
всегда проверяет данные wire-формата, даже когда декодирование отложено до
первого доступа.
Где можно задать вопросы или сообщить о проблемах?
Если вы нашли проблему с инструментом миграции open2opaque (например, некорректно
переписанный код), пожалуйста, сообщите об этом в
трекере проблем open2opaque.
Если вы нашли проблему с Go Protobuf, пожалуйста, сообщите об этом в трекере проблем Go Protobuf.
Каковы преимущества Opaque API?
Opaque API поставляется с многочисленными преимуществами:
- Он использует более эффективное представление в памяти, тем самым уменьшая память и стоимость Сборки Мусора.
- Он делает возможным ленивое декодирование, что может значительно улучшить производительность.
- Он исправляет ряд острых углов. Ошибки, возникающие из-за сравнения адресов указателей, случайного совместного использования или нежелательного использования рефлексии Go, все предотвращаются при использовании Opaque API.
- Он делает идеальное размещение в памяти возможным, включая оптимизации на основе профилей.
Смотрите блогпост Go Protobuf: The new Opaque API для более подробной информации по этим пунктам.
Что быстрее, Builders или Setters?
В общем, код, использующий builders:
_ = pb.M_builder{
F: &val,
}.Build()
медленнее, чем следующий эквивалент:
m := &pb.M{}
m.SetF(val)
по следующим причинам:
- Вызов
Build()перебирает все поля в сообщении (даже те, которые не установлены явно) и копирует их значения (если есть) в финальное сообщение. Эта линейная производительность имеет значение для сообщений со многими полями. - Есть потенциальное дополнительное выделение в куче (
&val). - Builder может быть значительно больше и использовать больше памяти при наличии полей oneof. Builders имеют поле для каждого члена объединения oneof, тогда как сообщение может хранить сам oneof как одно поле.
Помимо производительности во время выполнения, если размер бинарного файла является для вас проблемой, избегание builders приведет к меньшему количеству кода.
Как использовать Builders?
Builders предназначены для использования как значения и с немедленным вызовом Build().
Избегайте использования указателей на builders или хранения builders в переменных.
m := pb.M_builder{
// ...
}.Build()
// ПЛОХО: Избегайте использования указателя
m := (&pb.M_builder{
// ...
}).Build()
// ПЛОХО: избегайте хранения в переменной
b := pb.M_builder{
// ...
}
m := b.Build()
Сообщения Proto неизменяемы в некоторых других языках, поэтому пользователи склонны передавать тип builder в вызовы функций при конструировании сообщения proto. Сообщения Go proto изменяемы, следовательно, нет необходимости передавать builder в вызовы функций. Просто передавайте сообщение proto.
// ПЛОХО: избегайте передачи builder
func populate(mb *pb.M_builder) {
mb.Field1 = proto.Int32(4711)
//...
}
// ...
mb := pb.M_builder{}
populate(&mb)
m := mb.Build()
func populate(mb *pb.M) {
mb.SetField1(4711)
//...
}
// ...
m := &pb.M{}
populate(m)
Builders предназначены для имитации композитного литерального конструирования Open Struct API, а не как альтернативное представление сообщения proto.
Рекомендуемый шаблон также более производителен. Предполагаемое использование Build(),
где он вызывается непосредственно на литерале структуры builder, может быть хорошо оптимизировано.
Отдельный вызов Build() гораздо сложнее оптимизировать, так как компилятор может не
легко определить, какие поля заполнены. Если builder живет дольше, также
высока вероятность того, что небольшие объекты, такие как скаляры, должны быть выделены в куче и
позже должны быть освобождены сборщиком мусора.
Следует ли использовать Builders или Setters?
При создании пустого протокольного буфера вы должны использовать new или пустой
композитный литерал. Оба одинаково идиоматичны для создания нулевого
инициализированного значения в Go и более производительны, чем пустой builder.
m1 := new(pb.M)
m2 := &pb.M{}
// ПЛОХО: избегайте: излишне сложно
m1 := pb.M_builder{}.Build()
В случаях, когда вам нужно создать непустые протокольные буферы, у вас есть выбор между использованием сеттеров или использованием builders. Любой вариант допустим, но большинство людей сочтут builders более читаемыми. Если код, который вы пишете, должен хорошо работать, сеттеры, как правило, немного более производительны, чем builders.
// Рекомендуется: использование builders
m1 := pb.M1_builder{
Submessage: pb.M2_builder{
Submessage: pb.M3_builder{
String: proto.String("hello world"),
Int: proto.Int32(42),
}.Build(),
Bytes: []byte("hello"),
}.Build(),
}.Build()
// Также допустимо: использование сеттеров
m3 := &pb.M3{}
m3.SetString("hello world")
m3.SetInt(42)
m2 := &pb.M2{}
m2.SetSubmessage(m3)
m2.SetBytes([]byte("hello"))
m1 := &pb.M1{}
m1.SetSubmessage(m2)
Вы можете комбинировать использование builder и сеттеров, если определенные поля требуют условной логики перед установкой.
m1 := pb.M1_builder{
Field1: value1,
}.Build()
if someCondition() {
m1.SetField2(value2)
m1.SetField3(value3)
}
Как я могу повлиять на поведение Builder в open2opaque?
Флаг --use_builders инструмента open2opaque может иметь следующие значения:
--use_builders=everywhere: всегда использовать builders, без исключений.--use_builders=tests: использовать builders только в тестах, в остальных случаях использовать сеттеры.--use_builders=nowhere: никогда не использовать builders.
Какого выигрыша в производительности можно ожидать?
Это сильно зависит от вашей рабочей нагрузки. Следующие вопросы могут направлять ваше исследование производительности:
- Какой процент использования вашего CPU приходится на Go Protobuf? Некоторые рабочие нагрузки, такие как конвейеры анализа логов, которые вычисляют статистику на основе записей Protobuf, могут тратить около 50% использования CPU на Go Protobuf. Улучшения производительности, вероятно, будут четко видны в таких рабочих нагрузках. На другом конце спектра, в программах, которые тратят только 3-5% использования CPU на Go Protobuf, улучшения производительности часто будут незначительными по сравнению с другими возможностями.
- Насколько ваша программа поддается ленивому декодированию? Если большие части входных сообщений никогда не доступны, ленивое декодирование может сэкономить много работы. Этот шаблон обычно встречается в таких задачах, как прокси-серверы (которые передают входные данные как есть), или конвейеры анализа логов с высокой избирательностью (которые отбрасывают многие записи на основе предиката высокого уровня).
- Содержат ли ваши определения сообщений много элементарных полей с явным присутствием? Opaque API использует более эффективное представление в памяти для элементарных полей, таких как целые числа, логические значения, перечисления и числа с плавающей точкой, но не для строк, повторяющихся полей или подсообщений.
Как Proto2, Proto3 и Editions относятся к Opaque API?
Термины proto2 и proto3 относятся к разным версиям синтаксиса в ваших файлах .proto.
Редакции Protobuf — это
преемник как proto2, так и proto3.
Opaque API влияет только на сгенерированный код в файлах .pb.go, а не на то, что вы
пишете в ваших файлах .proto.
Opaque API работает одинаково, независимо от того, какой синтаксис или редакцию используют ваши
файлы .proto. Однако, если вы хотите выбрать Opaque API для каждого файла
(в отличие от использования флага командной строки при запуске protoc),
вы должны сначала перенести файл на editions. Смотрите
Как включить новый Opaque API для моих сообщений? для подробностей.
Почему изменяется только макет памяти элементарных полей?
Раздел "Opaque structs use less memory" в анонсном блогпосте объясняет:
Это улучшение производительности [более эффективное моделирование присутствия поля] сильно зависит от формы вашего сообщения protobuf: изменение затрагивает только элементарные поля, такие как целые числа, логические значения, перечисления и числа с плавающей точкой, но не строки, повторяющиеся поля или подсообщения.
Естественный последующий вопрос: почему строки, повторяющиеся поля и подсообщения остаются указателями в Opaque API. Ответ двоякий.
Соображение 1: Использование памяти
Представление подсообщений как значений вместо указателей увеличило бы использование памяти: каждый тип сообщения Protobuf несет внутреннее состояние, которое потребляло бы память, даже когда подсообщение фактически не установлено.
Для строк и повторяющихся полей ситуация более нюансированная. Давайте сравним использование памяти строкового значения по сравнению с указателем на строку:
| Тип переменной Go | установлено? | словоа | #байт |
|---|---|---|---|
string | да | 2 (данные, длина) | 16 |
string | нет | 2 (данные, длина) | 16 |
*string | да | 1 (данные) + 2 (данные, длина) | 24 |
*string | нет | 1 (данные) | 8 |
(Ситуация похожа для срезов, но заголовки срезов нуждаются в 3 словах: данные, длина, емкость.)
Если ваши строковые поля подавляюще не установлены, использование указателя экономит ОЗУ. Конечно, эта экономия достигается за счет введения большего количества выделений памяти и указателей в программу, что увеличивает нагрузку на Сборщик Мусора.
Преимущество Opaque API в том, что мы можем изменить представление без каких-либо изменений в пользовательском коде. Текущий макет памяти был оптимальным для нас, когда мы вводили его, но если бы мы измеряли сегодня или через 5 лет в будущем, возможно, мы выбрали бы другой макет.
Как описано в разделе "Making the ideal memory layout possible" анонсного блогпоста, мы стремимся принимать эти решения по оптимизации для каждой рабочей нагрузки в будущем.
Соображение 2: Ленивое декодирование
Помимо соображений использования памяти, есть еще одно ограничение: поля, для которых включено ленивое декодирование, должны быть представлены указателями.
Сообщения Protobuf безопасны для конкурентного доступа (но не конкурентной модификации),
поэтому если две разные горутины вызывают ленивое декодирование, им нужно как-то координироваться.
Эта координация реализована через использование
пакета sync/atomic, который может обновлять
указатели атомарно, но не заголовки срезов (которые превышают слово).
Хотя protoc в настоящее время разрешает ленивое декодирование только для (неповторяющихся)
подсообщений, это рассуждение применимо ко всем типам полей.
DART
API Dart
Сгенерированный код для Dart
Описание того, какой код на Dart генерирует компилятор protocol buffer для любого заданного определения протокола.
Различия между сгенерированным кодом для proto2, proto3 и editions выделены - обратите внимание, что эти различия находятся в сгенерированном коде, как описано в этом документе, а не в базовом API, который одинаков в обеих версиях. Вам следует прочитать руководство по языку proto2, руководство по языку proto3 или руководство по языку editions перед чтением этого документа.
Вызов компилятора
Компилятору protocol buffer требуется плагин для генерации Dart кода. Его установка в соответствии с инструкциями предоставляет бинарный файл protoc-gen-dart, который protoc использует при вызове с флагом командной строки --dart_out. Флаг --dart_out указывает компилятору, куда записывать исходные файлы Dart. Для входного файла .proto компилятор создает среди прочих файл .pb.dart.
Имя файла .pb.dart вычисляется путем взятия имени файла .proto и внесения двух изменений:
- Расширение (
.proto) заменяется на.pb.dart. Например, файл с именемfoo.protoприводит к созданию выходного файла с именемfoo.pb.dart. - Путь proto (указанный с помощью флага командной строки
--proto_pathили-I) заменяется на выходной путь (указанный с помощью флага--dart_out).
Например, при вызове компилятора следующим образом:
protoc --proto_path=src --dart_out=build/gen src/foo.proto src/bar/baz.proto
компилятор прочитает файлы src/foo.proto и src/bar/baz.proto. Он создаст: build/gen/foo.pb.dart и build/gen/bar/baz.pb.dart. Компилятор автоматически создаст директорию build/gen/bar, если это необходимо, но он не создаст build или build/gen; они должны уже существовать.
Сообщения
Для простого объявления сообщения:
message Foo {}
Компилятор protocol buffer генерирует класс с именем Foo, который расширяет класс GeneratedMessage.
Класс GeneratedMessage определяет методы, которые позволяют проверять, манипулировать, читать или записывать все сообщение. В дополнение к этим методам, класс Foo определяет следующие методы и конструкторы:
Foo(): Конструктор по умолчанию. Создает экземпляр, в котором все единичные поля не установлены, а повторяющиеся поля пусты.Foo.fromBuffer(...): СоздаетFooиз сериализованных данных protocol buffer, представляющих сообщение.Foo.fromJson(...): СоздаетFooиз строки JSON, кодирующей сообщение.Foo clone(): Создает глубокую копию полей в сообщении.Foo copyWith(void Function(Foo) updates): Создает записываемую копию этого сообщения, применяет к нейupdatesи помечает копию доступной только для чтения перед возвратом.static Foo create(): Фабричная функция для создания единичногоFoo.static PbList<Foo> createRepeated(): Фабричная функция для создания List, реализующего изменяемое повторяющееся поле элементовFoo.static Foo getDefault(): Возвращает singleton-экземплярFoo, который идентичен вновь созданному экземпляру Foo (так что все единичные поля не установлены, а все повторяющиеся поля пусты).
Вложенные типы
Сообщение может быть объявлено внутри другого сообщения. Например:
message Foo {
message Bar {
}
}
В этом случае компилятор генерирует два класса: Foo и Foo_Bar.
Поля
В дополнение к методам, описанным в предыдущем разделе, компилятор protocol buffer генерирует методы доступа для каждого поля, определенного в сообщении в файле .proto.
Обратите внимание, что сгенерированные имена всегда используют верблюжий регистр (camelCase), даже если имя поля в файле .proto использует нижний регистр с подчеркиваниями (как и должно быть). Преобразование регистра работает следующим образом:
- Для каждого подчеркивания в имени подчеркивание удаляется, а следующая буква capitalizes.
- Если к имени будет присоединен префикс (например, "has"), первая буква capitalizes. В противном случае она приводится к нижнему регистру.
Таким образом, для поля foo_bar_baz геттер становится get fooBarBaz, а метод с префиксом has будет hasFooBarBaz.
Единичные примитивные поля
Все поля имеют явное присутствие в реализации на Dart.
Для следующего определения поля:
int32 foo = 1;
Компилятор сгенерирует следующие методы доступа в классе сообщения:
-
int get foo: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию. -
bool hasFoo(): Возвращаетtrue, если поле установлено. -
set foo(int value): Устанавливает значение поля. После вызоваhasFoo()будет возвращатьtrue, аget fooбудет возвращатьvalue. -
void clearFoo(): Очищает значение поля. После вызоваhasFoo()будет возвращатьfalse, аget fooбудет возвращать значение по умолчанию.{{% alert title="Примечание" color="note" %}} Из-за особенности в реализации Dart proto3 следующие методы генерируются даже если настроено неявное присутствие.{{% /alert %}}
-
bool hasFoo(): Возвращаетtrue, если поле установлено.{{% alert title="Примечание" color="note" %}} Этому значению нельзя действительно доверять, если proto был сериализован на другом языке, который поддерживает неявное присутствие (например, Java). Несмотря на то, что Dart отслеживает присутствие, другие языки этого не делают, и циклическая обработка поля с неявным присутствием и нулевым значением приведет к его "исчезновению" с точки зрения Dart. {{% /alert %}}
-
void clearFoo(): Очищает значение поля. После вызоваhasFoo()будет возвращатьfalse, аget fooбудет возвращать значение по умолчанию.
Для других простых типов полей соответствующий тип Dart выбирается в соответствии с таблицей скалярных типов значений. Для типов сообщений и перечислений тип значения заменяется классом сообщения или перечисления.
Единичные поля сообщений
Для данного типа сообщения:
message Bar {}
Для сообщения с полем Bar:
// proto2
message Baz {
optional Bar bar = 1;
// Сгенерированный код будет тем же результатом, если используется required вместо optional.
}
// proto3 и editions
message Baz {
Bar bar = 1;
}
Компилятор сгенерирует следующие методы доступа в классе сообщения:
Bar get bar: Возвращает текущее значение поля. Если поле не установлено, возвращает значение по умолчанию.set bar(Bar value): Устанавливает значение поля. После вызоваhasBar()будет возвращатьtrue, аget barбудет возвращатьvalue.bool hasBar(): Возвращаетtrue, если поле установлено.void clearBar(): Очищает значение поля. После вызоваhasBar()будет возвращатьfalse, аget barбудет возвращать значение по умолчанию.Bar ensureBar(): Устанавливаетbarв пустой экземпляр, еслиhasBar()возвращаетfalse, а затем возвращает значениеbar. После вызоваhasBar()будет возвращатьtrue.
Повторяющиеся поля
Для этого определения поля:
repeated int32 foo = 1;
Компилятор сгенерирует:
List<int> get foo: Возвращает список, лежащий в основе поля. Если поле не установлено, возвращает пустой список. Изменения в списке отражаются в поле.
Поля Int64
Для этого определения поля:
int64 bar = 1;
Компилятор сгенерирует:
Int64 get bar: Возвращает объектInt64, содержащий значение поля.
Обратите внимание, что Int64 не встроен в основные библиотеки Dart. Для работы с этими объектами вам может потребоваться импортировать библиотеку Dart fixnum:
import 'package:fixnum/fixnum.dart';
Поля Map
Для определения поля map like this:
map<int32, int32> map_field = 1;
Компилятор сгенерирует следующий геттер:
Map<int, int> get mapField: Возвращает Dart map, лежащий в основе поля. Если поле не установлено, возвращает пустой map. Изменения в map отражаются в поле.
Any
Для поля Any like this:
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
google.protobuf.Any details = 2;
}
В нашем сгенерированном коде геттер для поля details возвращает экземпляр com.google.protobuf.Any. Это предоставляет следующие специальные методы для упаковки и распаковки значений Any:
/// Распаковывает сообщение в [value] в [instance].
///
/// Выбрасывает [InvalidProtocolBufferException], если [typeUrl] не соответствует
/// типу [instance].
///
/// Типичное использование: `any.unpackInto(new Message())`.
///
/// Возвращает [instance].
T unpackInto<T extends GeneratedMessage>(T instance,
{ExtensionRegistry extensionRegistry = ExtensionRegistry.EMPTY});
/// Возвращает `true`, если закодированное сообщение соответствует типу [instance].
///
/// Может использоваться с экземпляром по умолчанию:
/// `any.canUnpackInto(Message.getDefault())`
bool canUnpackInto(GeneratedMessage instance);
/// Создает новый [Any], кодирующий [message].
///
/// [typeUrl] будет [typeUrlPrefix]/`fullName`, где `fullName` - это
/// полное квалифицированное имя типа [message].
static Any pack(GeneratedMessage message,
{String typeUrlPrefix = 'type.googleapis.com'});
Oneof
Для определения oneof like this:
message Foo {
oneof test {
string name = 1;
SubMessage sub_message = 2;
}
}
Компилятор сгенерирует следующий тип перечисления Dart:
enum Foo_Test { name, subMessage, notSet }
Кроме того, он сгенерирует эти методы:
Foo_Test whichTest(): Возвращает перечисление, указывающее, какое поле установлено. ВозвращаетFoo_Test.notSet, если ни одно из них не установлено.void clearTest(): Очищает значение поля oneof, которое в настоящее время установлено (если есть), и устанавливает случай oneof вFoo_Test.notSet.
Для каждого поля внутри определения oneof генерируются обычные методы доступа к полям. Например, для name:
String get name: Возвращает текущее значение поля, если случай oneof -Foo_Test.name. В противном случае возвращает значение по умолчанию.set name(String value): Устанавливает значение поля и устанавливает случай oneof вFoo_Test.name. После вызоваget nameбудет возвращатьvalue, аwhichTest()будет возвращатьFoo_Test.name.void clearName(): Ничего не изменится, если случай oneof неFoo_Test.name. В противном случае очищает значение поля. После вызоваget nameбудет возвращать значение по умолчанию, аwhichTest()будет возвращатьFoo_Test.notSet.
Перечисления
Для определения перечисления like:
enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
COLOR_GREEN = 2;
COLOR_BLUE = 3;
}
Компилятор protocol buffer сгенерирует класс с именем Color, который расширяет класс ProtobufEnum. Класс будет включать static const Color для каждого из четырех значений, а также static const List<Color>, который содержит значения.
static const List<Color> values = <Color> [
COLOR_UNSPECIFIED,
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE,
];
Он также будет включать следующий метод:
static Color? valueOf(int value): ВозвращаетColor, соответствующий заданному числовому значению.
Каждое значение будет иметь следующие свойства:
name: Имя перечисления, как указано в файле .proto.value: Целочисленное значение перечисления, как указано в файле .proto.
Обратите внимание, что язык .proto позволяет нескольким символам перечисления иметь одно и то же числовое значение. Символы с одинаковым числовым значением являются синонимами. Например:
enum Foo {
BAR = 0;
BAZ = 0;
}
В этом случае BAZ является синонимом для BAR и будет определен следующим образом:
static const Foo BAZ = BAR;
Перечисление может быть определено вложенным в тип сообщения. Например, при определении перечисления like:
message Bar {
enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
COLOR_GREEN = 2;
COLOR_BLUE = 3;
}
}
Компилятор protocol buffer сгенерирует класс с именем Bar, который расширяет GeneratedMessage, и класс с именем Bar_Color, который расширяет ProtobufEnum.
Расширения (недоступно в proto3)
Для файла foo_test.proto, включающего сообщение с диапазоном расширений и определением расширения верхнего уровня:
message Foo {
extensions 100 to 199;
}
extend Foo {
optional int32 bar = 101;
}
Компилятор protocol buffer сгенерирует, в дополнение к классу Foo, класс Foo_test, который будет содержать static Extension для каждого поля расширения в файле вместе с методом для регистрации всех расширений в ExtensionRegistry:
static final Extension barstatic void registerAllExtensions(ExtensionRegistry registry): Регистрирует все определенные расширения в данном реестре.
Методы доступа расширений Foo могут использоваться следующим образом:
Foo foo = Foo();
foo.setExtension(Foo_test.bar, 1);
assert(foo.hasExtension(Foo_test.bar));
assert(foo.getExtension(Foo_test.bar)) == 1);
Расширения также могут быть объявлены вложенными внутри другого сообщения:
message Baz {
extend Foo {
int32 bar = 124;
}
}
В этом случае расширение bar вместо этого объявляется как статический член класса Baz.
При разборе сообщения, которое может иметь расширения, вы должны предоставить ExtensionRegistry, в котором вы зарегистрировали любые расширения, которые хотите иметь возможность разбирать. В противном случае эти расширения будут просто обрабатываться как неизвестные поля. Например:
ExtensionRegistry registry = ExtensionRegistry();
registry.add(Baz.bar);
Foo foo = Foo.fromBuffer(input, registry);
Если у вас уже есть разобранное сообщение с неизвестными полями, вы можете использовать reparseMessage в ExtensionRegistry для повторного разбора сообщения. Если набор неизвестных полей содержит расширения, присутствующие в реестре, эти расширения разбираются и удаляются из набора неизвестных полей. Расширения, уже присутствующие в сообщении, сохраняются.
Foo foo = Foo.fromBuffer(input);
ExtensionRegistry registry = ExtensionRegistry();
registry.add(Baz.bar);
Foo reparsed = registry.reparseMessage(foo);
Имейте в виду, что этот метод извлечения расширений в целом более затратен. По возможности мы рекомендуем использовать ExtensionRegistry со всеми необходимыми расширениями при выполнении GeneratedMessage.fromBuffer.
Сервисы
Для определения сервиса:
service Foo {
rpc Bar(FooRequest) returns(FooResponse);
}
Компилятор protocol buffer может быть вызван с опцией `grpc` (например, --dart_out=grpc:output_folder), в этом случае он сгенерирует код для поддержки gRPC. Смотрите руководство по быстрому старту gRPC Dart для более подробной информации.
Справочник Proto Buf
Справочник Proto Buf
Справочник по спецификации языка для редакции 2023 языка Protocol Buffers.
Синтаксис задается с использованием Расширенной формы Бэкуса-Наура (EBNF):
| альтернатива
() группировка
[] опция (ноль или один раз)
{} повторение (любое количество раз)
Лексические элементы
Буквы и цифры
letter = "A" ... "Z" | "a" ... "z"
capitalLetter = "A" ... "Z"
decimalDigit = "0" ... "9"
octalDigit = "0" ... "7"
hexDigit = "0" ... "9" | "A" ... "F" | "a" ... "f"
Идентификаторы
ident = letter { letter | decimalDigit | "_" }
fullIdent = ident { "." ident }
messageName = ident
enumName = ident
fieldName = ident
oneofName = ident
mapName = ident
serviceName = ident
rpcName = ident
streamName = ident
messageType = [ "." ] { ident "." } messageName
enumType = [ "." ] { ident "." } enumName
groupName = capitalLetter { letter | decimalDigit | "_" }
Целочисленные литералы
intLit = decimalLit | octalLit | hexLit
decimalLit = [-] ( "1" ... "9" ) { decimalDigit }
octalLit = [-] "0" { octalDigit }
hexLit = [-] "0" ( "x" | "X" ) hexDigit { hexDigit }
Литералы с плавающей точкой
floatLit = [-] ( decimals "." [ decimals ] [ exponent ] | decimals exponent | "."decimals [ exponent ] ) | "inf" | "nan"
decimals = [-] decimalDigit { decimalDigit }
exponent = ( "e" | "E" ) [ "+" | "-" ] decimals
Логический тип
boolLit = "true" | "false"
Строковые литералы
strLit = strLitSingle { strLitSingle }
strLitSingle = ( "'" { charValue } "'" ) | ( '"' { charValue } '"' )
charValue = hexEscape | octEscape | charEscape | unicodeEscape | unicodeLongEscape | /[^\0\n\\]/
hexEscape = '\' ( "x" | "X" ) hexDigit [ hexDigit ]
octEscape = '\' octalDigit [ octalDigit [ octalDigit ] ]
charEscape = '\' ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | '\' | "'" | '"' )
unicodeEscape = '\' "u" hexDigit hexDigit hexDigit hexDigit
unicodeLongEscape = '\' "U" ( "000" hexDigit hexDigit hexDigit hexDigit hexDigit |
"0010" hexDigit hexDigit hexDigit hexDigit
Пустой оператор
emptyStatement = ";"
Константа
constant = fullIdent | ( [ "-" | "+" ] intLit ) | ( [ "-" | "+" ] floatLit ) |
strLit | boolLit | MessageValue
MessageValue определяется в
Спецификации языка текстового формата.
Редакция (Edition)
Оператор edition заменяет устаревшее ключевое слово syntax и используется для
определения редакции, которую использует этот файл.
edition = "edition" "=" [ ( "'" decimalLit "'" ) | ( '"' decimalLit '"' ) ] ";"
Оператор импорта
Оператор импорта используется для импорта определений из другого .proto-файла.
import = "import" [ "weak" | "public" ] strLit ";"
Пример:
import public "other.proto";
Пакет (Package)
Спецификатор пакета может использоваться для предотвращения конфликтов имен между типами сообщений протокола.
package = "package" fullIdent ";"
Пример:
package foo.bar;
Опция (Option)
Опции могут использоваться в proto-файлах, сообщениях, перечислениях и сервисах. Опция может быть предопределенной опцией protobuf или пользовательской опцией. Для получения дополнительной информации см. Опции в руководстве по языку. Опции также используются для управления Настройками функций (Feature Settings).
option = "option" optionName "=" constant ";"
optionName = ( ident | "(" ["."] fullIdent ")" )
Примеры:
option java_package = "com.example.foo";
option features.enum_type = CLOSED;
Поля (Fields)
Поля — это основные элементы сообщения protobuf. Поля могут быть обычными полями, групповыми полями, полями oneof или полями map. Поле имеет метку, тип и номер поля.
label = [ "repeated" ]
type = "double" | "float" | "int32" | "int64" | "uint32" | "uint64"
| "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64"
| "bool" | "string" | "bytes" | messageType | enumType
fieldNumber = intLit;
Обычное поле
Каждое поле имеет метку, тип, имя и номер поля. Оно может иметь опции поля.
field = [label] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
fieldOptions = fieldOption { "," fieldOption }
fieldOption = optionName "=" constant
Примеры:
foo.bar nested_message = 2;
repeated int32 samples = 4 [packed=true];
Oneof и поле oneof
Oneof состоит из полей oneof и имени oneof. Поля oneof не имеют меток.
oneof = "oneof" oneofName "{" { option | oneofField } "}"
oneofField = type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
Пример:
oneof foo {
string name = 4;
SubMessage sub_message = 9;
}
Поле map
Поле map имеет тип ключа, тип значения, имя и номер поля. Тип ключа может быть любым целочисленным типом или строковым типом. Обратите внимание, что тип ключа не может быть перечислением.
mapField = "map" "<" keyType "," type ">" mapName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
keyType = "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" |
"fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string"
Пример:
map<string, Project> projects = 3;
Расширения и Зарезервированные
Расширения и зарезервированные элементы — это элементы сообщения, которые объявляют диапазон номеров полей или имен полей.
Расширения (Extensions)
Расширения объявляют, что диапазон номеров полей в сообщении доступен для сторонних расширений. Другие пользователи могут объявлять новые поля для вашего типа сообщения с этими числовыми тегами в своих собственных .proto-файлах без необходимости редактирования оригинального файла.
extensions = "extensions" ranges ";"
ranges = range { "," range }
range = intLit [ "to" ( intLit | "max" ) ]
Примеры:
extensions 100 to 199;
extensions 4, 20 to max;
Зарезервированные (Reserved)
Зарезервированные объявляют диапазон номеров полей или имен в сообщении или перечислении, которые нельзя использовать.
reserved = "reserved" ( ranges | reservedIdent ) ";"
fieldNames = fieldName { "," fieldName }
Примеры:
reserved 2, 15, 9 to 11;
reserved foo, bar;
Определения верхнего уровня
Определение перечисления
Определение перечисления состоит из имени и тела перечисления. Тело перечисления может содержать опции, поля перечисления и операторы reserved.
enum = "enum" enumName enumBody
enumBody = "{" { option | enumField | emptyStatement | reserved } "}"
enumField = fieldName "=" [ "-" ] intLit [ "[" enumValueOption { "," enumValueOption } "]" ]";"
enumValueOption = optionName "=" constant
Пример:
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 2 [(custom_option) = "hello world"];
}
Определение сообщения
Сообщение состоит из имени сообщения и тела сообщения. Тело сообщения может содержать поля, вложенные определения перечислений, вложенные определения сообщений, операторы extend, расширения, группы, опции, oneof, поля map и операторы reserved. Сообщение не может содержать два поля с одинаковым именем в одной схеме сообщения.
message = "message" messageName messageBody
messageBody = "{" { field | enum | message | extend | extensions | group |
option | oneof | mapField | reserved | emptyStatement } "}"
Пример:
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
required int64 ival = 1;
}
map<int32, string> my_map = 2;
extensions 20 to 30;
}
Ни один из элементов, объявленных внутри сообщения, не может иметь конфликтующих имен. Все следующее запрещено:
message MyMessage {
string foo = 1;
message foo {}
}
message MyMessage {
string foo = 1;
oneof foo {
string bar = 2;
}
}
message MyMessage {
string foo = 1;
extend Extendable {
string foo = 2;
}
}
message MyMessage {
string foo = 1;
enum E {
foo = 0;
}
}
Расширить (Extend)
Если сообщение в том же или импортированном .proto-файле зарезервировало диапазон для расширений, это сообщение может быть расширено.
extend = "extend" messageType "{" {field | group} "}"
Пример:
extend Foo {
int32 bar = 126;
}
Определение сервиса
service = "service" serviceName "{" { option | rpc | emptyStatement } "}"
rpc = "rpc" rpcName "(" [ "stream" ] messageType ")" "returns" "(" [ "stream" ]
messageType ")" (( "{" { option | emptyStatement } "}" ) | ";" )
Пример:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
Proto-файл
proto = [syntax] { import | package | option | topLevelDef | emptyStatement }
topLevelDef = message | enum | extend | service
Пример .proto-файла:
edition = "2023";
import public "other.proto";
option java_package = "com.example.foo";
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2 [(custom_option) = "hello world"];
}
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
int64 ival = 1 [features.field_presence = LEGACY_REQUIRED];
}
repeated Inner inner_message = 2;
EnumAllowingAlias enum_field = 3;
map<int32, string> my_map = 4;
extensions 20 to 30;
reserved reserved_field;
}
message Foo {
message GroupMessage {
bool a = 1;
}
GroupMessage groupmessage = [features.message_encoding = DELIMITED];
}
Спецификация языка Protocol Buffers редакции 2024
Справочник по спецификации языка для редакции 2024 языка Protocol Buffers.
Синтаксис задается с использованием Расширенной формы Бэкуса-Наура (EBNF):
| альтернатива
() группировка
[] опция (ноль или один раз)
{} повторение (любое количество раз)
Лексические элементы
Буквы и цифры
letter = "A" ... "Z" | "a" ... "z"
capitalLetter = "A" ... "Z"
decimalDigit = "0" ... "9"
octalDigit = "0" ... "7"
hexDigit = "0" ... "9" | "A" ... "F" | "a" ... "f"
Идентификаторы
ident = letter { letter | decimalDigit | "_" }
fullIdent = ident { "." ident }
messageName = ident
enumName = ident
fieldName = ident
oneofName = ident
mapName = ident
serviceName = ident
rpcName = ident
streamName = ident
messageType = [ "." ] { ident "." } messageName
enumType = [ "." ] { ident "." } enumName
groupName = capitalLetter { letter | decimalDigit | "_" }
Целочисленные литералы
intLit = decimalLit | octalLit | hexLit
decimalLit = [-] ( "1" ... "9" ) { decimalDigit }
octalLit = [-] "0" { octalDigit }
hexLit = [-] "0" ( "x" | "X" ) hexDigit { hexDigit }
Литералы с плавающей точкой
floatLit = [-] ( decimals "." [ decimals ] [ exponent ] | decimals exponent | "."decimals [ exponent ] ) | "inf" | "nan"
decimals = [-] decimalDigit { decimalDigit }
exponent = ( "e" | "E" ) [ "+" | "-" ] decimals
Логический тип
boolLit = "true" | "false"
Строковые литералы
strLit = strLitSingle { strLitSingle }
strLitSingle = ( "'" { charValue } "'" ) | ( '"' { charValue } '"' )
charValue = hexEscape | octEscape | charEscape | unicodeEscape | unicodeLongEscape | /[^\0\n\\]/
hexEscape = '\' ( "x" | "X" ) hexDigit [ hexDigit ]
octEscape = '\' octalDigit [ octalDigit [ octalDigit ] ]
charEscape = '\' ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | '\' | "'" | '"' )
unicodeEscape = '\' "u" hexDigit hexDigit hexDigit hexDigit
unicodeLongEscape = '\' "U" ( "000" hexDigit hexDigit hexDigit hexDigit hexDigit |
"0010" hexDigit hexDigit hexDigit hexDigit
Пустой оператор
emptyStatement = ";"
Константа
constant = fullIdent | ( [ "-" | "+" ] intLit ) | ( [ "-" | "+" ] floatLit ) |
strLit | boolLit | MessageValue
MessageValue определяется в
Спецификации языка текстового формата.
Редакция (Edition)
Оператор edition заменяет устаревшее ключевое слово syntax и используется для
определения редакции, которую использует этот файл.
edition = "edition" "=" [ ( "'" decimalLit "'" ) | ( '"' decimalLit '"' ) ] ";"
Оператор импорта
Оператор импорта используется для импорта определений из другого .proto-файла.
import = "import" [ "public" | "option" ] strLit ";"
Пример:
import public "other.proto";
import option "custom_option.proto";
Пакет (Package)
Спецификатор пакета может использоваться для предотвращения конфликтов имен между типами сообщений протокола.
package = "package" fullIdent ";"
Пример:
package foo.bar;
Опция (Option)
Опции могут использоваться в proto-файлах, сообщениях, перечислениях и сервисах. Опция может быть предопределенной опцией protobuf или пользовательской опцией. Для получения дополнительной информации см. Опции в руководстве по языку. Опции также используются для управления Настройками функций (Feature Settings).
option = "option" optionName "=" constant ";"
optionName = ( ident | "(" ["."] fullIdent ")" )
Примеры:
option java_package = "com.example.foo";
option features.enum_type = CLOSED;
Поля (Fields)
Поля — это основные элементы сообщения protobuf. Поля могут быть обычными полями, групповыми полями, полями oneof или полями map. Поле имеет метку, тип и номер поля.
label = [ "repeated" ]
type = "double" | "float" | "int32" | "int64" | "uint32" | "uint64"
| "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64"
| "bool" | "string" | "bytes" | messageType | enumType
fieldNumber = intLit;
Обычное поле
Каждое поле имеет метку, тип, имя и номер поля. Оно может иметь опции поля.
field = [label] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
fieldOptions = fieldOption { "," fieldOption }
fieldOption = optionName "=" constant
Примеры:
foo.bar nested_message = 2;
repeated int32 samples = 4 [packed=true];
Oneof и поле oneof
Oneof состоит из полей oneof и имени oneof. Поля oneof не имеют меток.
oneof = "oneof" oneofName "{" { option | oneofField } "}"
oneofField = type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
Пример:
oneof foo {
string name = 4;
SubMessage sub_message = 9;
}
Поле map
Поле map имеет тип ключа, тип значения, имя и номер поля. Тип ключа может быть любым целочисленным типом или строковым типом. Обратите внимание, что тип ключа не может быть перечислением.
mapField = "map" "<" keyType "," type ">" mapName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
keyType = "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" |
"fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string"
Пример:
map<string, Project> projects = 3;
Расширения и Зарезервированные
Расширения и зарезервированные элементы — это элементы сообщения, которые объявляют диапазон номеров полей или имен полей.
Расширения (Extensions)
Расширения объявляют, что диапазон номеров полей в сообщении доступен для сторонних расширений. Другие пользователи могут объявлять новые поля для вашего типа сообщения с этими числовыми тегами в своих собственных .proto-файлах без необходимости редактирования оригинального файла.
extensions = "extensions" ranges ";"
ranges = range { "," range }
range = intLit [ "to" ( intLit | "max" ) ]
Примеры:
extensions 100 to 199;
extensions 4, 20 to max;
Зарезервированные (Reserved)
Зарезервированные объявляют диапазон номеров полей или имен в сообщении или перечислении, которые нельзя использовать.
reserved = "reserved" ( ranges | reservedIdent ) ";"
fieldNames = fieldName { "," fieldName }
Примеры:
reserved 2, 15, 9 to 11;
reserved foo, bar;
Определения верхнего уровня
Видимость символов
Некоторые определения сообщений и перечислений могут быть аннотированы для переопределения их видимости символов по умолчанию.
Это контролируется с помощью features.default_symbol_visibility, и видимость символов дополнительно документирована в Ключевых словах export / local
symbolVisibility = "export" | "local"
Определение перечисления
Определение перечисления состоит из имени и тела перечисления. Тело перечисления может содержать опции, поля перечисления и операторы reserved.
enum = [ symbolVisibility ] "enum" enumName enumBody
enumBody = "{" { option | enumField | emptyStatement | reserved } "}"
enumField = fieldName "=" [ "-" ] intLit [ "[" enumValueOption { "," enumValueOption } "]" ]";"
enumValueOption = optionName "=" constant
Пример:
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 2 [(custom_option) = "hello world"];
}
Определение сообщения
Сообщение состоит из имени сообщения и тела сообщения. Тело сообщения может содержать поля, вложенные определения перечислений, вложенные определения сообщений, операторы extend, расширения, группы, опции, oneof, поля map и операторы reserved. Сообщение не может содержать два поля с одинаковым именем в одной схеме сообщения.
message = [ symbolVisibility ] "message" messageName messageBody
messageBody = "{" { field | enum | message | extend | extensions | group |
option | oneof | mapField | reserved | emptyStatement } "}"
Пример:
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
required int64 ival = 1;
}
map<int32, string> my_map = 2;
extensions 20 to 30;
}
Ни один из элементов, объявленных внутри сообщения, не может иметь конфликтующих имен. Все следующее запрещено:
message MyMessage {
string foo = 1;
message foo {}
}
message MyMessage {
string foo = 1;
oneof foo {
string bar = 2;
}
}
message MyMessage {
string foo = 1;
extend Extendable {
string foo = 2;
}
}
message MyMessage {
string foo = 1;
enum E {
foo = 0;
}
}
Расширить (Extend)
Если сообщение в том же или импортированном .proto-файле зарезервировало диапазон для расширений, это сообщение может быть расширено.
extend = "extend" messageType "{" {field | group} "}"
Пример:
extend Foo {
int32 bar = 126;
}
Определение сервиса
service = "service" serviceName "{" { option | rpc | emptyStatement } "}"
rpc = "rpc" rpcName "(" [ "stream" ] messageType ")" "returns" "(" [ "stream" ]
messageType ")" (( "{" { option | emptyStatement } "}" ) | ";" )
Пример:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
Proto-файл
proto = [syntax] { import | package | option | topLevelDef | emptyStatement }
topLevelDef = message | enum | extend | service
Пример .proto-файла:
edition = "2024";
import public "other.proto";
import option "custom_option.proto";
option java_package = "com.example.foo";
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2 [(custom_option) = "hello world"];
}
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
int64 ival = 1 [features.field_presence = LEGACY_REQUIRED];
}
repeated Inner inner_message = 2;
EnumAllowingAlias enum_field = 3;
map<int32, string> my_map = 4;
extensions 20 to 30;
reserved reserved_field;
}
message Foo {
message GroupMessage {
bool a = 1;
}
GroupMessage groupmessage = [features.message_encoding = DELIMITED];
}
Спецификация языка Protocol Buffers (Proto3)
Справочник по спецификации языка для языка Protocol Buffers (Proto3).
Синтаксис задается с использованием Расширенной формы Бэкуса-Наура (EBNF):
| альтернатива
() группировка
[] опция (ноль или один раз)
{} повторение (любое количество раз)
Для получения дополнительной информации об использовании proto3 см. руководство по языку.
Лексические элементы
Буквы и цифры
letter = "A" ... "Z" | "a" ... "z"
decimalDigit = "0" ... "9"
octalDigit = "0" ... "7"
hexDigit = "0" ... "9" | "A" ... "F" | "a" ... "f"
Идентификаторы
ident = letter { letter | decimalDigit | "_" }
fullIdent = ident { "." ident }
messageName = ident
enumName = ident
fieldName = ident
oneofName = ident
mapName = ident
serviceName = ident
rpcName = ident
messageType = [ "." ] { ident "." } messageName
enumType = [ "." ] { ident "." } enumName
Целочисленные литералы
intLit = decimalLit | octalLit | hexLit
decimalLit = [-] ( "1" ... "9" ) { decimalDigit }
octalLit = [-] "0" { octalDigit }
hexLit = [-] "0" ( "x" | "X" ) hexDigit { hexDigit }
Литералы с плавающей точкой
floatLit = [-] ( decimals "." [ decimals ] [ exponent ] | decimals exponent | "."decimals [ exponent ] ) | "inf" | "nan"
decimals = [-] decimalDigit { decimalDigit }
exponent = ( "e" | "E" ) [ "+" | "-" ] decimals
Логический тип
boolLit = "true" | "false"
Строковые литералы
strLit = strLitSingle { strLitSingle }
strLitSingle = ( "'" { charValue } "'" ) | ( '"' { charValue } '"' )
charValue = hexEscape | octEscape | charEscape | unicodeEscape | unicodeLongEscape | /[^\0\n\\]/
hexEscape = '\' ( "x" | "X" ) hexDigit [ hexDigit ]
octEscape = '\' octalDigit [ octalDigit [ octalDigit ] ]
charEscape = '\' ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | '\' | "'" | '"' )
unicodeEscape = '\' "u" hexDigit hexDigit hexDigit hexDigit
unicodeLongEscape = '\' "U" ( "000" hexDigit hexDigit hexDigit hexDigit hexDigit |
"0010" hexDigit hexDigit hexDigit hexDigit
Пустой оператор
emptyStatement = ";"
Константа
constant = fullIdent | ( [ "-" | "+" ] intLit ) | ( [ "-" | "+" ] floatLit ) |
strLit | boolLit | MessageValue
MessageValue определяется в
Спецификации языка текстового формата.
Синтаксис
Оператор syntax используется для определения версии protobuf.
syntax = "syntax" "=" ("'" "proto3" "'" | '"' "proto3" '"') ";"
Пример:
syntax = "proto3";
Оператор импорта
Оператор импорта используется для импорта определений из другого .proto-файла.
import = "import" [ "weak" | "public" ] strLit ";"
Пример:
import public "other.proto";
Пакет
Спецификатор пакета может использоваться для предотвращения конфликтов имен между типами сообщений протокола.
package = "package" fullIdent ";"
Пример:
package foo.bar;
Опция
Опции могут использоваться в proto-файлах, сообщениях, перечислениях и сервисах. Опция может быть предопределенной опцией protobuf или пользовательской опцией. Для получения дополнительной информации см. Опции в руководстве по языку.
option = "option" optionName "=" constant ";"
optionName = ( ident | bracedFullIdent ) { "." ( ident | bracedFullIdent ) }
bracedFullIdent = "(" ["."] fullIdent ")"
optionNamePart = { ident | "(" ["."] fullIdent ")" }
Пример:
option java_package = "com.example.foo";
Поля
Поля — это основные элементы сообщения protobuf. Поля могут быть обычными полями, полями oneof или полями map. Поле имеет тип и номер поля.
type = "double" | "float" | "int32" | "int64" | "uint32" | "uint64"
| "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64"
| "bool" | "string" | "bytes" | messageType | enumType
fieldNumber = intLit;
Обычное поле
Каждое поле имеет тип, имя и номер поля. Оно может иметь опции поля.
field = [ "repeated" | "optional" ] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
fieldOptions = fieldOption { "," fieldOption }
fieldOption = optionName "=" constant
Примеры:
foo.Bar nested_message = 2;
repeated int32 samples = 4 [packed=true];
Oneof и поле oneof
Oneof состоит из полей oneof и имени oneof.
oneof = "oneof" oneofName "{" { option | oneofField } "}"
oneofField = type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
Пример:
oneof foo {
string name = 4;
SubMessage sub_message = 9;
}
Поле map
Поле map имеет тип ключа, тип значения, имя и номер поля. Тип ключа может быть любым целочисленным или строковым типом.
mapField = "map" "<" keyType "," type ">" mapName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
keyType = "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" |
"fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string"
Пример:
map<string, Project> projects = 3;
Зарезервированные
Операторы reserved объявляют диапазон номеров полей или имен полей, которые не могут быть использованы в этом сообщении.
reserved = "reserved" ( ranges | strFieldNames ) ";"
ranges = range { "," range }
range = intLit [ "to" ( intLit | "max" ) ]
strFieldNames = strFieldName { "," strFieldName }
strFieldName = "'" fieldName "'" | '"' fieldName '"'
Примеры:
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
Определения верхнего уровня
Определение перечисления
Определение перечисления состоит из имени и тела перечисления. Тело перечисления может содержать опции, поля перечисления и операторы reserved.
enum = "enum" enumName enumBody
enumBody = "{" { option | enumField | emptyStatement | reserved } "}"
enumField = ident "=" [ "-" ] intLit [ "[" enumValueOption { "," enumValueOption } "]" ]";"
enumValueOption = optionName "=" constant
Пример:
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 2 [(custom_option) = "hello world"];
}
Определение сообщения
Сообщение состоит из имени сообщения и тела сообщения. Тело сообщения может содержать поля, вложенные определения перечислений, вложенные определения сообщений, опции, oneof, поля map и операторы reserved. Сообщение не может содержать два поля с одинаковым именем в одной схеме сообщения.
message = "message" messageName messageBody
messageBody = "{" { field | enum | message | option | oneof | mapField |
reserved | emptyStatement } "}"
Пример:
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
int64 ival = 1;
}
map<int32, string> my_map = 2;
}
Ни один из элементов, объявленных внутри сообщения, не может иметь конфликтующих имен. Все следующее запрещено:
message MyMessage {
optional string foo = 1;
message foo {}
}
message MyMessage {
optional string foo = 1;
oneof foo {
string bar = 2;
}
}
message MyMessage {
optional string foo = 1;
enum E {
foo = 0;
}
}
Определение сервиса
service = "service" serviceName "{" { option | rpc | emptyStatement } "}"
rpc = "rpc" rpcName "(" [ "stream" ] messageType ")" "returns" "(" [ "stream" ]
messageType ")" (( "{" {option | emptyStatement } "}" ) | ";")
Пример:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
Proto-файл
proto = [syntax] { import | package | option | topLevelDef | emptyStatement }
topLevelDef = message | enum | service
Пример .proto-файла:
syntax = "proto3";
import public "other.proto";
option java_package = "com.example.foo";
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2 [(custom_option) = "hello world"];
}
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
int64 ival = 1;
}
repeated Inner inner_message = 2;
EnumAllowingAlias enum_field = 3;
map<int32, string> my_map = 4;
}
Спецификация языка Protocol Buffers (Синтаксис Proto2)
Справочник по спецификации для синтаксиса proto2 и его связи с Редакциями Protobuf.
Синтаксис задается с использованием Расширенной формы Бэкуса-Наура (EBNF):
| альтернатива
() группировка
[] опция (ноль или один раз)
{} повторение (любое количество раз)
Для получения дополнительной информации об использовании proto2 см. руководство по языку.
Лексические элементы
Буквы и цифры
letter = "A" ... "Z" | "a" ... "z"
capitalLetter = "A" ... "Z"
decimalDigit = "0" ... "9"
octalDigit = "0" ... "7"
hexDigit = "0" ... "9" | "A" ... "F" | "a" ... "f"
Идентификаторы
ident = letter { letter | decimalDigit | "_" }
fullIdent = ident { "." ident }
messageName = ident
enumName = ident
fieldName = ident
oneofName = ident
mapName = ident
serviceName = ident
rpcName = ident
streamName = ident
messageType = [ "." ] { ident "." } messageName
enumType = [ "." ] { ident "." } enumName
groupName = capitalLetter { letter | decimalDigit | "_" }
Целочисленные литералы
intLit = decimalLit | octalLit | hexLit
decimalLit = [-] ( "1" ... "9" ) { decimalDigit }
octalLit = [-] "0" { octalDigit }
hexLit = [-] "0" ( "x" | "X" ) hexDigit { hexDigit }
Литералы с плавающей точкой
floatLit = [-] ( decimals "." [ decimals ] [ exponent ] | decimals exponent | "."decimals [ exponent ] ) | "inf" | "nan"
decimals = [-] decimalDigit { decimalDigit }
exponent = ( "e" | "E" ) [ "+" | "-" ] decimals
Логический тип
boolLit = "true" | "false"
Строковые литералы
strLit = strLitSingle { strLitSingle }
strLitSingle = ( "'" { charValue } "'" ) | ( '"' { charValue } '"' )
charValue = hexEscape | octEscape | charEscape | unicodeEscape | unicodeLongEscape | /[^\0\n\\]/
hexEscape = '\' ( "x" | "X" ) hexDigit [ hexDigit ]
octEscape = '\' octalDigit [ octalDigit [ octalDigit ] ]
charEscape = '\' ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | '\' | "'" | '"' )
unicodeEscape = '\' "u" hexDigit hexDigit hexDigit hexDigit
unicodeLongEscape = '\' "U" ( "000" hexDigit hexDigit hexDigit hexDigit hexDigit |
"0010" hexDigit hexDigit hexDigit hexDigit
Пустой оператор
emptyStatement = ";"
Константа
constant = fullIdent | ( [ "-" | "+" ] intLit ) | ( [ "-" | "+" ] floatLit ) |
strLit | boolLit | MessageValue
MessageValue определяется в
Спецификации языка текстового формата.
Синтаксис
Оператор syntax используется для определения версии protobuf. Если syntax
опущен, компилятор протокола будет использовать proto2. Для ясности
рекомендуется всегда явно включать оператор syntax в ваши .proto
файлы.
syntax = "syntax" "=" ("'" "proto2" "'" | '"' "proto2" '"') ";"
Оператор импорта
Оператор импорта используется для импорта определений из другого .proto-файла.
import = "import" [ "weak" | "public" ] strLit ";"
Пример:
import public "other.proto";
Пакет
Спецификатор пакета может использоваться для предотвращения конфликтов имен между типами сообщений протокола.
package = "package" fullIdent ";"
Пример:
package foo.bar;
Опция
Опции могут использоваться в proto-файлах, сообщениях, перечислениях и сервисах. Опция может быть предопределенной опцией protobuf или пользовательской опцией. Для получения дополнительной информации см. Опции в руководстве по языку.
option = "option" optionName "=" constant ";"
optionName = ( ident | bracedFullIdent ) { "." ( ident | bracedFullIdent ) }
bracedFullIdent = "(" ["."] fullIdent ")"
Примеры:
option java_package = "com.example.foo";
Поля
Поля — это основные элементы сообщения protobuf. Поля могут быть обычными
полями, групповыми полями, полями oneof или полями map. Поле имеет тип, имя и
номер поля. В proto2 поля также имеют метку (required, optional, или
repeated).
label = "required" | "optional" | "repeated"
type = "double" | "float" | "int32" | "int64" | "uint32" | "uint64"
| "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64"
| "bool" | "string" | "bytes" | messageType | enumType
fieldNumber = intLit;
Обычное поле
Каждое поле имеет тип, имя и номер поля. Оно может иметь опции поля. Обратите внимание, что метки являются опциональными только для полей oneof.
field = [ label ] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
fieldOptions = fieldOption { "," fieldOption }
fieldOption = optionName "=" constant
Примеры:
optional foo.bar nested_message = 2;
repeated int32 samples = 4 [packed=true];
Групповое поле
Обратите внимание, что эта функция устарела и не должна использоваться при создании новых типов сообщений. Вместо этого используйте вложенные типы сообщений.
Группы — это один из способов вложения информации в определениях сообщений. Имя группы должно начинаться с заглавной буквы.
group = label "group" groupName "=" fieldNumber messageBody
Пример:
repeated group Result = 1 {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
Oneof и поле oneof
Oneof состоит из полей oneof и имени oneof. Поля oneof не имеют меток.
oneof = "oneof" oneofName "{" { option | oneofField } "}"
oneofField = type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
Пример:
oneof foo {
string name = 4;
SubMessage sub_message = 9;
}
Поле map
Поле map имеет тип ключа, тип значения, имя и номер поля. Тип ключа может быть любым целочисленным или строковым типом. Обратите внимание, что тип ключа не может быть перечислением.
mapField = "map" "<" keyType "," type ">" mapName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
keyType = "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" |
"fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string"
Пример:
map<string, Project> projects = 3;
Расширения и Зарезервированные
Расширения и зарезервированные элементы — это элементы сообщения, которые объявляют диапазон номеров полей или имен полей.
Расширения
Расширения объявляют, что диапазон номеров полей в сообщении доступен для сторонних расширений. Другие пользователи могут объявлять новые поля для вашего типа сообщения с этими числовыми тегами в своих собственных .proto-файлах без необходимости редактирования оригинального файла.
extensions = "extensions" ranges ";"
ranges = range { "," range }
range = intLit [ "to" ( intLit | "max" ) ]
Примеры:
extensions 100 to 199;
extensions 4, 20 to max;
Для получения дополнительной информации по этой теме см. Объявления расширений.
Зарезервированные
Зарезервированные объявляют диапазон номеров полей или имен полей в сообщении, которые не могут быть использованы.
reserved = "reserved" ( ranges | strFieldNames ) ";"
strFieldNames = strFieldName { "," strFieldName }
strFieldName = "'" fieldName "'" | '"' fieldName '"'
Примеры:
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
Определения верхнего уровня
Определение перечисления
Определение перечисления состоит из имени и тела перечисления. Тело перечисления может содержать опции, поля перечисления и операторы reserved.
enum = "enum" enumName enumBody
enumBody = "{" { option | enumField | emptyStatement | reserved } "}"
enumField = ident "=" [ "-" ] intLit [ "[" enumValueOption { "," enumValueOption } "]" ]";"
enumValueOption = optionName "=" constant
Пример:
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 2 [(custom_option) = "hello world"];
}
Определение сообщения
Сообщение состоит из имени сообщения и тела сообщения. Тело сообщения может содержать поля, вложенные определения перечислений, вложенные определения сообщений, операторы extend, расширения, группы, опции, oneof, поля map и операторы reserved. Сообщение не может содержать два поля с одинаковым именем в одной схеме сообщения.
message = "message" messageName messageBody
messageBody = "{" { field | enum | message | extend | extensions | group |
option | oneof | mapField | reserved | emptyStatement } "}"
Пример:
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
required int64 ival = 1;
}
map<int32, string> my_map = 2;
extensions 20 to 30;
}
Ни один из элементов, объявленных внутри сообщения, не может иметь конфликтующих имен. Все следующее запрещено:
message MyMessage {
optional string foo = 1;
message foo {}
}
message MyMessage {
optional string foo = 1;
oneof foo {
string bar = 2;
}
}
message MyMessage {
optional string foo = 1;
extend Extendable {
optional string foo = 2;
}
}
message MyMessage {
optional string foo = 1;
enum E {
foo = 0;
}
}
Расширить (Extend)
Если сообщение в том же или импортированном .proto-файле зарезервировало диапазон для расширений, это сообщение может быть расширено.
extend = "extend" messageType "{" {field | group} "}"
Пример:
extend Foo {
optional int32 bar = 126;
}
Определение сервиса
service = "service" serviceName "{" { option | rpc | emptyStatement } "}"
rpc = "rpc" rpcName "(" [ "stream" ] messageType ")" "returns" "(" [ "stream" ]
messageType ")" (( "{" { option | emptyStatement } "}" ) | ";" )
Пример:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
Proto-файл
proto = [syntax] { import | package | option | topLevelDef | emptyStatement }
topLevelDef = message | enum | extend | service
Пример .proto файла:
syntax = "proto2";
import public "other.proto";
option java_package = "com.example.foo";
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2 [(custom_option) = "hello world"];
}
message Outer {
option (my_option).a = true;
message Inner { // Уровень 2
required int64 ival = 1;
}
repeated Inner inner_message = 2;
optional EnumAllowingAlias enum_field = 3;
map<int32, string> my_map = 4;
extensions 20 to 30;
}
message Foo {
optional group GroupMessage = 1 {
optional bool a = 1;
}
}
Спецификация языка текстового формата
Язык текстового формата protobuf определяет синтаксис для представления данных protobuf в текстовой форме, что часто полезно для конфигураций или тестов.
Этот формат отличается от формата текста
внутри схемы .proto, например. Этот документ содержит справочную
документацию с использованием синтаксиса, указанного в
ISO/IEC 14977 EBNF.
{{% alert title="Примечание" color="note" %}} Это черновой спецификация, созданный путем обратного проектирования из реализации текстового формата C++ implementation и может измениться на основе дальнейших обсуждений и проверки. Несмотря на усилия по сохранению согласованности текстовых форматов в поддерживаемых языках, вероятно существование несовместимостей. {{% /alert %}}
Пример
convolution_benchmark {
label: "NHWC_128x20x20x56x160"
input {
dimension: [128, 56, 20, 20]
data_type: DATA_HALF
format: TENSOR_NHWC
}
}
Обзор парсинга
Элементы языка в этой спецификации разделены на лексические и синтаксические
категории. Лексические элементы должны точно соответствовать входному тексту, как описано, но
синтаксические элементы могут разделяться необязательными токенами WHITESPACE и COMMENT.
Например, знаковое число с плавающей точкой состоит из двух синтаксических элементов: знака (-)
и литерала FLOAT. Необязательные пробелы и комментарии могут существовать между знаком и числом, но не внутри числа. Пример:
value: -2.0 # Допустимо: нет дополнительных пробелов.
value: - 2.0 # Допустимо: пробел между '-' и '2.0'.
value: -
# комментарий
2.0 # Допустимо: пробелы и комментарии между '-' и '2.0'.
value: 2 . 0 # Неверно: точка в числе с плавающей точкой является частью лексического
# элемента, поэтому дополнительные пробелы не допускаются.
Существует один крайний случай, требующий особого внимания: токен числа (FLOAT,
DEC_INT, OCT_INT или HEX_INT) не может немедленно следовать за токеном
IDENT. Пример:
foo: 10 bar: 20 # Допустимо: пробел разделяет '10' и 'bar'
foo: 10,bar: 20 # Допустимо: ',' разделяет '10' и 'bar'
foo: 10[com.foo.ext]: 20 # Допустимо: за '10' сразу следует '[', который
# не является идентификатором.
foo: 10bar: 20 # Неверно: нет пробела между '10' и идентификатором 'bar'.
Лексические элементы
Лексические элементы, описанные ниже, делятся на две категории: основные элементы в верхнем регистре и фрагменты в нижнем регистре. Только основные элементы включаются в выходной поток токенов, используемый во время синтаксического анализа; фрагменты существуют только для упрощения построения основных элементов.
При разборе входного текста побеждает самый длинный совпадающий основной элемент. Пример:
value: 10 # '10' разбирается как токен DEC_INT.
value: 10f # '10f' разбирается как токен FLOAT, несмотря на содержание '10', который
# также соответствовал бы DEC_INT. В этом случае FLOAT соответствует более длинной
# подпоследовательности входных данных.
Символы
char = ? Любой не-NUL символ Unicode ? ;
newline = ? ASCII #10 (перевод строки) ? ;
letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M"
| "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
| "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m"
| "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
| "_" ;
oct = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" ;
dec = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
| "A" | "B" | "C" | "D" | "E" | "F"
| "a" | "b" | "c" | "d" | "e" | "f" ;
Пробелы и комментарии
COMMENT = "#", { char - newline }, [ newline ] ;
WHITESPACE = " "
| newline
| ? ASCII #9 (горизонтальная табуляция) ?
| ? ASCII #11 (вертикальная табуляция) ?
| ? ASCII #12 (прогон страницы) ?
| ? ASCII #13 (возврат каретки) ? ;
Идентификаторы
IDENT = letter, { letter | dec } ;
Числовые литералы
dec_lit = "0"
| ( dec - "0" ), { dec } ;
float_lit = ".", dec, { dec }, [ exp ]
| dec_lit, ".", { dec }, [ exp ]
| dec_lit, exp ;
exp = ( "E" | "e" ), [ "+" | "-" ], dec, { dec } ;
DEC_INT = dec_lit
OCT_INT = "0", oct, { oct } ;
HEX_INT = "0", ( "X" | "x" ), hex, { hex } ;
FLOAT = float_lit, [ "F" | "f" ]
| dec_lit, ( "F" | "f" ) ;
Десятичные целые числа могут быть приведены к значениям с плавающей точкой с использованием суффиксов F и f.
Пример:
foo: 10 # Это целочисленное значение.
foo: 10f # Это значение с плавающей точкой.
foo: 1.0f # Также необязательно для литералов с плавающей точкой.
Строковые литералы
STRING = single_string | double_string ;
single_string = "'", { escape | char - "'" - newline - "\" }, "'" ;
double_string = '"', { escape | char - '"' - newline - "\" }, '"' ;
escape = "\a" (* ASCII #7 (звонок) *)
| "\b" (* ASCII #8 (забой) *)
| "\f" (* ASCII #12 (прогон страницы) *)
| "\n" (* ASCII #10 (перевод строки) *)
| "\r" (* ASCII #13 (возврат каретки) *)
| "\t" (* ASCII #9 (горизонтальная табуляция) *)
| "\v" (* ASCII #11 (вертикальная табуляция) *)
| "\?" (* ASCII #63 (вопросительный знак) *)
| "\\" (* ASCII #92 (обратная косая черта) *)
| "\'" (* ASCII #39 (апостроф) *)
| '\"' (* ASCII #34 (кавычка) *)
| "\", oct, [ oct, [ oct ] ] (* восьмеричное экранированное значение байта *)
| "\x", hex, [ hex ] (* шестнадцатеричное экранированное значение байта *)
| "\u", hex, hex, hex, hex (* Кодовая точка Unicode до 0xffff *)
| "\U000",
hex, hex, hex, hex, hex (* Кодовая точка Unicode до 0xfffff *)
| "\U0010",
hex, hex, hex, hex ; (* Кодовая точка Unicode между 0x100000 и 0x10ffff *)
Восьмеричные escape-последовательности потребляют до трех восьмеричных цифр. Дополнительные цифры
передаются без экранирования. Например, при разэкранировании ввода \1234,
парсер потребляет три восьмеричные цифры (123) для разэкранирования значения байта 0x53
(ASCII 'S', 83 в десятичной системе), и последующая '4' передается как значение байта 0x34 (ASCII '4'). Чтобы обеспечить правильный разбор, выражайте восьмеричные escape-
последовательности с 3 восьмеричными цифрами, используя ведущие нули по мере необходимости, например: \000,
\001, \063, \377. Менее трех цифр потребляется, когда за числовыми
символами следует нечисловой символ, например \5Hello.
Шестнадцатеричные escape-последовательности потребляют до двух шестнадцатеричных цифр. Например,
при разэкранировании \x213 парсер потребляет только первые две цифры (21) для
разэкранирования значения байта 0x21 (ASCII '!'). Чтобы обеспечить правильный разбор, выражайте
шестнадцатеричные escape-последовательности с 2 шестнадцатеричными цифрами, используя ведущие нули по мере
необходимости, например: \x00, \x01, \xFF. Менее двух цифр потребляется, когда
за числовым символом следует нешестнадцатеричный символ, например \xFHello или
\x3world.
Используйте побайтовое экранирование только для полей с типом bytes. Хотя это возможно
использовать побайтовое экранирование в полях с типом string, эти escape-последовательности
должны формировать действительные последовательности UTF-8. Использование побайтового экранирования для выражения
последовательностей UTF-8 чревато ошибками. Предпочитайте escape-последовательности Unicode для непечатаемых
символов и символов разрыва строки в литералах для полей типа string.
Более длинные строки могут быть разбиты на несколько строк в кавычках на последовательных строках. Например:
quote:
"Когда мы пришли к власти, больше всего меня удивило то, "
"что дела были такими же плохими, как мы и говорили.\n\n"
" -- Джон Ф. Кеннеди"
Кодовые точки Unicode интерпретируются в соответствии с Unicode 13 Table A-1 Extended BNF и кодируются как UTF-8.
{{% alert title="Предупреждение" color="warning" %}}
Реализация на C++ в настоящее время интерпретирует экранированные кодовые точки высоких суррогатов как
кодовые единицы UTF-16 и ожидает, что за ними немедленно последует кодовая точка низкого суррогата \uHHHH
без какого-либо разделения по отдельным строкам в кавычках.
Кроме того, неспаренные суррогаты будут отображаться непосредственно в также недопустимый UTF-8.
Оба эти поведения являются несоответствующими1 и на них не следует полагаться.
{{% /alert %}}
Синтаксические элементы
Сообщение
Сообщение — это набор полей. Файл текстового формата представляет собой одно Сообщение.
Message = { Field } ;
Литералы
Литеральные значения полей могут быть числами, строками или идентификаторами, такими как true или
значения перечислений.
String = STRING, { STRING } ;
Float = [ "-" ], FLOAT ;
Identifier = IDENT ;
SignedIdentifier = "-", IDENT ; (* Например, "-inf" *)
DecSignedInteger = "-", DEC_INT ;
OctSignedInteger = "-", OCT_INT ;
HexSignedInteger = "-", HEX_INT ;
DecUnsignedInteger = DEC_INT ;
OctUnsignedInteger = OCT_INT ;
HexUnsignedInteger = HEX_INT ;
Одно строковое значение может состоять из нескольких частей в кавычках, разделенных необязательными пробелами. Пример:
a_string: "первая часть" 'вторая часть'
"третья часть"
без_пробелов: "первая""вторая"'третья''четвертая'
Имена полей
Поля, которые являются частью содержащего сообщения, используют простые Identifier в качестве
имен.
Extension и
Any имена полей
заключены в квадратные скобки и полностью квалифицированы. Имена полей Any имеют префикс
с квалифицирующим доменным именем, таким как type.googleapis.com/.
FieldName = ExtensionName | AnyName | IDENT ;
ExtensionName = "[", TypeName, "]" ;
AnyName = "[", Domain, "/", TypeName, "]" ;
TypeName = IDENT, { ".", IDENT } ;
Domain = IDENT, { ".", IDENT } ;
Обычные поля и поля расширений могут иметь скалярные значения или значения сообщений. Поля Any
всегда являются сообщениями. Пример:
reg_scalar: 10
reg_message { foo: "bar" }
[com.foo.ext.scalar]: 10
[com.foo.ext.message] { foo: "bar" }
any_value {
[type.googleapis.com/com.foo.any] { foo: "bar" }
}
Неизвестные поля
Парсеры текстового формата не могут поддерживать неизвестные поля, представленные как необработанные номера полей
вместо имен полей, потому что три из шести типов проводов
представлены одинаково в textformat. Некоторые реализации сериализаторов text-format
кодируют неизвестные поля в формате, который использует номер поля и
числовое представление значения, но это по своей природе ущербно, потому что
информация о типе провода игнорируется. Для сравнения, wire-format не является ущербным,
потому что он включает тип провода в каждый тег поля как (field_number << 3) | wire_type. Для получения дополнительной информации о кодировании см. тему
Кодирование.
Без информации о типе поля из схемы сообщения значение не может быть правильно закодировано в сообщение proto в wire-формате.
Поля
Значения полей могут быть литералами (строками, числами или идентификаторами) или вложенными сообщениями.
Field = ScalarField | MessageField ;
MessageField = FieldName, [ ":" ], ( MessageValue | MessageList ) [ ";" | "," ];
ScalarField = FieldName, ":", ( ScalarValue | ScalarList ) [ ";" | "," ];
MessageList = "[", [ MessageValue, { ",", MessageValue } ], "]" ;
ScalarList = "[", [ ScalarValue, { ",", ScalarValue } ], "]" ;
MessageValue = "{", Message, "}" | "<", Message, ">" ;
ScalarValue = String
| Float
| Identifier
| SignedIdentifier
| DecSignedInteger
| OctSignedInteger
| HexSignedInteger
| DecUnsignedInteger
| OctUnsignedInteger
| HexUnsignedInteger ;
Разделитель : между именем поля и значением обязателен для скалярных полей,
но необязателен для полей сообщений (включая списки). Пример:
scalar: 10 # Допустимо
scalar 10 # Неверно
scalars: [1, 2, 3] # Допустимо
scalars [1, 2, 3] # Неверно
message: {} # Допустимо
message {} # Допустимо
messages: [{}, {}] # Допустимо
messages [{}, {}] # Допустимо
Значения полей сообщений могут быть окружены фигурными скобками или угловыми скобками:
message: { foo: "bar" }
message: < foo: "bar" >
Поля, помеченные repeated, могут иметь несколько значений, указанных путем повторения поля,
используя специальный синтаксис списка [] или комбинацию обоих способов.
Порядок значений сохраняется. Пример:
repeated_field: 1
repeated_field: 2
repeated_field: [3, 4, 5]
repeated_field: 6
repeated_field: [7, 8, 9]
эквивалентно:
repeated_field: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Не-repeated поля не могут использовать синтаксис списка. Например, [0] не
допустимо для optional или required полей. Поля, помеченные optional, могут быть
опущены или указаны один раз. Поля, помеченные required, должны быть указаны ровно один раз.
required — это устаревшая функция proto2 и недоступна в proto3.
Обратная совместимость доступна для сообщений в Редакциях с использованием
features.field_presence = LEGACY_REQUIRED.
Поля, не указанные в связанном сообщении .proto, не допускаются, если только
имя поля не присутствует в списке reserved полей сообщения. reserved
поля, если они присутствуют в любой форме (скалярные, списки, сообщения), просто игнорируются
текстовым форматом.
Типы значений
Когда известен связанный .proto тип значения поля, применяются следующие описания значений и ограничения. Для целей этого раздела мы объявляем следующие элементы-контейнеры:
signedInteger = DecSignedInteger | OctSignedInteger | HexSignedInteger ;
unsignedInteger = DecUnsignedInteger | OctUnsignedInteger | HexUnsignedInteger ;
integer = signedInteger | unsignedInteger ;
| .proto Тип | Значения |
|---|---|
float, double
|
Элемент Float, DecSignedInteger или
DecUnsignedInteger, или элемент Identifier
или SignedIdentifier, чья часть IDENT
равна "inf", "infinity" или
"nan" (без учета регистра). Переполнения обрабатываются как infinity или
-infinity. Восьмеричные и шестнадцатеричные значения недопустимы.
Примечание: "nan" следует интерпретировать как Тихий NaN |
int32, sint32, sfixed32
|
Любой из элементов integer в диапазоне
-0x80000000 до 0x7FFFFFFF.
|
int64, sint64, sfixed64
|
Любой из элементов integer в диапазоне
-0x8000000000000000 до 0x7FFFFFFFFFFFFFFF.
|
uint32, fixed32
|
Любой из элементов unsignedInteger в диапазоне
0 до 0xFFFFFFFF. Обратите внимание, что знаковые значения
(-0) недопустимы.
|
uint64, fixed64
|
Любой из элементов unsignedInteger в диапазоне
0 до 0xFFFFFFFFFFFFFFFF. Обратите внимание, что знаковые
значения (-0) недопустимы.
|
string
|
Элемент String, содержащий действительные данные UTF-8. Любые escape-
последовательности должны формировать действительные последовательности байтов UTF-8 при разэкранировании.
|
bytes
|
Элемент String, возможно, включая недопустимые UTF-8 escape-
последовательности.
|
bool
|
Элемент Identifier или любой из
элементов unsignedInteger, соответствующих одному из следующих
значений.Истинные значения: "True", "true", "t", 1 Ложные значения: "False", "false", "f", 0 Любое представление беззнакового целого числа 0 или 1 разрешено: 00, 0x0, 01, 0x1 и т.д. |
| значения перечислений |
Элемент Identifier, содержащий имя значения перечисления, или любой
из элементов integer в диапазоне
-0x80000000 до 0x7FFFFFFF, содержащий номер
значения перечисления. Недопустимо указывать имя, которое не является
членом определения enum типа поля. В зависимости от
конкретной реализации среды выполнения protobuf может быть допустимо или недопустимо
указывать номер, который не является членом определения enum
типа поля. Процессоры текстового формата, не привязанные к конкретной
среде выполнения (например, поддержка IDE), могут выдавать
предупреждение, когда предоставленное числовое значение не является допустимым членом. Обратите внимание,
что определенные имена, которые являются допустимыми ключевыми словами в других контекстах, такие как
"true" или "infinity", также являются допустимыми именами значений перечисления.
|
| значения сообщений |
Элемент MessageValue.
|
Поля расширений
Поля расширений указываются с использованием их квалифицированных имен. Пример:
local_field: 10
[com.example.ext_field]: 20
Поля расширений обычно определяются в других .proto файлах. Язык текстового формата не предоставляет механизма для указания местоположения файлов, которые определяют поля расширений; вместо этого парсер должен иметь предварительные знания об их местоположениях.
Поля Any
Текстовый формат поддерживает расширенную форму
google.protobuf.Any
известного типа с использованием специального синтаксиса, напоминающего поля расширений. Пример:
local_field: 10
# Значение Any с использованием обычных полей.
any_value {
type_url: "type.googleapis.com/com.example.SomeType"
value: "\x0a\x05hello" # сериализованные байты com.example.SomeType
}
# То же значение с использованием расширения Any
any_value {
[type.googleapis.com/com.example.SomeType] {
field1: "hello"
}
}
В этом примере any_value — это поле типа google.protobuf.Any, и оно
хранит сериализованное сообщение com.example.SomeType, содержащее field1: hello.
Поля group
В текстовом формате поле group использует нормальный элемент MessageValue в качестве своего
значения, но указывается с использованием имени группы с заглавной буквы, а не
неявного имени поля в нижнем регистре. Пример:
// proto2
message MessageWithGroup {
optional group MyGroup = 1 {
optional int32 my_value = 1;
}
}
При приведенном выше определении .proto следующий текстовый формат является допустимым
MessageWithGroup:
MyGroup {
my_value: 1
}
Аналогично полям сообщений, разделитель : между именем группы и значением является
необязательным.
Эта функциональность включена в Редакции для обратной совместимости. Обычно
поля DELIMITED сериализуются как обычные сообщения. Ниже показано
поведение с Редакциями:
edition = "2024";
message Parent {
message GroupLike {
int32 foo = 1;
}
GroupLike grouplike = 1 [features.message_encoding = DELIMITED];
}
Содержимое этого .proto файла будет сериализовано как группа proto2:
GroupLike {
foo: 2;
}
Поля map
Текстовый формат не предоставляет пользовательский синтаксис для указания записей полей карты.
Когда поле map определено в .proto файле, неявно определяется сообщение Entry
содержащее поля key и value. Поля карты всегда повторяются,
принимая несколько записей ключ/значение. Пример:
// Редакции
edition = "2024";
message MessageWithMap {
map<string, int32> my_map = 1;
}
При приведенном выше определении .proto следующий текстовый формат является допустимым
MessageWithMap:
my_map { key: "entry1" value: 1 }
my_map { key: "entry2" value: 2 }
# Вы также можете использовать синтаксис списка
my_map: [
{ key: "entry3" value: 3 },
{ key: "entry4" value: 4 }
]
Оба поля key и value являются необязательными и по умолчанию имеют нулевое значение
соответствующих типов, если не указано иное. Если ключ дублируется, только
последнее указанное значение будет сохранено в разобранной карте.
Порядок карт не сохраняется в textprotos.
Поля oneof
Хотя в текстовом формате нет специального синтаксиса, связанного с полями oneof, только
один член oneof может быть указан за раз. Указание нескольких членов
одновременно недопустимо. Пример:
// Редакции
edition = "2024";
message OneofExample {
message MessageWithOneof {
string not_part_of_oneof = 1;
oneof Example {
string first_oneof_field = 2;
string second_oneof_field = 3;
}
}
repeated MessageWithOneof message = 1;
}
Приведенное выше определение .proto приводит к следующему поведению текстового формата:
# Допустимо: установлено только одно поле из oneof Example.
message {
not_part_of_oneof: "всегда допустимо"
first_oneof_field: "допустимо само по себе"
}
# Допустимо: установлено другое поле oneof.
message {
not_part_of_oneof: "всегда допустимо"
second_oneof_field: "допустимо само по себе"
}
# Неверно: установлено несколько полей из oneof Example.
message {
not_part_of_oneof: "всегда допустимо"
first_oneof_field: "недопустимо"
second_oneof_field: "недопустимо"
}
Файлы текстового формата
Файл текстового формата использует суффикс имени файла .txtpb и содержит одно
Message. Файлы текстового формата кодируются в UTF-8. Пример файла textproto приведен ниже.
{{% alert title="Важно" color="warning" %}}
.txtpb является каноническим расширением файла текстового формата и ему следует отдавать предпочтение перед
альтернативами. Этот суффикс предпочтителен из-за своей краткости и согласованности с
официальным расширением файла wire-формата .binpb. Устаревшее каноническое расширение
.textproto все еще широко используется и имеет поддержку инструментов.
Некоторые инструменты также
поддерживают устаревшие расширения .textpb и .pbtxt. Все другие расширения,
кроме перечисленных выше, категорически не рекомендуются; в частности, расширения, такие как
.protoascii, ошибочно подразумевают, что текстовый формат является только ascii, а другие, такие как
.pb.txt, не распознаются распространенными инструментами.
{{% /alert %}}
# Это пример текстового формата Protocol Buffer.
# В отличие от файлов .proto, поддерживаются только строчные комментарии в стиле shell.
name: "Иван Иванов"
pet {
kind: DOG
name: "Пушистик"
tail_wagginess: 0.65f
}
pet <
kind: LIZARD
name: "Ящерица"
legs: 4
>
string_value_with_escape: "допустимый \n escape"
repeated_values: [ "один", "два", "три" ]
Заголовок
Комментарии заголовка proto-file и proto-message информируют инструменты разработки о
схеме, чтобы они могли предоставлять различные функции.
# proto-file: some/proto/my_file.proto
# proto-message: MyMessage
Работа с форматом программно
Из-за того, что отдельные реализации Protocol Buffer выдают ни последовательный, ни каноничный текстовый формат, инструменты или библиотеки, которые изменяют файлы TextProto или выводят вывод TextProto, должны явно использовать https://github.com/protocolbuffers/txtpbfmt для форматирования своего вывода.
-
См. Unicode 13 §3.8 Surrogates, §3.2 Conformance Requirements, C1 и §3.9 Unicode Encoding Forms, D92. ↩
Известные типы Protocol Buffers
Документация API для пакета google.protobuf.
Содержание
Any(сообщение)Api(сообщение)BoolValue(сообщение)BytesValue(сообщение)DoubleValue(сообщение)Duration(сообщение)Empty(сообщение)Enum(сообщение)EnumValue(сообщение)Field(сообщение)Field.Cardinality(перечисление)Field.Kind(перечисление)FieldMask(сообщение)FloatValue(сообщение)Int32Value(сообщение)Int64Value(сообщение)ListValue(сообщение)Method(сообщение)Mixin(сообщение)NullValue(перечисление)Option(сообщение)SourceContext(сообщение)StringValue(сообщение)Struct(сообщение)Syntax(перечисление)Timestamp(сообщение)Type(сообщение)UInt32Value(сообщение)UInt64Value(сообщение)Value(сообщение)
Известные типы, оканчивающиеся на "Value", являются сообщениями-обертками для других типов,
такими как BoolValue и EnumValue. Сейчас они устарели. Единственные причины использовать
обертки сегодня:
- Совместимость на уровне передачи данных с сообщениями, которые уже их используют.
- Если нужно поместить скалярное значение в сообщение
Any.
В большинстве случаев есть лучшие варианты:
- Для новых сообщений лучше использовать обычные поля с явным присутствием
(
optionalв proto2/proto3, обычное поле в редакции >= 2023). - Расширения, как правило, являются лучшим вариантом, чем поля
Any.
Any
Any содержит произвольное сериализованное сообщение вместе с URL, который описывает
тип сериализованного сообщения.
JSON
JSON-представление значения Any использует регулярное представление десериализованного,
встроенного сообщения с дополнительным полем @type, которое содержит URL типа. Пример:
package google.profile;
message Person {
string first_name = 1;
string last_name = 2;
}
{
"@type": "type.googleapis.com/google.profile.Person",
"firstName": <string>,
"lastName": <string>
}
Если тип встроенного сообщения является известным и имеет пользовательское JSON-представление,
это представление будет встроено с добавлением поля value, которое содержит
пользовательский JSON в дополнение к полю @type. Пример (для сообщения
google.protobuf.Duration):
{
"@type": "type.googleapis.com/google.protobuf.Duration",
"value": "1.212s"
}
| Имя поля | Тип | Описание |
|---|---|---|
type_url |
string |
URL/имя ресурса, содержимое которого описывает тип сериализованного сообщения. Для URL, которые используют схему
Схемы, отличные от |
value |
bytes |
Должен быть допустимым сериализованным данным указанного выше типа. |
Api
Api — это легковесный дескриптор для сервиса protobuf.
| Имя поля | Тип | Описание |
|---|---|---|
name |
string |
Полностью квалифицированное имя этого api, включая имя пакета, за которым следует простое имя api. |
methods |
Method
|
Методы этого api, в произвольном порядке. |
options |
Option
|
Любые метаданные, прикрепленные к API. |
version |
string |
Строка версии для этого api. Если указана, должна иметь вид
Схема версионирования использует семантическое версионирование, где мажорный номер версии указывает на критическое изменение, а минорный — на добавочное, некритическое изменение. Оба номера версий являются сигналами для пользователей о том, чего ожидать от разных версий, и должны быть тщательно выбраны на основе плана продукта.
Мажорная версия также отражается в имени пакета API,
которое должно оканчиваться на |
source_context |
|
Исходный контекст для сервиса protobuf, представленного этим сообщением. |
mixins |
|
Включенные API. См.
Mixin.
|
syntax |
Syntax
|
Исходный синтаксис сервиса. |
BoolValue
Сообщение-обертка для bool.
JSON-представление для BoolValue — это JSON true и false.
| Имя поля | Тип | Описание |
|---|---|---|
value |
bool |
Логическое значение. |
BytesValue
Сообщение-обертка для bytes.
JSON-представление для BytesValue — это JSON строка.
| Имя поля | Тип | Описание |
|---|---|---|
value |
bytes |
Значение в байтах. |
DoubleValue
Сообщение-обертка для double.
JSON-представление для DoubleValue — это JSON число.
| Имя поля | Тип | Описание |
|---|---|---|
value |
double |
Значение типа double. |
Duration
Duration представляет знаковый, фиксированной длины промежуток времени, представленный как количество секунд и долей секунд с наносекундным разрешением. Он не зависит от любого календаря и понятий, таких как «день» или «месяц». Он связан с Timestamp тем, что разница между двумя значениями Timestamp — это Duration, и его можно добавить или вычесть из Timestamp. Диапазон составляет приблизительно +-10 000 лет.
Пример 1: Вычисление Duration из двух Timestamps в псевдокоде.
Timestamp start = ...;
Timestamp end = ...;
Duration duration = ...;
duration.seconds = end.seconds - start.seconds;
duration.nanos = end.nanos - start.nanos;
if (duration.seconds < 0 && duration.nanos > 0) {
duration.seconds += 1;
duration.nanos -= 1000000000;
} else if (duration.seconds > 0 && duration.nanos < 0) {
duration.seconds -= 1;
duration.nanos += 1000000000;
}
Пример 2: Вычисление Timestamp из Timestamp + Duration в псевдокоде.
Timestamp start = ...;
Duration duration = ...;
Timestamp end = ...;
end.seconds = start.seconds + duration.seconds;
end.nanos = start.nanos + duration.nanos;
if (end.nanos < 0) {
end.seconds -= 1;
end.nanos += 1000000000;
} else if (end.nanos >= 1000000000) {
end.seconds += 1;
end.nanos -= 1000000000;
}
JSON-представление для Duration — это String, который оканчивается на s для
указания секунд и предваряется количеством секунд, с наносекундами,
выраженными как дробные секунды.
| Имя поля | Тип | Описание |
|---|---|---|
seconds |
int64 |
Знаковое количество секунд в промежутке времени. Должно быть от -315 576 000 000 до +315 576 000 000 включительно. |
nanos |
int32 |
Знаковые доли секунды с наносекундным разрешением промежутка времени.
Длительности менее одной секунды представлены с полем seconds, равным 0,
и положительным или отрицательным полем nanos.
Для длительностей в одну секунду или более ненулевое
значение для поля nanos должно иметь тот же знак,
что и поле seconds. Должно быть от -999 999 999 до
+999 999 999 включительно.
|
Empty
Универсальное пустое сообщение, которое можно повторно использовать, чтобы избежать определения дублированных пустых сообщений в ваших API. Типичный пример — использовать его как тип запроса или ответа метода API. Например:
service Foo {
rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
}
JSON-представление для Empty — это пустой JSON объект {}.
Enum
Определение типа перечисления.
| Имя поля | Тип | Описание |
|---|---|---|
name |
string |
Имя типа перечисления. |
enumvalue |
EnumValue
|
Определения значений перечисления. |
options |
Option
|
Опции protobuf. |
source_context |
SourceContext
|
Исходный контекст. |
syntax |
Syntax
|
Исходный синтаксис. |
edition |
string
|
Исходная редакция, если syntax равен SYNTAX_EDITIONS. |
EnumValue
Определение значения перечисления.
| Имя поля | Тип | Описание |
|---|---|---|
name |
string |
Имя значения перечисления. |
number |
int32 |
Числовое значение перечисления. |
options |
Option
|
Опции protobuf. |
Field
Одно поле типа сообщения.
| Имя поля | Тип | Описание |
|---|---|---|
kind |
Kind
|
Тип поля. |
cardinality |
Cardinality
|
Кардинальность поля. |
number |
int32 |
Номер поля. |
name |
string |
Имя поля. |
type_url |
string |
URL типа поля, без схемы, для типов сообщений или перечислений.
Пример:
"type.googleapis.com/google.protobuf.Timestamp".
|
oneof_index |
int32 |
Индекс типа поля в Type.oneofs, для типов сообщений или
перечислений. Первый тип имеет индекс 1; ноль означает, что тип
отсутствует в списке.
|
packed |
bool |
Использовать ли альтернативное упакованное представление при передаче. |
options |
Option
|
Опции protobuf. |
json_name |
string |
JSON-имя поля. |
default_value |
string |
Строковое значение значения по умолчанию этого поля. Только для синтаксиса Proto2. |
Cardinality
Является ли поле опциональным, обязательным или повторяющимся.
| Значение перечисления | Описание |
|---|---|
CARDINALITY_UNKNOWN |
Для полей с неизвестной кардинальностью. |
CARDINALITY_OPTIONAL |
Для опциональных полей. |
CARDINALITY_REQUIRED |
Для обязательных полей. Только для синтаксиса Proto2. |
CARDINALITY_REPEATED |
Для повторяющихся полей. |
Kind
Базовые типы полей.
| Значение перечисления | Описание |
|---|---|
TYPE_UNKNOWN |
Тип поля неизвестен. |
TYPE_DOUBLE |
Тип поля double. |
TYPE_FLOAT |
Тип поля float. |
TYPE_INT64 |
Тип поля int64. |
TYPE_UINT64 |
Тип поля uint64. |
TYPE_INT32 |
Тип поля int32. |
TYPE_FIXED64 |
Тип поля fixed64. |
TYPE_FIXED32 |
Тип поля fixed32. |
TYPE_BOOL |
Тип поля bool. |
TYPE_STRING |
Тип поля string. |
TYPE_GROUP |
Тип поля group. Только для синтаксиса Proto2 и устарел. |
TYPE_MESSAGE |
Тип поля message. |
TYPE_BYTES |
Тип поля bytes. |
TYPE_UINT32 |
Тип поля uint32. |
TYPE_ENUM |
Тип поля enum. |
TYPE_SFIXED32 |
Тип поля sfixed32. |
TYPE_SFIXED64 |
Тип поля sfixed64. |
TYPE_SINT32 |
Тип поля sint32. |
TYPE_SINT64 |
Тип поля sint64. |
FieldMask
FieldMask представляет набор символьных путей к полям, например:
paths: "f.a"
paths: "f.b.d"
Здесь f представляет поле в некотором корневом сообщении, a и b — поля в
сообщении, найденном в f, а d — поле, найденное в сообщении в f.b.
Маски полей используются для указания подмножества полей, которые должны быть возвращены операцией get (проекция), или изменены операцией обновления. Маски полей также имеют пользовательское кодирование JSON (см. ниже).
Маски полей в проекциях
Когда FieldMask указывает проекцию, API отфильтрует ответное
сообщение (или подсообщение), чтобы содержать только те поля, которые указаны в маске. Для
примера, рассмотрим это сообщение ответа «до маскирования»:
f {
a : 22
b {
d : 1
x : 2
}
y : 13
}
z: 8
После применения маски из предыдущего примера ответ API не будет содержать конкретных значений для полей x, y или z (их значение будет установлено в значение по умолчанию и опущено в текстовом выводе proto):
f {
a : 22
b {
d : 1
}
}
Повторяющееся поле не допускается, кроме как на последней позиции маски поля.
Если объект FieldMask отсутствует в операции get, операция применяется
ко всем полям (как если бы была указана FieldMask всех полей).
Обратите внимание, что маска поля не обязательно применяется к ответному сообщению верхнего уровня. В случае операции REST get маска поля применяется непосредственно к ответу, но в случае операции REST list маска вместо этого применяется к каждому отдельному сообщению в возвращаемом списке ресурсов. В случае пользовательского метода REST могут использоваться другие определения. Там, где применяется маска, будет четко задокументировано вместе с ее объявлением в API. В любом случае, влияние на возвращаемый ресурс/ресурсы является обязательным поведением для API.
Маски полей в операциях обновления
Маска поля в операциях обновления указывает, какие поля целевого ресурса будут обновлены. От API требуется изменить только значения полей, указанных в маске, и оставить остальные нетронутыми. Если ресурс передается для описания обновленных значений, API игнорирует значения всех полей, не охваченных маской.
Чтобы сбросить значение поля к значению по умолчанию, поле должно быть в маске и установлено в значение по умолчанию в предоставленном ресурсе. Следовательно, чтобы сбросить все поля ресурса, предоставьте экземпляр по умолчанию ресурса и установите все поля в маске, или не предоставляйте маску, как описано ниже.
Если маска поля отсутствует при обновлении, операция применяется ко всем полям (как если бы была указана маска поля всех полей). Обратите внимание, что в условиях эволюции схемы это может означать, что поля, которые клиент не знает и поэтому не заполнил в запросе, будут сброшены к их значению по умолчанию. Если это нежелательное поведение, конкретный сервис может потребовать от клиента всегда указывать маску поля, выдавая ошибку, если это не так.
Как и в случае с операциями get, расположение ресурса, который описывает обновленные значения в сообщении запроса, зависит от вида операции. В любом случае влияние маски поля должно соблюдаться API.
Соображения для HTTP REST
HTTP-вид операции обновления, которая использует маску поля, должен быть установлен в PATCH вместо PUT для удовлетворения семантики HTTP (PUT должен использоваться только для полных обновлений).
Кодирование JSON масок полей
В JSON маска поля кодируется как одна строка, где пути разделены запятой. Имена полей в каждом пути преобразуются в/из стиля lower-camel.
В качестве примера рассмотрим следующие объявления сообщений:
message Profile {
User user = 1;
Photo photo = 2;
}
message User {
string display_name = 1;
string address = 2;
}
В proto маска поля для Profile может выглядеть так:
mask {
paths: "user.display_name"
paths: "photo"
}
В JSON та же маска представлена ниже:
{
mask: "user.displayName,photo"
}
| Имя поля | Тип | Описание |
|---|---|---|
paths |
string |
Набор путей маски поля. |
FloatValue
Сообщение-обертка для float.
JSON-представление для FloatValue — это JSON число.
| Имя поля | Тип | Описание |
|---|---|---|
value |
float |
Значение типа float. |
Int32Value
Сообщение-обертка для int32.
JSON-представление для Int32Value — это JSON число.
| Имя поля | Тип | Описание |
|---|---|---|
value |
int32 |
Значение типа int32. |
Int64Value
Сообщение-обертка для int64.
JSON-представление для Int64Value — это JSON строка.
| Имя поля | Тип | Описание |
|---|---|---|
value |
int64 |
Значение типа int64. |
ListValue
ListValue — это обертка вокруг повторяющегося поля значений.
JSON-представление для ListValue — это JSON массив.
| Имя поля | Тип | Описание |
|---|---|---|
values |
Value
|
Повторяющееся поле значений динамического типа. |
Method
Method представляет метод api.
| Имя поля | Тип | Описание |
|---|---|---|
name |
string |
Простое имя этого метода. |
request_type_url |
string |
URL типа входного сообщения. |
request_streaming |
bool |
Если true, запрос передается потоком. |
response_type_url |
string |
URL типа выходного сообщения. |
response_streaming |
bool |
Если true, ответ передается потоком. |
options |
Option
|
Любые метаданные, прикрепленные к методу. |
syntax |
Syntax
|
Исходный синтаксис этого метода. |
Mixin
Объявляет API, который должен быть включен в этот API. Включающий API должен переобъявить все методы из включаемого API, но документация и опции наследуются следующим образом:
-
Если после удаления комментариев и пробелов строка документации переобъявленного метода пуста, она будет унаследована от исходного метода.
-
Каждая аннотация, принадлежащая конфигурации сервиса (http, visibility), которая не установлена в переобъявленном методе, будет унаследована.
-
Если аннотация http унаследована, шаблон пути будет изменен следующим образом. Любой префикс версии будет заменен версией включающего API плюс путь
root, если указан.
Пример простого mixin:
package google.acl.v1;
service AccessControl {
// Get the underlying ACL object.
rpc GetAcl(GetAclRequest) returns (Acl) {
option (google.api.http).get = "/v1/{resource=**}:getAcl";
}
}
package google.storage.v2;
service Storage {
// rpc GetAcl(GetAclRequest) returns (Acl);
// Get a data record.
rpc GetData(GetDataRequest) returns (Data) {
option (google.api.http).get = "/v2/{resource=**}";
}
}
Пример конфигурации mixin:
apis:
- name: google.storage.v2.Storage
mixins:
- name: google.acl.v1.AccessControl
Конструкция mixin подразумевает, что все методы в AccessControl также
объявлены с тем же именем и типами запроса/ответа в Storage. Генератор документации
или процессор аннотаций увидит эффективный метод Storage.GetAcl
после наследования документации и аннотаций следующим образом:
service Storage {
// Get the underlying ACL object.
rpc GetAcl(GetAclRequest) returns (Acl) {
option (google.api.http).get = "/v2/{resource=**}:getAcl";
}
...
}
Обратите внимание, как версия в шаблоне пути изменилась с v1 на v2.
Если указано поле root, это должен быть относительный путь,
под которым размещаются унаследованные HTTP-пути. Пример:
apis:
- name: google.storage.v2.Storage
mixins:
- name: google.acl.v1.AccessControl
root: acls
Это подразумевает следующую унаследованную HTTP-аннотацию:
service Storage {
// Get the underlying ACL object.
rpc GetAcl(GetAclRequest) returns (Acl) {
option (google.api.http).get = "/v2/acls/{resource=**}:getAcl";
}
...
}
| Имя поля | Тип | Описание |
|---|---|---|
name |
string |
Полностью квалифицированное имя API, которое включено. |
root |
string |
Если не пусто, указывает путь, под которым размещаются унаследованные HTTP-пути. |
NullValue
NullValue — это перечисление-одиночка для представления нулевого значения для
объединения типов Value.
JSON-представление для NullValue — это JSON null.
| Значение перечисления | Описание |
|---|---|
NULL_VALUE |
Нулевое значение. |
Option
Опция protobuf, которая может быть прикреплена к сообщению, полю, перечислению и т.д.
| Имя поля | Тип | Описание |
|---|---|---|
name |
string |
Имя опции. Например, "java_package".
|
value |
Any
|
Значение опции. Например,
"com.google.protobuf".
|
SourceContext
SourceContext представляет информацию об источнике элемента protobuf,
например, о файле, в котором он определен.
| Имя поля | Тип | Описание |
|---|---|---|
file_name |
string |
Имя файла .proto с указанием пути, который содержал связанный
элемент protobuf. Например:
"google/protobuf/source.proto".
|
StringValue
Сообщение-обертка для string.
JSON-представление для StringValue — это JSON строка.
| Имя поля | Тип | Описание |
|---|---|---|
value |
string |
Строковое значение. |
Struct
Struct представляет структурированное значение данных, состоящее из полей, которые отображаются в
значения динамического типа. В некоторых языках Struct может поддерживаться
нативным представлением. Например, в скриптовых языках, таких как JS, struct
представлен как объект. Детали этого представления описаны
вместе с поддержкой proto для языка.
JSON-представление для Struct — это JSON объект.
| Имя поля | Тип | Описание |
|---|---|---|
fields |
map<string, Value>
|
Карта значений динамического типа. |
Syntax
Синтаксис, в котором определен элемент protobuf.
| Значение перечисления | Описание |
|---|---|
SYNTAX_PROTO2 |
Синтаксис proto2. |
SYNTAX_PROTO3 |
Синтаксис proto3. |
SYNTAX_EDITIONS |
Синтаксис использует конструкцию edition. |
Timestamp
Timestamp представляет момент времени, не зависящий от любого часового пояса или календаря, представленный как секунды и доли секунд с наносекундным разрешением в UTC эпохи. Он кодируется с использованием Пролептического Григорианского календаря, который расширяет Григорианский календарь назад до первого года. Он кодируется в предположении, что все минуты состоят из 60 секунд, т.е. високосные секунды «размазаны», так что таблица високосных секунд не нужна для интерпретации. Диапазон от 0001-01-01T00:00:00Z до 9999-12-31T23:59:59.999999999Z. Ограничиваясь этим диапазоном, мы гарантируем, что можем конвертировать в строки дат RFC 3339 и обратно. См. https://www.ietf.org/rfc/rfc3339.txt.
Тип Timestamp кодируется как строка в формате RFC 3339:
"{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z", где {year}
всегда выражается четырьмя цифрами, а {month}, {day}, {hour}, {min},
и {sec} дополняются нулями до двух цифр каждая. Дробные секунды, которые
могут достигать 9 цифр (то есть до разрешения в 1 наносекунду), являются необязательными.
Суффикс "Z" указывает часовой пояс ("UTC"); часовой пояс обязателен.
Сериализатор proto3 JSON должен всегда использовать UTC (как указано "Z") при выводе
типа Timestamp, а парсер proto3 JSON должен быть способен принимать как UTC,
так и другие часовые пояса (как указано смещением).
Пример 1: Вычисление Timestamp из POSIX time().
Timestamp timestamp;
timestamp.set_seconds(time(NULL));
timestamp.set_nanos(0);
Пример 2: Вычисление Timestamp из POSIX gettimeofday().
struct timeval tv;
gettimeofday(&tv, NULL);
Timestamp timestamp;
timestamp.set_seconds(tv.tv_sec);
timestamp.set_nanos(tv.tv_usec * 1000);
Пример 3: Вычисление Timestamp из Win32 GetSystemTimeAsFileTime().
FILETIME ft;
GetSystemTimeAsFileTime(&ft);
UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
// Тик Windows равен 100 наносекундам. Эпоха Windows 1601-01-01T00:00:00Z
// на 11644473600 секунд раньше эпохи Unix 1970-01-01T00:00:00Z.
Timestamp timestamp;
timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
Пример 4: Вычисление Timestamp из Java System.currentTimeMillis().
long millis = System.currentTimeMillis();
Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
.setNanos((int) ((millis % 1000) * 1000000)).build();
Пример 5: Вычисление Timestamp из текущего времени в Python.
now = time.time()
seconds = int(now)
nanos = int((now - seconds) * 10**9)
timestamp = Timestamp(seconds=seconds, nanos=nanos)
| Имя поля | Тип | Описание |
|---|---|---|
seconds |
int64 |
Представляет секунды времени UTC с эпохи Unix 1970-01-01T00:00:00Z. Должно быть от 0001-01-01T00:00:00Z до 9999-12-31T23:59:59Z включительно. |
nanos |
int32 |
Неотрицательные доли секунды с наносекундным разрешением. Отрицательные значения секунд с долями все равно должны иметь неотрицательные значения наносекунд, которые отсчитываются вперед во времени. Должно быть от 0 до 999 999 999 включительно. |
Type
Тип сообщения protobuf.
| Имя поля | Тип | Описание |
|---|---|---|
name |
string |
Полностью квалифицированное имя сообщения. |
fields |
Field
|
Список полей. |
oneofs |
string |
Список типов, появляющихся в определениях oneof в этом
типе.
|
options |
Option
|
Опции protobuf. |
source_context |
SourceContext
|
Исходный контекст. |
syntax |
Syntax
|
Исходный синтаксис. |
UInt32Value
Сообщение-обертка для uint32.
JSON-представление для UInt32Value — это JSON число.
| Имя поля | Тип | Описание |
|---|---|---|
value |
uint32 |
Значение типа uint32. |
UInt64Value
Сообщение-обертка для uint64.
JSON-представление для UInt64Value — это JSON строка.
| Имя поля | Тип | Описание |
|---|---|---|
value |
uint64 |
Значение типа uint64. |
Value
Value представляет значение динамического типа, которое может быть либо null, числом,
строкой, логическим значением, рекурсивным структурным значением или списком значений. Ожидается, что
производитель значения установит один из этих вариантов, отсутствие любого варианта
указывает на ошибку.
JSON-представление для Value — это JSON значение.
| Имя поля | Тип | Описание |
|---|---|---|
| Объединение полей, только одно из следующих: | ||
null_value |
NullValue
|
Представляет нулевое значение. |
number_value |
double |
Представляет значение типа double. Обратите внимание, что попытка сериализовать NaN или Infinity приводит к ошибке. (Мы не можем сериализовать их как строковые значения "NaN" или "Infinity", как мы делаем для обычных полей, потому что они будут анализироваться как string_value, а не number_value). |
string_value |
string |
Представляет строковое значение. |
bool_value |
bool |
Представляет логическое значение. |
struct_value |
Struct
|
Представляет структурированное значение. |
list_value |
ListValue
|
Представляет повторяющийся Value. |
MIME типы
Стандартные MIME-типы для сериализаций Protobuf.
Все документы Protobuf должны иметь MIME-тип application и подтип protobuf, с суффиксом +json для
JSON-кодирований в соответствии со стандартом, с последующими параметрами:
encodingдолжен устанавливаться только вbinaryилиjson, обозначая соответствующие форматы.- С подтипом
protobuf+jsonпараметрencodingпо умолчанию имеет значениеjsonи не может быть установлен вbinary. С подтипомprotobuf(без+json) параметрencodingпо умолчанию имеет значениеbinaryи не может быть установлен вjson. - Используйте
+jsonдля JSON, даже в HTTP-ответах, которые используют "parser breakers" в качестве меры смягчения CORB.
- С подтипом
- Установите
charsetвutf-8для всех JSON- или Text Format-кодирований и никогда не устанавливайте его для бинарных кодировок.- Если
charsetне указан, предполагается, что он равен UTF-8. Предпочтительно всегда указыватьcharset, так как это может предотвратить определенные векторы атак, когда protos используются в HTTP-ответах.
- Если
- Protobuf резервирует параметр
versionдля потенциального будущего управления версиями наших wire-форматов. Не устанавливайте его, пока wire-формат не будет версионирован.
Таким образом, стандартные MIME-типы для распространенных кодировок protobuf:
application/protobufдля сериализованных бинарных protos.application/protobuf+json; charset=utf-8для protos в формате JSON.
Сервисы, которые читают Protobuf, также должны обрабатывать application/json, который может
использоваться для кодирования protos в формате JSON.
Парсеры должны завершаться ошибкой, если параметры MIME (encoding, charset или version) имеют
неизвестные или недопустимые значения.
Когда бинарные protos передаются через HTTP, Protobuf настоятельно рекомендует
кодировать их в Base64 и устанавливать X-Content-Type-Options: nosniff для предотвращения
XSS, поскольку возможно, что Protobuf будет разобран как активный контент.
Допустимо передавать дополнительные параметры для этих MIME-типов при необходимости, например, URL типа, который указывает на схему содержимого; но параметры MIME-типа не должны включать опции кодирования.
Download
Страница загрузок для protocol buffers.
Пакеты релизов
Последняя версия
Последний выпуск Protocol Buffers можно найти на странице релизов.
Старые версии
Более старые версии доступны в наших исторических релизах на GitHub.
Исходный код
Репозиторий GitHub
Исходный код Protocol Buffers размещен на GitHub.
История
Краткая история создания protocol buffers.
Понимание того, почему был создан protobuf и решений, которые меняли его с течением времени, может помочь вам лучше использовать возможности этого инструмента.
Почему вы выпустили Protocol Buffers?
Есть несколько причин, по которым мы выпустили Protocol Buffers.
Protocol buffers используются во многих проектах внутри Google. У нас были другие проекты, которые мы хотели выпустить в виде открытого исходного кода и которые используют protocol buffers, поэтому для этого нам нужно было сначала выпустить protocol buffers. На самом деле, части технологии уже просочились в открытый доступ; если вы покопаетесь в коде Google AppEngine, вы можете найти некоторые из них.
Мы хотели предоставить публичные API, которые принимают protocol buffers, а также XML, как потому что это более эффективно, так и потому что мы в любом случае конвертируем этот XML в protocol buffers на нашей стороне.
Мы подумали, что людям за пределами Google protocol buffers могут показаться полезными. Приведение protocol buffers в форму, которой мы были довольны для выпуска, было интересным побочным проектом.
Почему первый выпуск имеет версию 2? Что случилось с версией 1?
Первоначальная версия protocol buffers («Proto1») разрабатывалась, начиная с начала 2001 года, и развивалась на протяжении многих лет, порождая новые функции, когда кому-то это было нужно и он был готов проделать работу по их созданию. Как и все, созданное таким образом, это было немного беспорядочно. Мы пришли к выводу, что выпустить код в таком виде не представляется возможным.
Версия 2 («Proto2») была полной переработкой, хотя она сохранила большую часть дизайна и использовала многие идеи реализации из Proto1. Некоторые функции были добавлены, некоторые удалены. Однако самое главное, код был очищен и не имел каких-либо зависимостей от библиотек Google, которые еще не были открыты.
Почему название «Protocol Buffers»?
Название происходит из ранних дней формата, до того как у нас был
компилятор protocol buffer для генерации классов за нас. В то время существовал
класс с названием ProtocolBuffer, который фактически действовал как буфер для отдельного
метода. Пользователи добавляли пары тег/значение в этот буфер по отдельности, вызывая
методы типа AddValue(tag, value). Необработанные байты сохранялись в буфере, который
можно было затем записать после построения сообщения.
С тех пор часть названия «buffers» потеряла свой первоначальный смысл, но это все еще название, которое мы используем. Сегодня люди обычно используют термин «protocol message» (сообщение протокола) для обозначения сообщения в абстрактном смысле, «protocol buffer» (буфер протокола) для обозначения сериализованной копии сообщения и «protocol message object» (объект сообщения протокола) для обозначения объекта в памяти, представляющего разобранное сообщение.
Есть ли у Google патенты на Protocol Buffers?
В настоящее время у Google нет выданных патентов на protocol buffers, и мы рады разрешить любые опасения, связанные с protocol buffers и патентами, которые могут возникнуть у людей.
Чем Protocol Buffers отличаются от ASN.1, COM, CORBA и Thrift?
Мы считаем, что все эти системы имеют сильные и слабые стороны. Google полагается на protocol buffers внутри компании, и они являются жизненно важным компонентом нашего успеха, но это не значит, что они являются идеальным решением для каждой проблемы. Вам следует оценивать каждую альтернативу в контексте вашего собственного проекта.
Однако стоит отметить, что несколько из этих технологий определяют как формат обмена, так и протокол RPC (удаленного вызова процедур). Protocol buffers — это всего лишь формат обмена. Их можно легко использовать для RPC—и, действительно, у них есть ограниченная поддержка определения RPC-сервисов—но они не привязаны к какой-либо одной реализации или протоколу RPC.