Для определения файла proto, который определяет формат сервиса и сообщений, применяется специальный язык Protobuf. Protobuf имеет свою систему типов данных и опреления сообщений. Вкратце рассмотрим синтаксис файла proto и сопоставление его некоторых элементов с элементами языка C#.
Protobuf поддерживает использование многих стандартных примитивных типов, которые применяются в ряде наиболее популярных языков программирования:
Тип proto | Тип 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 (произвольная последовательность байтов длиной не более 232) | ByteString |
Типы int32 и int64 применяются преимущественно для положительных чисел, а для отрицательных лучше применять соответственно sint32 и sint64.
При парсинге сообщения если для какого-то поля сообщения не отсутствует значение, то это поле получает значение по умолчанию: для строк - пустая строка, для чисел - 0, для bool - false.
Для передачи данных в Protobuf используются сообщения. В языке C# им соответствуют классы. Определение сообщения начинается с ключевого слова message, за которым следует имя сообщения.
message имя_сообщения { }
Далее внутри фигурных скобок помещаются поля сообщения. Поля определяются в формате:
тип имя = значение;
Например, определим сообщение Person с двумя полями name и age:
message Person{ string name = 1; int32 age = 2; }
В качестве значения каждому полю передается уникальный номер для идентификации каждого поля при сериализации сообщения. Собственно поэтому каждое поле в рамках сообщения должно иметь уникальное числовое значение. Причем при использовании чисел от 1 до 15 в качестве значения в бинаром представлении в сообщение добавляется дополнительный байт. Значения от 16 до 2047 добавляют два дополнительных байта. Поэтому для наиболее часто используемых в сообщении данных лучше указывать значения от 1 до 15. Допустимые значения: от 1 до 536870911 (з исключением диапазона чисел от 19000 до 19999).
По этому сообщению в C# будет создаваться класс Person c двумя свойствами Age и Name. Если упрощенно, он будет соответствовать примерно следующему классу:
public class Person { public string Name { get; set; } public int Age { get; set; } public Person(string name, int age) { Name = name; Age = age; } }
В качестве типов поле сообщений также могут выступать не только примитивные типы Protobuf, но и другие сообщения.
Стоит отметить, что стайлгайд для Protobuf рекомендует использовать для именования полей так называемый "snake_case" (змеиный_регистр), при котором в составном имени составные части разделяются прочерком, например, "first_name". И Microsoft также рекомендует следовать этому стилю наименования, поскольку при генерации соответствующих типов C# будут автоматически применяться стандарты именования .NET. Например, для поля protobuf first_name в C# формируется свойство FirstName.
Для определения полей, которые допускают значение null, (например, для свойства с типом int? в коде C#), в Protobuf достпны типы-обертки:
Тип C# | Тип Protobuf |
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 |
Для их использования в файле .proto
необходимо импортировать файл wrappers.proto:
import "google/protobuf/wrappers.proto"; message Person{ google.protobuf.StringValue name = 1; google.protobuf.Int32Value age = 2; }
Сервис определяется с помощью ключевого слова service, после которого идет имя сервиса:
service Greeeter { }
Тело сервиса составляют функции, которые определяются с помощью ключевого слова rpc
rpc имя_функции (входящее_сообщение) returns (отправляемое_сообщение);
После rpc идет имя_функции, а затем в скобках тип сообщения, которое получает сервис. Далее после оператора returns - тип сообщения, которое возвращает сервис.
service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
Сервис может содержать множество функций, но стоит учитывать, что она обязательно должно принимать какое-то сообщение и возвращать какое-нибудь сообщения. Но при необходимости можно создавать также пустые сообщения
service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); rpc Test (VoidRequest) returns (TestResponse); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; } // пустое сообщение запроса message VoidRequest{ } message TestResponse{ string text = 1; }
Сервисы gRPC может определять различные типы методов. От типа методов зависит, как сервис будет получать и отправлять сообщения. Поддерживаются следующие типы методов:
Унарные (сервис отправляет и получает обычное сообщение)
Потоковая передача сервера (сервер получает обычное сообщение, а отправляет поток)
Потоковая передача клиента (сервер получает поток данных, а отправляет обычное сообщение
Двунаправленная потоковая передача (сервер получает поток данных и отправляет поток данных)
Потоки определяются с помощью ключевого слова stream, которое ставится перед названием сообщения:
syntax = "proto3"; service ExampleService { // унарный метод rpc UnaryCall (ExampleRequest) returns (ExampleResponse); // Потоковая передача сервера rpc StreamingFromServer (ExampleRequest) returns (stream ExampleResponse); // Потоковая передача клиента rpc StreamingFromClient (stream ExampleRequest) returns (ExampleResponse); // Двунаправленняя потоковая передача rpc StreamingBothWays (stream ExampleRequest) returns (stream ExampleResponse); }