Ajax для Java-разработчиков: Создание масштабируемых Comet-приложений с использованием Jetty и Direct Web Remoting

Создайте управляемое событиями Web-приложение, используя Continuations и Reverse Ajax

Ajax-приложения, управляемые асинхронными серверными событиями, могут быть сложными для реализации и трудными для масштабирования. Возвращаясь к своей популярной серии, Филип МакКарти демонстрирует эффективный подход: шаблон Comet позволяет передавать данные клиентам, а Continuations API Jetty 6 позволяет масштабировать Comet-приложение на большое число клиентов. Воспользоваться преимуществами Comet и Continuations можно при помощи технологии Reverse Ajax, входящей в состав библиотеки Direct Web Remoting 2.

Филипп Маккарти (Philip McCarthy), консультант и разработчик программного обеспечения, независимый специалист

Филипп Маккарти (Philip McCarthy) - консультант и разработчик программного обеспечения, специализирующийся в области языка Java и Web-технологий. В данный момент он работает в проекте Hewlett Packard над Digital Media Platform в HP Labs, Бристол. В течение последних лет Фил разработал несколько толстых Web-клиентов, применяя асинхронную связь между сервером и машиной сценариев DOM. Он рад, что сейчас у нас есть название для этого. Вы можете связаться с Филом по e-mail: philmccarthy@gmail.com.



19.10.2007

С утверждением Ajax в качестве широко распространенной технологии разработки Web-приложений появилось несколько общих шаблонов использования Ajax. Например, Ajax часто используется в ответ на запрос пользователя о замене части страницы новыми данными, извлеченными с сервера. Но иногда пользовательский интерфейс Web-приложения необходимо обновить в ответ на серверные события, которые возникают асинхронно, без действия пользователя - например, для показа новых сообщений, поступивших в Ajax-приложение, реализующее чат, или для отображения изменений, сделанных другим пользователем в совместно используемом текстовом редакторе. Поскольку HTTP-соединения между Web-браузером и сервером устанавливаются только самим браузером, сервер не может "протолкнуть" изменения в браузер при их возникновении.

В Ajax-приложениях существует два фундаментальных подхода для решения этой проблемы: либо браузер может ежесекундно опрашивать сервер на предмет наличия обновлений, либо сервер может хранить открытым соединение, установленное браузером, и передавать данные, когда они станут доступными. Такая технология долговременного соединения получила название Comet (см. раздел "Ресурсы"). В данной статье рассказывается о том, как можно совместно использовать движок Jetty servlet и DWR для простой и эффективной реализации Comet-приложения.

Почему Comet?

Главным недостатком подхода с опросом является объем генерируемого трафика при работе со многими клиентами. Каждый клиент должен периодически опрашивать сервер для проверки наличия обновлений, что требует выделения ресурсов на сервере. Наихудшая ситуация складывается с приложениями, обновляющимися не часто, как, например, папка входящих писем почтового Ajax-приложения (Inbox). В этом случае подавляющее большинство опросов клиентов окажется излишним, т.к. сервер будет просто отвечать "данных пока нет". Нагрузка на сервер может быть уменьшена путем увеличения интервала опроса, но это имеет нежелательные последствия в виде появления задержки между серверным событием и осведомленностью клиента о нем. Естественно, для многих приложений может найтись разумный компромисс, и опрос будет работать достаточно хорошо.

Тем не менее, одной из привлекательных особенностей стратегии Comet является ощущение ее эффективности. Клиенты не генерируют ненужный трафик, характерный для опроса, и как только события происходят, они могут быть опубликованы на клиенте. Но поддержка долговременно открытых соединений тоже потребляет ресурсы сервера. Удерживая персистентный запрос в состоянии ожидания, сервлет монополизирует использование потока (thread). Это ограничивает масштабируемость Comet-приложений с традиционным движком сервлетов, поскольку количество клиентов может быстро перекрыть количество потоков, которые серверный стек в состоянии эффективно обрабатывать.


Чем отличается Jetty 6

Jetty 6 предназначен для масштабирования до большого количества одновременных соединений. В нем используются не блокирующие библиотеки ввода/вывода языка программирования Java™ (java.nio) и оптимизированная архитектура выходного буфера (см. раздел "Ресурсы"). Кроме того Jetty умеет работать и с долговременными соединениями - функциональная возможность, известная под названием Continuations. Я продемонстрирую Continuations при помощи простого сервлета, который принимает запрос, ждет две секунды и передает ответ. Затем, я расскажу о том, что происходит в том случае, когда сервер имеет больше клиентов, чем потоков для их обслуживания. Наконец, я повторно реализую сервлет, используя Continuations, и вы увидите разницу.

Для облегчения наблюдения за происходящим в последующих примерах я ограничу движок сервлетов Jetty до одного потока обработки запросов. В листинге 1 приведена соответствующая конфигурация в jetty.xml. На самом деле мне нужны в сумме три потока в ThreadPool – один поток сервер Jetty использует сам, а второй запускает HTTP-коннектор, прослушивающий входящие запросы. И остается один поток для выполнения кода сервлета.

Листинг 1. Конфигурация Jetty для одиночного потока выполнения кода сервлета
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN"
  "http://jetty.mortbay.org/configure.dtd">
<Configure id="Server" class="org.mortbay.jetty.Server">
    <Set name="ThreadPool">
      <New class="org.mortbay.thread.BoundedThreadPool">
        <Set name="minThreads">3</Set>
        <Set name="lowThreads">0</Set>
        <Set name="maxThreads">3</Set>
      </New>
    </Set>
</Configure>

Чтобы симулировать ожидание асинхронного события, применяется метод service() для BlockingServlet (см. листинг 2), который просто использует вызов Thread.sleep() для паузы в 2000 миллисекунд перед завершением своей работы. Он также выводит на экран системное время в начале и конце работы. Чтобы устранить путаницу в информации, выводимой из разных запросов, он также регистрирует параметр запроса, используемый в качестве идентификатора.

Листинг 2. BlockingServlet
public class BlockingServlet extends HttpServlet {

  public void service(HttpServletRequest req, HttpServletResponse res)
                                              throws java.io.IOException {

    String reqId = req.getParameter("id");
    
    res.setContentType("text/plain");
    res.getWriter().println("Request: "+reqId+"\tstart:\t" + new Date());
    res.getWriter().flush();

    try {
      Thread.sleep(2000);
    } catch (Exception e) {}
    
    res.getWriter().println("Request: "+reqId+"\tend:\t" + new Date());
  }
}

Теперь можно проследить за поведением сервлета в ответ на несколько одновременных запросов. В листинге 3 приведена выводимая на экран информация от пяти параллельных запросов, использующих lynx. Из командной строки просто запускаются пять lynx-процессов с добавлением идентификатора к адресу URL запроса.

Листинг 3. Выводимая информация от нескольких одновременных запросов к BlockingServlet
$ for i in 'seq 1 5'  ; do lynx -dump localhost:8080/blocking?id=$i &  done
Request: 1      start:  Sun Jul 01 12:32:29 BST 2007
Request: 1      end:    Sun Jul 01 12:32:31 BST 2007

Request: 2      start:  Sun Jul 01 12:32:31 BST 2007
Request: 2      end:    Sun Jul 01 12:32:33 BST 2007

Request: 3      start:  Sun Jul 01 12:32:33 BST 2007
Request: 3      end:    Sun Jul 01 12:32:35 BST 2007

Request: 4      start:  Sun Jul 01 12:32:35 BST 2007
Request: 4      end:    Sun Jul 01 12:32:37 BST 2007

Request: 5      start:  Sun Jul 01 12:32:37 BST 2007
Request: 5      end:    Sun Jul 01 12:32:39 BST 2007

Данные, представленные в листинге 3, не являются неожиданными. Поскольку только один поток доступен в Jetty для выполнения метода service() сервлета, Jetty ставит каждый запрос в очередь и обслуживает их последовательно. Временные метки показывают, что сразу после отправки ответа на один запрос (сообщение end) сервлет начинает работать со следующим запросом (следующее сообщение start). Поэтому, даже несмотря на то, что все пять запросов были посланы одновременно, один из запросов должен ожидать восемь секунд перед тем, как сервлет сможет его обработать.

Помните, что во время блокировки сервлета не выполняется никакой полезной работы. Этот код симулирует ситуацию, когда запрос ожидает поступления события асинхронно из другой части приложения. Здесь ни при чем ни процессор сервера, ни его система ввода/вывода - запросы ставятся в очередь только в результате исчерпания пула потоков.

Теперь проверим, чем может быть полезна функциональность Continuations в Jetty 6 в ситуациях такого рода. В листинге 4 приведен код BlockingServlet из листинга 2, переписанный для использования Continuations API. Я объясню этот код немного позже.

Листинг 4. ContinuationServlet
public class ContinuationServlet extends HttpServlet {

  public void service(HttpServletRequest req, HttpServletResponse res)
                                              throws java.io.IOException {

    String reqId = req.getParameter("id");
    
    Continuation cc = ContinuationSupport.getContinuation(req,null);

    res.setContentType("text/plain");
    res.getWriter().println("Request: "+reqId+"\tstart:\t"+new Date());
    res.getWriter().flush();

    cc.suspend(2000);
    
    res.getWriter().println("Request: "+reqId+"\tend:\t"+new Date());
  }
}

В листинге 5 приведена информация, выводимая от пяти параллельных запросов к ContinuationServlet; сравните с листингом 3.

Листинг 5. Выводимая информация от нескольких одновременных запросов к ContinuationServlet
$ for i in 'seq 1 5'  ; do lynx -dump localhost:8080/continuation?id=$i &  done

Request: 1      start:  Sun Jul 01 13:37:37 BST 2007
Request: 1      start:  Sun Jul 01 13:37:39 BST 2007
Request: 1      end:    Sun Jul 01 13:37:39 BST 2007

Request: 3      start:  Sun Jul 01 13:37:37 BST 2007
Request: 3      start:  Sun Jul 01 13:37:39 BST 2007
Request: 3      end:    Sun Jul 01 13:37:39 BST 2007

Request: 2      start:  Sun Jul 01 13:37:37 BST 2007
Request: 2      start:  Sun Jul 01 13:37:39 BST 2007
Request: 2      end:    Sun Jul 01 13:37:39 BST 2007

Request: 5      start:  Sun Jul 01 13:37:37 BST 2007
Request: 5      start:  Sun Jul 01 13:37:39 BST 2007
Request: 5      end:    Sun Jul 01 13:37:39 BST 2007

Request: 4      start:  Sun Jul 01 13:37:37 BST 2007
Request: 4      start:  Sun Jul 01 13:37:39 BST 2007
Request: 4      end:    Sun Jul 01 13:37:39 BST 2007

В листинге 5 нужно отметить два важных момента. Во-первых, каждое сообщение start появляется дважды; пока об этом не беспокойтесь. Во-вторых, что более важно, запросы теперь обрабатываются параллельно, без постановки в очередь. Обратите внимание на то, что временные метки всех сообщений start и end одинаковы, по крайней мере, с данной точностью. Следовательно, нет запроса, ожидающего более двух секунд для завершения, даже несмотря на то, что выполняется только один поток.


Внутри механизма Continuations сервера Jetty

Понимание того, как реализован механизм Continuations сервера Jetty, поможет разобраться в результатах, приведенных в листинге 5. Для использования Continuations, Jetty должен быть настроен на обработку запросов с использованием коннектора SelectChannelConnector. Этот коннектор создан на базе java.nio API, что позволяет сохранять соединения открытыми, не расходуя по одному потоку на каждое. Когда используется SelectChannelConnector, метод ContinuationSupport.getContinuation() предоставляет экземпляр SelectChannelConnector.RetryContinuation (однако, вы должны кодировать, только используя интерфейс Continuation; см. раздел "Переносимость и Continuations API"). При вызове метода RetryContinuationsuspend() он генерирует специальную исключительную ситуацию времени исполнения RetryRequest, которая распространяется из сервлета обратно через цепочку фильтров и перехватывается в SelectChannelConnector. Но вместо передачи какого-либо ответа клиенту как результат исключительной ситуации, запрос задерживается в очереди ожидающих Continuations, и HTTP-соединение остается открытым. На этом этапе поток, который использовался для обслуживания запроса, возвращается в ThreadPool, где он может быть использован для обслуживания другого запроса.

Переносимость и Continuations API

Я уже упоминал, что нужно использовать Jetty SelectChannelConnector для разрешения функциональности Continuations. Однако Continuations API все еще действует для традиционного SocketConnector, что приводит к вызову сервером Jetty другой, старой реализации Continuation, использующей поведение wait()/notify(). Ваш код будет компилироваться и выполняться, но без использования преимуществ не блокирующих Continuations. Если вы хотите сохранить вариант использования сервера, отличного от Jetty, вы могли бы написать свою собственную оболочку над Continuation, использующую отражение (reflection) для проверки доступности библиотеки Jetty Continuations во время исполнения. DWR применяет именно эту стратегию.

Приостановленный запрос остается в очереди ожидающих Continuation до тех пор, пока либо не истечет указанный таймаут, либо не будет вызван метод resume() его Continuation (более подробно об этом позже). Когда возникает одно из данных событий, запрос повторно передается в сервлет (через цепочку фильтров). В результате весь запрос "проигрывается" до точки, где был в первый раз вызван suspend(). Когда выполнение достигает вызова suspend() во второй раз, исключительная ситуация RetryRequest не генерируется, и выполнение продолжается в нормальном режиме.

Теперь информация, приведенная в листинге 5, должна быть понятна. Когда каждый запрос по очереди достигает метода service() сервлета, в ответе передается сообщение start, а затем метод Continuation suspend() вызывает исключительную ситуацию для выхода из сервлета, освобождая поток для начала обслуживания следующего запроса. Все пять запросов быстро проходят через первую часть метода service() и переводятся в приостановленное состояние. Все сообщения start выводятся за миллисекунды. Двумя секундами позже, по истечении таймаутов suspend(), первый запрос извлекается из очереди ожидания и повторно передается в ContinuationServlet. Сообщение start выводится во второй раз, второй вызов suspend() завершается немедленно и в ответе передается сообщение end. Код сервлета затем выполняется снова для следующего поставленного в очередь запроса и т.д.

Итак, в обоих случаях (BlockingServlet и ContinuationServlet) запросы ставятся в очередь для доступа к единственному потоку сервлета. Однако двухсекундная пауза в BlockingServlet выполняется внутри потока выполнения сервлета, тогда как в ContinuationServlet эта пауза выполняется вне сервлета в SelectChannelConnector. Общая производительность сервлета ContinuationServlet выше, поскольку поток сервлета не тратит основную часть времени на вызов sleep().


Извлечение пользы из Continuations

Теперь, когда вы увидели, что Continuations позволяют задерживать запросы к сервлету, не расходуя поток, я должен подробнее рассказать о Continuations API, чтобы продемонстрировать способ использования Continuations для практических целей.

Метод resume() формирует пару с методом suspend(). Вы можете рассматривать их как Continuations-эквивалент стандартного механизма Objectwait()/notify(). То есть, suspend() переводит Continuation (и, следовательно, выполнение текущего метода) в состояние ожидания до тех пор, пока не истечет таймаут, либо другой поток не вызовет метод resume(). Пара suspend()/resume() является ключевой для реализации настоящего сервиса в Comet-стиле при помощи Continuations. Базовая схема такова: получить Continuation из текущего запроса, вызвать suspend() и ждать, пока не произойдет ваше асинхронное событие. Затем вызвать resume() и сгенерировать ответ.

Однако, в отличие от настоящих continuations уровня языка программирования, например в Scheme, или даже парадигмы языка Java wait()/notify(), вызов resume() в Jetty Continuation не означает, что код начнет выполняться с того места, где он прервался. Как вы заметили, на самом деле запрос, связанный с Continuation, проигрывается заново. Это приводит к появлению двух проблем: нежелательное повторное выполнение кода, как в ContinuationServlet в листинге 4, и потере состояния - все, имеющееся в области видимости при вызове suspend(), теряется.

Решением первой из этих проблем является метод isPending(). Если возвращаемое из isPending() значение равно true, это означает, что ранее вызывался метод suspend(), и выполнение повторяемого запроса еще не достигло suspend() во второй раз. Другими словами, делая выполнение кода до вызова suspend() зависящим от условия isPending(), мы гарантируем, что он будет выполняться только один раз на запрос. Лучше всего спроектировать код приложения до вызова suspend() идемпотентным, так чтобы вызов его дважды ни на что не влиял, но там, где это невозможно, можно использовать isPending(). Continuation также предлагает простой механизм для сохранения состояния - методы putObject(Object) и getObject(). Используйте их для сохранения объекта context с любым состоянием, которое вам нужно, когда Continuation приостанавливается. Можно также использовать этот механизм как способ передачи данных события между потоками (вы увидите применение этого способа позднее).


Написание основанного на Continuations приложения

В качестве сценария отчасти реального примера я собираюсь разработать стандартное Web-приложение отслеживания GPS-координат. Оно будет генерировать случайные пары широта-долгота с нерегулярными интервалами. Подключив воображение, сгенерированные координаты можно считать положением ближайшего общественного транспорта, марафонцев с GPS-устройствами, автомобилями на ралли или местом нахождения почтовой посылки. Интересным является то, как я укажу браузеру координаты. На рисунке 1 показана диаграмма классов этого простого приложения для GPS-навигатора.

Рисунок 1. Диаграмма классов, показывающая основные компоненты приложения для GPS-навигатора
Рисунок 1. Диаграмма классов, показывающая основные компоненты приложения для GPS-навигатора

Прежде всего, приложение нуждается в каком-нибудь генераторе координат. Этим занимается RandomWalkGenerator. Начиная с пары начальных координат, каждый вызов его private-метода generateNextCoord() делает случайный ограниченный шаг от этого места и возвращает новую позицию в виде объекта GpsCoord. При инициализации RandomWalkGenerator создает поток, вызывающий через случайные интервалы метод generateNextCoord() и затем передающий сгенерированные координаты во все экземпляры CoordListener, которые зарегистрировали себя с использованием вызова addListener(). В листинге 6 показана логика цикла RandomWalkGenerator:

Листинг 6. Метод RandomWalkGenerator run()
public void run() {

  try {
    while (true) {
      int sleepMillis = 5000 + (int)(Math.random()*8000d);
      Thread.sleep(sleepMillis);
      dispatchUpdate(generateNextCoord());
    }
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

CoordListener - это интерфейс обратного вызова, который просто определяет метод onCoord(GpsCoord coord). В данном примере класс ContinuationBasedTracker реализует CoordListener. Другим public-методом ContinuationBasedTracker является getNextPosition(Continuation, int). В листинге 7 показана реализация этих методов:

Листинг 7. Внутренности ContinuationBasedTracker
public GpsCoord getNextPosition(Continuation continuation, int timeoutSecs) {

  synchronized(this) {
    if (!continuation.isPending()) {
      pendingContinuations.add(continuation);
    }

    // Ждать следующего обновления
    continuation.suspend(timeoutSecs*1000);
  }

  return (GpsCoord)continuation.getObject();
}


public void onCoord(GpsCoord gpsCoord) {

  synchronized(this) {
    for (Continuation continuation : pendingContinuations) {

      continuation.setObject(gpsCoord);
      continuation.resume();
    }

    pendingContinuations.clear();
  }
}

Когда клиент вызывает getNextPosition() с Continuation, метод isPending проверяет, что этот запрос не повторяется в данной точке, затем добавляет его в коллекцию Continuations, ожидающих координаты. Затем Continuation приостанавливается. Тем временем onCoord, активизируемый при генерировании новых координат, просто просматривает в цикле все ожидающие Continuations, устанавливает в них GPS-координату и возобновляет их работу. Затем каждый повторный запрос завершает выполнение getNextPosition(), извлекая GpsCoord из Continuation, и возвращает координату вызвавшему его методу. Обратите внимание на необходимость синхронизации в этом месте как для предотвращения противоречивого состояния в коллекции pendingContinuations, так и для гарантирования того, что новый добавленный Continuation не продолжится без предварительной приостановки.

Последним фрагментом головоломки является код самого сервлета, приведенный в листинге 8:

Листинг 8. Реализация GPSTrackerServlet
public class GpsTrackerServlet extends HttpServlet {

    private static final int TIMEOUT_SECS = 60;
    private ContinuationBasedTracker tracker = new ContinuationBasedTracker();
  
    public void service(HttpServletRequest req, HttpServletResponse res)
                                                throws java.io.IOException {

      Continuation c = ContinuationSupport.getContinuation(req,null);
      GpsCoord position = tracker.getNextPosition(c, TIMEOUT_SECS);

      String json = new Jsonifier().toJson(position);
      res.getWriter().print(json);
    }
}

Как можно заметить, этот сервлет делает очень мало. Он просто получает Continuation запроса, вызывает getNextPosition(), преобразовывает GPSCoord в JavaScript Object Notation (JSON) и выводит его на экран. Здесь ничто не требует защиты от повторного выполнения, поэтому мне не нужно проверять isPending(). В листинге 9 показана выводимая информация при вызове GpsTrackerServlet, опять же, для пяти одновременных запросов, но только к одному доступному на сервере потоку:

Листинг 9. Информация, выводимая сервлетом GPSTrackerServlet
$  for i in 'seq 1 5'  ; do lynx -dump localhost:8080/tracker &  done
   { coord : { lat : 51.51122, lng : -0.08103112 } }
   { coord : { lat : 51.51122, lng : -0.08103112 } }
   { coord : { lat : 51.51122, lng : -0.08103112 } }
   { coord : { lat : 51.51122, lng : -0.08103112 } }
   { coord : { lat : 51.51122, lng : -0.08103112 } }

Этот пример не производит большого впечатления, но служит доказательством правильности концепции. После диспетчеризации запросов они сохраняются открытыми на несколько секунд до тех пор, пока не сгенерируются координаты, после чего быстро генерируются ответы. Это основа шаблона Comet с Jetty, обрабатывающего пять одновременных запросов в одном потоке благодаря Continuations.


Создание Comet-клиента

Теперь, увидев, как Continuations могут использоваться для создания не блокирующихся Web-сервисов, вы можете поинтересоваться, как создать клиентский код для использования этой возможности. Comet-клиент должен:

  1. Держать открытым XMLHttpRequest-соединение до получения ответа.
  2. Направить этот ответ соответствующему JavaScript-обработчику.
  3. Немедленно установить новое соединение.

Усовершенствованное Comet-приложение могло бы использовать одно соединение для передачи в браузер данных от различных источников с соответствующими механизмами маршрутизации на клиенте и сервере. Также можно было бы написать клиентский код с использованием такой JavaScript-библиотеки, как Dojo, который предоставляет основанные на Comet механизмы запросов типа dojo.io.cometd.

Однако если вы работаете с языком Java на сервере, отличным способом получения развитой поддержки Comet (как на клиенте, так и на сервере) является использование DWR 2 (см. раздел "Ресурсы"). Если вы не знакомы с DWR, можете прочитать третью часть данной серии статей "Ajax с Direct Web Remoting". DWR явно предоставляет транспортный уровень HTTP-RPC, делая доступными ваши Java-объекты для вызовов через Web из JavaScript-кода. DWR генерирует прокси на стороне клиента, автоматически маршаллизирует и демаршаллизирует данные, занимается вопросами защиты, предоставляет библиотеку вспомогательных функций на стороне клиента и работает во всех основных браузерах.


DWR 2: Reverse Ajax

В DWR 2 представлена новая концепция - Reverse Ajax. Это механизм "проталкивания" серверных событий клиенту. DWR-код на стороне клиента прозрачно работает с установленными соединениями и выполняет синтаксический разбор ответов, поэтому с точки зрения разработчика события могут просто публиковаться на клиенте из серверного Java-кода.

DWR может быть настроен на использование трех различных механизмов для Reverse Ajax. Одним из них является уже знакомый подход опроса. Второй, известный под названием piggyback (комбинирование), не создает никаких соединений с сервером. Вместо этого он ждет, пока появится другой вызов DWR-сервиса и накладывает ожидающие события поверх ответа для данного запроса. Это делает его очень эффективным, но уведомление клиента о событии задерживается до тех пор, пока клиент не сделает вызов, не имеющий никакого отношения к этому событию. Последний механизм использует долговременные соединения в стиле Comet. И что лучше всего, DWR может автоматически определить, работает ли он в Jetty, и переключиться на использование Continuations для не блокирующего Comet.

Я адаптирую GPS-пример для использования Reverse Ajax с DWR 2. Скоро вы более подробно узнаете, как работает Reverse Ajax.

Мне больше не нужен мой сервлет. DWR предоставляет сервлет controller, который служит посредником в передаче клиентских запросов непосредственно Java-объектам. Мне также больше не нужно явно работать с Continuations, поскольку DWR берет это на себя. Итак, мне просто нужна новая реализация CoordListener, которая публикует обновления координат на любых клиентских браузерах.

Магию DWR Reverse Ajax обеспечивает интерфейс ServerContext. ServerContext знает обо всех Web-клиентах, просматривающих в настоящее время данную страницу, и может предоставить объект ScriptSession для общения с каждым. Этот объект ScriptSession используется для передачи JavaScript-фрагментов клиенту из Java-кода. В листинге 10 показано, как ReverseAjaxTracker отвечает на уведомления о координатах, используя их для генерирования вызовов функции updateCoordinate(), находящейся на стороне клиента. Обратите внимание на то, что вызов appendData() в объекте DWR ScriptBuffer автоматически маршаллизирует Java-объект в JSON, если доступен подходящий конвертор.

Листинг 10. Метод обратного вызова уведомления в ReverseAjaxTracker
public void onCoord(GpsCoord gpsCoord) {

  // Сгенерировать JavaScript-код для вызова клиентской 
  // функции с данными о координатах
  ScriptBuffer script = new ScriptBuffer();
  script.appendScript("updateCoordinate(")
    .appendData(gpsCoord)
    .appendScript(");");

  // Передать сценарий клиентам, просматривающим страницу
  Collection<ScriptSession> sessions = 
            sctx.getScriptSessionsByPage(pageUrl);
            
  for (ScriptSession session : sessions) {
    session.addScript(script);
  }   
}

Далее, DWR должен быть настроен так, чтобы знать о ReverseAjaxTracker. В более крупном приложении можно было бы использовать преимущества интеграции DWR Spring для предоставления DWR создаваемых в Spring bean-компонентов. Однако здесь я просто дам DWR создать новый экземпляр ReverseAjaxTracker и помещу его в область видимости application. Все последующие DWR-запросы будут потом обращаться к этому единственному экземпляру.

Мне также понадобится указать DWR, как маршаллизировать данные из bean-компонентов GpsCoord в JSON. Поскольку GpsCoord является простым объектом, достаточно основанного на отражении (reflection-based) DWR-конвертора BeanConverter. В листинге 11 показана конфигурация для ReverseAjaxTracker:

Листинг 11. Конфигурация DWR для ReverseAjaxTracker
<dwr>
   <allow>
      <create creator="new" javascript="Tracker" scope="application">
         <param name="class" value="developerworks.jetty6.gpstracker.ReverseAjaxTracker"/>
      </create>

      <convert converter="bean" match="developerworks.jetty6.gpstracker.GpsCoord"/>
   </allow>
</dwr>

Атрибут javascript элемента create указывает имя, которое DWR использует для отображения навигатора в виде JavaScript-объекта. Однако в данном случае мой клиентский код не будет его использовать, а будет получать данные из навигатора. Также необходимы некоторые дополнительные настройки в web.xml для конфигурирования DWR на Reverse Ajax, что показано в листинге 12:

Листинг 12. Конфигурация web.xml для DwrServlet
<servlet>
   <servlet-name>dwr-invoker</servlet-name>
   <servlet-class>
      org.directwebremoting.servlet.DwrServlet
   </servlet-class>
   <init-param>
      <param-name>activeReverseAjaxEnabled</param-name>
      <param-value>true</param-value>
   </init-param>
   <init-param>
      <param-name>initApplicationScopeCreatorsAtStartup</param-name>
      <param-value>true</param-value>
   </init-param>
   <load-on-startup>1</load-on-startup>
</servlet>

Первый init-param сервлета (activeReverseAjaxEnabled) активирует опрос и функциональность Comet. Второй (initApplicationScopeCreatorsAtStartup) указывает DWR инициализировать ReverseAjaxTracker во время начала работы приложения. Это переопределяет обычное поведение "ленивой" инициализации, когда производится первый запрос bean-компоненту - здесь это необходимо, поскольку клиент никогда сам не вызывает метод в ReverseAjaxTracker.

Наконец, я должен реализовать JavaScript-функцию на стороне клиента, активизируемую из DWR. Функция обратного вызова (updateCoordinate()) передается в JSON-представление Java-компонента GpsCoord, автоматически сериализированного DWR-конвертором BeanConverter. Функция просто извлекает широту и долготу из координаты и добавляет их в список через вызовы Document Object Model (DOM). Это показано в листинге 13 вместе с функцией onload моей страницы. Функция onload содержит вызов dwr.engine.setActiveReverseAjax(true), который указывает DWR открыть персистентное соединение с сервером и ждать обратных вызовов.

Листинг 13. Реализация тривиального Reverse Ajax GPS-навигатора на стороне клиента
window.onload = function() {
  dwr.engine.setActiveReverseAjax(true);
}

function updateCoordinate(coord) {
  if (coord) {
    var li = document.createElement("li");
    li.appendChild(document.createTextNode(
            coord.longitude + ", " + coord.latitude)
    );
    document.getElementById("coords").appendChild(li);
  }
}

Обновление страницы без JavaScript

Если вы хотите минимизировать объем JavaScript-кода в вашем приложении, имеется альтернатива написанию JavaScript-функций обратного вызова с использованием ScriptSession: вы можете заключить экземпляры ScriptSession в объект DWR Util. Этот класс предоставляет простые Java-методы для прямого управления DOM-объектами браузера, и он автоматически генерирует необходимый сценарий.

Теперь я могу указать в браузере страницу навигатора, и DWR начнет передавать координаты клиенту по мере их генерирования. Данная реализация просто выводит список сгенерированных координат, как показано на рисунке 2.

Рисунок 2. Информация, выводимая ReverseAjaxTracker
Рисунок 2. Информация, выводимая ReverseAjaxTracker

Вот как просто создать управляемое событиями Ajax-приложение, используя Reverse Ajax. И помните, что, благодаря применению в DWR Jetty Continuations, никакие потоки на сервере не занимаются, пока клиент ожидает поступления нового события.

Теперь легко интегрировать виджет карты аналогично Yahoo! или Google. Изменяя клиентскую функцию обратного вызова, координаты можно просто передавать в map API вместо добавления непосредственно на страницу. На рисунке 3 показан DWR Reverse Ajax GPS-навигатор, рисующий случайное перемещение на таком компоненте карты.

Рисунок 3. ReverseAjaxTracker с пользовательским интерфейсом в виде карты
Рисунок 3. ReverseAjaxTracker с пользовательским интерфейсом в виде карты

Заключение

Вы увидели, как Jetty Continuations совместно с Comet может обеспечить эффективное, масштабируемое решение для управляемых событиями Ajax-приложений. Я не предоставил никаких показателей масштабируемости Continuations, поскольку производительность в реальных приложениях зависит от очень многих факторов. Оборудование сервера, выбор операционной системы, реализация JVM, конфигурация Jetty и конечно же дизайн вашего Web-приложения и профиль трафика, все это влияет на производительность Jetty Continuations под нагрузкой. Однако Грег Вилкинс (Greg Wilkins) из Webtide (главные разработчики Jetty) опубликовал официальный доклад (white paper) по Jetty 6, сравнивающий производительность Comet-приложения с и без Continuations, обрабатывающего 10000 одновременных запросов (см. раздел "Ресурсы"). В его тестах использование Continuations сокращает расход потоков и, попутно, потребление стека в памяти более чем в 10 раз.

Вы также узнали, насколько легко можно реализовать управляемое событиями Ajax-приложение, используя технологию DWR Reverse Ajax. DWR не только намного сокращает объем кодирования на стороне клиента и сервера, но Reverse Ajax также абстрагирует из вашего кода весь механизм передачи данных сервером. Можно свободно переключать методы (Comet, опрос или даже piggyback), просто изменяя конфигурацию DWR. Вы свободно можете экспериментировать и искать лучшую по производительности стратегию для вашего приложения, никак не изменяя код.

Если вы хотите поэкспериментировать с вашим собственным Reverse Ajax-приложением, отличным способом дополнительного изучения является загрузка и исследование кода демонстрационных материалов DWR (часть дистрибутива DWR в исходных кодах. См. раздел "Ресурсы"). Доступен также пример кода, использованный в данной статье (см. раздел "Загрузка"), если вдруг вы захотите выполнить примеры самостоятельно.


Загрузка

ОписаниеИмяРазмер
Пример кодаjetty-dwr-comet-src.tgz8KB

Ресурсы

Научиться

Получить продукты и технологии

  • Jetty: Загрузите Jetty.(EN)
  • DWR: Загрузите DWR.(EN)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=263428
ArticleTitle=Ajax для Java-разработчиков: Создание масштабируемых Comet-приложений с использованием Jetty и Direct Web Remoting
publish-date=10192007