Skip to main content

Ajax for Java developers: Write scalable Comet applications with Jetty and Direct Web Remoting

Create event-driven Web applications using Continuations and Reverse Ajax

Philip McCarthy (philmccarthy@gmail.com), Java development consultant, Independent
Philip McCarthy is a London-based software-development consultant specializing in Java and Web technologies. Past work includes projects for Orange and Hewlett Packard Labs. His current focus is Web-based financial systems built with open source frameworks.

Summary:  Ajax applications driven by asynchronous server-side events can be tricky to implement and difficult to scale. Returning to his popular series, Philip McCarthy shows an effective approach: The Comet pattern allows you to push data to clients, and Jetty 6's Continuations API lets your Comet application scale to a large number of clients. You can conveniently take advantage of both Comet and Continuations with the Reverse Ajax technology in Direct Web Remoting 2.

View more content in this series

Date:  17 Jul 2007
Level:  Intermediate
Activity:  17163 views

With Ajax firmly established as a widespread Web application-development technique, several common Ajax usage patterns have emerged. For instance, Ajax is often used in response to user input to modify parts of a page with new data fetched from the server. Sometimes, though, a Web application's user interface needs to update in response to server-side events that occur asynchronously, without user action -- for instance, to show new messages arriving in an Ajax chat application or to display changes from another user in a collaborative text editor. Because HTTP connections between a Web browser and a server can be established only by the browser, the server can't "push" changes to the browser as they occur.

Ajax applications can use two fundamental approaches to work around this problem: either the browser can poll the server for updates every few seconds, or the server can hold open a connection from the browser and pass data as it becomes available. This long-lived connection technique has become known as Comet (see Resources). This article shows how you can use the Jetty servlet engine and DWR together to implement a Comet Web application simply and efficiently.

Why Comet?

The main drawback of the polling approach is the amount of traffic generated as it scales to many clients. Each client must regularly hit the server to check for updates, which places a burden on the server's resources. The worst situation is an application involving infrequent updates, such as an Ajax mail Inbox. In this case, the vast majority of client polls would be redundant, with the server simply answering "no data yet." The server load can be alleviated by increasing the polling interval, but this has the undesirable consequence of introducing a lag between a server event and the client's awareness of it. Of course, a reasonable balance can be found for many applications, and polling works acceptably well.

Nevertheless, one of the appeals of the Comet strategy is its perceived efficiency. Clients don't produce the noisy traffic characteristic of polling, and as soon as events occur, they can be published to the client. But holding open long-lived connections consumes server resources too. While a servlet holds a persistent request in a waiting state, that servlet is monopolizing a thread. This limits Comet's scalability with a traditional servlet engine because the number of clients can quickly overwhelm the number of threads that the server stack can handle efficiently.


How Jetty 6 differs

Jetty 6 is designed to scale to large numbers of simultaneous connections, exploiting the Java™ language's nonblocking I/O (java.nio) libraries and using an optimised output-buffer architecture (see Resources). Jetty also has a trick up its sleeve for dealing with long-lived connections: a feature known as Continuations. I'll demonstrate Continuations with a simple servlet that receives a request, waits for two seconds, and then sends a response. Next, I'll show what happens when the server has more clients than it has threads to handle them. Finally, I'll reimplement the servlet using Continuations, and you'll see the difference that they make.

To make it easier to follow what's happening in the following examples, I'll restrict the Jetty servlet engine to a single request-handling thread. Listing 1 shows the relevant configuration in jetty.xml. I actually need to allow a total of three threads in the ThreadPool: the Jetty server itself uses one, and another runs an HTTP connector, listening for incoming requests. This leaves one thread to execute servlet code.


Listing 1. Jetty configuration for a single servlet thread
                
<?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>

Next, to simulate waiting for an asynchronous event, Listing 2 shows the service() method for BlockingServlet, which simply uses a Thread.sleep() call to pause for 2,000 milliseconds before completing. It also outputs the system time at the beginning and end of execution. To help disambiguate output from different requests, it also logs a request parameter used as an identifier.


Listing 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());
  }
}

Now you can observe the servlet's behaviour in response to several simultaneous requests. Listing 3 shows the console output of five parallel requests using lynx. The command line simply launches five lynx processes, appending an identifying ordinal to the request URL.


Listing 3. Output from several concurrent requests to 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

The output in Listing 3 is no surprise. Because only one thread is available for Jetty to execute the servlet's service() method, Jetty queues each request and services it serially. The time stamps show that immediately after a response is dispatched for one request (an end message), the servlet begins working on the next request (the subsequent start message). So even though all five requests were sent simultaneously, one request must wait eight seconds before the servlet can handle it.

Remember that no useful work is being performed while the servlet blocks. This code simulates a situation where the request is waiting for an event to arrive asynchronously from another part of the application. The server is neither CPU- nor I/O-bound here: requests are queued only as a result of thread-pool exhaustion.

Now, check out how the Continuations feature in Jetty 6 can be helpful in this kind of situation. Listing 4 shows the BlockingServlet from Listing 2 rewritten using the Continuations API. I'll explain the code a little later.


Listing 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());
  }
}

Listing 5 shows the output from five simultaneous requests to ContinuationServlet; compare with Listing 3.


Listing 5. Output from several concurrent requests to 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

There are two important things to note in Listing 5. First, each start message appears twice; don't worry about this for the moment. Second, and more important, the requests are now handled concurrently, without queueing. Note that the time stamps of all the start and end messages are the same, at least at this resolution. Consequently, no request takes longer than two seconds to complete, even though only a single servlet thread is running.


Inside Jetty's Continuations mechanism

An understanding of how Jetty's Continuations mechanism is implemented will explain the effects you see in Listing 5. To use Continuations, Jetty must be configured to handle requests with its SelectChannelConnector. This connector is built on the java.nio APIs, allowing it to hold connections open without consuming a thread for each. When the SelectChannelConnector is used, ContinuationSupport.getContinuation() provides an instance of SelectChannelConnector.RetryContinuation. (However, you should code against the Continuation interface only; see Portability and the Continuations API.) When suspend() is called on RetryContinuation, it throws a special runtime exception -- RetryRequest -- which propagates out of the servlet and back through the filter chain and is caught in SelectChannelConnector. But instead of sending any response to the client as a result of the exception, the request is held in a queue of pending Continuations, and the HTTP connection is kept open. At this point, the thread that was used to service the request is returned to the ThreadPool, where it can be used to service another request.

Portability and the Continuations API

I mentioned you should use Jetty's SelectChannelConnector to enable Continuations functionality. However, the Continuations API is still valid with a traditional SocketConnector, in which case Jetty falls back to a different Continuation implementation that uses wait()/notify() behaviour. Your code will still compile and run, but without the benefits of nonblocking Continuations. If you want to keep the option of using a non-Jetty server, you could consider writing your own Continuation wrapper that uses reflection to check for the availability of Jetty's Continuations library at run time. DWR uses this strategy.

The suspended request remains in the pending Continuations queue until either the specified timeout expires, or the resume() method is called on its Continuation (more on this later). When either of these conditions occurs, the request is resubmitted to the servlet (via the filter chain). In effect, the entire request is "replayed" up until the point where suspend() was first called. When execution reaches the suspend() call the second time, the RetryRequest exception is not thrown, and execution continues as normal.

The output in Listing 5 should make sense now. As each request, in turn, enters the servlet's service() method, the start message is sent in response, and then the Continuation's suspend() method causes execution to leave the servlet, freeing up the thread to begin servicing the next request. All five requests quickly run through the first part of the service() method and enter the suspended state, and all of the start messages are output within milliseconds. Two seconds later, as the suspend() timeouts expire, the first request is retrieved from the pending queue and resubmitted to the ContinuationServlet. The start message is output a second time, the second call to suspend() returns immediately, and the end message is sent in response. The servlet code then executes again for the next queued request, and so on.

So, in both the BlockingServlet and ContinuationServlet cases, requests are queued for access to the single servlet thread. However, while the two-second pause in BlockingServlet occurs inside the servlet's thread of execution, ContinuationServlet's pause occurs outside of the servlet in SelectChannelConnector. The overall throughput of ContinuationServlet is higher because the servlet thread isn't tied up most of the time in a sleep() call.


Making Continuations useful

Now that you've seen that Continuations allow servlet requests to be suspended without thread consumption, I need to explain a little bit more of the Continuations API to show you how to use Continuations for practical purposes.

A resume() method forms a pair with suspend(). You can think of them of as the Continuations equivalent of the standard Object wait()/notify() mechanism. That is, suspend() puts a Continuation (and therefore the execution of the current method) on hold until either its timeout expires or another thread calls resume(). The suspend()/resume() pair is key to implementing a real Comet-style service using Continuations. The basic pattern is to obtain the Continuation from the current request, call suspend(), and wait until your asynchronous event arrives. Then call resume() and generate a response.

However, unlike the true language-level continuations in languages such as Scheme, or indeed the Java language's wait()/notify() paradigm, calling resume() on a Jetty Continuation doesn't mean that code execution picks up exactly where it left off. As you've seen, what actually happens is that the request associated with the Continuation is replayed. This results in two problems: undesirable reexecution of code as in ContinuationServlet in Listing 4, and loss of state: anything in scope when the call is made to suspend() is lost.

The solution to the first of these issues is the isPending() method. If the return value of isPending() is true, this means that suspend() has been called previously, and execution of the retried request has not yet reached suspend() for the second time. In other words, making code prior to your suspend() call conditional on isPending() ensures that it executes only once per request. It's best to design your application code before the suspend() call to be idempotent, so that calling it twice won't matter anyway, but where that isn't possible you can use isPending(). Continuation also offers a simple mechanism for preserving state: the putObject(Object) and getObject() methods. Use these to hold a context object with any state you need to preserve when the Continuation is suspended. You can also use this mechanism as a way of passing event data between threads, as you'll see later on.


Writing a Continuations-based application

As a vaguely real-world example scenario, I'm going to develop a basic GPS coordinate-tracking Web application. It will generate randomised latitude-longitude pairs at irregular intervals. With some imagination, the coordinates generated could be the positions of nearby public transport, marathon runners carrying GPS devices, cars in a rally, or the location of a package in transit. The interesting part is how I tell the browser about the coordinates. Figure 1 shows a class diagram for this simple GPS-tracker application:


Figure 1. Class diagram showing major components of the GPS tracker application
UML class diagram of GPS tracker components

First, the application needs something that generates coordinates. This is what RandomWalkGenerator does. Starting from an initial coordinate pair, each call to its private generateNextCoord() method takes a random constrained step away from that location and returns the new position as a GpsCoord object. When initialized, RandomWalkGenerator creates a thread that calls the generateNextCoord() method at randomized intervals and then sends the generated coordinate to any CoordListener instances that have registered themselves with addListener(). Listing 6 shows the logic of RandomWalkGenerator's loop:


Listing 6. RandomWalkGenerator's run() method
                
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 is a callback interface that just defines the onCoord(GpsCoord coord) method. In this example, the ContinuationBasedTracker class implements CoordListener. The other public method on ContinuationBasedTracker is getNextPosition(Continuation, int). Listing 7 shows the implementation of these methods:


Listing 7. The innards of ContinuationBasedTracker
                
public GpsCoord getNextPosition(Continuation continuation, int timeoutSecs) {

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

    // Wait for next update
    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();
  }
}

When a client calls getNextPosition() with a Continuation, the isPending method checks that the request is not being retried at this point, then adds it to a collection of Continuations that are waiting for a coordinate. Then the Continuation is suspended. Meanwhile, onCoord -- invoked when a new coordinate is generated -- simply loops over any pending Continuations, sets the GPS coordinate on them, and resumes them. Each retried request then completes execution of getNextPosition(), retrieving the GpsCoord from the Continuation and returning it to the caller. Note the need for synchronization here, both to guard against inconsistent state in the pendingContinuations collection and to ensure that a newly added Continuation isn't resumed before it has been suspended.

The final piece of the puzzle is the servlet code itself, shown in Listing 8:


Listing 8. GPSTrackerServlet implementation
                
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);
    }
}

As you can see, this servlet does very little. It simply obtains the request's Continuation, calls getNextPosition(), converts the GPSCoord into JavaScript Object Notation (JSON), and writes it out. Nothing here needs protection from reexecution, so I don't need to check isPending(). Listing 9 shows the output of a call to the GpsTrackerServlet, again with five simultaneous requests but only a single available thread on the server:


Listing 9. Output of 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 } }

This example is unspectacular but serves as a proof-of-concept. After the requests are dispatched, they are held open for several seconds until the coordinate is generated, at which point the responses are quickly generated. This is the basis of the Comet pattern, with Jetty handling five concurrent requests on one thread, thanks to Continuations.


Creating a Comet client

Now that you've seen how Continuations can be used in principle to create nonblocking Web services, you might wonder how to create client-side code to exploit this ability. A Comet client needs to:

  1. Hold open an XMLHttpRequest connection until a response is received.
  2. Dispatch that response to the appropriate JavaScript handler.
  3. Immediately establish a new connection.

A more advanced Comet setup could use one connection to push data from several different services to the browser, with appropriate routing mechanisms on the client and server. One possibility would be to write client-side code against a JavaScript library such as Dojo, which provides Comet-based request mechanisms in the shape of dojo.io.cometd.

However, if you're working with the Java language on the server, a great way to get advanced Comet support on both client and server is to use the DWR 2 (see Resources). If you're not familiar with DWR, you can read Part 3 of this series, "Ajax with Direct Web Remoting." DWR transparently provides an HTTP-RPC transport layer, exposing your Java objects to calls across the Web from JavaScript code. DWR generates client-side proxies, automatically marshalls and unmarshalls data, handles security concerns, provides a convenient client-side utility library, and works in all major browsers.


DWR 2: Reverse Ajax

Newly introduced with DWR 2 is the concept of Reverse Ajax. This is a mechanism by which server-side events are "pushed" to the client. Client-side DWR code transparently deals with establishing connections and parsing responses, so from a developer's point of view, events can simply be published to the client from server-side Java code.

DWR can be configured to use three different mechanisms for Reverse Ajax. One is the familiar polling approach. The second, known as piggyback, doesn't create any connections to the server. Instead, it waits until another DWR service call occurs and piggybacks pending events onto this request's response. This makes it highly efficient but means that client notification of events is delayed until the client makes an unrelated call. The final mechanism uses long-lived, Comet-style connections. And best of all, DWR can auto-detect when it's running under Jetty and switch to using Continuations for nonblocking Comet.

I'll adapt my GPS example to use Reverse Ajax with DWR 2. On the way, you'll see in more detail how Reverse Ajax works.

I no longer need my servlet. DWR provides a controller servlet that mediates client requests directly onto Java objects. I also no longer need to deal explicitly with Continuations because DWR takes care of this under the hood. So I simply need a new CoordListener implementation that publishes coordinate updates to any client browsers.

An interface called ServerContext provides DWR's Reverse Ajax magic. ServerContext is aware of all Web clients currently viewing a given page and can provide a ScriptSession to talk to each. This ScriptSession is used to push JavaScript fragments to the client from Java code. Listing 10 shows how the ReverseAjaxTracker responds to coordinate notifications, using them to generate calls to the client-side updateCoordinate() function. Note that the appendData() call on the DWR ScriptBuffer object automatically marshalls a Java object to JSON, if a suitable converter is available.


Listing 10. The notification callback method in ReverseAjaxTracker
                
public void onCoord(GpsCoord gpsCoord) {

  // Generate JavaScript code to call client-side
  // function with coord data
  ScriptBuffer script = new ScriptBuffer();
  script.appendScript("updateCoordinate(")
    .appendData(gpsCoord)
    .appendScript(");");

  // Push script out to clients viewing the page
  Collection<ScriptSession> sessions = 
            sctx.getScriptSessionsByPage(pageUrl);
            
  for (ScriptSession session : sessions) {
    session.addScript(script);
  }   
}

Next, DWR must be configured to know about ReverseAjaxTracker. In a larger application, DWR's Spring integration could be leveraged to provide DWR with Spring-created beans. Here, however, I'll just have DWR create a new instance of ReverseAjaxTracker and place it in the application scope. All subsequent DWR requests will then access this single instance.

I also need to tell DWR how to marshall data from GpsCoord beans into JSON. Because GpsCoord is a simple object, DWR's reflection-based BeanConverter is sufficient. Listing 11 shows the configuration for ReverseAjaxTracker:


Listing 11. DWR configuration for 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>

The create element's javascript attribute specifies a name that DWR uses to expose the tracker as a JavaScript object. However, in this case, my client-side code won't use it, instead having data pushed to it from the tracker. Also, some extra configuration in web.xml is needed to configure DWR for Reverse Ajax, as shown in Listing 12:


Listing 12. web.xml configuration for 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>

The first servlet init-param, activeReverseAjaxEnabled, activates polling and Comet functionality. The second, initApplicationScopeCreatorsAtStartup, tells DWR to initialize the ReverseAjaxTracker at application startup time. This overrides the usual behaviour of lazy initialization when the first request on a bean is made -- necessary in this case, because the client never does actively call a method on the ReverseAjaxTracker.

Finally, I need to implement the client-side JavaScript function invoked from DWR. The callback -- updateCoordinate() -- is passed a JSON representation of a GpsCoord Java bean, auto-serialized by DWR's BeanConverter. The function just extracts the longitude and latitude fields from the coordinate and appends them to a list via Document Object Model (DOM) calls. This is shown in Listing 13, along with my page's onload function. The onload contains a call to dwr.engine.setActiveReverseAjax(true), which tells DWR to open a persistent connection to the server and await callbacks.


Listing 13. Client-side implementation of trivial Reverse Ajax GPS tracker
                
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);
  }
}

Updating the page without JavaScript

If you want to minimize the amount of JavaScript code in your application, there's an alternative to writing out JavaScript callbacks with ScriptSession: You can wrap ScriptSession instances in a DWR Util object. This class provides simple Java methods for manipulating the browser DOM directly, and it auto-generates the necessary script behind the scenes.

Now I can point my browser to the tracker page, and DWR will begin pushing coordinate data to the client as it is generated. This implementation simply outputs a list of the generated coordinates, as shown in Figure 2:


Figure 2. Output of the ReverseAjaxTracker
Simple Web page listing generated coordinates

That's how simple it is to create an event-driven Ajax application using Reverse Ajax. And remember, thanks to DWR's exploitation of Jetty Continuations, no threads are tied up on the server while the client is waiting for a new event to arrive.

From here, it's easy to integrate a map widget from the likes of Yahoo! or Google. By changing the client-side callback, coordinates can simply be passed to the map API, instead of appended directly onto the page. Figure 3 shows the DWR Reverse Ajax GPS tracker plotting the random walk on such a mapping component:


Figure 3. ReverseAjaxTracker with a map UI
Map showing path tracing generated coordinates

Conclusions

You've now seen how Jetty Continuations combined with Comet can provide an efficient, scalable solution for event-driven Ajax applications. I haven't given any figures for the scalability of Continuations because performance in a real-world application depends on so many variables. Server hardware, choice of operating system, JVM implementation, Jetty configuration, and indeed your Web application's design and traffic profile all affect the performance of Jetty's Continuations under load. However, Greg Wilkins of Webtide (the main Jetty developers) has published a white paper on Jetty 6 that compares the performance of a Comet application with and without Continuations, handling 10,000 concurrent requests (see Resources). In Greg's tests, using Continuations cuts thread consumption, and concomitantly stack memory consumption, by a factor of more than 10.

You've also seen how easy it is to implement an event-driven Ajax application using DWR's Reverse Ajax technology. Not only does DWR save you much client- and server-side coding, but Reverse Ajax also abstracts the whole server-push mechanism away from your code. You can switch freely among the Comet, polling, or even piggyback methods, simply by altering DWR's configuration. You're free to experiment and find the best-performing strategy for your application, without any impact on your code.

If you'd like to experiment with your own Reverse Ajax applications, a great way to learn more is to download and examine the code of the DWR demos (part of the DWR source-code distribution, see Resources). The sample code used in this article is also available (see Download) if you'd like to run the examples for yourself.



Download

DescriptionNameSizeDownload method
Sample codejetty-dwr-comet-src.tgz8KB HTTP

Information about download methods


Resources

Learn

Get products and technologies

  • Jetty: Download Jetty.

  • DWR: Download DWR.

Discuss

About the author

Philip McCarthy is a London-based software-development consultant specializing in Java and Web technologies. Past work includes projects for Orange and Hewlett Packard Labs. His current focus is Web-based financial systems built with open source frameworks.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Web development
ArticleID=240597
ArticleTitle=Ajax for Java developers: Write scalable Comet applications with Jetty and Direct Web Remoting
publish-date=07172007
author1-email=philmccarthy@gmail.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers