Using Java to implement comet style web apps

  • 2020-04-01 04:25:32
  • OfStack

start
      In this article, I'll show you how to build some simple comet-style Web applications using a variety of Java technologies. You should have some familiarity with Java servlets, Ajax, and JavaScript. We'll look at some of the comet-enabled features in Tomcat and Jetty, so you'll need to use the latest versions of both products. This article USES Tomcat 6.0.14 and Jetty 6.1.14. We also need a JDK that supports Java 5 or later. This article USES JDK 1.5.0-16.
      Understand the Comet
      You've probably heard of Comet, because it's been getting some attention lately. Comet is sometimes called reverse Ajax or server-side push. The idea is simple: push data directly from the server to the browser without waiting for the browser to request it. It sounds simple, but if you're familiar with Web applications, especially the HTTP protocol, you'll know that it's anything but simple. Implementing comet-style Web applications while ensuring scalability on browsers and servers has only become possible in the last few years. Later in this article, we'll look at how some popular Java Web servers support scalable Comet architectures, but first we'll look at why Comet applications are created and the common design patterns used to implement them.
      The motivation for using Comet
      The success of HTTP is not in doubt. It is the basis for most information exchange on the Internet. However, it has some limitations. In particular, it is a stateless, one-way protocol. The request is sent to the Web server, which processes the request and sends back a response-that's all. The request must be made by the client, and the server can only send data in response to the request. At the very least, this can affect the usability of many types of Web applications. A typical example is a chat program. Other examples include game scores, stock quotes, or email programs.
      These limitations of HTTP are also responsible for some of its success. The request/response cycle makes it the classic model of using one thread per connection. This approach is highly scalable as long as the requests can be serviced quickly. A large number of requests can be processed per second, and a large number of users can be processed with a small number of servers. This is ideal for many classic Web applications, such as content management systems, search applications, and e-commerce sites. In any of the above Web applications, the server provides the data requested by the user, closes the connection, and frees that thread to serve other requests. If interaction is possible after the initial data is provided, the connection is left open, so the thread cannot be freed and the server cannot serve many users.
      But what if you want to keep interacting with the user after responding to the request and sending the initial data? In the early days of the Web, this was often implemented using meta refresh. This automatically instructs the browser to reload the page after a specified number of seconds, enabling rudimentary polling. Not only is this a poor user experience, it is often very inefficient. What if there is no new data to display on the page? You have to rerender the same page. What if there are few changes to the page, and most of the page is unchanged? Again, you have to re-request and retrieve everything on the page, whether it's necessary or not.
      The invention and popularity of Ajax changed that. Now, the server can communicate asynchronously, so you don't have to rerequest the entire page. Incremental updates are now available. Simply poll the server using XMLHttpRequest. This technique is often referred to as Comet. There are variations of this technique, each with different performance and scalability. Let's take a look at these different styles of Comet.
 
Comet style
      The advent of Ajax made Comet possible. The one-way nature of HTTP can be effectively circumvented. There are actually different ways around this. As you might have guessed, the easiest way to support Comet is to poll. The call is made to the server using XMLHttpRequest, returns, waits for a fixed period of time (usually using JavaScript's setTimeout function), and then calls again. This is a very common technique. For example, most webmail applications use this technique to display E-mail when it arrives.
      The technology has advantages and disadvantages. In this case, you expect a quick response back, just like any other Ajax request. There must be a pause between requests. Otherwise, continuous requests can overwhelm the server, and this is obviously not scalable. This pause causes a delay in the application. The longer you pause, the more time it takes for new data on the server to reach the client. If you shorten the pause time, you will again face the risk of crashing the server. But on the other hand, this is clearly the easiest way to implement Comet.
      It should now be noted that many people believe that polling is not Comet. Instead, they see Comet as a solution to the limitations of polling. The most common "true" Comet technique is a variation on polling, called long polling. The main difference between polling and long polling is how long the server takes to respond. A long poll usually holds the connection for a long time -- usually a few seconds, but it can be a minute or more. When an event occurs on the server, the response is sent and then shut down, and polling immediately restarts.
      The advantage of long polling over regular polling is that data is sent from the server to the client as soon as it is available. The request may wait a long time, during which no data is returned, but as soon as new data is available, it is immediately sent to the client. So there's no delay. If you've ever used a web-based chat program, or any program that claims to be "real time," it's probably using this technique.
      There is a variant of long polling, which is the third style of Comet. This is often called streaming. In this style, the server pushes the data back to the client without closing the connection. The connection will remain open until it expires, causing the request to be reissued. The XMLHttpRequest specification shows that you can check whether the value of readyState is 3 or Receiving (instead of 4 or Loaded) and get the data that is "flowing out" of the server. Like long polling, there is no latency. When the data on the server is ready, it is sent to the client. Another advantage of this approach is that it can significantly reduce the number of requests sent to the server, thereby avoiding the overhead and latency associated with setting up a server connection. Unfortunately, XMLHttpRequest has many different implementations in different browsers. This technique works reliably only with newer versions of Mozilla Firefox. For Internet Explorer or Safari, you still need to use long polling.
      At this point, you might think that both long polling and streaming have a big problem. Requests need to be on the server for a long time. This breaks the model of using one thread per request, because the thread used for one request is never freed. Worse, the thread remains idle until data is sent back. This is clearly not scalable. Fortunately, there are many ways that modern Java Web servers can solve this problem.
      In Java Comet
      Many Web servers are now built in Java. One reason is that Java has a rich native threading model. So implementing a typical model of one thread per connection is very simple. This model doesn't work well for Comet, but Java has a way around it, too. In order to effectively handle Comet, non-blocking IO is required, and Java provides non-blocking IO through its NIO library. The two most popular open source servers, Apache Tomcat and Jetty, both leverage NIO to add non-blocking IO to support Comet. Let's look at Tomcat and Jetty's support for Comet.
 
Tomcat and Comet
      For Apache Tomcat, there are two main things you need to do to use Comet. First, the Tomcat configuration file server.xml needs to be slightly modified. The more typical synchronous IO connector is enabled by default. Now you simply switch it to the asynchronous version, as shown in listing 1.
      Listing 1. Modifying Tomcat's server.xml
 


<!-- This is the usual Connector, comment it out and add the NIO one --> 
  <!-- Connector URIEncoding="utf-8" connectionTimeout="20000" port="8084" 
protocol="HTTP/1.1" redirectPort="8443"/ --> 
<Connector connectionTimeout="20000" port="8080" protocol="org.apache. 
coyote.http11.Http11NioProtocol" redirectPort="8443"/> 

 
      Servlet. This is obviously a Tomcat specific interface. Listing 2 shows an example of this.
      Listing 2. Tomcat Comet servlet
 


public class TomcatWeatherServlet extends HttpServlet implements CometProcessor {
  private MessageSender messageSender = null;
  private static final Integer TIMEOUT = 60 * 1000;
  @Override
  public void destroy() {
  messageSender.stop();
  messageSender = null;
  }
  @Override
  public void init() throws ServletException {
  messageSender = new MessageSender();
  Thread messageSenderThread =
  new Thread(messageSender, "MessageSender[" + getServletContext()
  .getContextPath() + "]");
  messageSenderThread.setDaemon(true);
  messageSenderThread.start();
  }
  public void event(final CometEvent event) throws IOException, ServletException {
  HttpServletRequest request = event.getHttpServletRequest();
  HttpServletResponse response = event.getHttpServletResponse();
  if (event.getEventType() == CometEvent.EventType.BEGIN) {
  request.setAttribute("org.apache.tomcat.comet.timeout", TIMEOUT);
  log("Begin for session: " + request.getSession(true).getId());
  messageSender.setConnection(response);
  Weatherman weatherman = new Weatherman(95118, 32408);
  new Thread(weatherman).start();
  } else if (event.getEventType() == CometEvent.EventType.ERROR) {
  log("Error for session: " + request.getSession(true).getId());
  event.close();
  } else if (event.getEventType() == CometEvent.EventType.END) {
  log("End for session: " + request.getSession(true).getId());
  event.close();
  } else if (event.getEventType() == CometEvent.EventType.READ) {
  throw new UnsupportedOperationException("This servlet does not accept
  data");
  }
  }
  }

The CometProcessor interface requires that the event method be implemented. This is a lifecycle method for Comet interactions. Tomcat will be invoked using a different CometEvent instance. By checking the eventType of CometEvent, you can determine which phase of the life cycle you are in. The BEGIN event occurs when the request is first passed in. The READ event indicates that the data is being sent and is only needed if the request is a POST. The request terminates when an END or ERROR event is encountered.

In the example in listing 2, the Servlet sends data using a MessageSender class. An instance of this class is created in the servlet's init method in its own thread and destroyed in the servlet's destroy method. Listing 3 shows MessageSender.

Listing 3. The MessageSender


private class MessageSender implements Runnable {
  protected boolean running = true; 
  protected final ArrayList messages = new ArrayList(); 
  private ServletResponse connection; 
  private synchronized void setConnection(ServletResponse connection){
  this.connection = connection; 
  notify(); 
  }
  public void send(String message) {
  synchronized (messages) {
  messages.add(message); 
  log("Message added #messages=" + messages.size()); 
  messages.notify(); 
  }
  }
  public void run() {
  while (running) {
  if (messages.size() == 0) {
  try {
  synchronized (messages) {
  messages.wait(); 
  }
  } catch (InterruptedException e) {
  // Ignore
  }
  }
  String[] pendingMessages = null; 
  synchronized (messages) {
  pendingMessages = messages.toArray(new String[0]); 
  messages.clear(); 
  }
  try {
  if (connection == null){
  try{
  synchronized(this){
  wait(); 
  }
  } catch (InterruptedException e){
  // Ignore
  }
  }
  PrintWriter writer = connection.getWriter(); 
  for (int j = 0;  j < pendingMessages.length;  j++) {
  final String forecast = pendingMessages[j] + "
"; 
  writer.println(forecast); 
  log("Writing:" + forecast); 
  }
  writer.flush(); 
  writer.close(); 
  connection = null; 
  log("Closing connection"); 
  } catch (IOException e) {
  log("IOExeption sending message", e); 
  }
  }
  }
  } 


This class is basically boilerplate code, not directly related to Comet. But there are two caveats. This class contains a ServletResponse object. Looking back at the event method in listing 2, when the event is BEGIN, the response object is passed to MessageSender. In the run method of MessageSender, it USES the ServletResponse to send data back to the client. Note that once all the queued messages have been sent, it closes the connection. This enables long polling. If you want to implement streaming Comet, you need to keep the connection open but still refresh the data.

Looking back at listing 2, you can see that a Weatherman class is created. It is this class that USES MessageSender to send data back to the client. This class USES the Yahoo RSS feed to get weather information for different locales and sends that information to the client. This is a specially designed example to simulate a data source that sends data asynchronously. Listing 4 shows its code.

Listing 4. The Weatherman


private class Weatherman implements Runnable{
  private final List zipCodes; 
  private final String YAHOO_WEATHER = "http://weather.yahooapis.com/forecastrss?p="; 
  public Weatherman(Integer... zips) {
  zipCodes = new ArrayList(zips.length); 
  for (Integer zip : zips) {
  try {
  zipCodes.add(new URL(YAHOO_WEATHER + zip)); 
  } catch (Exception e) {
  // dont add it if it sucks
  }
  }
  }
  public void run() {
  int i = 0; 
  while (i >= 0) {
  int j = i % zipCodes.size(); 
  SyndFeedInput input = new SyndFeedInput(); 
  try {
  SyndFeed feed = input.build(new InputStreamReader(zipCodes.get(j)
  .openStream())); 
  SyndEntry entry = (SyndEntry) feed.getEntries().get(0); 
  messageSender.send(entryToHtml(entry)); 
  Thread.sleep(30000L); 
  } catch (Exception e) {
  // just eat it, eat it
  }
  i++; 
  }
  }
  private String entryToHtml(SyndEntry entry){
  StringBuilder html = new StringBuilder("
"); 
  html.append(entry.getTitle()); 
  html.append("
"); 
  html.append(entry.getDescription().getValue()); 
  return html.toString(); 
  }
  } 


This class USES the Project Rome library to parse RSS feeds from Yahoo Weather. This is a very useful library if you need to generate or use RSS or Atom feeds. In addition, the only thing worth noting about this code is that it generates another thread for sending weather data every 30 seconds. Finally, let's look at one more place: the client code that USES this Servlet. In this case, a simple JSP with a little JavaScript is enough. Listing 5 shows this code.

Listing 5. The client Comet code


  "http://www.w3.org/TR/html4/loose.dtd"> 
 
 
   
     
     
            var request = new XMLHttpRequest();  
        request.open("GET", url, true);  
        request.setRequestHeader("Content-Type","application/x-javascript; ");  
        request.onreadystatechange = function() { 
          if (request.readyState == 4) { 
            if (request.status == 200){ 
              if (request.responseText) { 
                document.getElementById("forecasts").innerHTML = 
request.responseText;  
              } 
            } 
            go();  
          } 
        };  
        request.send(null);  
      } 

This code simply starts a long poll when the user clicks the Go button. Note that it USES the XMLHttpRequest object directly, so this will not work in Internet Explorer 6. You may need to use an Ajax library to resolve browser differences. The only other thing to note is the callback function, or the closure created for the requested onreadystatechange function. This function pastes new data from the server and then calls the go function again.

Now we've seen what a simple Comet application looks like on Tomcat. There are two things that are closely related to Tomcat: one is to configure its connector and the other is to implement a Tomcat specific interface in the Servlet. You may wonder how difficult it is to "port" this code to Jetty. So let's look at that.
Jetty and Comet

The Jetty server USES a slightly different technique to support a scalable implementation of Comet. Jetty supports a programming structure called continuations. The idea is simple. The request is suspended and then resumed at some point in the future. The expiration of the specified time, or the occurrence of some meaningful event, may cause the request to continue. When the request is suspended, its thread is released.

Can use Jetty org. Mortbay. Util. Ajax. ContinuationSupport classes for any it create org. Mortbay. Util. Ajax. The Continuation of an instance. This approach is quite different from Comet. However, continuations can be used to achieve the logical equivalent of comet.listing 6 shows the code after the weather servlet in listing 2 was "ported" to Jetty.

Listing 6. Jetty Comet servlet


 public class JettyWeatherServlet extends HttpServlet {
  private MessageSender messageSender = null; 
  private static final Integer TIMEOUT = 5 * 1000; 
  public void begin(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
  request.setAttribute("org.apache.tomcat.comet", Boolean.TRUE); 
  request.setAttribute("org.apache.tomcat.comet.timeout", TIMEOUT); 
  messageSender.setConnection(response); 
  Weatherman weatherman = new Weatherman(95118, 32408); 
  new Thread(weatherman).start(); 
  }
  public void end(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
  synchronized (request) {
  request.removeAttribute("org.apache.tomcat.comet"); 
  Continuation continuation = ContinuationSupport.getContinuation
  (request, request); 
  if (continuation.isPending()) {
  continuation.resume(); 
  }
  }
  }
  public void error(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
  end(request, response); 
  }
  public boolean read(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
  throw new UnsupportedOperationException(); 
  }
  @Override
  protected void service(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException {
  synchronized (request) {
  Continuation continuation = ContinuationSupport.getContinuation
  (request, request); 
  if (!continuation.isPending()) {
  begin(request, response); 
  }
  Integer timeout = (Integer) request.getAttribute
  ("org.apache.tomcat.comet.timeout"); 
  boolean resumed = continuation.suspend(timeout == null ? 10000 :
  timeout.intValue()); 
  if (!resumed) {
  error(request, response); 
  }
  }
  }
  public void setTimeout(HttpServletRequest request, HttpServletResponse response,
  int timeout) throws IOException, ServletException,
  UnsupportedOperationException {
  request.setAttribute("org.apache.tomcat.comet.timeout", new Integer(timeout)); 
  }
  } 


The most important thing to note here is that the structure is very similar to the Tomcat version of the code. Begin, read, end, and error methods all match the same events in Tomcat. The Servlet's service method is overridden to create a continuation when the request first comes in and suspend the request until either the timeout has expired or an event occurs that causes it to restart. The init and destroy methods are not shown because they are the same as the Tomcat version. This servlet USES the same MessageSender as Tomcat. Therefore, no changes are required. Notice how the begin method creates a Weatherman instance. The use of this class is exactly the same as in the Tomcat version. Even the client code is the same. Only servlets have changed. Although the servlet changes are large, they still correspond to the event model in Tomcat.

Hopefully that's encouraging. Although the exact same code cannot be run in both Tomcat and Jetty, it is very similar. Of course, one of the attractions of JavaEE is its portability. Most of the code that runs in Tomcat runs in Jetty without modification, and vice versa. So it's no surprise that the next version of the Java Servlet specification includes standardization of asynchronous request processing, the underlying technology behind Comet. Let's take a look at this specification: the Servlet 3.0 specification.
The Servlet 3.0 specification

Instead of going into the full details of the Servlet 3.0 specification, let's look at what Comet servlets might look like if they were run in a Servlet 3.0 container. Pay attention to the word "maybe." The specification has been released in a public preview, but as of this writing, there is no final version. Therefore, listing 7 shows an implementation that complies with the public preview specification.

Listing 7. Servlet 3.0 Comet


  @WebServlet(asyncSupported=true, asyncTimeout=5000)
  public class WeatherServlet extends HttpServlet {
  private MessageSender messageSender; 
  // init and destroy are the same as other
  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
  throws ServletException, IOException {
  AsyncContext async = request.startAsync(request, response); 
  messageSender.setConnection(async); 
  Weatherman weatherman = new Weatherman(95118, 32444); 
  async.start(weatherman); ; 
  }
  } 


Happily, this version is much simpler. To be fair, if you don't follow Tomcat's event model, you can have a similar implementation in Jetty. This event model seems reasonable and easy to implement in containers other than Tomcat, such as Jetty, but there are no standards.

Looking back at listing 7, notice that its annotation declares that it supports asynchronous processing and sets a timeout. StartAsync method is a new method on it, it returns the new javax.mail. Servlet. AsyncContext an instance of a class. Note that MessageSender now passes a reference to the AsynContext, not the ServletResponse. In this case, instead of turning off the response, call the complete method on the AsyncContext instance. Also note that the Weatherman is passed directly to the start method of the AsyncContext instance. This will start a new thread in the current ServletContext.

And, while significantly different from Tomcat or Jetty, it's not too difficult to modify the same style of programming to handle the API proposed by the Servlet 3.0 specification. It should also be noted that Jetty 7 was designed to implement Servlet 3.0 and is currently in beta. However, as of this writing, it does not implement the latest version of the specification.

conclusion

Comet-style Web applications can bring a whole new level of interactivity to the Web. It presents some complex challenges for implementing these features on a large scale. However, the leading Java Web servers are providing mature, stable technologies for implementing Comet. In this article, you've seen the differences and similarities between the current style of Comet on Tomcat and Jetty, as well as the ongoing standardization of the Servlet 3.0 specification. Tomcat and Jetty make it possible to build scalable Comet applications today, and define a future upgrade path for Servlet 3.0 standardization.


Related articles: