[Из песочницы] Полноценная бинарная замена XML


В чем прелесть XML? Он реализован под все платформы, «человекочитаемый», для него созданы схемы данных (условно человекочитаемые). Открывая 25-мегабайтный файл в браузере сразу замечаешь недостатки этого текстового формата, и начинаешь задумываться. Делаем мы это, конечно, не часто, но все же — чем бы заменить XML?

Добавление самопальных бинарных контейнеров в проект заканчивается провалом, когда к вам приходят партнеры и просят подключить их к этому каналу данных. Google Protobuf поначалу выглядит хорошо, но вскоре понимаешь, что это не замена для XML, не хватает функциональности. BSON в 5 раз медленнее Protobuf, уступает в компактности и для него не реализованы схемы данных.

Разработаем же еще один бинарный формат.


USDS (или $S) — Universal serialized data structures — универсальные сериализованные структуры данных, бинарный формат, способный полностью заменить XML и JSON. Основные отличия:

  • Вместо текстовых тегов/ключей используются целые числа. Соотношение «Имя» — «Целочисленный идентификатор» задается отдельно, в «Словаре». Словарь может быть прикреплен к документу USDS или может быть передан отдельно.
  • Нет закрывающих тегов, как в XML;
  • Документы USDS формируются строго по схеме, которая также задается в Словаре. Поддерживаются полиморфизм и опциональные поля.
  • Числовые значения в документе USDS хранятся в бинарном виде (не как текст).


Допустим, мы задокументировали этот формат и создали первую версию библиотеки для работы с ним. Есть ли профит? Бенчмарк расставит все по своим местам:

image

image

Что-то в этом уже есть, хотя работы еще не мало: Basic Parser всегда будет уступать Google Protobuf, но не на столько же.


Хоть формат и бинарный, использовать его не сложнее, чем XML. Посмотрим, как это будет выглядеть на С++ (а в далеком светлом будущем и на других языках).

Шаг 1: составляем Словарь


Как было сказано выше, документ USDS строится только по схеме, которая может выглядеть так:

USDS DICTIONARY ID=1000000 v.1.0
{
    1: STRUCT internalObject
    {
        1: UNSIGNED VARINT varintField;
            1: UNSIGNED VARINT varintField;
            2: DOUBLE doubleField;
            3: STRING<UTF-8> stringField;
            4: BOOLEAN booleanField;
        } RESTRICT {notRoot;}

        2: STRUCT rootObject
        {
            1: INT intField;
            2: LONG longField;
            3: ARRAY<internalObject> arrayField;
        }
}


Все правила построения схемы можно посмотреть здесь. Библиотека USDS Basic Parser пока что поддерживает далеко не все элементы схемы, но пример выше — рабочий. Сохраняем схему в текстовый файл, или вставляем прямо в исходный код, что дальше?

Шаг 2: инициализируем парсер:


Так или иначе, схема данных оказалась в массиве «text_dictionary», скормим его парсеру:

BasicParser* clientParser = new BasicParser();
clientParser->addDictionaryFromText(text_dictionary, strlen(text_dictionary), USDS_UTF8);


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

Шаг 3: создаем бинарный документ:


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

UsdsStruct* tag = clientParser->addStructTag("rootObject");
tag->setFieldValue("intField", 1234);
tag->setFieldValue("longField", 5000000000);
...
BinaryOutput* usds_binary_doc = new BinaryOutput();
clientParser->encode(usds_binary_doc, true, true, true);

const unsigned char* binary_data = usds_binary_doc->getBinary();
size_t binary_size = usds_binary_doc->getSize(); 


Особенности работы с массивами опущены, вы можете посмотреть их отдельно, скачав исходный код примера.

Шаг 4: декодирование бинарного документа:


Для чистоты эксперимента создадим отдельный объект парсера, не будем его инициализировать словарем и посмотрим, разберет ли он наш бинарный документ:

BasicParser* serverParser = new BasicParser();
serverParser->decode(binary_data, binary_size);

int int_value = 0;
long long long_value = 0;
tag->getFieldValue("intField", &int_value);
std::cout << "\tintField = " << int_value << "\n";
tag->getFieldValue("longField", &long_value);
std::cout << "\tlongField = " << long_value << "\n";


Обратите внимание, что «Сервер» заранее ничего не знает о схеме данных, но спокойно получил бинарник, нашел в нем поля по их текстовым именам и корректно преобразовал их в значения переменных С++. Именно эта функция недоступна в Google Protobuf и ASN.1.

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


Это действительно очень важная функция: вы не можете прочитать посторонний бинарный пакет Google Protobuf или ASN.1 (кроме XER), а иногда очень хочется. При использовании BSON можно преобразовать любой пакет данных в JSON, что уже неплохо. Не отстает от него и USDS:

std::string json;
serverParser->getJSON(USDS_UTF8, &json);
std::cout << "JSON:\n" << json << "\n";


Сервер не только получил произвольный бинарный документ, но и смог преобразовать его в JSON. Ту же операцию можно было выполнить и на стороне «Клиента»: сформировать DOM-объект и сразу преобразовать его в JSON, который также строго соответствует схеме данных.

В планы разработки USDS заложен редактор документов USDS с полноценным GUI. В ближайшем будущем в USDS Basic Parser будет реализована конвертация между XML, JSON и USDS в любом направлении.


Зачем я опубликовал сырой продукт (Pre-Alpha), который настоятельно не рекомендуется использовать в проектах? Мне важен ваш отклик:

  • чего не хватает в продукте?
  • нужен ли он вообще?
  • понятно ли написана документация и исходный код?

Отвечу в комментариях на любые вопросы.

Источники:

Страница проекта: USDS 1.0
Скачать библиотеку и исходный код примера можно здесь.
Исходный код библиотеки доступен здесь.

© Habrahabr.ru