Разработка игр-Дизайн протокола-protobuf

игра protobuf
Разработка игр-Дизайн протокола-protobuf

Эта статья является второй статьей в серии о разработке игр. Список других статей выглядит следующим образом:

Разработка игр — дизайн протокола

Разработка игр - Протокол - 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---------------------------------------------------

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