О протоколах передачи данных

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

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

Какими свойствами и особенностями обладает хороший, годный грамотный, качественный протокол?

В идеале, протокол должен быть абстрагирован от более нижнего уровня взаимодействия, будь то передача по TCP, UDP, по serial порту, USB, Bluetooth, через цифровой радиосигнал, или даже по голубиной почте. И надо учитывать, что далеко не все из них гарантируют доставку и\или достоверность передающихся данных.

Небольшой дисклеймер: говоря о достоверности данных, я имею ввиду их неискаженность вследствие помех и иных ошибок в среде передачи. В статье я не буду затрагивать темы пласта технологий, связанных с безопасностью в ИТ. Допустим что наши Алиса и Боб могут друг другу доверять, и никакая Ева им помешать не может. (Например у коллег вопрос безопасности решается включением всех территориально разделенных участников взаимодействия в хорошо защищенный VPN, не имеющий в свою очередь доступа наружу)

В большинстве протоколов реализована схема «Вопрос-Ответ». Это можно представить как разговор, в котором на каждую реплику своего собеседника вы реагируете вербально, и в том же смысловом ключе. Таким образом участниками взаимодействия достигается уверенность в том, что их сообщения переданы и адекватно восприняты. Однако эта схема допустима и эффективна не для всех задач: в случаях когда задержка в общении должна быть минимизирована, или ответ на каждую из многочисленных реплик признается избыточным (например для отладочных сообщений), реализуется схема «Старт-Стоп». При получении сообщения на «Старт» ваш собеседник начинает сыпать в вас потоком реплик, и замолкает лишь при слове «Стоп». Сообщения, отправляемые в потоке, обычно имеют инкрементируемый порядковый номер, и если при принятии потока сообщений были проблемы с обработкой\было пропущено одно из них, его можно перезапросить отдельно по этому самому номеру.

Все протоколы можно разделить на две группы, (по представлению данных): символьные и бинарные.
Символьные протоколы, с которыми мне приходилось встречаться, базировались либо на XML, либо на JSON-строках. Из их достоинств можно упомянуть о более простой отладке взаимодействия (вследствие их читаемости), о простоте реализации (наличия готовых парсеров), и пресловутой универсальности.
Теперь о недостатках. Очевидно, что такие протоколы являются крайне избыточными, мизерная доля полезной информации плавает в массивной, неэффективной обёртке. При передаче любой числовой информации приходиться заниматься их конвертацией в строковое представление и обратно. Больным местом является передача бинарных данных (и хорошо, что без них бывает можно обойтись, но в ряде случаев это невозможно). Составители протоколов обычно выкручиваются применением Base64, или даже просто передачей бинарной строки в её hex-овом представлении, по два символа на байт.
Также хочется отметить, что полная спецификация того же XML крайне обширна, и стандартные парсеры, при всей их полноте возможностей, достаточно громоздки и медлительны, поэтому распространена практика, когда отдел или контора в итоге пишет и пользуется собственным парсером.

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

Теперь бинарные протоколы. Сразу же надо вспомнить о Гулливерских войнах тупоконечников и остроконечников. Лично я симпатизирую big-endian, т.к. не считаю неявную типизацию little-endian «чем-то хорошим», да и в моей среде разработки big-endian является нативным.
Бинарные протоколы (не все, но те, которые я отношу к грамотным) можно разделить на два уровня: уровень контейнера и уровень данных. На плечи первого уровня ложится ответственность за целостность и достоверность передачи данных, а так же за доступность обнаружения сообщения в байтовом потоке, и, само собой, за хранение в себе сообщения уровня данных. Второй уровень должен содержать информацию, ради которой всё сетевое взаимодействие и затевалось, в удобном для обработки формате. Его структура в основном зависит от решаемых задач, но и по нему есть общие рекомендации (о которых ниже).

Размеры сообщений (дискретных пакетов байт, которые можно обрабатывать независимо от предыдущих и последующих принимаемых данных) бывают фиксированными и переменными. Понятно, что с фиксированным размером сообщений всё проще — вычитается, начиная с заголовка (о нём позже), определенное количество байт и отправляется на обработку. Зачастую, для обеспечения гибкости, составители таких протоколов включают в сообщение область фиксированного размера (иногда до 80% от общего объема), зарезервированное под модификации нынешнего протокола. На мой взгляд, это не самый эффективный путь обеспечения гибкости, зато избыточность появляется еще какая.
Рассмотрим сообщения переменной длины.
Тут уже можно подробней поговорить о непременном атрибуте бинарного сообщения в любом протоколе — о заголовке (Это вышеупомянутый уровень контейнера).
Обычно заголовки начинаются с константной части, позволяющей, с определенной вероятностью обнаружить начало сообщения в непрерывном байтовом потоке. Очевидно, что имеется риск появления такой константы в произвольном потоке байт, и, хотя увеличение объема этот риск снижает (я встречал константы вида 0123456789VASIA9876543210), целесообразней использовать проверки на основе подсчета контрольной суммы.
За константой обычно следует номер версии протокола, который дает нам понять, в каком формате должно происходить дальнейшее считывание (и имеем ли мы вообще возможность обработать это сообщение — вдруг такая версия нам неизвестна). Следующая важная часть заголовка: информация о самом содержимом контейнера. Указывается тип содержимого (по факту, тот же номер версии протокола для уровня данных), его длина и контрольная сумма. Имея эту информацию, можно уже без проблем и опасений считать содержимое и приступить к его разбору.
Но не прямо сразу! Заголовок должна заключать контрольная сумма его самого (исключая из расчета конечно саму контрольную сумму) — только так мы можем быть уверены в том, что считали только что не белиберду, а валидный заголовок, за которым следуют предназначенные нам данные. Не совпала контрольная сумма? Придётся искать следующее начало нового заголовка дальше по потоку…

Представим, что мы дошли до этапа, что получили наконец неискаженное сообщение уровня данных. Его структура зависит от той области задач той системы, в которой реализован ваш сетевой обмен, однако в общем виде у сообщения тоже бывает быть свой заголовочек, содержащий информацию о типе сообщения. Можно различить как общую специфику сообщения, (например «Запрос Set», «Утвердительный Ответ на Set», «Отрицательный Ответ на Set», «Запрос Get», «Ответ Get», «Потоковое сообщение»), так и конкретную область применение сообщения. Попробую привести пример с потолка:
Тип запроса: Запрос Set (0x01)
Идентификатор модуля-адресата сообщения: PowerSupplyModule (0x0A)
Идентификатор группы сообщений: UPS Management (0x02)
Идентификатор типа сообщения: Reboot (0x01)
Дальше тело сообщения может содержать информацию об адресе ИБП, который Модуль управления энергообеспечением должен перезагрузить, через сколько секунд это сделать и т.п.
На это сообщение мы рассчитываем получить ответное сообщение с типом запроса «Утвердительный Ответ» и последующими 0x0A0201 в заголовке.
Конечно, такое подробное описание типа сообщения может быть избыточным когда межсетевое взаимодействие не предусматривает большого числа команд, так что формировать структуру сообщения надо исходя из требований ТЗ.
Так же будет полезно, если сообщение с «Отрицательным Ответом» будет содержать код ошибки, из-за которой не удалось ответить на команду утвердительно.

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

upd.
Имел удовольствие пообщаться с критиком своей статьи, и теперь прихожу к осознанию, что я осветил вопрос со своей если можно так выразиться, «байтолюбской», точки зрения. Конечно, раз идет курс на универсальность обработки хранения и передачи данных, то в таком ключе символьные протоколы (в первую очередь говорю об XML) могут дать фору любым другим решениям. Но относительно попытки повсеместного их применения позволю себе процитировать Вирта:
Инструмент должен соответствовать задаче. Если инструмент не соответствует задаче, нужно придумать новый, который бы ей соответствовал, а не пытаться приспособить уже имеющийся.
Поделиться публикацией
Комментарии 22
    +10
    Тема интересная и нужная, спасибо!
    Вот бы еще для любителей бинарных протоколов и не-любителей изобретать их с нуля рассказать про protobuf, msgpack, bson и другие.
      +4
      Вы только что дали мне идею для следующей статьи :) Благодарю
        +4
        На мой взгляд писать о пользе текстовой форме представления данных и убеждать читателя не имеет смысла. Потому что есть книга Реймонда «Искусство программирования для UNIX», там очень подробно и доходчиво поясняется о том когда же все-таки нужно данные представлять в бинарном\текстовом виде.
        Думаю Вам вполне достаточно привести ссылку на эту книгу и привести примеры, как уже указывалось выше:
        >>protobuf, msgpack, bson и другие"
          +1
          Навязывать свою точку зрения на то, в какой области применять тот или иной вид и в мыслях не было :) А вот сузить тему статьи до разбора одного-двух известных решений из мира «любителей бинарных протоколов» — это может оказаться интересным и полезным.
          +5
          Тогда ещё ASN.1 не забудьте :) Хотя это само по себе тема для целой статьи…
            0
            Включите в планы EDIFACT. Это что-то среднее между бинарными протоколами и XML. Эдакий малосимвольный XML.
            Пришлось как-то разбирать дамп общения неких программ в этом формате: приятного было мало :)
          0
          Тема бинарный/символьный религиозна так же, как и тема windows/linux.
            +1
            Но это не значит, что статья не годная. Статья хорошая! Я бы ещё немного оформил и всемиоченьлюбимых картинок парочку вкрутил.
              0
              Каюсь, неразбавленная картинками статья действительно несколько теряет в читабельности и усвояемости. Приму на заметку!
            +2
            Какое-то у Вас сильно странное и довольно надуманное деление на символьные и бинарные протоколы. Вот скажите, HTTP это символьный или бинарный?

            Если из символьных на ум приходит на ум только то, что на базе json и xml, то подумайте об smtp и pop :)

            Так называемые «символьные» протоколы происходят от терминалов с подмножеством 7-8-битного алфавита, а «бинарные» от куска памяти со структурой на Си, засунутого как есть в буфер ввода-вывода. Но в действительности сущность данных куда более сложна.

            Рекомендую для начала посмотреть, как обходятся с интами в protobuf, очень полезно для освобождения разума от байтовых рамок :)
              +1
              >>Вот скажите, HTTP это символьный или бинарный?
              Это не «символьный» это текстовая форма представления данных, так правильней называть. Да, вы правы Json, XML, pop, smtp это все — текстовое.

              Основное преимущество текстовой формы, это облегчение отладки систем! Программист может «поговорить» с системой!

              В случае бинарного представления, вам надо написать конвертер из ваших текстовых приказов в бинарную форму, либо взять Hex-редактор и «нафигачить» перед подачей на вход системы. А это уже значительно сложнее, чем просто написать текст!
                0
                На самом деле тут все немного сложнее. Основной смысл «текстовых» протоколов это сохранять понятийную область. Точнее сказать, в некоторых случаях просто нет особой надобности «компилировать» данные в не текстовый вид, а потом обратно.

                Программисту нет проблем взять себе удобный инструмент для «разговора». Например, когда вы смотрите свой json в каком-нибудь fierbug'е, то можете не задумываться, что на самом деле он пришел сюда в gzip'е, строки в нем были оформлены в таком виде,
                "\u0430\u0431\u0432", а от вас на сервер он уходил вообще в urlencode форме (но это уже из области извращений).

                Еще одна особенность текстовых протоколов незаслуженно забывается — они как правило line buffered. Поток информации разбивается на удобоваримые куски, причем, удобство человека здесь как раз не причем.
                  0
                  На самом деле тут все немного сложнее. Основной смысл «текстовых» протоколов это сохранять понятийную область. Точнее сказать, в некоторых случаях просто нет особой надобности «компилировать» данные в не текстовый вид, а потом обратно.

                  Программисту нет проблем взять себе удобный инструмент для «разговора». Например, когда вы смотрите свой json в каком-нибудь fierbug'е, то можете не задумываться, что на самом деле он пришел сюда в gzip'е, строки в нем были оформлены в таком виде,
                  "\u0430\u0431\u0432", а от вас на сервер он уходил вообще в urlencode форме (но это уже из области извращений).

                  Еще одна особенность текстовых протоколов незаслуженно забывается — они как правило line buffered. Поток информации разбивается на удобоваримые куски, причем, удобство человека здесь как раз не причем.
                    0
                    Сорри, как-то оно раздвоилось тут…
                  0
                  Соглашаясь с EvilsInterrupt, добавлю следующее: протокол, ориентированный на передачу данных, абстрагированных от машинной интерпретации (числа в строковом представлении, строковые константы вместо численных энумераторов и т.д.), но более понятных в таком виде человеку, как раз и подпадает под моё определение «текстового».
                    0
                    HTTP изначально символьный, но в теле сообщения содержит бинарные блоки. SMTP/POP вообще символьные, в них бинарные блоки кодируются в символьные строки UUE/Base64

                    protobuf вообще знатное извращение. =) Мне больше bencode нравится, его можно глазами читать
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Сжатие конечно позволит уменьшить избыточность текстовых протоколов, но приведет к еще большему отставанию по скорости обработки :)
                      0
                      С отрывом от прикладной задачи писать о протоколе высокого уровня — довольно спорная затея, так как тема слишком широка. Для одних протоколов важна скорость, для других важна синхронизация событий, для третьих маршрутизация, для четвертых надежность и т. д. Все эти требования противоречат друг другу, поэтому выбор протокола — широкое поле для компромиссов и творчества. После прочтения статьи понятнее, по крайней мере, ничего не стало =).

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

                        Первейший из них — это тип передаваемых данных — fast datapath или slow datapath. Fast подразумевает, что нагрузка при передаче и приёме такова, что мы жертвуем всеми второстепенными вопросами (например, читаемостью) ради скорости. slow — означает, что мы можем посвятить сколько-то времени каждому сообщению/потоку.

                        Дальше — realtime/non-realtime, ordered/unordered, с подтвержением/без, с защитой от повторов/без.
                        Дальше — допустимые типы данных и структуры.

                        А мелочи, типа порядка байт, уже обсуждаются на последнем этапе.
                          0
                          Главное, что бы для штатных задач не городили монстров. Когда работаешь с > 10 разными форматами, становится очень грустно каждый раз, когда видишь очередной текстовый формат класса «значение X ищите с 42го столбца по 52й, с забивкой пустых знакомест символом % справа». Хочется сразу таким дизайнерам пожелать остаток жизни сверять подобные файлы с 100к+ записей каждый день (и каждый день нового формата). :)
                          0
                          контрольная сумма дла заголовка — это верно.
                          при этом желательно чтобы контрольная сумма сообщения была частью заголовка…

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое