Назад Вверх Вперёд

Глава V. Структурированные форматные схемы.

В данной главе мы рассматриваем структурированные форматные схемы - ASN.1, SGML, XML, JSON, YAML и аналоги. Мы намеренно отказались от использования для их описания слова "формат", потому что мы не можем говорить про формат как таковой, но про общие правила составления таких форматов.

V.1. Общее.

Принципиальными отличиями рассматриваемых здесь форматных схем являются:

1. Иерархическая структурность построения. Каждый элемент данных (как элементарный, так и структурный) должен относиться к одному и только одному структурному элементу более высокого уровня (кроме случая, когда он единственный в посылке) и иметь в нём своё определённое место.

При этом, нет принципиального ограничения сложности структуры. Ничто не мешает ей иметь элементы даже на 300-м уровне вложенности, хотя на практике такое не встретишь, если нет ошибки программирования:)

2. Следствием предыдущего является то, что каждый элемент имеет чётко определённую роль. Невозможно строить, например, как в предшествующем примере в главе III - "если в поле X бит 0 равен 0, то в байтах 8-15 находится поле Y; иначе, в 8-11 находится Z+, а в 12-15 - Z-". Для структуризованных форматных схем, если даже используются биты значений, то Y будет в одном месте, а пара {Z+,Z-} в другом, но более вероятно, что непередаваемые данные будут пропущены при генерации посылки и этих полей просто не будет. (С другой стороны, никто не мешает ввести такую интерпретацию, как описана в примере, уже над форматом конкретного элемента посылки; но это будет примером плохого стиля реализации.)

3. Часто определяется внешняя спецификация структуры, которой должны следовать элементы всех уровней такой посылки. Для ASN.1 и XML они выглядят принципиально по-разному, но приводят к одному и тому же результату - спецификации структуры посылки, аналогичной грамматике - какие элементы могут присутствовать в каком другом элементе. Часто допустимо работать и без такой спецификации, но это считается "непромышленным" подходом, потому что отсутствуют средства надёжной верификации "синтаксиса" посылки, конверсии её в другие форматы, и т.д. В некоторых случаях (Packed Encoding Rules - PER) кодирование и декодирование без такой спецификации невозможно. Тем не менее, "обычное" взаимодействие с участием JSON и аналогов делается "ad hoc", без чётко определённой и проверяемой структуры.

4. Фактически в таких схемах можно говорить о двух "слоях", в каждом из которых свои определения лексики, синтаксиса и семантики. На нижнем уровне, лексическими элементами являются отдельные байты или даже биты, синтаксическими - тела элементарных посылок и указатели типов/размеров/etc.; семантика начинается с неструктурных элементов. На верхнем уровне, лексическими элементами являются неструктурные элементы, синтаксис определяет структуры согласно спецификации, а семантика начинается с понимания спецификации, кодирования и декодирования полных посылок.

5. Все структурные элементы должны быть явно закодированы. Исключаются подходы типа "со смещения 88 начинается AV-список"; вместо этого, должен присутствовать и быть явно закодированным элемент "AV-список параметров", внутри которого находятся параметры. (Но могут быть специфические оптимизации, которые нарушают это правило, при сохранении однозначности разбора.)

6. Размеры и расположение элементов нефиксированы. Нельзя предполагать, что конкретный элемент будет находиться в том месте, которое было увидено в каком-то случае; нельзя также предполагать его конкретный размер, за исключением особых случаев. Как следствие, у большинства подобных схем невозможно предполагать выравнивание элементов и наличие незаполненных полей, предназначенных для выравнивания; характерное исключение - формат посылок Linux netlink socket.

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

На этом заканчивается общее между разными схемами и начинаются различия.

V.2. XML.

В качестве первого важного примера рассмотрим XML. Не будем вдаваться в историю развития, детализировать апологетику и критику - этому посвящено достаточное количество ресурсов.

XML является доведенным до логического конца методом построения структурированного формата поверх общих принципов построения текстового формата. Посылка (называемая в XML документом) состоит из спецификации типа посылки (DTD) и корневого элемента (который должен быть единственным). Каждый элемент, от корневого до элементарных, синтаксически единообразно представляется своим названием и содержанием (хотя здесь есть широкие возможности исключения из этих принципов за счёт так называемых атрибутов элементов). Название элемента является именно его названием, а не определением типа (хотя можно так сделать, но бессмысленно). Содержание элементов и атрибутов должно быть представлено в текстовом виде или хотя бы закодировано для беспроблемного нахождения в тексте XML документа, для чего используется эскейпинг. Символами, которые обязательно эскейпятся в содержании элементов, являются '<', '>', '&' и управляющие символы ASCII, а в значениях атрибутов элементов - также применённые для этих атрибутов ограничители содержания (апострофы или кавычки). Альтернатива - элемент CDATA - имеет недопустимым элементом "]]". Кодировка текста, если явно не переопределена - Unicode в UTF-8 или UTF-16.

Например, структура клиента может выглядеть так:


<client>
 <id value="123456"/>
 <family-name>Пупкин</family-name>
 <given-name>Василий</given-name>
 <birth>1900-12-12</birth>
</client>
но может и так:

<client id="123456" family-name="Пупкин">
 <given-name value="Василий"/>
 <birth value="1900-12-12"/>
</client>

Принципиальным ограничением для выбора между элементом в составе структуры и атрибутом является возможность наличия более одного элемента с таким названием - базовый синтаксис XML запрещает наличие разных одноимённых атрибутов у одного элемента, но не возражает против одноимённых подэлементов. Однако, кроме этого, должны учитываться соображения совместимости, здравого смысла, POLA, эстетики и так далее. Выбор между значением одного элемента через атрибут или символьные данные (character data) решается аналогично через общие принципы и эффективность обработки (короткие данные можно представить атрибутом, но чем они длиннее, тем больше проблем с этим). Частое требование POLA - в атрибутах может передаваться только первичный ключ записи.

XML сам по себе не определяет, как именно будет храниться содержание элементов. Можно, например, передавать число в десятичном виде или шестнадцатиричном, записать цифры в обратном порядке или менять на буквы; передавать вместо текстового представления числа с плавающей точкой - байтовое представление в IEEE754, эскейпя через синтаксис entity непредставимые напрямую в тексте значения, и так далее. Однако, для XSLT такие нормировки уже есть, и их обычно принято распространять на все представления XML. Поэтому, для представления элементарных значений рекомендуем принять без изменений правила XSLT, где они есть, а в остальных случаях - принципы, описанные в главе IV ("Организация текстового протокола").

Вопрос избыточности представления данных в XML решается во многих случаях использованием дополнительного сжатия результата (например, через gzip).

V.3. ASN.1

ASN.1 (Abstract Syntax Notation number 1) - другой выдающийся пример структурной форматной схемы. Сама по себе ASN.1 достаточно сложно соотносится с другими форматами и форматными схемами; "чистая" ASN.1 это "общая теория всего" в представлении данных, при единственном однозначном требовании - структурности представления, и с набором элементарных типов данных в основе (хотя без ограничения ими). Реальное представление возникает при использовании конкретного набора правил кодирования (encoding rules). Набор стандартных типов данных и их правил представления в конкретном наборе правил кодирования выполняет две роли - собственно практическую и методологическую; последняя заключается в подаче хорошего примера представления данных. Например, рекомендация присутствия часового пояса (он же time zone) в типе данных GeneralizedTime является достаточным, чтобы задуматься о необходимости хранения и передачи такого параметра времени для устранения некорректных скрытых конверсий.

Самым тяжёлым в ASN.1 является необходимость использования, согласно рекомендованному пути реализации, описаний структуры данных, применённых типов данных, и т.д. согласно языку такого описания; этот язык тяжёл для понимания человека, имеет достаточно специфический нестандартный синтаксис, требует построения трансляторов для понимания компьютером. Многие реализации игнорируют это и пользуются более простыми средствами (особенно типично это для SNMP).

Практическое использование ASN.1 требует использования конкретного набора правил кодирования; на данный момент это как минимум следующие:

1. BER (Basic Encoding Rules) - двоичный формат представления, с побайтовой (пооктетной) подложкой представления, неоднозначный (много параметров, выбираемых реализацией).

Характерное приложение - SNMP.

2. CER (Canonic Encoding Rules) и DER (Distinguished Encoding Rules) - два разных, но сходных набора правил, определяющих единственное представление структуры данных в виде байтового потока. Оба являются уточнениями BER; правила декодирования BER полностью пригодны для CER и DER, правила кодирования CER и DER накладывают дополнительные ограничения, но не меняют основы правил.

Характерное приложение DER - X.509. Единственность представления приводит к возможности простого (без полного парсинга) сравнения элементов сертификатов на равенство.

3. PER (Packed Encoding Rules) - максимально экономное двоичное представление с удалением всех характерных для BER указаний типа и длины, а в некоторых случаях и сокращениях полей до единиц битов; если в BER посылке ещё можно опознать переданные типы данных и их значения без спецификации конкретного корневого типа данных, то в PER это невозможно. Характерное приложение - H.323.

4. XER (XML Encoding Rules) - текстовое представление в XML.

Представление в ASN.1 использует следующие типы данных:

  • элементарные типы;
  • структурные типы - последовательность (sequence), множество (set); если элементы структурного типа однотипны, то название типа соответственно меняется на sequence of, set of.

    Структурные типы принципиально сходны с ранее рассматривавшимися AV-списками, но характерным свойством ASN.1 является то, что в конкретном представлении согласно правилам кодирования передаются типы данных - элементов структурного типа, а не их названия. Название элемента сохраняется на уровне спецификации. Это общий принцип, активно используемый во всех стандартных форматах. С некоторыми натяжками, можно нарушить этот принцип, за счёт трюка - активного использования неявного тегирования контекстно-специфичными типами; это имеет смысл в том случае, когда конкретный набор данных структуры обычно содержит малую часть от всего возможного состава полей структуры. Но в этом случае спецификация всё равно определяет твёрдый порядок таких полей. Оборотной стороной является то, что передача произвольного списка параметров с их названиями требует как минимум выделенной подструктуры (типа set).

    И элементарные, и структурные типы делятся на 4 класса - universal (общий); специфичный для данного контекста (context-specific); для применения (application); частный (private). В общий класс входят представления числа, строки, даты, etc.; их достаточно для подавляющего большинства применений. Класс типов для применения расширяет общий класс тем, что нужно конкретному применению, причём понятие "применения" здесь ближе всего к понятию "общая задача". Например, в SNMP это IP-адрес, счётчики. Контекстно-специфичный тип имеет значение только в пределах определённой грамматической позиции (структура определённого типа); пространства таких типов в разных грамматических позициях не пересекаются. Контекстно-специфичные типы ближе всего к понятию названия элемента, выраженного числом (номером типа).

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

    В качестве примера представим себе развитие формата данных "ответ на запрос". Пусть в начальной версии такого ответа предполагалось только наличие кода ошибки, взятого из errno C-среды Unix, и текста, который является расшифровкой этого кода стандартной функцией strerror(). В таком случае формат представляется следующим образом:

    Response ::= SEQUENCE {
        ErrorCode                   INTEGER,
        ErrorDescription            IA5STRING OPTIONAL
    }
    

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

    Пример кодирования такого Response в BER, если ErrorCode == 198256, а ErrorDescription == "Bar impact":

    
    30 11 02 03 03 06 70 16 0A 42 61 72 20 69 6D 70 61 63 74
    

    Он состоит из:

    Полное представление вложенного данного "целое число" занимает 5 байт, строки - 12, в сумме получается 17 байт - длина sequence.

    Далее мы не приводим детальную расшифровку представления в BER.

    Потом требования поменялись, и нам надо расширить данную структуру следующими данными:

    1. Код ошибки, сгенерированный приложением на основании своей логики, из своего пространства кодов ошибок. (Напоминаю, что разные версии Unix имеют разные значения для одноимённых кодов errno, поэтому идея передавать число, специфичное для конкретной машины, была изначально ошибочной.)
    2. Уточнящий код для основного кода ошибки (не всегда присутствует).
    3. Сообщение, описывающее код ошибки из первого пункта; необязательное.
    4. Сообщение, описывающее уточняющий код.
    5. Сопроводительный текст для диагностики.

    Нарисуем предположительную схему получившегося:

    Response ::= SEQUENCE {
        SysErrorCode                INTEGER,
        SysErrorDescription         IA5STRING OPTIONAL,
        MyErrorCode                 INTEGER OPTIONAL,
        MyErrorDescription          UTF8STRING OPTIONAL,
        MyErrorDetailCode           INTEGER OPTIONAL,
        MyErrorDetailDescription    UTF8STRING OPTIONAL,
        DiagnosticsText             UTF8STRING OPTIONAL
    }
    

    (обратите внимание на переименование первых двух полей - оно абсолютно ни на что не влияет, но полезно для уточнения их смысла.)

    Это не пройдёт проверку компилятором описания ASN.1 хотя бы из-за двух последних пунктов: невозможно, разбирая содержимое, однозначно определить, какой из двух необязательных (optional) параметров присутствует. Также, встретив данное типа UTF8STRING, можно предположить, что это MyErrorDetailDescription (при пропущенном MyErrorDetailCode). Как такое лечится? Например, можно всем новым полям назначить теги:

    Response ::= SEQUENCE {
        SysErrorCode                INTEGER,
        SysErrorDescription         IA5STRING OPTIONAL,
        MyErrorCode                 [0] INTEGER OPTIONAL,
        MyErrorDescription          [1] UTF8STRING OPTIONAL,
        MyErrorSubDetailCode        [2] INTEGER OPTIONAL,
        MyErrorSubDetailDescription [3] UTF8STRING OPTIONAL,
        DiagnosticsText             [4] UTF8STRING OPTIONAL
    }
    

    Такое тегирование - один из самых простых способов разрешить конфликты; при чтении, например, видя context-specific type 2, код разбора будет сразу понимать, что это MyErrorDetailCode (и его тип INTEGER), а 0 - MyErrorCode (с тем же типом); строковые данные тоже будут различаться тегом. Пропуск любого из элементов сохраняет однозначность разбора всей структуры. С другой стороны, этот вариант разрешает комбинации, которые с точки зрения здравого смысла должны быть запрещены: например, наличие MyErrorDescription при отсутствии MyErrorCode. Если это является существенным, можно переделать следующим образом:

    ErrorDescription ::= SEQUENCE {
        Code                        INTEGER,
        TextComment                 UTF8STRING OPTIONAL
    }
    
    Response ::= SEQUENCE {
        SysErrorCode                INTEGER,
        SysErrorDescription         IA5STRING OPTIONAL,
        MyError                     ErrorDescription OPTIONAL,
        MyErrorSubDetail            [0] ErrorDescription OPTIONAL,
        DiagnosticsText             [1] UTF8STRING OPTIONAL
    }
    

    В этом варианте возможно появление MyError без MyErrorSubDetail, и наоборот; допустимо и наличие, и отсутствие обоих; наличие или отсутствие DiagnosticsText никак не связано с наличием предыдущих полей; для каждого описания ошибки может быть код без текста, но не текст без кода. Цена за это - бо́льшая вложенность структуры, и то, что нельзя будет добавить ещё одно поле типа ErrorDescription, не зависящее от наличия предыдущих полей, не выделяя ему свой контекстный тег. В таком варианте разрешено иметь MyErrorSubDetail без MyError; средствами представления структуры это не запрещается, что является некоторым недостатком такого построения. Если это важно, то надо было бы их выделить в отдельную подструктуру.

    Осталось решить, что делать с первыми двумя полями, если не можем задать им разумные значения. Так как SysErrorCode (ErrorCode в первой версии) уже описано как существующее, совсем не передавать его нельзя, в отличие от SysErrorDescription (ErrorDescription в первой версии), которое можно опустить из-за того, что IA5STRING имеет другой код типа, чем UTF8STRING. Для совместимости с прежними реализациями можно в SysErrorCode или передавать заведомо ранее невозможное значение типа -1, или выбрать какой-то "универсальный" код из errno. Эта проблема показывает, что несовместимое изменение имеет определённые проблемы даже в случае таких форматных схем.

    V.4. JSON и YAML.

    Группа форматных схем, представителями которых являются JSON и YAML, моложе своих конкурентов (XML, ASN.1); она появилась как средство предельного и быстрого решения задачи по переносу данных в достаточно произвольной структуре между двумя сторонами, основанными на так называемых "скриптовых" языках - то есть интерпретируемых и с динамической типизацией. Но практика показала пригодность их использования и для множества других случаев.

    Общий принцип построения такой схемы предельно прост. Элементом данных является число (целое или вещественное), строка, булевские значения (истина/ложь), специальное значение "ничего" ("null" в JSON), и два сложных типа данных - список и словарь (объект). В списке могут быть разнородные или однородные данные, это ничего не меняет в его конструкции. Словарь (объект) является набором пар ключ-значение, причём ключи уникальны в пределах словаря, и может быть ограничение (JSON), что ключ может быть только строкой. Нетрудно убедиться, что при всей простоте данного метода его достаточно для представления любых данных. В конкретных схемах могут быть непринципиальные расширения возможностей (которые на практике очень редко используются именно из-за выхода за пределы общего базиса).

    Например, рассмотренная выше в обсуждении ASN.1 структура ответа может получить такую конкретную реализацию (объект ответа):

    {
        "ErrorCode": -1,
        "MyError": {"Code": 242, "Description": "Principal data not found"},
        "MyErrorSubDetail": {"Code": 11, "Description": "File not found"}
    }
    

    Сравним эти схемы (в качестве представителя возьмём JSON) с конкурентами. Для начала, сравним с XML.

    Представление в JSON точно так же текстовое и опирается на имена атрибутов, но в среднем оно существенно экономнее за счёт отсутствия дублирования названий тегов.

    JSON требует представления данных (строк, чисел) в каноническом виде, в отличие от XML, который позволяет в принципе любое представление; это можно обойти, представив всё в виде строк с собственным содержимым, но это уже явный хак.

    Атрибуты объекта отсутствуют (впрочем, как и собственно его название), что требует иной организации данных в структурах. Для начала, в XML возможно различение содержимого по типу документа и по названию типа коренной структуры (если поступило <foo>...</foo> - один метод разбора, а если <foo2>...</foo2> - другой); для JSON это напрямую невозможно, а если требуется - надо самому делать что-то вроде {"foo":{...}}, но обычно такое не закладывается с самого начала. Далее, представим себе следующий объект XML:

    <foo>
      <bar name="a">1</bar>
      <bar name="b">2</bar>
      <baz name="p">99</bar>
      <baz name="q">88</bar>
    </foo>
    
    Можно, конечно, напрямую перевести это в JSON следующей структурой:
    [                       ## представляем как тройку - имя, атрибуты, вложенное
        "foo",
        {},                 ## атрибуты - отсутствуют
        [                   ## список подэлементов
            ["bar", {"name": "a"}, ["1"]],
            ["bar", {"name": "b"}, ["2"]],
            ["baz", {"name": "p"}, ["99"]],
            ["baz", {"name": "q"}, ["98"]]
        ]
    ]
    

    но более естественной будет следующее (предполагая уникальность имён объектов типа bar и baz):

    {
        "bar": {"a": 1, "b": 2},
        "baz": {"p": 99, "q": 88}
    }
    

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

    Наконец, представление в JSON напрямую отображается на внутренние структуры таких языков, как JavaScript/ECMAScript (откуда он и происходит), Perl, Python, Ruby и так далее, и обратно, в отличие от XML, которому требуется специальный кодер или декодер (парсер). Это и преимущество, и недостаток. Преимущество - значительно более простая работа с ним в типичных случаях. Недостаток - отсутствие валидации; тривиально как прочесть извне некорректную структуру, так и выдать её внешнему получателю. Валидация требует отдельного явного обработчика.

    Теперь сравнение с ASN.1 и её стандартными форматами передачи:

    Представление опирается на имена атрибутов, а не на типы. Соответственно, уже назначенное имя не переименуешь без изменения всего поддерживающего кода. С другой стороны, лежащее за конкретным именем содержимое может легко менять тип; для ASN.1 это возможно только при заранее предусмотренном явном тегировании (explicit tagging), а иначе изменение типа записи невозможно или крайне ограничено.

    XXX TBC

    V.5. Другие структурированные форматные схемы.

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

    V.6. Использовать ли структурированную форматную схему?

    Можно определить следующее правило: если нет суровой необходимости экономии ресурсов на представление (объём), кодирование и декодирование (соответственно, затраты процессора на собственно работу с данными и работу с метаданными), но есть потенциальная возможность введения новых требований и расширения формата, является желательным использовать структурированной форматной схемы как основы форматов данных протокола. Желательность здесь происходит из того, что, с одной стороны, в этих схемах уже решена основная (часто и подавляющая) часть вопросов представления, с другой стороны, заведомая неограниченная расширяемость позволяет следовать постоянно меняющимся внешним требованиям. На сейчас XML является одним из наиболее распространённых промышленных стандартов представления данных и для передачи по сети, и для локального хранения, хотя это не означает, что именно его надо использовать по умолчанию для новых разработок; почти всегда, JSON лучше подходит для этого.

    V.7. Место структурированных форматных схем в протоколах

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

    Одним из типичных вариантов является, особенно в "enterprise" применениях, использование какого-то общедоступного протокола, как HTTP, для реализации транспортного слоя, и тел запроса и ответа, представленных как XML или JSON.


    Copyright (C) 2010-2015 Valentin Nechayev. All rights reserved.
    Разрешается полное или частичное копирование, цитирование. При полном или частичном копировании ссылка на оригинал обязательна.

    Назад Вверх Вперёд