WebSocket Реалізація web-додатка з використанням Jetty Web Socket

Доброго дня, Хабражителю!

Вітаю всіх та кожного з великим Днем Програміста! Бажаю робочого коду, впевнених сокетів та найпросунутіших користувачів!

Працюючи над автоматизацією концертної агенції, мені на якомусь етапі розробки знадобилася система повідомлень. Доступ до автоматизації відбувається через написаний мною web-додаток. І, відповідно, миттєві повідомлення повинні надходити до браузера користувача.

Для реалізації такого завдання є три рішення:

  • «нескінченний iframe»,
  • використовуючи XMLHttpRequest (a.k.a. Ajax),
  • використовуючи WebSocket.
Перше рішення я відразу «відкидаю» (причини пояснювати не буду, web-розробники мене зрозуміють).

Друге рішення подобається набагато більше, але має свої мінуси:

  • браузер відправляє запит кожну секунду, створюючи зайве навантаження на:
  • сервер;
  • ОС, де працює браузер;
  • і ще раз на сервер, оскільки сервер постійно виконує запит БД на вибір останніх повідомлень.
  • важко відстежити онлайн-статус користувача (тобто потрібно, наприклад, зберігати сесії у БД і постійно моніторити кожну на timeout).
  • Третє рішення - саме те, що лікар прописав.

    Серед мінусів WebSocket важливий тільки той, що його поки що підтримують лише браузери webkit (вони ж Google Chrome і Apple Safari).

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

    Реалізація клієнтської частини

    Надсилати повідомлення на сервер можна методом send(): socket.send(messageString);

    Реалізація серверної частини

    Реалізація рішення на сервері виглядає значно складніше. У мережі можна знайти кілька варіантів реалізації, з них найбільш ближчими до мене були:

    • JWebSocket,
    • Jetty WebSocket,
    • node.js WebSocket.
    З них JWebSocket виділяється тим, що це великий фреймворк для роботи з WebSocket, що одночасно є ще й надійним standalone-сервером. JWebSocket вимагає окремої посади. А зараз ми зупинимося на найпростішому варіанті рішення на платформі J2EE: Jetty.

    Що ми маємо? Погляньмо на схему.

    У моєму випадку ми маємо контейнер сервлетів GlassFish на порту 8080. Браузер відправляє запит на GlassFish [1], який передає сторінці чату браузеру [2], яка за бажанням користувача з'єднується з сервером через порт 8081 за протоколом WebSocket [3]. Далі відбувається повнодуплексний обмін даними між браузером та сервером [4].

    На момент написання посту остання версія Jetty 8.0.1.v20110908. Завантажуємо, розпаковуємо (вибачаюся перед усіма розробниками, які використовують Maven), з дистрибутива нас цікавлять 6 бібліотек:

    • jetty-continuation-8.0.1.v20110908.jar,
    • jetty-http-8.0.1.v20110908.jar,
    • jetty-io-8.0.1.v20110908.jar,
    • jetty-server-8.0.1.v20110908.jar,
    • jetty-util-8.0.1.v20110908.jar,
    • jetty-websocket-8.0.1.v20110908.jar.
    Додаємо ці бібліотеки до проекту.

    Повертаючись до клієнтської частини, завантажуємо її звідси (не хочу засмічувати пост листингом HTML-коду). Додаємо файл chat.html у Web Pages проекту. У дескрипторі розгортання (web.xml) вказуємо chat.html як "welcome-file".

    Тепер трохи про Jetty.

    Server jetty = новий Server (8081);

    Далі нам потрібно додати в jetty потрібні handler і запустити його. Запускається та зупиняєтьсяjetty методами без параметрів start() та stop() відповідно. А ось handler нам треба буде написати свій, створюємо новий клас. Щоб ми мали handler для обробки з'єднань за протоколом WebSocket, ми маємо його успадковувати від org.eclipse.jetty.websocket.WebSocketHandler:

    public class ChatWebSocketHandler extends WebSocketHandler … > У класу WebSocketHandler є один абстрактний метод doWebSocketConnect(). Він, власне, і викликається, коли браузер відкриває нове з'єднання з Jetty і повертає об'єкт WebSocket.

    Клас WebSocket нам теж слід визначити свій, і ми будемо успадковувати його від інтерфейсу org.eclipse.jetty.websocket.WebSocket.OnTextMessage — цей інтерфейс працює з даними, що проходять через протокол WebSocket, як з текстовими даними. Інтерфейс org.eclipse.jetty.websocket.WebSocket.OnTextMessage містить три методи:

    • onOpen() – викликається після відкриття сокету;
    • onClose() – викликається перед закриттям сокету;
    • onMessage() – викликається коли надходить повідомлення від клієнта.
    Начебто все просто! Давайте подивимося на листинг ChatWebSocketHandler.

    Що таке протокол обміну?

    Тепер перейдемо до відповідального моменту — запуск сервера jetty. Отже, ми маємо завдання: при старті контейнера сервлетів запустити jetty; перед зупинкою контейнера сервлетів – зупинити jetty. Найпростіший спосіб це реалізувати в GlassFish – написати свій ContextListener. Давайте подивимося чому? Створюємо новий клас та успадковуємо його від інтерфейсу javax.servlet.ServletContextListener. Дивимося листинг.

    Інтерфейс javax.servlet.ServletContextListener має два методи з іменами, що говорять самими за себе: contextInitialized(), contextDestroyed().

    Тепер нам лишаєтьсятільки підключити наш ContextListener в дескриптор розгортання (web.xml). Наведу його листинг:

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

    Висновок

    Весь проект у зборі можна завантажити за посиланням Проект повністю сумісний з GlassFish 2.x, 3.x та Tomcat не нижче 5.0. Проект створював у Netbeans 7.0.1. Всередині проекту можна знайти ant-deploy.xml для розгорнення Ant. Ще раз дико перепрошую перед розробниками, які використовують Maven.

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

    Дякую за увагу. Усіх ще раз зі святом!

    Хардкорна конфа за С++. Ми запрошуємо лише профі.