Привіт! Мене звати Роман Дашківський, я Java Developer в NIX та спікер IT-конференції NIX MultiConf. У цій статті я розповім, з якими труднощами можна зіткнутися під час інтегрування на проєкті WebSocket та як їх подолати.
Уявімо простий ToDo-менеджер. Його клієнт (тобто вікно браузера) надсилає запити на сервер та отримує відповідь. Для того, щоб інший клієнт побачив будь-які оновлення, він має зробити запит та отримати ці дані.
Але що робити, коли клієнт вимагає від користувачів спостерігати всі апдейти «на льоту»?
На жаль, класичний HTTP не дозволяє вирішити цю задачу. Скоріш за все, коли ви почнете гуглити якусь альтернативу, то зустрінете багато інформації про вебсокети.
То що ж таке сокети, навіщо їх використовувати та як взагалі працюють вебзастосунки? Все це я поясню далі на простому прикладі.
Вебсокет — це протокол, який на відміну від HTTP, дозволяє організувати постійний двонаправлений звʼязок між клієнтом і сервером.
Життєвий цикл зʼєднання складається з трьох етапів:
Давайте розглянемо, як це все підключити до класичного SpringBoot-застосунку.
Будь-які апдейти починаються з чого? Правильно — з додавання необхідних залежностей:
Попередня підготовка завершена. Перейдемо до конфігурації Spring.
Зазвичай значна частина подібної «магії» відбувається у классах відмічених відповідною анотацією Configuration
. Важливо додати ще одну — @EnableWebSockets
.
У документації до неї сказано, що ми маємо реалізувати інтерфейс WebSocketConfigurer
. Як бачите, інтерфейс має лише один метод. Тут ми можемо додати обробники повідомлень та перехоплювач хендшейку. Таким чином у нас відбувається валідація зʼєднання.
Враховуючи те, що при ініціалізації сокету ми не маємо можливості передати жодних параметрів у тілі або хедерах, єдиним варіантом для того, щоб ідентифікувати користувача, залишаються URL-параметри. Тож тут ми можемо зчитати наш токен, верифікувати його та, якщо він валідний, підтвердити створення зʼєднання.
Перейдемо до наступного кроку — обробник подій. У нашому прикладі запланована обробка лише текстових повідомлень, тому покладаємось на абстрактний клас TextWebSocketHandler
. Повідомлення ми будемо надсилати лише з серверу на клієнт, тож метод handleTextMessage
можна залишити пустим.
Сховищем зʼєднань буде звичайна мапа, де ключем є юзернейм користувача, а значенням — перелік усіх його відкритих сесій (вкладок браузеру).
Тепер відправка повідомлень вже тривіальна задача. Спробуємо реалізувати можливість надсилати повідомлення як усім, так і одному окремому користувачу.
Як це виглядає з точки зору 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 — неймовірна простота у використанні. Ось приклад зчитування та логування меседжу.
Однак просто логувати повідомлення недостатньо. Далі необхідно відправити його всім клієнтам.
Для цього створюємо обробник:
Нарешті переходимо до тесту. Маємо клієнтів, підʼєднаних до двох різних інстансів.
Виконуємо апдейт на одному та спостерігаємо оновлення на одразу обох:
Тепер все працює. Хотів би я так сказати, але…
Усе чудово працюватиме, коли ми оперуємо невеликими обʼєктами. Але знову ж таки у реальних проєктах дуже багато систем, які обробляють великі обʼєми даних. Тож нехай і у нас буде такий ендпоінт, який повністю переписуватиме всі ToDo-списки, які стосуються поточного користувача. На вхід він приймає массив цих списків, розмір яких обмежений лише фантазією.
Таким чином повідомлення з оновленими списками будет надходити до брокера, звідки бродкаститись на всі інстанси. Однак брокери повідомлень не призначені для таких цілей. Вони швидко й ефективно працюють із невеликими обʼємами даних.
В іншому випадку це може призвести до проблем із продуктивністю та формуванням так названого Bottlneck. Отже, як це пофіксити?
Застосуємо оновлений підхід. Для подібного типу повідомлень будемо відправляти не весь оновлений обʼєкт, а інформацію про те, який обʼєкт змінився та як саме. Після цього SpecProcessor
на інстансах самостійно зберуть усі нові дані та розішлють їх клієнтам.
Усе що потрібно нам зробити — трохи змінити формат даних, які надсилатимуться на брокер. Створимо примітивну версію спеки, яка включає:
Далі створюємо процесор, який витягуватиме оновлені дані з БД, 3rd Party Service або з іншого місця та передаватиме їх далі.
Після цього оновлюємо наш Data Flow у роутері. Якщо знайдено відповідний процесор, на нього і йде обробка. Потім відбувається відправка повідомлення клієнтам по WebSocket.
Ось спрощена схема, як це працює:
Давайте повернемося до того, з чого починали, і поглянемо, як це все перетворилося на систему з двонаправленим звʼязком. Ця система може легко масштабуватися і не створює зайвого навантаження на наші ресурси:
Сподіваюсь, тепер при необхідності реалізувати сокети ви заздалегідь продумаєте всі деталі, будете знати, з якими труднощами можете зіткнутися та як їх подолати. А якщо хочете детальніше розглянути наведений у цій статті код, переходьте за посиланнями на репозиторії з фронтом і бекендом:
Днями я завзято нила про щось ChatGPT (експериментую між сеансами з живим терапевтом). І от…
«Крутіть колесо, щоб отримати знижку до 50%!» «Натисніть тут, щоб відкрити таємничу пропозицію!» «Зареєструйтесь зараз,…
Дуже хочеться робити якісь десктопні апки. Сумую за часами коли всі програми були offline-first, і…
Надсилаючи криптовалюту, багато новачків ставлять запитання: як працюють комісії та чому вони відрізняються в різних…
Нова афера набирає обертів — ось детальний розбір того, як фальшиві потенційні роботодавці намагаються вкрасти…
Соцмережа з можливістю вбудовувати повноцінні додатки прямо в пости — звучить як фантастика, але Farcaster…