Архив за Март 2011

Reverse Ajax Chat

Март 24, 2011

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


Follow

Get every new post delivered to your Inbox.