Pull to refresh

Websockets. Некоторый опыт разработки и эксплуатации. Модифицируем клиента

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

Но помимо серверной части, как известно, есть и клиентская часть. И вот здесь хотелось бы остановится более основательно и описать, с чем пришлось столкнуться и на что можно было повлиять. Конечно, при использовании 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 прикреплён. Каких-либо претензий на использование этих текстов в ваших проектах я не буду. Конструктивная критика принимается. Отвечать на вопросы — по мере возможности.
Tags:
Hubs:
+3
Comments6

Articles