Добрый день, Хабражитель!
Поздравляю всех и каждого с великим Днем Программиста! Желаю рабочего кода, уверенных сокетов и самых продвинутых пользователей!
Работая над автоматизацией концертного агентства, мне на каком-то этапе разработки понадобилась система уведомлений. Доступ к автоматизации происходит через написанное мною web-приложение. И, соответственно, моментальные уведомления должны приходить в браузер пользователя.
Для реализации такой задачи есть три решения:
Первое решение я сразу «отметаю» (причины объяснять не буду, web-разработчики меня поймут).
Второе решение нравится гораздо больше, но у него есть свои минусы:
Третье решение — как раз то, что доктор прописал.
Итак, WebSocket.
Среди минусов WebSocket важен только тот, что его пока поддерживают только браузеры webkit (они же Google Chrome и Apple Safari).
Давайте попробуем реализовать простой чат, как web-приложение c базовой возможностью полнодуплексного обмена сообщениями между клиентом и сервером.
Реализация WebSocket-ов на JavaScript на клиентской стороне простая, на неё не будем тратить много времени. Смотрим листинг.
Отправлять сообщения на сервер можно методом send():
Реализация решения на сервере выглядит заметно сложнее. В сети можно найти несколько вариантов реализации, из них наиболее ближе ко мне были:
Из них JWebSocket выделяется тем, что это большой фреймворк для работы с WebSocket, одновременно являю��ийся еще и надежным standalone-сервером. JWebSocket требует отдельного поста. А сейчас мы остановимся на самом простом варианте решения на платформе J2EE: Jetty.
Что мы имеем? Давайте взглянем на схему.

В моем случае мы имеем контейнер сервлетов GlassFish на порту 8080. Браузер отправляет запрос на GlassFish [1], который передает браузеру страничку чата [2], которая по желанию пользователя соединяется с сервером через порт 8081 по протоколу WebSocket [3]. Далее происходит полнодуплексный обмен данными между браузером и сервером [4].
На момент написания поста последняя версия Jetty 8.0.1.v20110908. Скачиваем, распаковываем (извиняюсь перед всеми разработчиками, использующих Maven), из дистрибутива нас интересуют 6 библиотек:
Добавляем эти библиотеки в проект.
Возвращаясь к клиентской части, скачиваем ее отсюда (не хочу засорять пост листингом HTML-кода). Добавляем файл chat.html в Web Pages проекта. В дескрипторе развёртывания (web.xml) указываем chat.html как «welcome-file».
Теперь немного о Jetty.
Embedded-сервер Jetty находится в классе org.eclipse.jetty.server.Server. И конструктор может содержать либо адрес сервера, либо, как в нашем случае номер порта:
Далее нам нужно добавить в jetty нужные handlerы и запустить его. Запускается и останавливается jetty методами без параметров start() и stop() соответственно. А вот handler нам надо будет написать свой, создаем новый класс. Чтобы у нас был handler для обработки соединений по протоколу WebSocket, мы должны его наследовать от org.eclipse.jetty.websocket.WebSocketHandler:
У класса WebSocketHandler есть один абстрактный метод doWebSocketConnect(). Он, собственно, и вызывается, когда браузер открывает новое соединение с jetty, и возвращает объект WebSocketа.
Класс WebSocket нам тоже следует определить свой, и наследовать мы его будем от интерфейса org.eclipse.jetty.websocket.WebSocket.OnTextMessage — этот интерфейс работает с данными, проходящими через протокол WebSocket, как с текстовыми данными. Интерфейс org.eclipse.jetty.websocket.WebSocket.OnTextMessage содержит три метода:
Вроде бы, все просто! Давайте посмотрим на листинг ChatWebSocketHandler.
И так, что мы видим в ChatWebSocketHandler:
В классе ChatWebSocket нас интересует метод onMessage(). В нем мы будем реализовывать протокол обмена данными клиентской и серверной части. По комментариям видно, принцип его работы.
Что из себя представляет протокол обмена?
Сервер принимает от клиента все текстовые сообщения, из них может выделить три команды:
Клиент принимает от сервера сообщения с такими шаблонами:
Остальное не буду расписывать повторно, — все описано в комментариях.
Теперь перейдем к самому ответственному моменту — запуск сервера 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.
Спасибо за внимание. Всех еще раз с праздником!
Поздравляю всех и каждого с великим Днем Программиста! Желаю рабочего кода, уверенных сокетов и самых продвинутых пользователей!
Работая над автоматизацией концертного агентства, мне на каком-то этапе разработки понадобилась система уведомлений. Доступ к автоматизации происходит через написанное мною web-приложение. И, соответственно, моментальные уведомления должны приходить в браузер пользователя.
Для реализации такой задачи есть три решения:
- «бесконечный iframe»,
- используя XMLHttpRequest (a.k.a. Ajax),
- используя WebSocket.
Первое решение я сразу «отметаю» (причины объяснять не буду, web-разработчики меня поймут).
Второе решение нравится гораздо больше, но у него есть свои минусы:
- браузер отправляет запрос каждую секунду создавая лишнюю нагрузку на:
- сервер;
- ОС, на которой работает браузер;
- и еще раз на сервер, так как сервер постоянно выполняет запрос БД на выборку последних уведомлений.
- тяжело отследить онлайн-статус пользователя (то есть нужно, например, хранить сессии в БД и постоянно мониторить каждую на timeout).
Третье решение — как раз то, что доктор прописал.
Итак, WebSocket.
Среди минусов WebSocket важен только тот, что его пока поддерживают только браузеры webkit (они же Google Chrome и Apple Safari).
Давайте попробуем реализовать простой чат, как web-приложение c базовой возможностью полнодуплексного обмена сообщениями между клиентом и сервером.
Реализация клиентской части
Реализация WebSocket-ов на JavaScript на клиентской стороне простая, на неё не будем тратить много времени. Смотрим листинг.
var socket = new WebSocket("ws://myserver.com:8081/"); socket.onopen = function () { console.log("Соединение открылось"); }; socket.onclose = function () { console.log ("Соединение закрылось"); }; socket.onmessage = function (event) { console.log ("Пришло сообщение с содержанием:", event.data); };
Отправлять сообщения на сервер можно методом 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.
Embedded-сервер Jetty находится в классе org.eclipse.jetty.server.Server. И конструктор может содержать либо адрес сервера, либо, как в нашем случае номер порта:
Server jetty = new 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.
import java.io.IOException; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.websocket.WebSocket; import org.eclipse.jetty.websocket.WebSocketHandler; public class ChatWebSocketHandler extends WebSocketHandler { /** * Набор открытых сокетов */ private final Set<ChatWebSocket> webSockets = new CopyOnWriteArraySet<ChatWebSocket>(); /** * Выполняется когда пытается открыться новое соединение * @param request * @param protocol протокол (бывает двух видов ws и wss) * @return */ @Override public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { // У нас есть два варианта // Либо мы не пускаем клиента и вызываем исключение // throw new Exception(); // Либо возвращаем объект, который будет соединять сервер с клиентом // и обрабатывать запросы от клиента return new ChatWebSocket(); } private class ChatWebSocket implements WebSocket.OnTextMessage { /** * Хранилище соединения */ private Connection connection; /** * Ник пользователя */ private String userName = null; /** * Шаблон входящей команды авторизации */ private final Pattern authCmdPattern = Pattern.compile("^\\/auth ([\\S]+).*"); /** * Шаблон входящей команды получения списка пользователей */ private final Pattern getUsersCmdPattern = Pattern.compile("^\\/getUsers.*"); /** * Шаблон входящей команды получения помощи */ private final Pattern helpCmdPattern = Pattern.compile("^\\/help.*"); /** * Выполняется когда открыто новое соединение * @param connection */ @Override public void onOpen(Connection connection) { // Сохраняем соединение в свойство ChatWebSocket::connection this.connection = connection; // Добавляем себя в глобальный набор ChatWebSocketHandler::webSockets webSockets.add(this); } /** * Выполняется когда пришло новое сообщение * @param data */ @Override public void onMessage(String data) { // На всякий случай удаляем теговые дескрипторы data = data.replaceAll("<", "<").replaceAll(">", ">"); // Если пришла команда авторизации if (authCmdPattern.matcher(data).matches()) { Matcher matcher = authCmdPattern.matcher(data); matcher.find(); // Устанавливаем новый ник пользователю userName = matcher.group(1); try { // Цикл шарит по набору сокетов ChatWebSocketHandler::webSockets for (ChatWebSocket webSocket : webSockets) { // и отправляет сообщение, что подключился новый пользователь webSocket.connection.sendMessage("inf|" + (webSocket.equals(this) ? "Вы успешно авторизировались" : ("В чат подключился <b>" + userName + "</b>"))); } } catch (IOException x) { // Все ошибки будут приводить к разъединению клиента от сервера connection.disconnect(); } // Если пришла команда получения списка пользователей } else if (getUsersCmdPattern.matcher(data).matches()) { String userList = ""; // Цикл шарит по набору сокетов ChatWebSocketHandler::webSockets for (ChatWebSocket webSocket : webSockets) { userList += webSocket.userName + ", "; } userList = userList.substring(0, userList.length() - 2); try { // Отсылаем список активных пользователей connection.sendMessage("inf|Список активных пользователей: " + userList); } catch (IOException x) { // Все ошибки будут приводить к разъединению клиента от сервера connection.disconnect(); } // Если пришла команда получения помощи } else if (helpCmdPattern.matcher(data).matches()) { String helpMessage = "Отправлять сообщения можно просто написав " + "их в поле для ввода и нажать Enter.<br />" + "Чат поддерживает три команды:<br />" + "<ul><li><b>/help</b> - для распечатки этого сообщения</li>" + "<li><b>/getUsers</b> - для получения списка пользователей</li>" + "<li><b>/auth <i>ник</i></b> - для авторизации</li></ul>"; try { // Отсылаем инструкцию connection.sendMessage("inf|" + helpMessage); } catch (IOException x) { // Все ошибки будут приводить к разъединению клиента от сервера connection.disconnect(); } // Если пришла не команда а сообщение } else { try { // Если пользователь не авторизирован if (userName == null) { connection.sendMessage("err|Вы не авторизированны<br />" + "Используйте команду <b>/help</b> для помощи"); return; } // Цикл шарит по набору сокетов ChatWebSocketHandler::webSockets for (ChatWebSocket webSocket : webSockets) { // и каждому рассылает сообщение с флагом in для всех // кроме автора, автору - флаг out webSocket.connection.sendMessage((webSocket.equals(this) ? "out|" : ("in|" + userName + "|")) + data); } } catch (IOException x) { // Все ошибки будут приводить к разъединению клиента от сервера connection.disconnect(); } } } /** * Выполняется когда клиент разъединяется от сервера * @param closeCode * @param message */ @Override public void onClose(int closeCode, String message) { // Удаляем себя из глобального набора ChatWebSocketHandler::webSockets webSockets.remove(this); } } }
И так, что мы видим в ChatWebSocketHandler:
- 1 свойство – набор сокетов;
- 1 метод, который нам создает и возвращает новый сокет;
- 1 приватный класс, который реализует этот WebSocket.
В классе ChatWebSocket нас интересует метод onMessage(). В нем мы будем реализовывать протокол обмена данными клиентской и серверной части. По комментариям видно, принцип его работы.
Что из себя представляет протокол обмена?
Сервер принимает от клиента все текстовые сообщения, из них может выделить три команды:
- /auth ник //Для авторизации
- /getUsers //Для получения списка пользователей
- /help //Для получения помощи
Клиент принимает от сервера сообщения с такими шаблонами:
- inf|информация // Пришла информация
- in|ник|сообщение // Входящее сообщение от пользователя ник
- out|сообщение // Мое сообщение отправлено
- err|ошибка // Пришла информация об ошибке
Остальное не буду расписывать повторно, — все описано в комментариях.
Теперь перейдем к самому ответственному моменту — запуск сервера jetty. Значит, у нас задача: при старте контейнера сервлетов запустить jetty; перед остановкой контейнера сервлетов — остановить jetty. Самый простой способ это реализовать в GlassFish — написать свой ContextListener. Давайте посмотрим почему? Создаем новый класс и наследуем его от интерфейса javax.servlet.ServletContextListener. Смотрим листинг.
import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.DefaultHandler; public class ChatServerServletContextListener implements ServletContextListener { /** * Хранилище сервера Jetty */ private Server server = null; /** * Метод вызывается когда контейнер сервлетов запускается * @param event */ @Override public void contextInitialized(ServletContextEvent event) { try { // Создание сервера Jetty на 8081 порту this.server = new Server(8081); // Регистрируем ChatWebSocketHandler в сервере Jetty ChatWebSocketHandler chatWebSocketHandler = new ChatWebSocketHandler(); // Это вариант хэндлера для WebSocketHandlerContainer chatWebSocketHandler.setHandler(new DefaultHandler()); // Вставляем наш хэндлер слушаться jetty server.setHandler(chatWebSocketHandler); // Запускаем Jetty server.start(); } catch (Throwable e) { e.printStackTrace(); } } /** * Метод вызывается когда контейнер сервлетов останавливается * @param event */ @Override public void contextDestroyed(ServletContextEvent event) { // Если сервер jetty когда-нибудь запустился if (server != null) { try { // останавливаем Jetty server.stop(); } catch (Exception e) { e.printStackTrace(); } } } }
Интерфейс javax.servlet.ServletContextListener имеет два метода с говорящими самими за себя именами: contextInitialized(), contextDestroyed().
Теперь нам остается только подключить наш ContextListener в дескриптор развёртывания (web.xml). Приведу его листинг:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <session-config> <session-timeout> 30 </session-timeout> </session-config> <welcome-file-list> <welcome-file>chat.html</welcome-file> </welcome-file-list> <listener> <listener-class>ChatServerServletContextListener</listener-class> </listener> </web-app>
Ну теперь можно и запустить проект. В браузере открываем сразу два окна и балуемся. Прошу обратить внимание на время, сообщения доходят практически моментально. Такую скорость работы на ajax получить практически невозможно.


Заключение
Весь проект в сборе можно скачать по ссылке
Проект полностью совместим с GlassFish 2.x, 3.x и Tomcat не ниже 5.0. Проект создавал в Netbeans 7.0.1. Внутри проекта можно найти ant-deploy.xml для развертки на Ant. Еще раз дико извиняюсь перед разработчиками, использующих Maven.
Во второй части статьи я подробно опишу проблемы, которые возникают при работе с WebSocket, и их решения.
В третьей части я опишу способы борьбы с ddos-атаками на серверы Jetty.
Спасибо за внимание. Всех еще раз с праздником!
