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

Но помимо серверной части, как известно, есть и клиентская часть. И вот здесь хотелось бы остановится более основательно и описать, с чем пришлось столкнуться и на что можно было повлиять. Конечно, при использовании JavaScript особо “порезвиться” не удастся, ибо всё готово и закрыто, а вот про клиента на Java можно что-то рассказать.

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

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

  • apache-mime4j-core-0.7.2.jar;
  • httpclient-4.2.1.jar;
  • httpcore-4.2.1.jar;
  • httpmime-4.2.1.jar.

Что можно сказать об их использовании? Программное обеспечение Apache всегда славилось своей надёжностью, проработанностью, оптимальностью. Клиент для Android успешно работал. Размер готового файла *.apk не был критически большим. Каких-то особых нареканий по работе этих библиотек не было. Но жизнь всегда умнее нас. И время (а это период примерно четыре-пять лет) вносит свои коррективы. Приложение было написано, когда была версия Android 4.2 — 4.4. А необходимость новых решений возникла уже в этом году, когда уже вовсю пошли устройства с версией 10.

Разработка велась в своё время на Eclipse для Windows 7. Обновление SDK для Android до нужного уровня привело к тому, что маленький винчестер SSD ёмкостью 128 Гб переполнился. Пришлось перейти на Android Studio. Более того, пришлось сменить и базовую операционную систему. Попробовал установить Ubuntu (точно не помню номер версии) и уже в этой среде использовать Studio. Но опять неудача, Andriod Studio упорно не хотела устанавливаться.

Почему — уже подзабылось. В конце-концов, по совету друзей, установил последнюю версию Linux-Mint, и, о чудо, инструментарий лёг на неё без нареканий. Далее собственно пошло то, из-за чего именно эти все подробности и описывались, а именно рутина написания тестов.

Итак, что же ожидало в этой тягомотине? Начнём пожалуй с того, что с официального сайта Apache скопировал более актуальные версии вышеупомянутых библиотек. Добавил их в проект и… И посыпались ошибки компиляции. Время прошло, интерфейсы классов поменялись. Так что пришлось (из-за нехватки времени на изучение новых библиотек) вернуться к старым версиям. Но…

Но опять же, мы не ищем простых путей. Подумалось, а зачем мне полностью эти библиотеки? Тексты этих пакетов есть. Что если взять и вытащить только необходимые классы из них? Тем более, что при рассмотрении текстов модуля для работы с вебсокетами, можно было увидеть только два класса из этих библиотек.

Итак создал новый проект, и стал добавлять необходимые классы. И в итоге получилось, что для успешной компиляции необходимо вытащить 32 класса. И таки да, проект заработал. Всё задышало. Соединение с сервисом вебсокетов проходило успешно. И всё бы хорошо, но заметил следующее, непонятное для меня, событие. При закрытии соединения, модуль отвечающий за соединение, выкидывал исключение:

java.io.EOFException
at java.io.DataInputStream.readByte(DataInputStream.java:77)
at com.example.wsci.HybiParser.start(HybiParser.java:112)
at com.example.wsci.WebSocketClient$1.run(WebSocketClient.java:144)
at java.lang.Thread.run(Thread.java:818)

Я был в недоумении. Смущало следующее. Соединение с сервером успешно проходило. Пакеты успешно и приходили, и уходили. Но, почему именно закрытие выкидывало исключение? Причём на сервере всё было штатно. Явно где-то в тексте клиенты была какая-то мелочь, которая влияла на закрытие. Более того, тексты показали такую особенность. Согласно пункта 7.1.1 документа закрытие со стороны клиента заключается не просто в вызове метода close(), а в формировании и отправки пакета с кодом операции 8 (операция закрытия). В этом случае сервер посылал бы свой пакет закрытия, после принятия которого клиент закрывал бы соединение. Но в нашем случае такой последовательности вызовов не наблюдалось. Просто вызывалась функция close и всё. В общем было над чем подумать. И чем больше я всматривался и изучал тексты этого модуля с парсером пакетов, тем меньше он мне нравился, тем больше возникало желание переписать их со своим видением работы данного протокола. В конце-концов было принято решение осуществить этот “трудовой подвиг”.

Что собственно не устраивало, что вызывало “гражданский протест” в этих модулях? Во-первых, организация взаимодействия между модулем непосредственного соединения с сервером и парсером пакетов. Получалось так, что модуль соединения выполнял взаимодействие с сервером, порождал парсер, которому в качестве параметра передавал ссылку на себя. А итоге парсеру делегировались полномочия по принятию решения на наступающие, сетевые события. В связи с этим возник вопрос, а хорошо ли это? Не лучше ли было бы, если бы модуль парсера выполнял бы свою соответствующую миссию, возвращал бы результат своей работы, а вот управляющее решение по событиям выполнял бы уже объект, породивший парсер? В этом случае определялась бы строгая иерархия взаимодействия между объектами. (Тут, конечно, можно подискутировать, что лучше — иерархия или сеть, но тогда мы отойдём от темы.)

Второе, что вызывало желание переписать всё — это структура парсера. Этот объект (класс) должен выполнять две основные функции, а именно формировать пакет данных для передачи на сервер и парсинг принятых от сервера пакетов. Так вот, именно эти две функции и не устраивали по большому счёту. И вот чем.

Представим, что произошло сетевое событие, пришёл некий пакет. Что делал HybiParser в этом случае? Этот объект по-байтно считывал из входящего потока сокета первые два байта и определял уже последующие свои действия: парсинг размера данных, маски и т.д. В итоге это реализовывалась в несколько операций считывания из входного потока сокета. Более того, парсинг усложнялся стадиями считывания, что ещё более запутывало алгоритм. И опять возникал вопрос, а правильно ли это, зачем такие сложности? Не лучше ли считать пакет одной операцией, тем более, что размер входящих данных можно определить?

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

В результате оценки всех этих нюансов определились следующие требования к проектированию необходимых модулей:

  • Модули должны быть независимыми от сторонних библиотек;
  • Модули должны быть простыми и легко встраиваемыми в другие проекты;
  • Модули должны быть готовы для последующего функционального расширения.

Ну, и чтобы не обвинили в излишней критике к прошлому готовому решению, добавим к этому дополнительно то, что часть готовых и не вызывающих нареканий функций, можно было бы спокойно перенести в новую реализацию. Ну, всё, и как говорилось на XXII съезде КПСС: “Наши цели ясны, задачи определены. За работу, товарищи! За новые победы коммунизма!”.

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

  • Модуль глобальный констант протокола WebSocket уровня 07;
  • Вспомогательный класс исключений;
  • Вебсокет-клиент;
  • Модуль для парсинга пакетов протокола WebSocket уровня 07.

В первых двух модулях реализация тривиальная, рассматривать там нечего. В модуле клиента реализуется управление соединением с сервером, и здесь хотелось бы остановится на следующих моментах. В функции открытия соединения есть цикл заголовков, приходящих от сервера. Здесь собственно реализуется парсинг ключа “Sec-WebSocket-Accept”, и в нашем случае парсинг осуществляется без использования библиотек Apache.

Далее следует обратить внимание на функции управления циклом пакетов. Реализация тривиальная, через объект синхронизации.

Следующим моментом, требующим почтения, относится к функции цикла. Цикл не “вечный”, а с выходом по условию проверки объекта синхронизации. В цикле прибывший пакет считывается одной операцией. Пакет разбирается соответствующим объектом для парсинга. Далее принимается управляющее решение по наступившему сетевому событию.

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

Класс, реализующий разбор WebSocket-пакетов содержит два метода, требующих внимания: собственно парсинг и формирование пакета для передачи по соответствующим параметрам. При парсинге все флаги и данные из принятого пакета запоминаются в публичных, переменных класса. Почему публичных? Да для простоты, чтобы не создавать дополнительные функции get/set к ним.

Ну, собственно всё, дорогой читатель. Архив с проектом для Android Studio прикреплён. Каких-либо претензий на использование этих текстов в ваших проектах я не буду. Конструктивная критика принимается. Отвечать на вопросы — по мере возможности.

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


  1. juDge
    10.12.2019 00:01

    Буквально на дня пересобрал пару вариантов из «интернета» на базе PHP.
    Суть задачи: сделать шлюз данных с прокидыванием части команд на HomeAssistant.
    Как итог получил решение «tcpserver+php скрипт». Сам сервер общается через стандартные дескрипторы STDIN и STDOUT. Решение получилось асинхронное. Получается команда через WebSocket соединение. Далее команда отправляется в отдельный поток. По окончании выполнения потока, серверная часть забирает результат и уже отдается клиенту. Так же в серверном потоке мониториться папка на предмет появления новых файлов, получается что-то типа общения с внешними источниками. Например событие с IP домофона прилетает и кладется в папку событий. Сервер WebSocket видит событие и оповещает об этом уже клиента. Так же в серверном потоке крутиться клиент WebSocket. Который обеспечивает мониторинг событий и состояний элементов умного дома подключенный к HomeAssistant. Как результат, самописный интерфейс умного дома имеет одну точку входа. Которая объединяет и собирает разнородные элементы умного дома в той форме и в том виде какой мне нужен.
    В принципе работает достаточно стабильно, правда пришлось принудительно слать «пинги» для поддержания подключения как в серверной части так и в клиентской. WebSocket cервер может отдавать данные по погоде, прогноз погоды на два дня вперед, данные по трафику в городе (кол-во баллов), отправлять SMS через 4G модем, оповещать о движении перед дверью (IP домофон сам оповещает и это событие проваливается всем клиентам). Ну и получать события о изменении состояния элементов умного дома, а так же изменять это стояние.


    1. WitNt Автор
      10.12.2019 08:32

      Я сам, коллега, тоже присматриваюсь к «интернету вещей». И, как вариант, использовать канал с такой технологией представляется весьма перспективным. Кстати, была идея сделать специальную службу для «операционки» (для windows и linux), с которой можно было бы через интернет связаться по этой технологии. Для чего? Ну, например, чтобы отдать какую-то команду. В общем для удалённого контроля. Я понимаю, что можно использовать для этого туннели, но просто, как и такой вариант


      1. juDge
        10.12.2019 12:07

        Для удаленного управления прощу и функциональней поставить SSH сервер.
        В данном случае у меня была задача сделать более интересный интерфейс для экрана висящего на стене, чем стандартный от Home Assistant. И плюс подключить функционал сторонний, который не заведен на Home Assistant. Например общение с ip видео домофоном происходит через SipJS напрямую прямо через экран на стене. Погода опять же берется не с Home Assistant а с городского портала и красивыми картинками подстать погоде и тд. Пока еще не весь функционал умного дома реализован и подключен к Home Assistant, на текущий момент пока установлено только управление освещением, управление отоплением (батареям, через регуляторы MAX!) и бризерами Тион. Плюсом будет управление умным пылесосом, бойлером и тд. При любом раскладе все команды почти прозрачно будут просто проваливаться через WebSocket клиента до Home Assistant. Эдакий прокси сервер получается. Правда ответ от Home Assistant «заужается», потому как «экрану» не интересны все поля и как результат ему отдается только нужная ему информация.
        В качестве серверной части как вариант можно было еще использовать http long polling но WebSocket показался более интересным. По крайней мере для моей задачи это уменьшает шанс потерять сообщение от Home Assistant о например изменении состояния выключателя. Вопрос для меня только сейчас один интересен. Как обеспечить большое кол-во подключений WebSocket, тогда бы я его внедрил уже в решение B2B над которым с некоторой периодичностью приходиться заниматься.


        1. WitNt Автор
          10.12.2019 13:17

          Home Assistant… Что-то не попадалось мне такое. Это какой-то стандарт?


          1. juDge
            10.12.2019 14:50

            Платформа для организации умного дома. С ним мой шлюз общается по средствам WebSocket соединения. Собственно туда и прокидываю часть команд поступающих на мой websocket сервер.


            1. WitNt Автор
              12.12.2019 08:39

              Почитал за Home Assistant. Прикольно. Т.е. я понял, что в доме устанавливается что-то типа сервера? Там промышленный компьютер ставится что ли? Мне вот эта штука очень интересная. Появляется много информации по этому поводу. Но натурально пока не доводилось встретить. В принципе можно было бы и заказчика подыскать под это дело. Появляются «богатенькие буратины». Правда у них максимальные запросы выливаются полка в установке видеонаблюдения, в том числе и через интернет.