Назад Вверх

Данный раздел посвящён документированию наработок по проектированию протоколов и форматов данных.

Когда документировать? Оптимальный для большинства случаев ответ: даже если документация полностью неформальна (файл в каталоге проекта, или даже область комментариев в исходном коде), надо её писать как можно раньше, но не раньше, чем устоятся основы. Как правило, первая версия может считаться устоявшейся не раньше, чем она прошла основной цикл тестирования (по частям - юнит-тестирование, функциональное и нагрузочное), до завершения такого тестирования могут обнаруживаться проблемы, требующие тотального редизайна. Таким образом, оптимальный момент времени - сразу после "фуух..." и отмечания (включая распивание напитков) результатов успешного тестирования.

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

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

Дальнейшее изложение будет вестись по порядку:


Участие сторон

Один стиль рисования взаимодействия:


S->C: привет, я сервер
C->S: дай миллион
S->C: а ключ от квартиры, где деньги лежат?
тут чётко видно, от какой стороны какой идёт сообщение.

Варианты:


I-> привет
T-> ты кто???
(RFC3720, iSCSI)

C: a001 CAPABILITY
S: * CAPABILITY IMAP4rev1 STARTTLS LOGINDISABLED
S: a001 OK CAPABILITY completed
C: a002 STARTTLS
S: a002 OK Begin TLS negotiation now
(RFC3501, IMAP4rev1)

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

Другой стиль:


>>> привет, я сервер
<<< дай миллион
>>> а ключ от квартиры, где деньги лежат?

Во втором случае стрелка показывает направление, то, что "за экраном", считается удалённой стороной. Какая сторона в каком случае удалённая - решается по роли:)

Стиль диаграмм SIP рассчитывается на 1) наличие более чем двух участников, 2) возможность краткого описания сути каждого сообщения. Пример (из RFC3665):


   Alice           Proxy 1          Proxy 2            Bob
     |                |                |                |
     |   INVITE F1    |                |                |
     |--------------->|                |                |
     |     407 F2     |                |                |
     |<---------------|                |                |
     |     ACK F3     |                |                |
     |--------------->|                |                |
     |   INVITE F4    |                |                |
     |--------------->|   INVITE F5    |                |
     |     100  F6    |--------------->|   INVITE F7    |
     |<---------------|     100  F8    |--------------->|
     |                |<---------------|                |
     |                |                |     180 F9     |
     |                |    180 F10     |<---------------|
     |     180 F11    |<---------------|                |
     |<---------------|                |     200 F12    |

Обозначение F+цифра ссылается на полный текст сообщения из последующего текста. Могут использоваться уточнённые обозначения (например, INVITE 101 получает ответ в виде 180 101/I). В такой диаграмме значительно лучше виден ход времени, чем в предыдущих.

В типичном протоколе взаимодействия имеет смысл говорить о состояниях конечного автомата каждой из сторон (типичные состояния - установление связи, аутентификация, бездействие установленного соединения в готовности к обмену, передача запроса, приём ответа, завершение...) Часто протокол имеет несколько уровней с подобными автоматами на каждом; один из самых выдающихся вариантов - SIP - минимум 4 уровня (транспортный, транзакций, диалогов, сессии). Стандартизованные правила перехода между состояниями записываются в таблицу, блок-схему или иным легко понятным человеку методом. Пример из RFC3261 (SIP, уровень транзакций):


                               |INVITE from TU
             Timer A fires     |INVITE sent
             Reset A,          V                      Timer B fires
             INVITE sent +-----------+                or Transport Err.
               +---------|           |---------------+inform TU
               |         |  Calling  |               |
               +-------->|           |-------------->|
                         +-----------+ 2xx           |
                            |  |       2xx to TU     |
                            |  |1xx                  |
    300-699 +---------------+  |1xx to TU            |
   ACK sent |                  |                     |
resp. to TU |  1xx             V                     |
            |  1xx to TU  -----------+               |
            |  +---------|           |               |
            |  |         |Proceeding |-------------->|
            |  +-------->|           | 2xx           |
            |            +-----------+ 2xx to TU     |
            |       300-699    |                     |
            |       ACK sent,  |                     |
            |       resp. to TU|                     |
            |                  |                     |      NOTE:
            |  300-699         V                     |
            |  ACK sent  +-----------+Transport Err. |  transitions
            |  +---------|           |Inform TU      |  labeled with
            |  |         | Completed |-------------->|  the event
            |  +-------->|           |               |  over the action
            |            +-----------+               |  to take
            |              ^   |                     |
            |              |   | Timer D fires       |
            +--------------+   | -                   |
                               |                     |
                               V                     |
                         +-----------+               |
                         |           |               |
                         | Terminated|<--------------+
                         |           |
                         +-----------+

                 Figure 5: INVITE client transaction

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

XXX TBC


Обозначения битов и байтов

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

В области обозначения порядка битов в байтах и словах наблюдается жуткий бардак.

Стиль 1: биты называются по логарифму их числового веса - бит номер N имеет вес 2**N при интерпретации как целое беззнаковое. В этом случае самый младший бит имеет номер 0, следующий за ним - 1, и так далее; максимальный номер в одном октете - 7, в 4-октетном слове - 31. Этот стиль популярен у большинства производителей оборудования и, например, используется во всей документации Intel. Порядок битов в байте определяется однозначно (за пределами сериализации), но порядок битов в слове зависит от порядка байтов в слове - в случае 32-битного little endian слова, в байте с минимальным смещением (0) будут биты 7-0, а с максимальным (3) - 31-24; для big endian, очевидно, наоборот.

Коротко этот стиль можно обозначить так: 0=LSB (LSB значит Least Significant Bit).

Стиль 0=LSB имеет "естественное" обоснование для little-endian архитектур, за счёт доступа к участку памяти как к массиву битов. Если использовать эту нумерацию, то, независимо от размера поля данных, через которое ведётся доступ к битам (обычно это 1, 2, 4 или 8 байт, в виде целого числа), можно использовать универсальный алгоритм: индекс бита делится на число бит в поле, частное используется для индексирования поля, а остаток - бита в нём (имеющего числовое значение (1<<r) в терминах языка C):


void set_bit(void *bit_field_start, unsigned index)
{
        ACCESS_TYPE *ptr = (ACCESS_TYPE*) bit_field_start;
        const size_t BITS_PER_CHUNK = sizeof(ACCESS_TYPE) * CHAR_BIT;
        ptr[index/BITS_PER_CHUNK] |= 1u << (index % BITS_PER_CHUNK);
}

(Разумеется, для всех реально встреченных случаев, когда CHAR_BIT равен 8 или другому значению - степени двойки, деление упрощается до сдвигов и логического "и" по маске.)

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

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


+----------+--------+--------------------------------------+
| Смещение | Размер | Содержание                           |
+----------+--------+--------------------------------------+
|        0 |    2   | Тип сообщения, unsigned int          |
|        2 |    2   | Общие флаги:                         |
|          |        | Бит 15 - требуется ответ             |
|          |        | Бит 14 - допускается                 |
|          |        |   асинхронный ответ                  |
|          |        | Биты 1-0 - требуемый класс           |
|          |        |   обслуживания                       |
+----------+--------+--------------------------------------+

Пример диаграммы в этом стиле - описание команды READ(6) из стандарта T10 SBC - SCSI Block Commands:


     Bit | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Byte     |                               |
---------+-------------------------------+
  0      |     OPERATION CODE (08h)      |
---------+-------------------------------+
  1      |  Reserved |(MSB)              |
  2      |     LOGICAL BLOCK ADDRESS     |
  3      |                          (LSB)|
---------+-------------------------------+
  4      |       TRANSFER LENGTH         |
---------+-------------------------------+
  5      |             CONTROL           |
---------+-------------------------------+

Здесь: OPERATION CODE, TRANSFER LENGTH и CONTROL занимают по полному байту (со смещением 0, 4 и 5 соответственно); резервное поле занимает биты 7-5 байта 1; поле LOGICAL BLOCK ADDRESS занимает 21 бит - от бита 4 байта 1 и до бита 0 байта 3. Дополнительно метки "(MSB) " и "(LSB)" показывают места размещения соответственно самого старшего и самого младшего бита данного поля.

Стиль 2: нумерация от старших к младшим битам начиная от 0, полностью обратно стилю 1. Этот стиль популярен у IETF (и рекомендован в RFC2360). Например, в RFC791 (протокол IP) диаграмма формата IP пакета:


    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
[продолжение удалено]

В этой диаграмме октет со смещением 0 от начала пакета состоит из полей Version и IHL, причём Version занимает старшие 4 бита октета, а IHL - младшие 4 бита; Identification занимает 2 октета по смещениям 4 и 5. Объяснение обозначений приводится в приложении B той же спецификации.

Также этот стиль применяется в документации к S/360 и потомкам. Вообще, применение этого стиля заметно коррелирует с big-endian порядком байт, точно так же как стиль 0=LSB - с little-endian порядком байт. Обоснование такой нумерации аналогично приведённому выше для случая 0=LSB для little-endian архитектур: если представлять участок памяти как массив бит, и использовать доступ, не зависящий от размера поля для доступа (хоть 1 байт, хоть 8), при делении на размер поля (в битах), частное используется как индекс поля, а остаток - как позиция бита в поле. В отличие от little-endian, выражение для маски бита - не (1<<r), а (M>>r), где M - значение, в котором 1 в самом старшем бите, и 0 в остальных (128 для 1-байтного поля, 32768 для 2-байтного, и так далее), или, альтернативно, (1<<(BITS_PER_CHUNK-1-r)); для процессоров, которые из маски сдвига воспринимают только необходимое число младших бит, можно писать (1<<~r).
Тем не менее, массово существуют характерные контрпримеры - см. выше диаграммы SCSI - при big-endian записи чисел в структурах данных, биты нумеруются по 0=LSB; можно даже утверждать это как типичную тенденцию.

В случае z/Architecture (64-битных потомков S/360) использование этого стиля приводит к необходимости внимательно учитывать контекст нумерации; одни и те же биты могут называться 0-31, если регистр рассматривается как 32-битный (только 32 младших бита полного 64-битного регистра), и 32-63, если он же рассматривается как 64-битный; и наоборот, имея номер бита менее 32, надо знать, о какой части регистра идёт речь. В этом смысле little endian более "дружественно" к расширению полей.

Коротко этот стиль можно обозначить так: 0=MSB (MSB значит Most Significant Bit).

Стиль 3: биты нумеруются от 1 начиная с самого младшего. Пример использования: ISO спецификация X.690; ГОСТы (например, 26.201.2.94 - КАМАК); стандарты ECMA, такие, как ECMA-006 (IA5/ISO-646/ASCII и его обобщения). Похоже, его источником являются исключительно стандартизующие организации во главе с ISO:)

Короткое обозначение стиля: 1=LSB.

Стиль "1=MSB", в котором биты нумеруются от 1 начиная с самого старшего, дополнил бы этот набор до логически завершённого, но мне не встречалось случаев его употребления.

В принципе этот набор 4 вариантов покрывает все возможности, как можно нумеровать биты в порции данных без неестественных подходов.

В общем случае мы рекомендуем использовать стиль 1 (0=LSB) как имеющий наиболее "естественное" обоснование и соответствующий традициям гигантов типа Intel и Microsoft. Однако, при необходимости адаптации описания под определённую традицию (например, для IETF) предпочтительнее выбирать стиль по обстановке.

Для нумерации байтов в посылке или её части первым общепринятым стилем является указание минимального смещения объекта от начала посылки. Например, если 32-битное число занимает 4 байта по смещениям 4,5,6,7 от начала посылки, то говорится, что оно находится по смещению 4. Тогда состав посылки рисуется таблицей.

Пример таблицы:


+----------+---------+-----------------------------------------+
| Смещение |   Тип   |  Содержание                             |
+----------+---------+-----------------------------------------+
|    0     |  U32BE  |  Тег запроса                            |
|    4     |  U16BE  |  Код типа запроса                       |
|    6     |  U16BE  |  Флаги - битовое поле                   |
|          |         |  * биты 15-2 зарезервированы и          |
|          |         |    должны быть равны 0                  |
|          |         |  * бит 1 - запрос должен быть записан   |
|          |         |    в специальный журнал                 |
|          |         |  * бит 0 - запрос требует               |
|          |         |    квалифицированной обработки          |
| 8...last |         |  Данные запроса, зависят от типа        |
+----------+---------+-----------------------------------------+

(Интересно, что Intel в части документации по процессорам линии x86 в таких таблицах использует противоположное направление роста - минимальные смещения у них внизу. Но это не общее правило даже для их документации по процессорам.)

Несколько другой стиль таблицы был применён ранее - там вместо типа значения явно писался его размер, а интерпретация (целое/другое, со знаком или без знака...) определялось в содержании.

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

Другой вариант, применяемый IETF - диаграмма полей посылки, как в примере выше для заголовка IP пакета. Её преимущество - в наглядности размещения, включая биты. Однако, чтобы вычислить, на каком смещении находится поле, надо высчитывать его по диаграмме, для удобства тыкая пальцем в экран:) Почему-то у IETF не принято рисовать сбоку от такой таблицы смещения начала строк (было бы 0, 4, 8, 12...)

Ещё достаточно часто используется формат описания записи (тип struct) в Си. При рассмотрении записи в этом формате следует не забывать учитывать выравнивание, которое может быть явно не указано соответствующими полями, а также подразумеваемые размеры полей; тут могут быть тонкости (например, если поле просто обозначено как long, это 4 байта, 8 или что-то совсем другое?), их следует решать по контексту, а при формализации - требовать обязательного уточнения. Желательным вариантом является явное указание всех полей, не заполненных данными и размещённых для выравнивания. Со введением в C99 типов [u]intN_t, где N может быть 8, 16, 32 и так далее, стало удобным и желательным использовать именно такие типы для конкретизации размеров полей. Перенося такие описания напрямую в код, следует не забывать про указание отказа от локального выравнивания - это __attribute__((packed)) для GCC и #pragma pack(1) для компиляторов Microsoft.

Как правило, такие свойства формата, как порядок байт, едины для всей посылки. (Заметные исключения: ISO9660, где ключевые поля записываются в двух копиях; каждая для своего порядка; IPMI, где ASF транспорт использует big-endian, а вложенный в него IPMI - little-endian.) Поэтому их могут опускать при указании каждого отдельного элемента, если используют определения типов в стиле uint32_t. А вот размеры отдельных полей и интерпретация числа (как минимум - signed или unsigned) выбирается под задачу, поэтому совершенно нормально в одной посылке видеть одновременно поля в 8, 16, 32 и 64 бита и разной знаковости.


Документирование синтаксиса (грамматики)

В этой области в основном применяются вариации на тему формы Бэкуса-Наура (БНФ; англ. - Backus-Naur Form). Фактически такая форма представляет собой описание контекстно-свободной формальной грамматики. В основном БНФ разрабатывались для текстовых форматов, однако ограниченно применимы (особенно ABNF) к двоичным форматам.

ABNF определяется IETF RFC 5234 (STD68), c корректировками вроде IETF RFC 7405 (для значений, зависящих от регистра). Она достаточно удобна тем, что определяет синтаксис с точностью до конкретных значений октетов, и удобнее остальных задаёт правила повторений.

Контекстные ограничения вводятся отдельными средствами, как правило это текстовые комментарии к документированию. Именно контекстные ограничения делают БНФ менее пригодными для двоичных, нежели для текстовых протоколов: например, задача описания того, что перед значением элемента передаётся его длина (неважно, в каком формате), уже недоступна таким формальным средствам. Публичные стандарты на двоичные форматы данных используют разные, но всегда жёсткие стили объяснения, как именно следует заполнять поля данных.

XXX развернуть


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

Назад Вверх