Русский
Русский
English
Статистика
Реклама

Сериализация

Micro Property минималистичный сериализатор двоичных данных для embedded систем

12.09.2020 20:07:24 | Автор: admin
Micro Property библиотека для сериализации данных с минимальными накладными расходами. Она разработана для использования в микроконтроллерах и различных встраиваемых устройствах с ограничениями по размеру памяти, которым приходится работать по низкоскоростным линиям связи.

Конечно, я знаю про такие форматы как xml, json, bson, yaml, protobuf, Thrift, ASN.1. Даже нашел экзотический Tree, который сам является убийцей JSON, XML, YAML и иже с ними.

Так почему же они все не подошли? Зачем я был вынужден написать еще один сериализатор?

image


Исходные требования


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

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

Также, часть этих устройств подключены к общей линии связи CAN, которая и обеспечивает передачу данных в рамках всей системы в целом. Скорость передачи данных по линии связи Modbus до 115200 Бод, а скорость по шине CAN ограничена скоростью до 50кБод из-за её протяженности и присутствия серьезных индустриальных помех.

Устройства в подавляющем большинстве разработаны на микроконтроллерах серий STM32F1x и STM32F2x. Хотя часть из них работает и на STM32F4x. Ну и конечно, Windows/Linux на базе систем с x86 микропроцессорами в качестве контроллеров верхнего уровня.

Для оценки объема данных, которые обрабатываются и передаются между устройствами или хранятся в качестве настроек/параметров работы: В одном случае 2 числа по 1 байт и 6 чисел по 4 байта, в другом 11 чисел по 1 байту и 1 число 4 байта и т.д. Для справки, размер данных в стандартном кадре CAN до 8 байт, а во фрейме Modbus, до 252 байт полезных данных.

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

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

Поэтому, при переработке архитектуры пришлось отказаться от использования стандартных Modbus регистров. И даже не из-за того, что кроме этого протокола используются и другие линии связи, а сколько из-за чрезмерной ограниченности организации структур данных, основанных на 16-битных регистрах.

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

Конечно, можно передавать данные как двоичный блоб с привязкой к версии протокола и типу блока. И хотя на первый взгляд такая идея может показалось здравой, ведь фиксируя определенные требования к архитектуре, можно раз и навсегда определить форматы данных, чем существенно сэкономить на накладных расходах, которые будут неизбежны при использовании таких форматов, как XML или JSON.

Чтобы было удобнее сравнивать варинаты, для себя я составил такую табличку:
Передача двоичных данных без идентификации полей:
Плюсы:
  • Минимальные накладные расходы. В идеале их вообще быть не должно, но в реальности требуется указывать в каждом фрагменте версию протокола и тип передаваемых данных.

Минусы:
  • Требуется контролировать однозначную идентификацию блоков данных на более высоком уровне, в том числе и вручную организационными методами.
  • Необходимо анализировать и управлять версионностью данных. В противном случае, рано или обязательно получим несовместимость бинарных форматов.
  • Сложности с идентификацией одинаковых данных в разных бинарных пакетах. Например, дата и время в одном пакете передается в его начале, а во втором пакете в середине данных. И идентифицировать эти данные будет невозможно, если не знать формат каждого.
  • Сложности с передачей бинарных данные переменной длины, в том числе текстовых строк.


Передача двоичных данных с идентификацией полей:
Минусы:
  • Неизбежные накладные расходы для передачи имени и типа данных для каждого поля.

Плюсы:
  • Не требуется управлять версионностью на уровне бинарных данных. А в случае возникновения такой необходимости, номер версии можно передавать как одно и полей данных пакета.
  • Возможность сквозной идентификации данных в любом сообщении, что обеспечивает совместимость как в настоящий момент, так и в отдаленной перспективе.



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

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

image

А какие есть варианты?


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

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

Основные форматы xml, json, yaml и другие текстовые варианты с очень удобным и простым формальным синтаксисом, который хорошо подходит для обработки документов и одновременно удобен для чтения и редактирования человеком, пришлось сразу отбросить. И как раз из-за своего удобства и простоты они имеют очень большие накладные расходы при хранении бинарных данных, которые как раз и требовалось обрабатывать.

Поэтому, в виду ограниченности ресурсов и низкоскоростных линий связи, было решено использовать бинарный формат представления данных. Но и в случае форматов, умеющих преобразовывать данные в бинарное представление, таких как Protocol Buffers, Flat Buffers, ASN.1 или Apache Thrift, накладные расходы при сериализации данных, а так же общее удобство их применения не способствовало к немедленному внедрению любой из подобных библиотек.

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

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

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


Что получилось


В результате раздумий и нескольких экспериментов получился сериализатор со следующими особенностями и характеристиками:
  • Оверхед для данных фиксированного размера 1 байт (без учета длины имени поля данных).
  • Оверхед для данных переменного размера, таких как блоб, текстовая строка или массив 2 байта (так же без учета длины имени поля данных). Так как использовать данный формат предполагается в устройствах, работающих по протоколам CAN и Modbus, то для хранения размера можно ограничиться одним байтом.
  • Ограничения на размер имени поля 16 байт.
  • В качестве идентификатора поля используется текстовая строка с завершающим нулевым символом, которая обрабатывается как бинарные данные, т.е. без учета завершающего нуля. Вместо текстовой строки в качестве идентификаторов полей, можно использовать целые числа или произвольные бинарные данные размером до 16 байт.
  • Максимальный размер полей данных переменной длины (блоб, текстовая строка или массив) 252 байта (т.к. размеры полей хранятся в одном байте).
  • Общий размер сериализованных данных без ограничений.
  • При работе память не выделяется. Все действия происходят только с внешним буфером без внутреннего выделения и освобождения памяти.
  • Возможен режим работы только чтение, например для работы с настройками приложения, которые сохранены в программной памяти микроконтроллера. В том числе, корректно отрабатывается ситуация, когда данные размещены в очищенной флеш-памяти (заполненной 0xFF).
  • В режиме редактирования поддерживается только добавление новых данных до заполнения буфера. Возможность обновления полей штатным способом не реализована, потому что для изначальных задач подобный функционал не требовался. Хотя при необходимости есть возможность редактировать данные по указателю в буфере.
  • Ну а в случае крайней необходимости, можно будет попробовать добавить возможность обновления полей. Для этого даже оставлен в резерве один из типов.


Поддерживаемые типы данных:


  • Целые числа размером от 8 до 64 бит с преобразованием в сетевой порядок байт и обратно.
  • Логические значения и числа с плавающей запятой одинарной и двойной точности.
  • Двоичные данные переменной длины (блоб или массив байт).
  • Текстовые строки двоичные данные с завершающим нулевым символом в конце. При сериализации строк после данных записывается нулевой символ, чтобы потом было удобно с ними работать как с обычными строками, без необходимости копировать данные в промежуточный буфер или высчитывать количество символов в строке. Хотя есть возможность выстрелить себе в ногу и сохранить текстовую строку с нулевым символом в где нибудь в середине строки ;-)
  • Одномерные массивы для всех типов целых и вещественных чисел. При работе с массивами целых чисел, они автоматически преобразуются в сетевой порядок байт и обратно.


Хотелось бы отметить отдельно


Реализация сделана на С++ x11 в единственном заголовочном файле с использованием механизма шаблонов SFINAE (Substitution failure is not an error).

Поддерживается корректное чтение данных в буфер (переменную) бОльшего размера, чем сохраненный тип данных. Например, целое число 8 бит можно прочитать в переменную от 8 до 64 бит. Подумываю, может быть стоит добавить упаковку целых чисел, размер которых превышает 8 бит, на предмет возможности их передачи числом меньшего размера.

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

Сериализация структур или более сложных объектов даже не планировалась. Структуры вообще опасно передавать в бинарном виде из-за возможного выравнивания её полей. Но если эта проблема все же решается относительно простым способом, то все равно будет проблема конвертации в сетевой порядок байт и обратно всех полей объектов, содержащих целые числа.

Тем более, в случае крайней необходимости, структуры всегда можно сохранять и восстанавливать как массив байт. Естественно, в этом случае конвертацию целых чисел необходимо будет выполнять вручную.

Реализация


Реализация находится тут: https://github.com/rsashka/microprop

Как пользоваться, написано в примерах с различной степенью подробности:

Быстрое использование
#include "microprop.h"Microprop prop(buffer, sizeof (buffer));// Создать сериализатор и назначить ему буферprop.FieldExist(string || integer); // Проверить наличие поля с указанным IDprop.FieldType(string || integer); // Получить тип данных поляprop.Append(string || integer, value); // Добавить данныеprop.Read(string || integer, value); // Прочитать данные



Медленное и вдумчивое использование
#include "microprop.h"Microprop prop(buffer, sizeof (buffer)); // Создать сериализаторprop.AssignBuffer(buffer, sizeof (buffer)); // Назначить буферprop.AssignBuffer((const)buffer, sizeof (buffer)); // Назначить read only буферprop.AssignBuffer(buffer, sizeof (buffer), true); // Тоже read only буферprop.FieldNext(ptr); // Получить указатель на следующее полеprop.FieldName(string || integer, size_t *length = nullptr); // Указатель на ID поляprop.FieldDataSize(string || integer); // Размер сериализованных данных// Дальше все прозрачноprop.Append(string || blob || integer, value || array);prop.Read(string || blob || integer, value || array);prop.Append(string || blob || integer, uint8_t *, size_t);prop.Read(string || blob || integer, uint8_t *, size_t);prop.AppendAsString(string || blob || integer, string);const char * ReadAsString(string || blob || integer);



Пример реализации с использованием enum в качестве идентификатора данных
class Property : public Microprop {public:    enum ID {    ID1, ID2, ID3  };  template <typename ... Types>  inline const uint8_t * FieldExist(ID id, Types ... arg) {    return Microprop::FieldExist((uint8_t) id, arg...);  }  template <typename ... Types>  inline size_t Append(ID id, Types ... arg) {    return Microprop::Append((uint8_t) id, arg...);  }  template <typename T>  inline size_t Read(ID id, T & val) {    return Microprop::Read((uint8_t) id, val);  }  inline size_t Read(ID id, uint8_t *data, size_t size) {    return Microprop::Read((uint8_t) id, data, size);  }      template <typename ... Types>  inline size_t AppendAsString(ID id, Types ... arg) {    return Microprop::AppendAsString((uint8_t) id, arg...);  }  template <typename ... Types>  inline const char * ReadAsString(ID id, Types... arg) {    return Microprop::ReadAsString((uint8_t) id, arg...);  }};



Код выложен под лицензией MIT, так что пользуйтесь на здоровье.
Буду рад любому фидбеку, в том числе и замечаниям и/или предложениям.
Подробнее..

Перевод Четыре приема быстрой разработки на Unity3D

08.12.2020 12:05:45 | Автор: admin

Больше гибкости, меньше кода продуктивнее разработка.

Уже долгое время Unity3D мой любимый инструмент разработки игр, которым я пользуюсь уже более 8лет и для профессиональных продуктов, и для личных проектов, и при обучении программированию и гейм-дизайну. Более того, я писал на Unity почти на всех гейм-джемах, в которых участвовал, благодаря чему получалось создавать основу игры всего за несколько часов.

Как вы, наверное, знаете, гейм-джем это конкурс разработчиков игр, участники которого делают игру с нуля за короткий период времени. Гейм-джем обычно идет от 24 до 72часов, но бывают и более длительные например, GitHub Game Off, который длится весь ноябрь.

После участия в различных гейм-джемах, в том числе и с самодельным движком моей группы на C++ (к сожалению, он только на португальском языке), я составил список правил быстрого прототипирования, которые вскоре превратились в мой основной принцип разработки ПО: меньше кода быстрее работа.

Основная идея писать меньше кода (или, иначе говоря, держать меньшую кодовую базу) решает две задачи:

  1. Защита от ошибок: чем меньше размер кода, тем меньше вероятность сделать ошибку.

  2. Экономия времени: при каждом изменении кода нужны обновления и тесты, что требует времени.

А для Unity есть и третья причина: каждое изменение в коде запускает обновление Unity, на что регулярно тратится некоторое количество времени.

В этой статье я расскажу о нескольких приемах, позволяющих легко реализовать этот принцип в Unity3D и таким образом ускорить прототипирование и создание игр в целом.

Для справки: Unity 3D Technologies мне ничего не платили (пока что).

1. Сериализация классов и структур

Сериализация это процесс автоматического преобразования структур данных или состояний объекта в другой формат. В случае Unity это упрощает хранение и реконструкцию данных.

Класс и структуру можно пометить как сериализуемые указав [Serializable] над именем. Ниже пример из документации Unity:

[Serializable]public struct PlayerStats{   public int movementSpeed;   public int hitPoints;   public bool hasHealthPotion;}

Главное преимущество этого подхода в том, что он дает прямой доступ к соответствующим свойствам через инспектор, что особенно удобно при использовании списков и массивов.

Список характеристик игрока в инспекторе свойств UnityСписок характеристик игрока в инспекторе свойств Unity

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

То же можно сделать для перечислений (их использование повышает безопасность типов) и более сложных конструкций например, спрайтов. Оба случая приведены на изображении и в коде ниже:

Список характеристик игрока со спрайтами и перечислениями в инспекторе UnityСписок характеристик игрока со спрайтами и перечислениями в инспекторе Unity
public enum PlayerType{    ARCHER, KNIGHT}[Serializable]public struct PlayerStats{    public int movementSpeed;    public int hitPoints;    public bool hasHealthPotion;    public Sprite face;    public PlayerType type;}

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

2. По возможности используйте RequireComponent

Сценарии с зависимостями от компонентов обычное дело. Например, сценарий контроллера игрока, скорее всего, будет зависеть от Rigidbody и коллайдеров игрока. Самый безопасный способ обеспечить наличие всех зависимостей у игрового объекта во время выполнения сценария это пометить его атрибутом RequireComponent.

У этого атрибута три основных функции:

  1. Обеспечить наличие необходимых компонентов у игрового объекта.

  2. Заблокировать удаление необходимых компонентов.

  3. Автоматически добавлять необходимые компоненты к игровому объекту, когда к нему прикрепляется сценарий.

Пример кода с использованием RequireComponent показан ниже:

[RequireComponent(typeof(Rigidbody))]public class PlayerScript : MonoBehaviour{    Rigidbody rigidbody;    void Awake()    {        rigidbody = GetComponent<Rigidbody>();    }}

Эта функциональность значительно повышает безопасность кода и снижает вероятность непредвиденного исключения пустого указателя при попытке получить доступ к недействительным или несуществующим компонентам. Компоненты можно непосредственно прикрепить в окне редактора или получить их с помощью методов Awake или Start, как показано в примере кода выше.

С точки зрения прототипирования этот подход ускоряет подготовку объектов. Более того, у классов и структур может быть несколько атрибутов RequireComponent сразу. Достаточно указать их в сценарии, а затем просто добавить его в игровой объект и все компоненты будут прикреплены.

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

3. Кнопки интерфейса для нескольких событий

Это, пожалуй, самый простой и эффективный прием из рассматриваемых: использовать одну и ту же кнопку пользовательского интерфейса Unity для нескольких событий одновременно. Многие новички в Unity используют кнопки либо для выполнения одной задачи, либо, что еще хуже, для вызова определенного метода в сценарии, обрабатывающего логику кнопки.

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

Лучше всего перечислять все эффекты кнопки прямо в ее списке событий OnClick: в нем может быть столько событий, сколько нужно, и к ним легче получить доступ и изменить их.

С помощью этого подхода можно, например, использовать onClick одной кнопки для отображения панели Unity, воспроизведения звука и запуска анимации. Конкретно для этих задач дополнительный код не потребуется: отображаем панель ( setActive(true) ), воспроизводим звук ( play() ) и вызываем Animator ( setTrigger() ). Примеры вызова этих методов ниже.

Пример списка с событиями OnClickПример списка с событиями OnClick

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

4. Широкое применение событий Unity

В Unity есть специальный класс с именем UnityEvent, который ведет как себя метод OnClick кнопки интерфейса Unity. Переменная UnityEvent, доступ к которой есть у сценария, дает тот же интерфейс, что и метод OnClick:

Одна переменная UnityEvent с именем EventsToBeCalledОдна переменная UnityEvent с именем EventsToBeCalled

Такая переменная используется практически так же, за исключением того, что для выполнения списка событий переменной UnityEvent ее нужно вызывать через сценарий. Во фрагменте кода ниже показано, как добавить переменную UnityEvent в сценарий и как пишется простая функция вызова Invoke:

using UnityEngine;using UnityEngine.Events;public class CallEventsScript : MonoBehaviour{   public UnityEvent eventsToBeCalled;   public void CallEvents()   {      eventsToBeCalled.Invoke();   }}

Метод CallEvents вызывает список событий для переменной UnityEvent. Это общедоступный метод, поэтому к нему имеют доступ другие сценарии, в том числе сигналы временной шкалы и события анимации. Для этих двух случаев писать код для доступа к методу не нужно всё делается простым перетаскиванием.

Временная шкала анимации с добавленным событием, которое вызывает метод CallEventsВременная шкала анимации с добавленным событием, которое вызывает метод CallEvents

UnityEvent также может использоваться для создания очень гибких сценариев, например, для вызова списка событий в таких методах, как Awake, Start, OnEnable, OnDisable ит.д. Например, можно написать сценарий, который выполняет список событий в методе Start, это позволит быстро создать функции без необходимости писать код.

Пример из практики триггерный ящик, как я его называю: это игровой объект с коллайдером, который выполняет одно или несколько действий при столкновении с другими игровыми объектами. Его можно легко реализовать с помощью UnityEvent:

[RequireComponent(typeof(Collider))]public class TriggerBoxScript : MonoBehaviour{    public UnityEvent eventsToBeCalledOnCollision;    public List<string> objectsTagToActivate;    private void OnCollisionEnter(Collision other)    {        if (OtherHasWantedTag(other.gameObject))        {            InvokeEvents();        }    }    private void OnTriggerEnter(Collider other)    {        if (OtherHasWantedTag(other.gameObject))        {            InvokeEvents();        }    }    private bool OtherHasWantedTag(GameObject other)    {        var found = objectsTagToActivate.Find(other.CompareTag);        return found != null;    }    private void InvokeEvents()    {         eventsToBeCalledOnCollision.Invoke();       }}

Пример выше работает и для триггеров, и для коллайдеров без триггера (метод вызывается и через OnTriggerEnter, и через OnCollisionEnter). При этом его могут использовать только игровые объекты, у которых есть коллайдер.

Пример необходимых компонентов для триггерного ящикаПример необходимых компонентов для триггерного ящика

В примере выше показан игровой объект с триггерным ящиком, использующий этот подход. В этом примере всякий раз, когда игровой объект Player сталкивается с объектом триггерным ящиком, последний воспроизведет звук и деактивируется. Возможности такой структуры почти безграничны: активация врагов, изменение фоновой музыки, точки появления, точки сохранения ит.д.

Заключение

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

В случае событий (OnClick и UnityEvent) разработчику не нужно беспокоиться о настройке зависимостей объекта (методы объектов можно вызывать напрямую из списков) и проверке их действительности (в списке можно будет привязать только действительные существующие объекты).

Конечно, бывают случаи, в которых эти подходы лучше не использовать: они усложнят работу. Например, если одну и ту же серию задач необходимо выполнить для разных элементов, было бы разумно передать ее определенному методу и вызывать его, а не перечислять все задачи для каждого элемента. Это относится непосредственно ко кнопкам интерфейса Unity, для которых в таких ситуациях может быть удобно назначить конкретные методы.

В любом случае, гибкость описанных подходов наверняка перевесит их недостатки и, конечно же, их легко игнорировать или устранить. Более того, эти приемы позволяют быстро создать прототип и проверить идею, и только после этого приступать к написанию более стабильного и эффективного кода.

Благодарю завнимание :)

Побережье, коричневый песок. Альберт Эдельфельт (1935) [USEUM]Побережье, коричневый песок. Альберт Эдельфельт (1935) [USEUM]

О переводчике

Перевод статьи выполнен в Alconost.

Alconost занимается локализацией игр, приложений и сайтов на 70 языков. Переводчики-носители языка, лингвистическое тестирование, облачная платформа с API, непрерывная локализация, менеджеры проектов 24/7, любые форматы строковых ресурсов.

Мы также делаем рекламные и обучающие видеоролики для сайтов, продающие, имиджевые, рекламные, обучающие, тизеры, эксплейнеры, трейлеры для Google Play и App Store.

Подробнее..

Micro Property минималистичный сериализатор двоичных данных для embedded систем. Часть 2

06.01.2021 16:04:08 | Автор: admin
Некоторое время назад я опубликовал свою статью о разработке велосипедного велосипеда, в которой описал причины, побудившие меня этим заняться.

Если вкратце, то мне была нужна миниатюрная библиотека для микроконтроллеров с сериализатором двоичных данных для последующей передачи этих сообщений по низко скоростным линиям связи, тогда как обычные форматы xml, json, bson, yaml, protobuf, Thrift, ASN.1 и др. мне по разным причинам не подходили.

Как и ожидалось, решение оказалось более чем велосипедом, и тем не менее, сама публикация статьи на Хабре мне очень сильно помогла. Дело в том, что при первоначальном анализе возможных библиотек, я почему то упустил из вида сериализаторы MessagePack, CBOR и UBJSON.

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

После этого я прикрутил к библиотеке CBOR свой интерфейс (чтобы не перелопачивать исходники), и решил от этого формата отказаться в пользу MessagePack :-)




CBOR vs. MessagePack


На самом деле CBOR и MessagePack форматы используют один и тот же принцип сериализации данных. В их основе лежит практичный метод записи TLV, за тем лишь исключением, что в классическом виде TLV всегда содержит поля значения тега и размера значения. А вот последнее поле может отсутствовать, если его размер ноль.

А в этих сериализаторах разработчики пошли еще дальше, и сделали практически гениальные форматы, в которых наличие поля с размером данных зависит от типа данных, и не требуется для полей фиксированного размера, а в первом байте хранится одновременно и тип поля с размером данных и его непосредственным значением (конечно, если позволяет разрядность).

В исходной статье я писал про то, что мне требуется максимальная упаковка бинарных данных и оба эти два формата справляются с такой задачей на ура. Они между собой очень похожи, и отличаются только количеством бит, в котором сохранится значения типа поля.

В формате CBOR минимальные накладные расходы на хранение каждого поля три бита. т.е. в первом байте каждого поля первые три бита отвечают за тип содержимого, и в зависимости от него интерпретируют наличие и размер других полей, а оставшиеся 5 бит могут содержать уже само значение поля (0 до 31).

А вот в MessagePack пошли еще дальше! В этом формате минимальные накладные расходы на хранение значения составляют всего 1 (ОДИН!) бит информации. Соответственно и диапазон возможных значений для хранения с помощью одного байта значительно больше (0 до 127). А для указания дополнительной информации о типе поля используются значения с установленным старшим битом.

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

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

В результате, изначальный формат сериализатора удалось сделать еще компактнее, в том числе и за счет некоторых соглашений (например, структуру кодируемых данных ограничить только плоским списком и отказом от использования невостребованных типов), а мой сон стал спокойнее, т.к. уже не болит голова насчет совместимости на уровне форматов пересылаемых сообщений между устройствами.
Большое спасибо Хабра-юзерам Spym и edo1h, что ответили предыдущую публикацию и тем самым помогли найти решение действительно серьезной проблемы такими малыми усилиями!

Первоисточники:


Спецификация CBOR. Есть хорошая статья с описанием на Хабре.

Спецификация MessagePack очень легко читается в документации и не требует какого либо перевода или дополнительных пояснений.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru