Один из набирающих популярность протоколов общения между двумя гетерогенными системами — Protocol Buffers. Сегодня мы разберемся с тем, что это такое, для чего нужно и как применять.
Контекст
Когда перед нами встает задача общения двух удалённых систем, первое что приходит в голову сегодня это HTTP запросы, кто-то любит использовать слово REST (REpresentational State Transfer), хотя и на самом деле строит API в виде RPC (Remote Procedure Call), реализация которого опирается на HTTP вызовы. Наиболее распространенный сегодня HTTP/1.1 был принят в далёком 1999 году.
У протокола была и есть одна особенность (язык не поворачивается назвать это недостатком) — он текстовый, это говорит об одновременно двух следствиях. Процесс общения легко отлаживать, мы сразу видим в человекочитаемом виде какая информация передается. Однако, информация часто занимает больше места чем нужно. На смену HTTP/1.1 приходит относительно новый HTTP/2 — бинарный протокол. Сама процедура бинарной передачи данных как бы намекает: “ты использовал json и xml для передачи компактных и читаемых данных, но теперь данные передаются в бинарном виде, может быть нам нужен другой формат?”
Что такое gRPC за 10 минут
Основная идея
Protocol Buffers — это бинартый протокол сериализации (передачи) структурированных данных. Google предложили его как эффективную альтернативу xml и правильно сделали. В моём окружении все не легаси проекты уже давно используют json и счастливы, а здесь следующий шаг, если быть точнее, другой взгляд на передаваемые данные. Данные хранятся в виде набора байт, но как работать с бинарнарным протоколом сериализации, где взять сериализаторы и десериализаторы, как они поймут, что именно нужно сделать?
Язык общения
Для того, чтобы обе стороны взаимодействия общались на “одном языке”, необходимо создать специальный .proto файл, который опишет виды сообщений и будет основой для построения бинарного формата. Пример такого файла вы увидите дальше. Когда файл с требуемой сруктурой данных готов его необходимо скомпилировать специально для вашего языка программирования.
Результат компиляции это код в терминах необходимого вам языка, который упрощает процесс работы с данными, сериализацию и десериализацию. В Java это классы и их методы. Сгенерированный класс будет содержать методы доступа ко всем полям, а также методы сериализации и десириализации в/из массива байт.
Общие приемущества
- Сокращение издержек на передачу в сравнении с текстовыми форматами.
- Хорошо дружит с HTTP/2
- При добавлении новых полей на старых клиентах они игнорируются, сохраняя совместимость.
Недостатки
Пожалуй, о недостатках лучше всего скажет тот, кто с ними столкнулся и здесь я посоветую вам прочитать вот эту статью на хабре, дабы развеять ненужные иллюзии безоблачного неба.
Будьте внимательны с использованием обязательных полей. Нужно понимать, что если у клиента версия .proto файла, где поле Х обязательно, а сервер решит удалить его из следующей версии API, то такое изменение будет обратно-несовместимым. Таким образом, обязательные поля могут принести больше вреда чем пользы. Рекомендуется, следуя паттерну TolerantReader, быть готовым к изменениям модели для максимально долгого сохранения обратной совместимости.
04 Введение в protobuf
Пример
Хорошая новость. Как минимум для Intellij IDEA есть плагин для .proto файлов. В тот момент, когда вы создадите и откроете такой файл, вы увидите хинт сверху, который предложит вам установить плагин. Здесь вы увидите пример .proto файла для второй версии протобафа, хотя сейчас уже появилась третья. Возможно, о ней я буду писать позже, а любопытный читатель уже сейчас может посмотреть Language Guide (proto3).
Шаг 1. Определяем формат протокола
На первом шаге нам нужно описать .proto файл. Разберем сокращенную версию предметной области учебного заведения. Исходный .proto файл выглядит следующим образом:
syntax = «proto2»; package academy; option java_package = «ru.i_osipov.academy»; option java_outer_classname = «AcademyProtos»; message Student < required string name = 1; optional int32 repeated string email = 3; optional Gender gender = 4 [default = MALE]; enum Gender < MALE = 0; FEMALE = 1; >> message Group
Разберемся с синтаксисом файла. Прежде всего мы указываем какую версию protobuf мы используем, в нашем случае это вторая версия. Затем указываем package, который необходим здесь для разделения пространств имён.
Т.к. мы знаем, что будем пользоваться java, то указываем две дополнительные настройки: java_package и java_outer_classname . Первая, очевидно, говорит в какой пакет и соответственно иерархию директорий необходимо сложить результат компиляции, а java_outer_classname определяет имя файла, который будет в себя заворачивать весь сгенерированный контент. Если это не будет сделано, то компилятор определит имя в соответствии с CamelCase по названию .proto файла. Эти настройки, как вы понимаете, java-специфичны.
Далее мы указываем описание наших messages, по сути, message (сообщение) — это структура данных и, судя по документации, без возможности наследования. Каждое сообщение, состоит из полей. В нашем примере, каждое поле имеет тип, название, уникальный в контексте сообщения тег и модификатор. Тег — это уникальный маркер поля, т.е. пока вы не задействовали один и тот же тег для нового поля в сообщении — ваши поля остаются совместимыми с предыдущей версией. Итак, мы определили тип студента, определили его поля: строковое имя, целочисленный идентификатор, строковый email и пол.
Модификаторы дают нам больше представления о том как поле используется, например, модификатор required позволяет описать обязательное поле в сообщении, если десериализатор не обнаружит этого поля, то весь процесс десериализации закончится с ошибкой. Это важно учитывать при проектировании API (снова взгляните на второй абзац в разделе “Недостатки” этой статьи). Модификатор optional, говорит о том, что поле может быть, а может отсутствовать, своего рода nullable поле. Модификатор repeated используется для работы с множеством значений для одного поля (аналогично коллекциям в Java).
Вы можете вкладывать messages друг в друга, использовать перечисления enum, в общем очень похоже на Java. Кроме того, есть возможность определить значения по умолчанию.
*Шаг 2. Компилируем файл
* опциональный, для понимания
Созданный .proto файл нужно скомпилировать и прежде всего нам нужен компилятор. Скачиваем protoc архив. В архиве к нам прилетает компилятор и некоторый набор типов, которые мы можем использовать из коробки. Когда вы нашли место для файла в вашей файловой системе добавьте его в PATH. В Windows это делается в Параметрах окружения, а в linux будет достаточно выполнить export PATH=$PATH:your_path . Теперь нам доступен компилятор из терминала, давайте скомпилируем.
Перейдем в папку с .proto файлом и выполним команду:
protoc —java_out=./ ./academy.proto
Флаг —java_out указывает на папку куда будет сгенерирован java код. В этой папке мы получили иерархию, которая определяет java package, который мы указали в .proto файле. Результат компиляции — .java файл, который пока не компилируется javac’ом, для этого нам необходима дополнительная библиотека для работы с protobuf из java. В целях избежения ненужных проблем, перенесем наши эксперименты в плоскость обычного проекта.
Шаг 3. Собираем проект
Прежде всего хочу сказать, что не смотря на то, что все примеры на java, работа на других платформах с protobuf аналогична.
Поигрались с терминалом и хватит, перейдем к практическому применению. Создадим gradle проект, цель которого будет перегнать через массив байт группу со студентами. Для автоматизации рутинной деятельности нам поможет инструмент автоматизации сборки gradle. Для вашего случая инструмент может отличаться, но идея должна быть понятна. Для того, чтобы добавить поддержку protocol buffers в цикле сборки нашего проекта, дополним типичный build.gradle файл следующими настройками:
/* добавляем в проект плагин, который добавляет к процессу сборки проекта генерацию java файлов по .proto файлам */ plugins < id «com.google.protobuf» version «0.8.3» >protobuf < /* мы можем брать протобаф компилятор прямо из репозитория в качестве зависимости, при желании мы можем указать путь до protoc файла */ protoc < artifact = ‘com.google.protobuf:protoc:3.5.1-1’ >// указываем нашу директорию в проекте для сгенерированных файлов generatedFilesBaseDir = «$projectDir/src» // по умолчанию плагин ищет .proto файлы в /src/main/proto > dependencies < // + зависимость без которой сгенерированный код не скомпилируется compile group: ‘com.google.protobuf’, name: ‘protobuf-java’, version: ‘3.5.1’ >
Комментарии к коду исчерпывающие, а в конце статьи я оставлю ссылку на репозиторий, в котором вы найдете запускаемый код.
В папку проекта src/main/proto помещаем наш .proto файл из первого шага. Теперь при сборке проекта или при выполнении gradle команды generateProto мы получим сгенерированный код по .proto файлу внутри нашего проекта.
Шаг 4. Взаимодействуем со сгенерированным кодом
Компилятор создает весь код внутри файла AcademyProtos.java , это название мы указали в .proto файле. Весь сгенерированный код доступен в одноименном классе. Messages превратились в несколько внутренних классов, которые помогают создавать, сериализовывать и десериализовывать описанную модель. По message Student компилятор создал класс AcademyProtos.Student и AcademyProtos.Student.Builder.
Это типичная реализация паттерна “Строитель”. Объекты класса Student всегда неизменяемы, т.е. после создания мы не можем изменить каких-либо значений. Все манипуляции происходят с классом Builder, для этого у него есть достаточно методов.
Разберем код. Нам небходимо создать группу, для которой определено обязательное имя и набор студентов в виде repeated поля. Создание группы выглядит следующим образом:
AcademyProtos.Group group = AcademyProtos.Group.newBuilder() .setName(«Math») .addStudent(. ) .addStudent(. ) .build();
Для того, чтобы создать новый объект мы должны вызвать его Builder, заполнить его поля, а затем, в качестве звершающей изменения операции вызвать метод build() , который создаст группу. Repeated поля мы можем заполнять как по одному, так и добавлять целую коллецию.
Как вы уже поняли, создавать студентов мы можем аналогично:
Итак, данные мы создали, получили заполненный объект типа Group, теперь необходимо перегнать его в массив байт. Сделать это можно следующим образом:
byte[] serializedGroup = group.toByteArray();
Вот так просто! Сериализованная группа теперь — набор байт в protocol buffers формате.
Затем нам необходимо прочитать сохраненные данные. Воспользуемся статическим методом parseFrom .
AcademyProtos.Group unserialinedGroup = AcademyProtos.Group.parseFrom(serializedGroup);
Для того, чтобы проверить результат выведем его на экран (компилятор создает человекопонятные методы toString для классов, так что с отладкой нет проблем).
System.out.println(unserialinedGroup);
В результате, в консоли мы видим:
За ширмой, для полноты примера, я добавил еще одного студента к группе.
Заключение
Protocol Buffers — отличный инструмент для кросс-платформенной сериализации данных. В некоторых случаях, он позволяет сохранять обратную совместимость, однако, при безрассудном подходе может и нанести вред. Сегодня мы познакомились с основами формата, разобрали .proto файл и пример Java кода, который работает с описанными структурами. Protocol Buffers — это кирпичик, который стоит в основе других технологий для интеграции гетерогенных систем, также существуют и аналоги, которые мы рассмотрим позже. Как всегда — это не серебряная пуля, но хороший инструмент интеграции.
Источник: i-osipov.ru
Использование Google Protocol Buffers (protobuf) в Java
Привет, хабровчане. В рамках курса «Java Developer. Professional» подготовили для вас перевод полезного материала.
Недавно вышло третье издание книги «Effective Java» («Java: эффективное программирование»), и мне было интересно, что появилось нового в этой классической книге по Java, так как предыдущее издание охватывало только Java 6. Очевидно, что появились совершенно новые разделы, связанные с Java 7, Java 8 и Java 9, такие как глава 7 «Lambdas and Streams» («Лямбда-выражения и потоки»), раздел 9 «Prefer try-with-resources to try-finally» (в русском издании «2.9. Предпочитайте try-с-ресурсами использованию try-finally») и раздел 55 «Return optionals judiciously» (в русском издании «8.7. Возвращайте Optional с осторожностью»). Но я был слегка удивлен, когда обнаружил новый раздел, не связанный с нововведениями в Java, а обусловленный изменениями в мире разработки программного обеспечения. Именно этот раздел 85 «Prefer alternatives to Java Serialization» (в русском издании «12.1 Предпочитайте альтернативы сериализации Java») и побудил меня написать данную статью об использовании Google Protocol Buffers в Java.
В разделе 85 «Prefer alternatives to Java Serialization» (12.1 «Предпочитайте альтернативы сериализации Java») Джошуа Блох (Josh Bloch) выделяет жирным шрифтом следующие два утверждения, связанные с сериализацией в Java:
«Лучший способ избежать проблем, связанных с сериализацией, — никогда ничего не десериализовать».
«Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».
После описания в общих чертах проблем с десериализацией в Java и, сделав эти смелые заявления, Блох рекомендует использовать то, что он называет «кроссплатформенным представлением структурированных данных» (чтобы избежать путаницы, связанной с термином «сериализация» при обсуждении Java). Блох говорит, что основными решениями здесь являются JSON (JavaScript Object Notation) и Protocol Buffers (protobuf). Мне показалось интересным упоминание о Protocol Buffers, так как в последнее время я немного читал о них и игрался с ними. В интернете есть довольно много материалов по использованию JSON (даже в Java), в то время как осведомленность о Protocol Buffers среди java-разработчиков гораздо меньше. Поэтому я думаю, что статья об использовании Protocol Buffers в Java будет полезной.
На странице проекта Google Protocol Buffers описывается как «не зависящий от языка и платформы расширяемый механизм для сериализации структурированных данных». Также там есть пояснение: «Как XML, но меньше, быстрее и проще». И хотя одним из преимуществ Protocol Buffers является поддержка различных языков программирования, в этой статье речь пойдет исключительно про использование Protocol Buffers в Java.
Использование Protocol Buffers в Java описано в туториале «Protocol Buffer Basics: Java». В нем рассматривается гораздо больше возможностей и вещей, которые необходимо учитывать в Java, по сравнению с тем, что я расскажу здесь. Первым шагом является определение формата Protocol Buffers, не зависящего от языка программирования. Он описывается в текстовом файле с расширением .proto. Для примера опишем формат протокола в файле album.proto, который показан в следующем листинге кода.
album.proto
syntax = «proto3»; option java_outer_classname = «AlbumProtos»; option java_package = «dustin.examples.protobuf»; message Album
Несмотря на простоту приведенного выше определения формата протокола, в нем присутствует довольно много информации. В первой строке явно указано, что используется proto3 вместо proto2, используемого по умолчанию, если явно ничего не указано. Две строки, начинающиеся с option, указывают параметры генерации Java-кода (имя генерируемого класса и пакет этого класса) и они нужны только при использовании Java.
Ключевое слово «message» определяет структуру «Album», которую мы хотим представить. В ней есть четыре поля, три из которых строки (string), а одно — целое число (int32). Два из них могут присутствовать в сообщении более одного раза, так как для них указано зарезервированное слово repeated. Обратите внимание, что формат сообщения определяется независимо от Java за исключением двух option, которые определяют детали генерации Java-классов по данной спецификации.
Файл album.proto , приведенный выше, теперь необходимо «скомпилировать» в файл исходного класса Java ( AlbumProtos.java в пакете dustin.examples.protobuf ), который можно использовать для записи и чтения бинарного формата Protocol Buffers. Генерация файла исходного кода Java выполняется с помощью компилятора protoc, соответствующего вашей операционной системе. Я запускаю этот пример в Windows 10, поэтому я скачал и распаковал файл protoc-3.5.1-win32.zip. На изображении ниже показан мой запущенный protoc для album.proto с помощью команды protoc —proto_path=src —java_out=distgenerated album.proto
Перед запуском вышеуказанной команды я поместил файл album.proto в каталог src, на который указывает —proto_path , и создал пустой каталог buildgenerated для размещения сгенерированного исходного кода Java, что указано в параметре —java_out .
Сгенерированный Java-класс AlbumProtos.java содержит более 1000 строк, и я не буду приводить его здесь, он доступен на GitHub. Среди нескольких интересных моментов относительно сгенерированного кода я хотел бы отметить отсутствие выражений import (вместо них используются полные имена классов с пакетами). Более подробная информация об исходном коде Java, сгенерированном protoc, доступна в руководстве Java Generated Code. Важно отметить, что данный сгенерированный класс AlbumProtos пока никак не связан с моим Java-приложением, и сгенерирован исключительно из текстового файла album.proto, приведенного ранее.
Теперь исходный Java-код AlbumProtos надо добавить в вашем IDE в перечень исходного кода проекта. Или его можно использовать как библиотеку, скомпилировав в .class или .jar.
Прежде чем двигаться дальше, нам понадобится простой Java-класс для демонстрации Protocol Buffers. Для этого я буду использовать класс Album, который приведен ниже (код на GitHub).
Album.java
Теперь у нас есть data-класс Album , Protocol Buffers-класс, представляющий этот Album ( AlbumProtos.java ) и мы готовы написать Java-приложение для «сериализации» информации об Album без использования Java-сериализации. Код приложения находится в классе AlbumDemo , полный код которого доступен на GitHub.
Создадим экземпляр Album с помощью следующего кода:
Класс AlbumProtos , сгенерированный Protocol Buffers, включает в себя вложенный класс AlbumProtos.Album , который используется для бинарной сериализации Album. Следующий листинг демонстрирует, как это делается.
final Album album = instance.generateAlbum(); final AlbumProtos.Album albumMessage = AlbumProtos.Album.newBuilder() .setTitle(album.getTitle()) .addAllArtist(album.getArtists()) .setReleaseYear(album.getReleaseYear()) .addAllSongTitle(album.getSongsTitles()) .build();
Как видно из предыдущего примера, для заполнения иммутабельного экземпляра класса, сгенерированного Protocol Buffers, используется паттерн Строитель (Builder). Через ссылку экземпляр этого класса теперь можно легко преобразовать объект в бинарный вид Protocol Buffers, используя метод toByteArray() , как показано в следующем листинге:
final byte[] binaryAlbum = albumMessage.toByteArray();
Чтение массива byte[] обратно в экземпляр Album может быть выполнено следующим образом:
Как вы заметили, при вызове статического метода parseFrom(byte []) может быть брошено проверяемое исключение InvalidProtocolBufferException . Для получения «десериализованного» экземпляра сгенерированного класса, по сути, нужна только одна строка, а остальной код — это создание исходного класса Album из полученных данных.
Демонстрационный класс включает в себя две строки, которые выводят содержимое исходного экземпляра Album и экземпляра, полученного из бинарного представления. В них есть вызов метода System.identityHashCode() на обоих экземплярах, чтобы показать, что это разные объекты даже при совпадении их содержимого. Если этот код выполнить с примером Album , приведенным выше, то результат будет следующим:
BEFORE Album (1323165413): ‘Songs from the Big Chair’ (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen] AFTER Album (1880587981): ‘Songs from the Big Chair’ (1985) by [Tears For Fears] features songs [Shout, The Working Hour, Everybody Wants to Rule the World, Mothers Talk, I Believe, Broken, Head Over Heels, Listen]
Здесь мы видим, что в обоих экземплярах соответствующие поля одинаковы и эти два экземпляра действительно разные. При использовании Protocol Buffers, действительно, нужно сделать немного больше работы, чем при «почти автоматическом» механизме сериализации Java, когда надо просто наследоваться от интерфейса Serializable, но есть важные преимущества, которые оправдывают затраты. В третьем издании книги Effective Java («Java: эффективное программирование») Джошуа Блох обсуждает уязвимости безопасности, связанные со стандартной десериализацией в Java, и утверждает, что «Нет никаких оснований для использования сериализации Java в любой новой системе, которую вы пишете».
Источник: habr.com
Создание сообщений protobuf для приложений .NET
gRPC использует protobuf в качестве языка определения интерфейса (IDL). Protobuf IDL — это не зависящий от языка формат для указания сообщений, отправляемых и получаемых службами gRPC. Сообщения protobuf определяются в файлах .proto . В этом документе объясняется, как концепции protobuf соотносятся с .NET.
Сообщения Protobuf
Сообщения — это основной объект для обмена данными в protobuf. Они концептуально схожи с классами .NET.
syntax = «proto3»; option csharp_namespace = «Contoso.Messages»; message Person
В предыдущем определении сообщения указывается три поля в качестве пар «имя — значение». Как и свойства типов .NET, каждое поле имеет имя и тип. Типом поля может быть скалярный тип значения protobuf, например int32 , или другое сообщение.
В инструкции по стилю protobuf underscore_separated_names рекомендуется использовать для имен полей. Новые сообщения protobuf, созданные для приложений .NET, должны следовать рекомендациям по стилю protobuf. Инструментарий .NET автоматически создает типы .NET, использующие стандарты именования .NET. Например, поле protobuf first_name формирует свойство .NET FirstName .
Помимо имени, каждое поле в определении сообщения имеет уникальный номер. Номера полей используются для задания полей при сериализации сообщения в protobuf. Сериализация небольшого числа выполняется быстрее, чем сериализация всего имени поля. Поскольку номера полей указывают на поле, важно соблюдать осторожность при их изменении. Дополнительные сведения об изменении сообщений protobuf см. в статье Управление версиями gRPC Services.
При построении приложения средства protobuf создают типы .NET из файлов .proto . Сообщение Person создает класс .NET:
public class Person < public int Id < get; set; >public string FirstName < get; set; >public string LastName < get; set; >>
Дополнительные сведения о сообщениях protobuf см. в разделе Руководство по языку protobuf.
Скалярные типы значений
Protobuf поддерживает ряд собственных скалярных типов значений. В следующей таблице перечислены все типы с эквивалентными им типами в C#.
Тип protobuf Тип C#double | double |
float | float |
int32 | int |
int64 | long |
uint32 | uint |
uint64 | ulong |
sint32 | int |
sint64 | long |
fixed32 | uint |
fixed64 | ulong |
sfixed32 | int |
sfixed64 | long |
bool | bool |
string | string |
bytes | ByteString |
Скалярные значения всегда имеют значение по умолчанию. Им невозможно задать значение null . Это ограничение действует для string и ByteString , которые являются классами C#. Значением по умолчанию string является пустое строковое значение, а для ByteString по умолчанию используется пустое байтовое значение. При попытке задать для них значение null возникает ошибка.
Типы оболочек, допускающие значение NULL, могут использоваться для поддержки значений NULL.
Даты и время
Собственные скалярные типы не предоставляют значения даты и времени, что эквивалентно DateTimeOffset, DateTime и TimeSpan в .NET. Эти типы можно задавать с помощью некоторых из расширений хорошо известных типов Protobuf. Эти расширения обеспечивают поддержку создания кода и среды выполнения для сложных типов полей в поддерживаемых платформах.
В следующей таблице показаны типы даты и времени.
Тип .NET Хорошо известный тип ProtobufDateTimeOffset | google.protobuf.Timestamp |
DateTime | google.protobuf.Timestamp |
TimeSpan | google.protobuf.Duration |
syntax = «proto3»; import «google/protobuf/duration.proto»; import «google/protobuf/timestamp.proto»; message Meeting
Созданные свойства в классе C# не являются типами даты и времени .NET. Свойства используют классы Timestamp и Duration в пространстве имен Google.Protobuf.WellKnownTypes . Эти классы предоставляют методы для преобразования в DateTimeOffset , DateTime и TimeSpan .
// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan. var meeting = new Meeting < Time = Timestamp.FromDateTimeOffset(meetingTime), // also FromDateTime() Duration = Duration.FromTimeSpan(meetingLength) >; // Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan. var time = meeting.Time.ToDateTimeOffset(); var duration = meeting.Duration?.ToTimeSpan();
Тип Timestamp работает с временем в формате UTC. Значения DateTimeOffset всегда имеют нулевое смещение, а свойство DateTime.Kind всегда имеет значение DateTimeKind.Utc .
Типы, допускающие значение NULL
При создании кода protobuf для C# используются собственные типы, например int для int32 . Поэтому значения всегда включаются и не могут быть null .
Для значений, требующих явного задания null , например при использовании int? в коде C#, хорошо известные типы Protobuf включают оболочки, которые компилируются в типы C#, допускающие значение NULL. Чтобы использовать их, импортируйте wrappers.proto в файл .proto , как в следующем коде.
syntax = «proto3»; import «google/protobuf/wrappers.proto»; message Person < // . google.protobuf.Int32Value age = 5; >
Типы wrappers.proto не представлены в созданных свойствах. Protobuf автоматически сопоставляет их с соответствующими типами .NET, допускающими значение NULL, в сообщениях C#. Например, поле google.protobuf.Int32Value создает свойство int? . Свойства ссылочного типа, такие как string и ByteString , не изменяются, за исключением того, что значение null может быть назначено им без ошибок.
В следующей таблице приведен полный список типов оболочек с эквивалентным им типом C#.
Тип C# Оболочка хорошо известного типаbool? | google.protobuf.BoolValue |
double? | google.protobuf.DoubleValue |
float? | google.protobuf.FloatValue |
int? | google.protobuf.Int32Value |
long? | google.protobuf.Int64Value |
uint? | google.protobuf.UInt32Value |
ulong? | google.protobuf.UInt64Value |
string | google.protobuf.StringValue |
ByteString | google.protobuf.BytesValue |
Байты
Двоичные полезные данные поддерживаются в Protobuf со скалярным типом значений bytes . Созданное свойство в C# использует ByteString как тип свойства.
Используйте ByteString.CopyFrom(byte[] data) для создания экземпляра из массива байтов:
var data = await File.ReadAllBytesAsync(path); var payload = new PayloadResponse(); payload.Data = ByteString.CopyFrom(data);
Доступ к данным ByteString осуществляется напрямую с помощью ByteString.Span или ByteString.Memory . Или вызовите ByteString.ToByteArray() , чтобы преобразовать экземпляр обратно в массив байтов:
var payload = await client.GetPayload(new PayloadRequest()); await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
Десятичные знаки
Protobuf изначально не поддерживает тип .NET decimal , просто double и float . В проекте Protobuf ведется обсуждение возможности добавления к хорошо известным типам стандартного десятичного типа с платформенной поддержкой языков и инфраструктур, поддерживающих эту возможность. Однако к настоящему моменту еще ничего не реализовано.
Можно создать определение сообщения, представляющее тип decimal , которое подходит для безопасной сериализации между клиентами и серверами .NET. Однако разработчикам на других платформах следует понимать, какой формат используется, и реализовать свою собственную обработку.
Создание настраиваемого десятичного типа для protobuf
package CustomTypes; // Example: 12345.6789 -> < units = 12345, nanos = 678900000 >message DecimalValue < // Whole units part of the amount int64 units = 1; // Nano units of the amount (10^-9) // Must be same sign as units sfixed32 nanos = 2; >
Поле nanos представляет значения из 0.999_999_999 в -0.999_999_999 . Например, значение decimal 1.5m будет представлено как < units = 1, nanos = 500_000_000 >. Именно поэтому в поле nanos в этом примере используется тип sfixed32 , который более эффективно кодируется, чем int32 для больших значений. Если поле units имеет отрицательное значение, поле nanos также должно быть отрицательным.
Для кодирования значений decimal в виде байтовых строк доступны дополнительные алгоритмы. Алгоритм, используемый DecimalValue :
- несложный в эксплуатации;
- не затрагивается обратным порядком байтов или прямым порядком байтов в разных платформах;
- поддерживает десятичные числа в диапазоне от положительного 9,223,372,036,854,775,807.999999999 до отрицательного 9,223,372,036,854,775,808.999999999 с максимальной точностью до девяти десятичных разрядов, что не является полным диапазоном для decimal .
Преобразование между этим типом и типом BCL decimal может быть реализовано в C# следующим образом:
namespace CustomTypes < public partial class DecimalValue < private const decimal NanoFactor = 1_000_000_000; public DecimalValue(long units, int nanos) < Units = units; Nanos = nanos; >public static implicit operator decimal(CustomTypes.DecimalValue grpcDecimal) < return grpcDecimal.Units + grpcDecimal.Nanos / NanoFactor; >public static implicit operator CustomTypes.DecimalValue(decimal value) < var units = decimal.ToInt64(value); var nanos = decimal.ToInt32((value — units) * NanoFactor); return new CustomTypes.DecimalValue(units, nanos); >> >
- Добавляет разделяемый класс для DecimalValue . Разделяемый класс объединяется с DecimalValue , полученным из файла .proto . Созданный класс объявляет свойства Units и Nanos .
- Содержит неявные операторы для преобразования между типами DecimalValue и BCL decimal .
Коллекции
Списки
Списки в protobuf указываются с помощью ключевого слова префикса repeated в поле. В следующем примере показано, как создать список.
message Person < // . repeated string roles = 8; >
В созданном коде поля repeated представлены универсальным типом Google.Protobuf.Collections.RepeatedField .
public class Person < // . public RepeatedFieldRoles < get; >>
var person = new Person(); // Add one item. person.Roles.Add(«user»); // Add all items from another collection. var roles = new [] < «admin», «manager» >; person.Roles.Add(roles);
Словари
message Person < // . mapattributes = 9; >
var person = new Person(); // Add one item. person.Attributes[«created_by»] = «James»; // Add all items from another collection. var attributes = new Dictionary < [«last_modified»] = DateTime.UtcNow.ToString() >; person.Attributes.Add(attributes);
Неструктурированные и условные сообщения
Protobuf — это формат обмена сообщениями на основе контракта. Сообщения приложения, включая поля и типы, должны быть указаны в файлах .proto при сборке приложения. Проектирование на основе контракта Protobuf отлично подходит для обеспечения содержимого сообщения, но может ограничивать сценарии, в которых не требуется строгий контракт:
- Сообщения с неизвестными полезными данными. Например, сообщение с полем, которое может содержать любое сообщение.
- Условные сообщения. Например, служба gRPC может возвращать сообщение об успехе или ошибке.
- Динамические значения. Например, сообщение с полем, которое содержит неструктурированную коллекцию значений, аналогично формату JSON.
Для поддержки этих сценариев Protobuf предлагает языковые функции и типы.
Любой
Тип Any позволяет использовать сообщения как внедренные типы без определения .proto . Чтобы использовать тип Any , импортируйте any.proto .
import «google/protobuf/any.proto»; message Status
// Create a status with a Person message set to detail. var status = new ErrorStatus(); status.Detail = Any.Pack(new Person < FirstName = «James» >); // Read Person message from detail. if (status.Detail.Is(Person.Descriptor)) < var person = status.Detail.Unpack(); // . >
Oneof
Поля oneof — это функция языка. Компилятор обрабатывает ключевое слово oneof при формировании класса сообщения.
Использование oneof для указания ответного сообщения, которое может возвращать Person или Error , может выглядеть следующим образом.
message Person < // . >message Error < // . >message ResponseMessage < oneof result < Error error = 1; Person person = 2; >>
Поля в наборе oneof должны иметь уникальные номера полей в общем объявлении сообщения.
При использовании oneof созданный код C# включает перечисление, указывающее, какое из полей было задано. Можно проверить перечисление, чтобы узнать, какое поле задается. Поля, которые не заданы, возвращают null или значение по умолчанию вместо создания исключения.
var response = await client.GetPersonAsync(new RequestMessage()); switch (response.ResultCase)
Значение
Тип Value представляет динамически типизированное значение. Это может быть либо null , число, строка, логическое значение, словарь значений ( Struct ), либо список значений ( ValueList ). Value — это хорошо известный тип Protobuf, использующий описанную выше функцию oneof . Чтобы использовать тип Value , импортируйте struct.proto .
import «google/protobuf/struct.proto»; message Status < // . google.protobuf.Value data = 3; >
// Create dynamic values. var status = new Status(); status.Data = Value.ForStruct(new Struct < Fields = < [«enabled»] = Value.ForBool(true), [«metadata»] = Value.ForList( Value.ForString(«value1»), Value.ForString(«value2»)) >>); // Read dynamic values. switch (status.Data.KindCase) < case Value.KindOneofCase.StructValue: foreach (var field in status.Data.StructValue.Fields) < // Read struct fields. >break; // . >
Использование Value напрямую может быть подробным.
Альтернативный способ использования Value — встроенная поддержка Protobuf для сопоставления сообщений с JSON. Типы JsonFormatter и JsonWriter protobuf можно использовать с любым сообщением protobuf. Value особенно хорошо подходит для преобразования в формат JSON и обратно.
Вот эквивалент предыдущего кода в формате JSON:
Дополнительные ресурсы
- Руководство по языку protobuf
- Управление версиями gRPC Services
Источник: learn.microsoft.com