Эта статья написана по итогам разработки геоинформационной платформы «RndFlow.Кругозор» и конкретной прикладной системы на её основе.

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

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

Сама система представляет из себя интеграционную платформу, которая поддерживает

  • Собственно интеграцию с большим количеством достаточно разнородной аппаратуры различных вендоров

  • Организацию распределённого сетевого взаимодействия

  • Формирование и анализ исторических данных (полный слепок входящих данных плюс полная история действий оператора)

  • Стандартный набор эвристик для препроцессинга, фильтрации и классификации входящих данных

  • Инструменты для реализации скриптинга (если не хватает встроенных эвристик или требуется специфическая визуализация)

  • Собственно, визуализацию как статуса подключенной аппаратуры, так и поступающих геоданных

  • Инструменты управления тревогами и информацией об отказах оборудования

  • Бизнес-процессы в области квитирования и обслуживания операторами определённых ситуаций

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

Система реализована как набор десктопных Java приложений, интегрированных посредством стандартных (шифрованных аппаратным способом) TCP/IP каналов между собой и с интегрируемой аппаратурой.

Система работает в реальном времени. Отдельно отметим, что на рынке бытует верование, что Java не применима в системах реального времени. (Точнее сказать, считается, что всё кроме Си не применимо. Разве что ассемблер.)  Мы реализовали несколько значимого размера систем реального времени на Java, причём первая появилась более 10 лет тому назад и работала на ещё тех, древних компьютерах, в условиях достаточно скромной производительности процессора. Практика показала, что существенных препятствий на этом пути нет. Впрочем, на эту тему я планирую написать отдельную статью.

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

Единственный момент, который мне хочется упомянуть в контексте применения языка программирования со сборкой мусора – это работа с объектами, которые имеют двойное или тройное владение. Пример – закешированные битмепы, хранящие анимированные графические объекты. Ими владеют три сущности – собственно, объект, который они визуализируют, кеш графических элементов и animation engine, который генерирует поток анимационных событий по всей системе. (Честно скажем, можно перепроектировать архитектуру так, чтобы избежать множественного владения этими объектами, но у такой архитектуры будут свои минусы.)

Наивная реализация такой архитектуры приведёт к тому, что графика, которая более не востребована владельцем будет вечно храниться в кеше и незримо анимироваться, то есть – тратить зря процессор и память. Решение – применение мягких ссылок (SoftReference). Решение достаточно простое, и достаточно проработанное.

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

Впрочем, я отвлёкся от заявленной темы статьи.

Каждая из аппаратных систем, с которой нам требовалось интегрироваться, фактически, конечно, была программно-аппаратным комплексом. Типично такой комплекс представляет собой два или три слоя компонент – собственно, аппаратура на нижнем уровне, микроконтроллерная программно-аппаратная часть на среднем и, как правило, обычный PC с Линуксом на верхнем. Иногда отсутствует 2 или 3 слой.

В сторону клиентского софта верхний слой, как правило, реализует тот или иной протокол, на базе TCP, UDP, HTTP или даже WebSockets.

Кратко классифицируем эти виды протоколов

Представление данных

  • Бинарный: 0x1B <pkt type> <pkt len> <binary pkt data> <CRC> – классический пример

  • Текстовый: JSON, XML

Использование готового решения

  • Протокол не требует внешних библиотек (парсер XML/JSON не считается)

  • Протокол опирается на protobuf, websockets, etc.

Синхронизм

  • Строго синхронный, без запроса клиента сервер молчит. (Пример: MODBUS)

  • Частично асинхронный: большинство пакетов только в ответ на запрос, но асинхронно (с гарантированной регулярностью) прилетает keepalive и/или актуальные данные.

  • Полностью асинхронный, ответы могут приходить не в порядке запросов (такого я не видел)

  • Симплексный протокол - никаких запросов вообще нет, сервер просто транслирует поток апдейтов по UDP на указанный в конфигурации адрес

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

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

Пример обмена пакетами для симплексного протокола

Скорость реакции

  • Время ответа не зависит от выполнения операции, итог операции считывается отдельно

  • Каждая операция выполняется синхронно, ответ может занять существенное время

Тут требуется пояснение. Одно из устройств, которые нам требовалось интегрировать, выполняло запрос на установку параметров аппаратуры действительно существенное время – буквально несколько секунд. В это время верхний уровень опрашивал микроконтроллеры блоков устройства, а контроллеры переконфигурировали аппаратуру.

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

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

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

Поллинг результатов длинного запроса. Клиент выполняет дополнительные запросы, чтобы получить результат длительной операции.

Аутентификация

  • Отсутствует (и это правильный выбор)

  • Никакая часть протокола не работает, пока не скажешь пароль

  • Часть протокола (keepalive, частичный статус) работает

Вообще, разработчики аппаратуры, как правило, делятся на две группы. Вообще нет аутентификации (и слава богу!), или она есть и реализована совершенно параноидально.

Одна из систем и вовсе загнала нас в угол своими требованиями к безопасности. Система требовала при соединении с ней логина и пароля, а при отсоединении – обязательного сообщения о выходе. С каждым логином мог одновременно подсоединяться только один пользователь, а разрыв TCP соединения система не рассматривала как выход клиента из системы. Если при отладке или в силу нестабильности связи (а тестовый контур работал по LTE) соединение рвалось, то последний юзер зависал в системе в залогиненном статусе навечно! Повторное соединение с таким же логином, как следствие, блокировалось до полной перезагрузки сервера.

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

Почему аутентификация не нужна

Потому же, почему у вас дома нет замка на входе в кухню. Все такие системы разворачиваются в закрытом контуре, и внутри контура все свои. Ключ для подсоединения к VPN – необходимая и достаточная часть защиты. Остальное только создаёт проблемы. Особенно при учёте того очевидного факта, что во всех реальных протоколах, которые нам встретились, логин и пароль передаются открытым текстом. То есть, если закрытый контур будет таки взломан, то первый же запуск wireshark сведёт ценность защиты паролем к абсолютному нулю.

 Документация

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

В целом, документация на протокол, как правило, содержит описание отдельных пакетов и ответов на них.

К сожалению, логика, смысл и последовательность отправки пакетов, как правило, не описаны. Конечно, если протокол содержит один пакет с одним битом «лампочка горит», разработчик, вероятно, осилит реализацию и восстановит семантику даже имея скромное описание.

Но, к примеру, если у вас есть пакет «регулировка усиления», то хорошо бы хотя бы грубо понимать замысел разработчика – это конфигурационный или оперативный параметр? Его будут крутить только при первичной настройке, при профилактике (раз в месяц) или прямо во время работы каждую секунду? Иногда это знание можно извлечь из руководства оператора, но чаще и оно написано ключницей содержит только скриншот родной операторской программы с движком и подписью «подвиньте вправо, чтобы увеличить усиление».

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

Отличная практика – включать в описание примеры пакетов и ответов на них – строго в том виде, в котором они бегают по сети. Мы имели немало счастливых минут, отлаживая реализацию протокола на основе protobuf. В документации забыли уточнить, что разработчик (ЗАЧЕМ!!?) обернул пакеты protobuf в собственные фемтопакеты – добавил перед ними двоичную длину пакета. Надо ли говорить, что это приводило к ошибке вида «взрыв на макаронной фабрике» – парсеру protobuf эта пара байт сносила крышу напрочь.

Отдельная беда документации – единицы измерения. Если написано «азимут», то опытный разработчик поймёт, что это значение в градусах по часовой стрелке от севера. А если написано «угол», то может быть и в радианах против часовой от востока. Если проектировщик протокола – взрослый ответственный человек, то всё будет в единицах СИ (не путать с Си), но с углом и это не спасает.

Кроме единиц хорошо бы указывать диапазон, типовое значение и наиболее применимый диапазон. Усиление от 1 до 100, стандартное значение 75, рекомендованный диапазон от 60 до 80.

Идеальный протокол

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

Транспорт

Я встречал четыре варианта. UDP, TCP, HTTP, Websockets.

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

Применение HTTP чуть менее ужасно, но, аналогично, не приносит какой-то осмысленной пользы.

Оставшиеся UDP и TCP примерно равноценны по применимости. Главное преимущество TCP вовсе не в том, что он обеспечивает надёжность – в обычной проводной сети потерять пакет ещё надо суметь, а если теряется много пакетов, как, например, при плохой радиосвязи – то TCP встаёт колом, а UDP хоть что-то доносит. Так что, фактически, в такой ситуации UDP – надёжнее.

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

Выигрывает TCP потому, что хорошо проходит через NAT, и иногда это ценно.

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

Пакеты и формат

Мне 300 лет, я выполз из тьмы и держал в руках дюймовую магнитную ленту с реализациями протокола Kermit на паре десятков языков программирования, среди которых Си шёл как «ну и вот вам версия на новомодном языке программирования от молодой операционной системы Юникс».

Число реализаций протокола Kermit

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

То есть - протоколов передачи данных я насмотрелся и напроектировался. От советского модема на 300 бод в формате кухонного шкафчика до современных 10-гигабитных сетей со всеми остановками типа xmodem, ymodem, zmodem, uucp, зоопарка протоколов FIDO, mobus, бит-ориентированных спутниковых протоколов и ещё чёрт-те-знает чего.

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

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

Почему:

  1. Потому, что уже на следующий день после того, как вы любовно распределите битики пакета между необходимыми к передаче значениями к вам зайдёт инженер и скажет, что усиление канала, которое вы так удачно влепили в 7 бит третьего байта, рядом с битом признака включения канала, теперь имеет диапазон не 0-127, а 2-255. Если протокол не бинарный, вам не придётся убивать инженера и проводить остаток жизни в тюрьме, а уже одно это – достойный аргумент, согласитесь.

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

  3. Потому, что ошибка или неполнота документации для двоичного протокола – смерть, а для текстового – неприятность.

Хорошо, предположим, я вас уговорил и вы не захлопнули сейчас крышку ноутбука с криком «двоичный протокол требует меньшей пропускной способности канала!» – а если захлопнули, то хотя бы посчитали, сколько вам этой полосы надо. А то в поганые 3 мегабита плохого LTE пролезает в секунду 360 пакетов размером в килобайт. И почти 4 тысячи пакетов по 128 байт.

Кстати. Пока мы не перешли к пакетам в текстовом формате, нельзя не упомянуть protobuf. Это шикарная библиотека для организации протоколов передачи данных от Google. Двоичных протоколов.

Очень хорошая. Прекрасная. Мультиязычная. Просто прелесть.

Не используйте ее никогда.

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

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

Имейте в виду, что пакеты protobuf НЕ СОДЕРЖАТ версии спецификации протокола и не имеют никакой избыточности. Если на двух сторонах разная спецификация – библиотека будет просто сходить с ума без внятной диагностики.

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

Текстовые протоколы

Executive summary: Пакет должен представлять собой JSON в отдельной строке, которая заканчивается символом \n. Это идеал, который нельзя превзойти. Дальше статью можно не читать.

Отдельная ремарка для программистов, которые до сих пор не покидали лона великого и бессмертного языка Си: в других языках символ двоичного нуля не имеет сакрального статуса и заканчивать им строку в протоколе – плохая идея. Просто потому, что функция считывания строки до \n есть везде, а до \0 – не везде.

Почему не XML:

  • Закрывающие теги реально впустую тратят полосу в канале. Даже мне жалко.

  • Его тяжелее парсить. Это мелочь, но она будет зря потреблять ваш процессор.

  • Его тяжелее читать глазами. Отладка будет сложнее.

Критичной разницы нет, и если XML вам очень мил, то – пожалуйста. Но по сумме баллов он уступает везде, и ни в чём не выигрывает.

Кроме того, библиотеки работы с JSON типично поддерживают прямое преобразование пакета в объект целевого языка.

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

Аутентификация

Как я уже написал выше, никакого смысла защита с помощью открытого логина и пароля не имеет. Просто откажитесь от аутентификации, и люди будут вам благодарны. Если же процент вахтёра в Вашей душе зашкаливает и не велит Вам принимать простые и честные решения – сделайте парольную защиту опциональной и реализуйте её в режиме запрос-ответ. Сервер передаёт клиенту случайную комбинацию байт, клиент добавляет к ней пароль, вычисляет хеш от результата и передаёт его серверу. Сервер вычисляет хеш аналогичным образом и сравнивает результат. Это обеспечивает хоть какую-то защиту.

Итоги

Завершая эту статью я хочу зафиксировать один факт: мы реализовали несколько десятков различных протоколов, разработанных авторами различных российских и китайских устройств. Ни разу не получилось так, чтобы код, реализованный согласно описанию, заработал сразу и полностью и с реализацией не случилось никаких проблем. В 100% случаев уже написанный код потребовал отладки, в описании были найдены ошибки или неполнота и в сумме объём работы по реализации протокола оказался в 3-10 раз больше, чем можно было бы предполагать.

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

В целом цикл интеграции прибора выглядит примерно так:

  • Читаем спецификацию.

  • Всё понимаем, радостно киваем головой.

  • Пишем реализацию за полдня.

  • Неделю бегаем за контактом с той стороны со словами «вы обещали симулятор». За эту неделю его на той стороне дописывают до состояния «падает не сразу, а только через 15 минут».

  • Запускаем симулятор. Ничего не работает.

  • Добиваемся, чтобы дали прямой контакт разработчика, который с той стороны писал код.

  • Он читает спецификацию, пытается уточнить, какой нехороший человек, редиска, её писал. Вспоминает, что он сам и писал. Грустит.

  • Уточняет, что CRC считается по другом алгоритму, а в пакете есть ещё поле статус. Которое, правда, пока всегда нулевое, но потом обязательно будет использоваться.

  • Всё понимаем, грустно киваем головой. Корректируем код.

  • Проходят первые два пакета. Ещё пара дней работы – и жизнь с симулятором начинает налаживаться.

  • Настала пора тестировать на реальном железе. Тут идеально, если у разработчика железа в офисе есть экземпляр устройства, к которому можно организовать доступ по VPN. И это устройство не позапрошлой версии.

  • Если нет – нужно получить экземпляр на тестирование, где-то его разместить, организовать канал связи и физический доступ к устройству. Чтобы его иногда перезагружать.

  • Дальше выяснится, что симулятор местами большой оптимист и реальное железо работает иначе. Цикл коррекции кода может занять прилично времени.

  • Но – всё со временем наладится, вы напишете полноценный драйвер, UI компоненты для управления устройством (а в нашем случае ещё и транспортные компоненты для управления с других узлов нашей распределённой системы), прогоните тесты, и радостно объявите, что дело сделано, устройство интегрировано.

  • Устройство будет закуплено вашим заказчиком, установлено на площадку, ваша система будет запущена для работы с ним...

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

На сём почти прощаюсь. Проектируйте простые тестовые протоколы. Или обращайтесь к нам, мы поможем.

Если эта тема вам интересна – 10 декабря мы (DZ Systems и AXIOM) планируем провести вебинар, на котором будем поднимать тему разработки на Java того, что, как было принято считать, можно написать только на Си. А именно – систем, работающих с аппаратурой в реальном времени.

Участие бесплатное, но необходима предварительная регистрация.

 

Комментарии (0)