Эта статья является второй статьей в серии о разработке игр. Список других статей выглядит следующим образом:
Разработка игр — дизайн протокола
Разработка игр - Протокол - protobuf
Подробное объяснение принципа разработки игр-протокола-protobuf
WHAT
Введение
Давайте посмотрим, что говорит официальная документация:
Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.
Буферы протоколов — это межъязыковой, кроссплатформенный и расширяемый формат для сериализации структурированных данных.
Проще говоря, Protocol Buffers — это формат структурированных данных, определенный Google для сериализации и десериализации данных. Поскольку он напрямую работает с двоичными исходными данными, он достаточно мал, быстр и прост по сравнению с xml и не имеет ничего общего с языком и платформой, поэтому совместимость также имеет хорошую производительность. В настоящее время он очень подходит для хранения данных или передачи данных между сетевыми коммуникациями.
В настоящее время поддерживается целых 10 языков разработки, включая C++, Java, Python, Objective-C, C#, JavaNano, JavaScript, Ruby, Go и PHP, В основном поддерживаются все основные языки. Конечно, есть и неофициальные поддерживаемые языки (такие как Lua. В частности, добавлена библиотека для синтаксического анализа. Если у вас есть особые потребности, вы можете обратиться к официальной документации, чтобы написать ее самостоятельно. В настоящее время поддерживаются следующие языки (с исходным адресом):
Language | Source |
---|---|
C++ (include C++ runtime and protoc) | src |
Java | java |
Python | python |
Objective-C | objectivec |
C# | csharp |
JavaNano | javanano |
JavaScript | js |
Ruby | ruby |
Go | golang/protobuf |
PHP |
Как производительность:
Официальное представление его производительности достаточно сильное, насколько оно хорошее? Давайте посмотрим на сравнение тестов производительности.
Вышеприведенное основано на сериализаторах Full Object Graph, включая весь процесс создания объекта, его сериализации в последовательность байтов в памяти и последующей его десериализации. На рис. 1 показаны общие затраты времени (сериализация + десериализация), а на рис. 2 — сжатый размер. Мы видим, что protocolBuffer имеет очевидные преимущества как в скорости сериализации, так и в размере данных. Конкретные данные испытанийкликните сюда.
HOW
Как им пользоваться, официальныйguideОн был представлен подробно.Мы разбираем пакет на основе официальной демонстрации, чтобы понять его процесс сериализации и исходную структуру, чтобы иметь общее представление о всем механизме (следующий язык основан на java).
demo
В этой демонстрации предполагается, что у вас уже есть компилятор для текущей платформы (.proto Компилятор, который генерирует код целевого языка.), если нет, обратитесь к официальному сайту для компиляцииC++ runtime and protoc, Если платформа окна, вы также можете нажатьздесьЗагрузите один, не компилируя его самостоятельно.
шаг 1: представить maven
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.2.0</version>
</dependency>
Шаг 2: Определите файл .proto
syntax = "proto3";
package msg;
option java_package = "com.example.msg";
option java_outer_classname = "LoginMsg";
message Login {
string useranme = 1;
int32 pw=2;
}
Поддерживаемые типы данных:
СмотретьОфициальный сайт
шаг 3: производственный код компилятора
//--java_out是目标语言代码目录 紧跟着空格之后是.proto文件目录,生成多个可用-I
protoc --java_out=java resources/protoc/login.proto
Окончательные сгенерированные файлы и каталоги:
Reader&Writer
Приведенный выше файл LoginMsg.java, сгенерированный определением .proto, интегрировал код сериализации и десериализации LoginMsg.Нам нужно только управлять классом для чтения и записи сообщения входа. Например, если вы хотите записать loginMsg в поток и отправить его, вам нужно только присвоить значение loginMsg, а затем записать, и объект будет сериализован как двоичные данные для записи, или когда принимающая сторона читает LoginMsg, он вызовет свой ParserbyReader, который может быть реверсирован на основе двоичного потока Сериализован в объект LoginMsg.
Write:
public void write() throws Exception{
//构建Login消息对象
LoginMsg.Login.Builder builder = LoginMsg.Login.newBuilder();
builder.setUseranme("wier");
builder.setPwd(111);
//序列化并写出到磁盘
FileOutputStream output = new FileOutputStream("/Users/wier/login_msg");
builder.build().writeTo(output);
output.close();
}
Read
public void read() throws Exception{
FileInputStream inputStream = new FileInputStream("/Users/wier/login_msg");
LoginMsg.Login login = LoginMsg.Login.parseFrom(inputStream);
System.out.print("login.username:"+login.getUseranme());
System.out.print("login.pwd:"+login.getPwd());
}
Мы видим, что приведенный выше код очень прост для чтения и записи сообщений, вам нужно только преобразовать приведенный выше поток в сокет для передачи сообщений на основе tcp.
Структура класса сообщений
На основе LoginMsg рассмотрим основную информацию, содержащуюся во всем объекте сообщения.
Класс сообщения в основном содержит следующую информацию:
Основная часть объекта структуры сообщения входа в систему в основном хранит данные и наследует GeneratedMessageV3, сериализацию и десериализацию внутренних объектов инкапсуляции, сериализацию writeTo и десериализацию paser.
LoginOrBuilder используется для подключения Login и Builder, предоставления информации о типе и метода получения внешнего поля.
Построитель объекта сообщения Builder, который внешне инкапсулирует метод набора полей.
Дескриптор Информация описания метаданных объекта сообщения обычно не используется.Если у вас есть требования к динамическому анализу, вы можете использовать это для обработки.
Парсер Parser, обслуживающий номера десериализации сообщений
Давайте посмотрим на иерархию классов
Интерфейс MessageLite/Message является абстрактным интерфейсом всех сообщений.Сообщение может создавать объект из данных потока байтов на основе синтаксического анализатора или сериализовать объект, созданный Builder, и записывать данные потока байтов в конвейер ввода-вывода.Оба MessageLite и Message определены внутри.Он имеет свой собственный класс Builder, наследуется от MessageLiteOrBuilder и MessageOrBuiler и определяет общий интерфейс MessageLite/Message и соответствующих им классов Builder.
время вызова
write
В приведенном выше процессе записи мы видим, что инкапсуляция данных в основном обрабатывается сборкой, GeneratedMessageV3 инкапсулирует некоторые основные операции чтения полей, а окончательная запись полей в основном зависит от CodedOutputStream, который инкапсулируется CodedOutputStream (определение типов). Способ преобразования полей в двоичные, такие как int, String и т. д., вам нужно только перейти на основе определенных полей. OutputStreamEncoder является подклассом CodedOutputStream.
read
Процесс чтения также является процессом распаковки.Синтаксический анализатор в основном используется для управления синтаксическим анализом.Например, он может анализироваться на основе двоичных данных или операций ввода-вывода, или некоторые поля расширения могут вызывать предварительно зарегистрированный ExtensionRegister для самостоятельного определения синтаксического анализа. Окончательное чтение поля вызывает CodedInputStream для чтения.CodedInputStream, как и вышеупомянутый CodedOutputStream, также выполняет операции чтения на основе некоторых определенных полей и преобразует двоичные данные в указанные типы полей. Конструктор сообщения читается на основе CodedInputStream, а порядок чтения основан на теге. Последующее объяснение того, что такое конкретный тег каждого поля.
двоичная структура сообщения
В приведенном выше процессе чтения и записи мы видим, что при чтении каждого поля сообщения сначала вызывается readTag или writeTag, а затем, что делает этот тег, давайте сначала посмотрим на двоичную структуру сообщения.
Двоичный поток состоит из группы упорядоченных байтовых данных. Каждое поле на приведенном выше рисунке состоит из тега и значения. Тег равен описанию или определению информации о значении, которая сообщает синтаксическому анализатору, какой тип поля текущие поля: , и порядок чтения.С помощью этой информации синтаксический анализатор знает начальную и конечную позиции поля в потоке, поэтому поле успешно декодируется, независимо от порядка полей.
Состав тега:
(fieldNumber << 3) | wireType;
Зачем нужен fieldNumber? Во-первых, он может сообщить синтаксическому анализатору порядок, в котором текущее поле анализируется в потоке байтов, а также может расширить протокол. Например, в сообщении протокола, которое вы использовали, вам нужно добавить поле или изменить поле. Поле может быть fieldNumber+1, поэтому даже если это одно и то же сообщение, независимо от того, обновляет ли клиент протокол (например, по-прежнему использует старое сообщение), это все равно не влияет на синтаксический анализ на серверная сторона. Такой механизм гарантирует, что даже если в сообщение будет добавлено новое поле, это не повлияет на нормальную работу старой программы кодирования/декодирования.
Descriptor
Дескриптор — это информация описания метаданных объекта сообщения.Когда компиляторы генерируют класс объекта сообщения, для каждого сообщения будет определено статическое поле Descriptor, а также будет определено статическое поле FieldAccessorTable для использования отражения для чтения/установки поля. стоимость.
Конечно, они не используются в общей сериализации и десериализации, потому что порядок анализа и тип сообщения были созданы на основе файла конфигурации во время создания, и нет необходимости анализировать значение тега.
Если у вас есть требования к динамическому парсингу, например, при добавлении или обновлении Message, вам не нужно менять код, перезапускать процесс и автоматически создавать конкретный объект Protobuf Message на основе полученных данных и файла конфигурации, а затем десериализовать Это. Дескриптор очень поможет вам в это время. Давайте взглянем на структуру уровня класса в Descriptor.
В конце концов
extensions
Во время протокола 2 также поддерживалось определение поля расширения, которое использовалось для решения мультиплексирования сообщений через расширение.В настоящее время от него отказались в протоколе 3 и оно поддерживается Any.
Unknown Fields
Во время протокола 2, если есть неразборчивые поля (например, после обновления сообщения клиент использует старое сообщение для передачи), решение по умолчанию выглядит следующим образом:
default:
if (!parseUnknownField(input, unknownFields, extensionRegistry, tag)) {
done = true;
}
Теперь, когда протокол 3 обновил это решение, если вы столкнетесь с неопределенным полем, сразу пропустите поле.
default:
if (!input.skipField(tag)) {
done = true;
}
break;
В этом разделе рассказывается только о том, что такое буфер протокола и как его использовать, и не выясняется, почему протокол имеет преимущества небольшого размера, высокой скорости синтаксического анализа и совместимости.Если вас интересует эта часть, обратите внимание на В соответствующем тексте я попытаюсь разобраться в вопросе «почему».
---------------------------------------------------end---------------------------------------------------
Сканируйте, чтобы уделять больше внимания, уделяйте внимание личностному росту и техническому обучению и с нетерпением ждите, когда ваши собственные небольшие изменения принесут вам вдохновение и идеи.