Идея проекта превратилась во что-то, но как мне это удалось? Ну, я, конечно, расскажу! Как работает вся эта коммуникационная штука и насколько умно это было сделано.

Раньше, когда кофе был в моде

Броское название, все о последней статье о создании сервера Minecraft в Go. Итак, я говорил об общих чертах проекта, но не было никакой подробной информации о том, как этот пакет выглядит, так что без лишних слов представляю Вам, как это выглядит и как я его закодировал.

Держи лошадей

Эти два изображения взяты с https://wiki.vg/Protocol, где авторы статей проделали всю работу по обратной инженерии связи и протокола Minecraft. Основываясь на информации, которую я там нашел, я начал развивать этот проект. Для ясности: доступные там данные часто обновляются, поэтому последняя версия протокола описана для Minecraft 1.17.10.

Формат пакета очень прост, и в моем проекте я сейчас поддерживаю только несжатую версию. Формат пакета состоит из:

  • Длина (длина идентификатора пакета и данных означает, что это поле длины не включено в эту информацию)
  • ID пакета (идентификация пакета)
  • Данные (данные, которые передаются с этим пакетом)

Идентификатор пакета почти всегда можно идентифицировать (что означает, что для одной конкретной вещи существует только один идентификатор пакета). Почему почти? Что ж, некоторые пакеты идентифицируются по содержимому данных (подробнее об этом позже).

Первое, что меня смутило, - это параметр VarInt в столбце "Тип поля". Я думал, что это за штука VarInt, никогда раньше с ней не встречался. Короче говоря, это широко используемый числовой формат, благодаря которому меньшие числа занимают меньше места. Более подробное объяснение выглядит так:

Формат переменной длины, при котором меньшие числа используют меньше байтов. Они очень похожи на Protocol Buffer Varints: 7 младших битов используются для кодирования значения, а самый старший бит указывает, есть ли после него еще один байт для следующей части числа. Первой записывается наименее значимая группа, за которой следует каждая из наиболее значимых групп; таким образом, VarInts фактически являются прямым порядком байтов (однако группы состоят из 7 бит, а не из 8).

Интересный факт: этот формат VarInt используется в протоколе обмена данными с буфером, но он немного отличается, и в golang также есть пакет, называемый двоичным, где есть этот VarInt, но, как я уже писал, она немного отличается, поэтому я ее не использовал. (Ну, я использовал, но долгое время у меня были ошибки, и я не знал почему, потому что у меня не было информации о разнице между VarInt Minecraft и VarInt двоичного файла)

Как это работает?

0, подчеркнутый красным, является самым старшим битом в байте и используется в основном для обозначения того, что в следующем байте также есть закодированная часть того же числа. Таким образом, в основном, пока на первом месте слева стоит 0, мы можем быть уверены, что следующий байт - это совсем другое, и нам не нужно сейчас об этом думать 😃.

Я создал пакет mbinary, который можно использовать рядом с двоичным пакетом go по умолчанию, а также создал функцию, которая вызывается так же, как и в стандартной.

По сути, эта функция делает то, что было описано выше. Но я постараюсь объяснить вам несколько вещей:

bitOffset с проверкой на 35 - максимальная длина VarInt составляет 5 байтов, но, как было сказано: мы заботимся только о 7 битах, а 8-й - это просто часть информации, если есть другой байт код с цифрой или нет.

Значение - каждый раз, когда мы добавляем его с текущим байтом в логическом И с 0x7F (0b111 111), а затем перемещаем его на текущее смещение.

Проверьте наличие логического И между currentByte и 0x80 - в основном мы проверяем, есть ли 0 в первую очередь (вместо наиболее значимых немного)

Первая строка - это просто какое-то случайное число, которое закодировано в байте. Как вы можете видеть, число имеет 0 как самый старший бит, что означает, что это число просто закодировано внутри одного байта. Вторая строка - это байт 0x80, который в двоичном виде представлен как на изображении. Затем мы просто выполняем логическое И между ними обоими, и если результат 0x80, это означает, что число также закодировано внутри следующего байта, если нет, мы можем вернуть значение из функции.

В этом примере у нас есть номер с единицей на первом месте слева. Да! Это число также закодировано в следующем байте, поэтому мы перейдем к следующему циклу.

Сэр, чай был хорош?

В этой статье я попытался показать вам, как кодируются varInts (var longs кодируются одинаково, меняется только максимальное значение bitOffset).

Как всегда, приглашаю вас в репозиторий проекта посмотреть, как дела идут!



Увидимся в следующий раз! ❤