Рубріки: Досвід

Як інтегрувати на проєкті WebSocket та не згоріти: покрокова інструкція

Роман Дашківський

Привіт! Мене звати Роман Дашківський, я Java Developer в NIX та спікер IT-конференції NIX MultiConf. У цій статті я розповім, з якими труднощами можна зіткнутися під час інтегрування на проєкті WebSocket та як їх подолати.

Уявімо простий ToDo-менеджер. Його клієнт (тобто вікно браузера) надсилає запити на сервер та отримує відповідь. Для того, щоб інший клієнт побачив будь-які оновлення, він має зробити запит та отримати ці дані.

Але що робити, коли клієнт вимагає від користувачів спостерігати всі апдейти «на льоту»?

На жаль, класичний HTTP не дозволяє вирішити цю задачу. Скоріш за все, коли ви почнете гуглити якусь альтернативу, то зустрінете багато інформації про вебсокети.

Що таке вебсокет

То що ж таке сокети, навіщо їх використовувати та як взагалі працюють вебзастосунки? Все це я поясню далі на простому прикладі.

Вебсокет — це протокол, який на відміну від HTTP, дозволяє організувати постійний двонаправлений звʼязок між клієнтом і сервером.

Життєвий цикл зʼєднання складається з трьох етапів:

  1. Ініціалізація зʼєднання — відбувається handshake-запит по HTTP, після чого зʼєднання оновлюється до WebSocket.
  2. Пересилання даних — відправка повідомлення може відбуватися від клієнта на сервер і навпаки.
  3. Розірвання зʼєднання — ініціатором розриву може бути як клієнт (наприклад, закриття вкладки), так і сервер (програмно).

Підключення до SpringBoot-застосунку

Давайте розглянемо, як це все підключити до класичного SpringBoot-застосунку.

Попередня підготовка

Будь-які апдейти починаються з чого? Правильно — з додавання необхідних залежностей:

  • Для простоти використання Spring має безліч стартерів різноманітних кольорів та розмірів. Нас цікавлять сокети, тому додамо відповідний стартер.
  • Так як ми будемо працювати з даними JSON-формату, додамо JSON від гуглів.
  • У кількох місцях я використовуватиму деякі утиліти, що спрощують процес парсингу URL. Додаємо і третю залежність від Apache.

Попередня підготовка завершена. Перейдемо до конфігурації Spring.

Конфігурація Spring

Зазвичай значна частина подібної «магії» відбувається у классах відмічених відповідною анотацією Configuration. Важливо додати ще одну — @EnableWebSockets.

У документації до неї сказано, що ми маємо реалізувати інтерфейс WebSocketConfigurer. Як бачите, інтерфейс має лише один метод. Тут ми можемо додати обробники повідомлень та перехоплювач хендшейку. Таким чином у нас відбувається валідація зʼєднання.

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

 

Обробник подій

Перейдемо до наступного кроку — обробник подій. У нашому прикладі запланована обробка лише текстових повідомлень, тому покладаємось на абстрактний клас TextWebSocketHandler. Повідомлення ми будемо надсилати лише з серверу на клієнт, тож метод handleTextMessage можна залишити пустим.

Сховищем зʼєднань буде звичайна мапа, де ключем є юзернейм користувача, а значенням — перелік усіх його відкритих сесій (вкладок браузеру).

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

UI-частина

Як це виглядає з точки зору UI-частини? Як я вже зазначав, при ініціалізації сокету ми не можемо додавати ні хедери, ні тіло запиту. Але завжди є можливість передати параметри через URL, що ми і зробимо.

Далі додаємо всі необхідні слухачі подій — і ось, як це виглядає схематично:

У реальності ж один клієнт надсилає HTTP-запит на оновлення даних. Після чого в контролері ми викликаємо метод notifyAll або notifyUser з однієї з попередніх схем. Тут з’являється трошки магії React. Як бачимо, дані оновлюються одразу у двох вікнах.

Усе кльово, все працює. Та якби все було так просто, я би не писав цю статтю…

В enterprise-проєктах зазвичай важливим фактором є продуктивність. Однак часто одні й ті самі мікросервіси можуть існувати у декількох екземплярах, доступ до яких здійснюється безпосередньо через Load Balancer. І в залежності від завантаження CPU, зайнятості оперативної памʼяті або інших параметрів можуть створюватися нові інстанси або видалятись існуючі. Але як поточна реалізація працюватиме в такому випадку?

Наша мапа з конекшинами зберігається In-Memory всередині кожного інстансу. Коли ми ініціалізуємо зʼяднання по WS, ELB обирає, до якого сервісу буде надіслано запит, і конекшн зберігається лише в ньому. Коли будь-який сервіс проходитиме по мапі, ймовірно буде така ситуація, що частина клієнтів так і не отримає повідомлення.

Для того, щоб змоделювати подібний кейс, піднімаємо другий інстанс нашого SpringBoot-застосунку на іншому порті та моделюємо поведінку LoadBalancer, додавши звичайний рандомайзер на отриманні хосту.

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

Вдалим способом вирішення проблеми є створення єдиного сховища подій, яке буде повідомляти всім нашим інстансам про те, що кожен має відправити повідомлення клієнтам, які збережені у них самих. У нагоді тут може стати Message Broker, але я обрав ActiveMQ з простої та банальної причини — він один із найпопулярніших сьогодні. Тож продовжуємо кодити…

Як завжди, спершу оновлюємо залежності. Я вирішив заюзати таку круту штуку, як Apache Camel. Це відкритий кросплатформенний Java-фреймворк, який дозволяє проводити інтеграцію застосунків у простій та зрозумілій формі. Camel дуже спрощує флоу обробки даних. Особливо це зручно, якщо треба побудувати ланцюг із декількох обробників. Також він дуже легко інтегрується зі Spring.

Не порушуємо традицій і далі переходимо до Spring-конфігів. Тут необхідно додати два біни:

  • ActiveMqConnectionFactory, якому ми скормлюємо пропси з application.properties;
  • ActiveMQComponent — компонент Camel, який надалі спростить процес відправки й отримання повідомлень із брокера.

Далі створюємо два методи з тими ж сигнатурами, що й були раніше. Так як я розповідаю про сокети, а не про Camel, довго зупинятись на ньому не будемо.

Єдине, що хочу підкреслити — спосіб, у який ми передаємо інформацію про користувача, котрому відправляємо повідомлення. Це робиться за допомогою Header, які пізніше можно буде зчитати при отриманні повідомлення від брокера.

Після того, як ми надіслали ці повідомлення, їх необхідно отримати на кожному інстансі. Тут виникає одна із причин, чому я додав до проєкту Camel — неймовірна простота у використанні. Ось приклад зчитування та логування меседжу.

Однак просто логувати повідомлення недостатньо. Далі необхідно відправити його всім клієнтам.

Для цього створюємо обробник:

  • парсимо тіло повідомлення зі звичайної String у JSON-обʼєкт та потім до нашого Java-обʼєкту;
  • зчитуємо хідер, який ми засетили раніше;
  • пересилаємо далі вже існуючими методами;
  • додаємо його до нашого Data Flow в один рядок.

Нарешті переходимо до тесту. Маємо клієнтів, підʼєднаних до двох різних інстансів.

Виконуємо апдейт на одному та спостерігаємо оновлення на одразу обох:

Тепер все працює. Хотів би я так сказати, але…

Додаємо ще ендпоінт

Усе чудово працюватиме, коли ми оперуємо невеликими обʼєктами. Але знову ж таки у реальних проєктах дуже багато систем, які обробляють великі обʼєми даних. Тож нехай і у нас буде такий ендпоінт, який повністю переписуватиме всі ToDo-списки, які стосуються поточного користувача. На вхід він приймає массив цих списків, розмір яких обмежений лише фантазією.

Таким чином повідомлення з оновленими списками будет надходити до брокера, звідки бродкаститись на всі інстанси. Однак брокери повідомлень не призначені для таких цілей. Вони швидко й ефективно працюють із невеликими обʼємами даних.

Як фіксити проблеми з продуктивністю

В іншому випадку це може призвести до проблем із продуктивністю та формуванням так названого Bottlneck. Отже, як це пофіксити?

Застосуємо оновлений підхід. Для подібного типу повідомлень будемо відправляти не весь оновлений обʼєкт, а інформацію про те, який обʼєкт змінився та як саме. Після цього SpecProcessor на інстансах самостійно зберуть усі нові дані та розішлють їх клієнтам.

Усе що потрібно нам зробити — трохи змінити формат даних, які надсилатимуться на брокер. Створимо примітивну версію спеки, яка включає:

  • класнейм процессора для обробки повідомлень;
  • параметри у форматі мапи.

Далі створюємо процесор, який витягуватиме оновлені дані з БД, 3rd Party Service або з іншого місця та передаватиме їх далі.

Після цього оновлюємо наш Data Flow у роутері. Якщо знайдено відповідний процесор, на нього і йде обробка. Потім відбувається відправка повідомлення клієнтам по WebSocket.

Ось спрощена схема, як це працює:

Замість висновку

Давайте повернемося до того, з чого починали, і поглянемо, як це все перетворилося на систему з двонаправленим звʼязком. Ця система може легко масштабуватися і не створює зайвого навантаження на наші ресурси:

 

Сподіваюсь, тепер при необхідності реалізувати сокети ви заздалегідь продумаєте всі деталі, будете знати, з якими труднощами можете зіткнутися та як їх подолати. А якщо хочете детальніше розглянути наведений у цій статті код, переходьте за посиланнями на репозиторії з фронтом і бекендом:

Якщо ви знайшли помилку, будь ласка, виділіть фрагмент тексту та натисніть Ctrl+Enter.

Останні статті

IT в Україні йде до свого фінального кінця. І потраплятимуть туди виключно за покликом душі

Коротко про українську IT-сферу у 2024 році Це коли на одну вакансію Middle розробника по…

26.03.2024

Блокчейн-розробка сьогодні: зарплати і перспективи на ринку праці

Формування криптовалютної галузі в Україні почалося ще у 2014 – саме тоді з'явилися перші стартапи,…

18.03.2024

Скільки рішень ухвалює розробник? Погляд новачка, який запускає продукт

Автор цього блогу — Python-девелопер Сергій Солдатов, який вирішив створити досить унікальний продукт. І це…

12.03.2024

Чи треба готуватись до співбесіди?

Думки шукачів діляться на: «так, однозначно» і «ні, не вартує, я все і так про…

04.03.2024

Відкладаєте до останнього? Що таке «синдром студента» і як з ним боротися

Синдром студента — це форма прокрастинації, яка полягає в тому, що людина, якій дали завдання,…

23.02.2024

Вчимося працювати з Git: основи конфігурації, гілки, додавання файлів та директорій

Git — це найпопулярніша CVS прямо зараз, яка дозволяє відстежувати історію розробки і спільно працювати.…

20.02.2024