Не так давно был релиз Tomcat 7.0, где в полной мере реализована поддержка Servlet 3.0. Одно из наиболее интересных нововведений – это поддержка асинхронных сервлетов (asynchronous servlets), что в полной мере позволяет нам реализовать технологию Reverse AJAX.
Если вкратце, то суть ее в том, что клиент открывает долгоживущее HTTP-соединение, которое хранится на сервере до того момента, пока сервер не будет готов отослать ответ обратно. Посылка ответа инициируется сервером (поэтому это и называется AJAX наоборот). Такой подход позволяет избавиться от многократного опроса сервера множеством клиентов с целью получить как можно более свежую информацию. Например, такой сценарий имеет место в онлайн аукционах, разного рода службах информирования об изменении курса акций, чатах и т.д. Суть в том, что момент обновления информации неизвестен, но все хотят получить ее как можно быстрее после опубликования, поэтому начинают бомбить сервер запросами с большой частотой в надежде не пропустить обновление информации на сервисе. Как это влияет на производительность, думаю рассказывать не надо – имеем вполне себе DoS-атаку. Reverse AJAX избавляет нас от необходимости все время опрашивать сервер – соединение открывается один раз, и потом сервер сам отошлет ответ, когда будет что отсылать. Естесственно, когда с сервера придет ответ, нужно установить соединение заново.
Обзоры этого уже есть в интернете, их можно почитать тут, тут и тут. Теории хватает, но я так и не смог найти ни одного работаюшего примера. Куски кода конечно встречались, но увы, я так и не нашел, где можно скачать и посмотреть работающий пример. Итак, исправляем ситуацию и пишем простейший онлайн-чат с использованием Asynchronous Servlets.
Класс сообщения:
public class Message {
public final String message;
public final String username;
public Message(final String message, final String username) {
this.message = message;
this.username = username;
}
}
Сам сервлет:
@WebServlet(name = "chatServlet", urlPatterns = { "/chat" }, asyncSupported = true)
public class ChatServlet extends HttpServlet {
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
AsyncContext aCtx = req.startAsync(req, resp);
aCtx.setTimeout(1000*60*5L); //5 min timeout
ServletContext servletContext = req.getServletContext();
((Queue<AsyncContext>)servletContext.getAttribute("chatUsers")).add(aCtx);
}
@Override
protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
AsyncContext aCtx = req.startAsync(req, resp);
ServletContext servletContext = req.getServletContext();
String message = req.getParameter("message");
String username = req.getParameter("username");
Queue<Message> msgQueue = (Queue<Message>) servletContext.getAttribute("messages");
msgQueue.add(new Message(message, username));
aCtx.complete();
}
}
Hаша служба асинхронных сообщений, которая будет проверять, не пришло ли чего нового и отсылать пришедшее сообщение зарегистрированным адресатам:
@WebListener
public class ChatService implements ServletContextListener {
@Override
public void contextDestroyed(final ServletContextEvent sce) {
}
@Override
public void contextInitialized(final ServletContextEvent sce) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
final Queue<AsyncContext> chatUsers = new ConcurrentLinkedQueue<AsyncContext>();
sce.getServletContext().setAttribute("chatUsers", chatUsers);
Queue<Message> messages = new ConcurrentLinkedQueue<Message>();
sce.getServletContext().setAttribute("messages", messages);
Executor messageExecutor = Executors.newCachedThreadPool();
final Executor chatExecutor = Executors.newCachedThreadPool();
while (true) {
if (!messages.isEmpty()) {
final Message message = messages.poll();
messageExecutor.execute(new Runnable() {
@Override
public void run() {
while(!chatUsers.isEmpty()) {
final AsyncContext aCtx = chatUsers.poll();
chatExecutor.execute(new Runnable() {
@Override
public void run() {
try {
ServletResponse response = aCtx.getResponse();
response.setContentType("text/xml");
response.getWriter().write(messageAsXml(message));
aCtx.complete();
} catch (IOException e) {
e.printStackTrace();
}
}
private String messageAsXml(final Message message) {
StringBuffer sb = new StringBuffer();
sb.append("<message>")
.append("<username>")
.append(message.username)
.append("</username>")
.append("<text>")
.append(message.message)
.append("</text>")
.append("</message>");
return sb.toString();
}
});
}
}
});
}
}
}
});
t.start();
}
}
Ну и страничка чата собственно. Используется JQuery. Как видно, никакой разницы с точки зрения JQuery между Reverse AJAX и просто АJAX нет:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Servlet 3.0 example</title>
<script src="js/jquery-1.5.1.js"></script>
</head>
<body>
<script>
$(document).ready(function(){
function getData() {
$.ajax({
url: "chat",
type: "GET",
dataType: "xml",
context: document.body,
success: function(data){
var username = $(data).find('username').text();
var text = $(data).find('text').text();
var history = $('#chat_msgs').text();
$('#chat_msgs').html(history + username + ": " + text + "\n");
getData();
}
});
}
$("#sendMsg").click(function(event){
$.post("chat", $("#msgForm").serialize());
$('#message').val('');
});
getData();
});
</script>
<h3>Asynchronous Servlet 3.0 Based Reverse Ajax Chat</h3>
<textarea cols="60" rows="5" id="chat_msgs" name="chat_msgs"></textarea>
<br/>
<form id="msgForm" name="msgForm">
Username: <input type="text" id="username" name="username" value="" />
Message: <input type="text" id="message" name="message" value="" />
</form>
<br/>
<input type="submit" id="sendMsg" name="sendMsg" value="Send message" />
</body>
</html>
Вот так это выглядит в действии:
Сразу хочу сказать, что это минимально возможный рабочий вариант. Здесь нет много чего важного, например, обработки ответа сервера по истечении таймаута, обработки ошибок и т.д. Но всё это можно прочитать в специфииации Servlets 3.0. Архив с кодом можно скачать здесь. Проект распаковать, импортировать в Eclipse, деплоить на Tomcat 7.0. Проверено в Opera 11, FF4, Chrome 10, работает без нареканий. IE как всегда на высоте – завис намертво. Если после прочтения статей по теории в интернете не совсем понятно, что и зачем тут нужно, пишите в коменты, будем обсуждать.
Метки: asynchronous servlet, chat, comet, асинхронный сервлет, чат, reverse ajax, servlet 3.0, tomcat 7

Ноябрь 29, 2011 в 3:15 пп |
Большое спасибо за статью. Все кортко и понятно. Не могли бы вы еще одновиты ссылки на сам проект?
Ноябрь 29, 2011 в 6:58 пп |
Спасибо, рад что Вам понравилось! Ссылку обновлю в ближайшее время
Декабрь 19, 2011 в 2:21 пп |
А какие могут быть препятствия в использовании: ScheduledThreadPoolExecutor executor = new ThreadPoolExecutor(10);
а потом
executor.execute(new нашСервис);
?
Январь 13, 2012 в 9:36 дп |
Автор, а вас не смутила бесконечная рекурсия в “function getData()” и как следствие этого – сервер грузит процессор на 99%? Это ли не есть бомбление сервера, с целью получения свежей информации?
Или я чего-то не понимаю? В чем “фишка” данного метода? Обновите пожалуйста ссылку на свой проект.
Январь 13, 2012 в 6:35 пп |
Виктор, соединение с сервером устанавливается один раз, соответственно происходит один request, и соединение потом поддерживается сервером до момента отправки response либо завершения по таймауту. В этом и заключается отличие от классического ajax. Собственно, со стороны клиента это действительно выглядит как рекурсия и никакой разницы между классическим подходом и этим с точки зрения javascript нет. Тут разница в серверной части. Например, пусть у нас время жизни соединения 5 минут. Тогда клиент один раз пошлет request и установит соедининие, которое будет поддерживаться 5 минут. В течение этих 5 минут никаких запросов с клиента на сервер больше не будет – сервер сам пришлёт response, когда появится обновление. Зачем нужна рекурсия – после получения ответа от сервера соединение закрывается, равно как оно закрывается по таймауту. Поэтому необходимо переподключиться. Если опять вернуться к нашему примеру с соединением, которое живет 5 минут, то при ситуации, когда за эти 5 минут на сервере нет никакой информации и слать клиенту ничего не надо, на сервер будет отправлено всего 2 запроса – в начале периода (открытие соединения) и в конце (когда оно отвалится по таймауту и потребуется переподключиться). Все эти 5 минут сервер будет поддерживать его и в случае появления новой информации отправит ответ. Поэтому клиенту нет необходимости слать запрос каждую секунду чтоб проверить, нет ли на сервере чего нового.
Проект я искал, но похоже я его потерял. По сути, там кроме приведенных фрагментов кода больше ничего и не было. Это была просто попытка попробовать новую технологию в действии, поэтому я не заморачивался созданием того же репозитория на github – код в любом случае слишком сырой для его переиспользования кем бы то ни было.
Январь 15, 2012 в 3:50 пп |
Юрий Новиков, в любом случае, спасибо за то что нашли время более детально разъяснить принцип действия. Я только недавно начал изучать reverse ajax, ваш код работает, просто я не могу найти место зацикливания, у меня получается, что клиентская часть шлет запросы без задержки… ,буду разбираться
Январь 15, 2012 в 11:02 пп
Виктор, если найдёте более лучшее/эффективное решение, напишите в комментариях пожалуйста. Всем будет полезно.