Не так давно был релиз 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 как всегда на высоте – завис намертво. Если после прочтения статей по теории в интернете не совсем понятно, что и зачем тут нужно, пишите в коменты, будем обсуждать.
