Reverse Ajax Chat

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

Метки: , , , , , , ,

Комментариев: 7 на “Reverse Ajax Chat”

  1. Igor:

    Большое спасибо за статью. Все кортко и понятно. Не могли бы вы еще одновиты ссылки на сам проект?

  2. kdiv:

    А какие могут быть препятствия в использовании: ScheduledThreadPoolExecutor executor = new ThreadPoolExecutor(10);
    а потом
    executor.execute(new нашСервис);
    ?

  3. Виктор:

    Автор, а вас не смутила бесконечная рекурсия в “function getData()” и как следствие этого – сервер грузит процессор на 99%? Это ли не есть бомбление сервера, с целью получения свежей информации?
    Или я чего-то не понимаю? В чем “фишка” данного метода? Обновите пожалуйста ссылку на свой проект.

  4. Юрий Новиков:

    Виктор, соединение с сервером устанавливается один раз, соответственно происходит один request, и соединение потом поддерживается сервером до момента отправки response либо завершения по таймауту. В этом и заключается отличие от классического ajax. Собственно, со стороны клиента это действительно выглядит как рекурсия и никакой разницы между классическим подходом и этим с точки зрения javascript нет. Тут разница в серверной части. Например, пусть у нас время жизни соединения 5 минут. Тогда клиент один раз пошлет request и установит соедининие, которое будет поддерживаться 5 минут. В течение этих 5 минут никаких запросов с клиента на сервер больше не будет – сервер сам пришлёт response, когда появится обновление. Зачем нужна рекурсия – после получения ответа от сервера соединение закрывается, равно как оно закрывается по таймауту. Поэтому необходимо переподключиться. Если опять вернуться к нашему примеру с соединением, которое живет 5 минут, то при ситуации, когда за эти 5 минут на сервере нет никакой информации и слать клиенту ничего не надо, на сервер будет отправлено всего 2 запроса – в начале периода (открытие соединения) и в конце (когда оно отвалится по таймауту и потребуется переподключиться). Все эти 5 минут сервер будет поддерживать его и в случае появления новой информации отправит ответ. Поэтому клиенту нет необходимости слать запрос каждую секунду чтоб проверить, нет ли на сервере чего нового.

    Проект я искал, но похоже я его потерял. По сути, там кроме приведенных фрагментов кода больше ничего и не было. Это была просто попытка попробовать новую технологию в действии, поэтому я не заморачивался созданием того же репозитория на github – код в любом случае слишком сырой для его переиспользования кем бы то ни было.

    • Виктор:

      Юрий Новиков, в любом случае, спасибо за то что нашли время более детально разъяснить принцип действия. Я только недавно начал изучать reverse ajax, ваш код работает, просто я не могу найти место зацикливания, у меня получается, что клиентская часть шлет запросы без задержки… ,буду разбираться

      • Юрий Новиков:

        Виктор, если найдёте более лучшее/эффективное решение, напишите в комментариях пожалуйста. Всем будет полезно.

Добавить комментарий

Fill in your details below or click an icon to log in:

Логотип WordPress.com

You are commenting using your WordPress.com account. Log Out / Изменить )

Фотография Twitter

You are commenting using your Twitter account. Log Out / Изменить )

Фотография Facebook

You are commenting using your Facebook account. Log Out / Изменить )

Connecting to %s


Follow

Get every new post delivered to your Inbox.