In this article, I will show you how to use different Java technologies to build some simple Comet Web applications. Readers should have some knowledge about Java Servlet, Ajax and JavaScript. We will examine some of the features that support Comet in Tomcat and Jetty, so we need to use the latest versions of these two products. This document uses Tomcat 6.0.14 and Jetty 6.1.14. In addition, a JDK supporting Java 5 or later is required. This document uses JDK 1.5.0-16. We also need to check the pre-release version of Jetty 7 because it implements the Servlet 3.0 specification. We will study this specification in this article.
Understanding Comet
You may have heard of Comet because it has received some attention recently. 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 data. It sounds simple, but if you are familiar with Web applications, especially HTTP, you will know that this is not simple. Implementing Comet Web applications and ensuring scalability on browsers and servers is only possible in recent years. Later in this article, we will look at how some popular Java Web servers support the Scalable Comet architecture, but first let's look at why we need to create a Comet application, and common design patterns used to implement them.
Motivation for using Comet
There is no doubt about the success of the HTTP protocol. It is the basis for most information exchange on the Internet. However, it also has some limitations. In particular, it is a stateless, one-way protocol. The request is sent to the Web server, and the server processes the request and returns a response concurrently-that's all. The request must be sent by the client, while the server can only send data in the response to the request. This will at least affect the practicability of many types of Web applications. A typical example is a chat program. There are also some examples, such as the score of the game, stock quotes or email programs.
These limitations of HTTP are also the reason for its success. The request/response cycle makes it a classic model, that is, each connection uses a thread. This method has huge scalability as long as it can quickly provide services for requests. A large number of requests can be processed every second, and a large number of users can be processed with only 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 preceding Web applications, the server provides the data requested by the user, closes the connection, and releases the thread so that it can serve other requests. If interaction still exists after the initial data is provided, the connection is kept in the open state, so the thread cannot be released and the server cannot serve many users.
However, what if I want to maintain interaction with users after responding to the request and sending the initial data? In the early days of the Web, meta Refresh was often used. This will automatically instruct the browser to re-load the page after a specified number of seconds, so as to support simple round robin (polling ). This is not only a bad user experience, but also usually very inefficient. What if no new data is displayed on the page? At this time, you have to re-render the same page. What if there are few changes to the page and most of the pages do not change? Similarly, whether necessary or not, you have to request and retrieve all the content on the page.
The invention and popularity of Ajax have changed the above situation. Now, the server can communicate asynchronously, so you do not have to request the whole page again. You can perform incremental updates. You only need to use XMLHttpRequest to poll the server. This technology is usually called Comet. This technology has some variants, each of which has different performance and scalability. Let's take a look at these different types of Comet.
Comet Style
The emergence of Ajax makes Comet possible. The unidirectional nature of HTTP can be effectively avoided. There are actually some different ways to bypass this. As you may have guessed, the easiest way to support Comet is poll ). Use XMLHttpRequest to send a call to the server. Wait for a fixed period of time (usually using the JavaScript setTimeout Function) after the return, and then call it again. This is a very common technology. For example, most webmail applications use this technology to display emails when an email arrives.
This technology has advantages and disadvantages. In this case, you expect a quick response, just like any other Ajax request. There must be a pause between requests. Otherwise, continuous requests will throw the server, and in this case it is obviously not scalable. This pause causes a latency for the application. The longer the pause time, the more time it takes for new data on the server to arrive at the client. If the pause time is shortened, the server may be overwhelmed again. On the other hand, this is obviously the simplest way to implement Comet.
It should be pointed out that many people think that polling does not belong to Comet. On the contrary, they think that Comet is a solution to the limitations of round-robin. The most common "true" Comet technology is a variant of polling, namely long polling ). The main difference between polling and long polling is how long the server takes to respond. Long polling usually keeps the connection for a long period of time-usually several seconds, but it may also be one minute or longer. When an event occurs on the server, the response is sent and immediately closed, and the polling starts again immediately.
The advantage of long polling over general polling is that once data is available, it is immediately sent from the server to the client. The request may wait for a long time, during which no data is returned, but once new data is generated, it will be immediately sent to the client. Therefore, there is no latency. If you have used a Web-based chat program or any program that claims to be "real-time", it is likely to use this technology.
Long polling has a variation, which is the third type of Comet. This is usually called streaming ). In this style, the server pushes data back to the client, but does not close the connection. The connection will remain open until it expires and the request is resent. The XMLHttpRequest specification indicates that you can check whether the value of readyState is 3 or loading ing (instead of 4 or Loaded) and obtain the data that is "Flowing" from the server. Like long polling, this method has no latency. When the data on the server is ready, the data is sent to the client. Another advantage of this method is that it can greatly reduce the number of requests sent to the server, thus avoiding overhead and latency related to setting server connections. Unfortunately, XMLHttpRequest has many different implementations in different browsers. This technology can only be used reliably in later versions of Mozilla Firefox. Long polling is still required for Internet Explorer or Safari.
At this point, you may think that long polling and stream have a big problem. The request must exist on the server for a long time. This breaks the model of using one thread for each request, because the thread used for one request has never been released. Worse, the thread remains idle unless data is sent back. This is obviously not scalable. Fortunately, modern Java Web servers can solve this problem in many ways.
Comet in Java
Currently, many Web servers are built in Java. One reason is that Java has a rich local thread model. Therefore, implementing a typical model that connects one thread is very simple. This model is not suitable for Comet, but Java has the same solution. To effectively process Comet, non-blocking IO is required. Java provides non-blocking IO through its NIO library. Both Apache Tomcat and Jetty, the two most popular open-source servers, use NIO to add non-blocking IO to support Comet. However, the implementations of these two servers are different. Let's take a look at Tomcat and Jetty's support for Comet.
Tomcat and Comet
For Apache Tomcat, to use Comet, you mainly need to do two things. First, you need to slightly modify the Tomcat configuration file server. XML. By default, more typical synchronous I/O connectors are enabled. Now you only need to switch it to the asynchronous version, as shown in Listing 1.
Listing 1. Modifying Tomcat 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 Unique API of Tomcat. Listing 2 shows an example.
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,324 08 );
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 the event method. This is a lifecycle Method for Comet interactions. Tomcat will be called using different CometEvent instances. By checking the eventType of CometEvent, you can determine which stage is in the lifecycle. The BEGIN event occurs when the request is passed in for the first time. The READ event indicates that the data is being sent. This event is required only when the request is POST. In case of an END or ERROR event, the request is terminated.
In the example in Listing 2, the Servlet uses a MessageSender class to send data. The instance of this class is created in the servlet init method in its own thread and destroyed in the servlet destroy method. Listing 3 shows MessageSender.
Listing 3. 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. Policy ();
}
}
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 a sample code and has no direct relationship with Comet. However, there are two points to note. This class contains a ServletResponse object. Let's look back at the event Method in Listing 2. When the event is BEGIN, the response object is passed into MessageSender. In the run method of MessageSender, it uses ServletResponse to send data back to the client. Note: once all messages waiting in queue are sent, the connection is closed. In this way, long polling is realized. If you want to implement stream-style Comet, you need to keep the connection open, but still refresh the data.
Looking back at listing 2, we can see that a Weatherman class is created. This class uses MessageSender to send data back to the client. This class uses Yahoo RSS feed to obtain weather information from different regions and send the information to the client. This is a specially designed example for simulating data sources that send data asynchronously. Listing 4 shows its code.
Listing 4. 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. If you need to generate or use RSS or Atom feed, this is a very useful library. In addition, there is only one note in this code, that is, it generates another thread for sending weather data every 30 seconds. Finally, let's look at the client code using the Servlet. In this case, it is enough to add a small amount of JavaScript to a simple JSP. Listing 5 shows the code.
Listing 5. Client Comet code
<% @ Page contentType = "text/html" pageEncoding = "UTF-8" %>
<! Doctype html public "-// W3C // dtd html 4.01 Transitional // EN"
Http://www.w3.org/TR/html4/loose.dtd>
<Html>
<Head>
<Meta http-equiv = "Content-Type" content = "text/html; charset = UTF-8">
<Title> Comet Weather </title>
<Script type = "text/Javascript">
Function go (){
Var url = "http: // localhost: 8484/WeatherServer/Weather"
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 );
}
</SCRIPT>
</Head>
<Body>
<H1> Rapid Fire Weather
<Input type = "button" onclick = "go ()" value = "Go! "> </Input>
<Div id = "forecasts"> </div>
</Body>
</Html>
This Code only starts long polling when you click 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 solve the browser difference problem. In addition, you must note the callback function or the closure created for the onreadystatechange function. This function is used to paste new data from the server and then call the go function again.
Now we have seen what a simple Comet application looks like on Tomcat. There are two closely related things to do: one is to configure its connector, and the other is to implement a Tomcat-specific interface in the Servlet. You may want to know how difficult it is to "Port" the Code to Jetty. Next let's take a look at this problem.
Jetty and Comet
The Jetty server uses a slightly different technology to support Comet's scalable implementation. Jetty supports the programming structure called continuations. The idea is simple. The request is paused first and then resumed at a certain time point in the future. The request may continue if the specified time expires or a meaningful event occurs. When a request is paused, its thread is released.
You can use the org. mortbay. util. ajax. ContinuationSupport class of Jetty to create an org. mortbay. util. ajax. Continuation instance for any HttpServletRequest. This method is very different from Comet. However, continuations can be used to implement logically equivalent Comet. Listing 6 shows the code after weather servlet in Listing 2 is "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,324 08 );
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 ));
}
}
Note that this structure is very similar to the Tomcat version code. The begin, read, end, and error methods all match the same events in Tomcat. The service method of the Servlet is overwritten to create a continuation when the request first enters and pause the request until the time-out period has reached, or an event that causes it to start again occurs. The init and destroy methods are not shown above, because they are of the same version as Tomcat. The servlet uses the same MessageSender as Tomcat. Therefore, no modification is required. Note how to create a Weatherman instance using the begin method. The usage of this class is exactly the same as that of Tomcat. Even the client code is the same. Only servlet changes. Although the servlet changes a lot, it still corresponds to the event model in Tomcat.
Hopefully this is encouraging. Although the same Code cannot be run in both Tomcat and Jetty, It is very similar. Of course, one attractive aspect of JavaEE is portability. Most of the code running in Tomcat can be run in Jetty without modification, and vice versa. Therefore, it is not surprising that the Java Servlet specification of the next version includes the standardization of asynchronous request processing (that is, the underlying technology behind Comet. Let's take a look at this specification: Servlet 3.0 specification.
Servlet 3.0 specification
Here, we will not go into all the details of the Servlet 3.0 specification, but just look at what Comet servlet may look like if it runs in the Servlet 3.0 container. Note the word "possible. This specification has been published in the Public Preview version, but there is no final version at the time of writing this article. 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,324 44 );
Async. start (weatherman );;
}
}
Fortunately, this version is much simpler. In all fairness, if the event model of Tomcat is not followed, a similar implementation can be implemented in Jetty. This event model seems reasonable and can be easily implemented in containers other than Tomcat (such as Jetty), but there is no relevant standard.
Let's look back at listing 7. Note that its annotation Declaration supports asynchronous processing and sets the timeout time. The startAsync method is a new method on HttpServletRequest, which returns an instance of the new javax. servlet. AsyncContext class. Note that MessageSender now transmits the AsynContext reference, instead of the reference of ServletResponse. Here, the response should not be closed, but the complete method on the AsyncContext instance should be called. It should also be noted that Weatherman is passed directly to the start method of the AsyncContext instance. This will start a new thread in the current ServletContext.
Besides, although Tomcat and Jetty are quite different from each other, it is not too difficult to modify the APIS proposed by the Servlet 3.0 standard by programming in the same style. It should also be noted that Jetty 7 is designed to implement Servlet 3.0 and is currently in beta state. However, at the time of writing this article, it has not yet implemented the latest version of this specification.
Conclusion
Comet Web applications bring new Web interactions. It brings some complex challenges for large-scale implementation of these features. However, the leading Java Web server is providing mature and stable technologies for implementing Comet. In this article, you will see the differences and similarities between the current Comet style on Tomcat and Jetty, and the ongoing standardization of Servlet 3.0 specifications. Tomcat and Jetty make it possible to build scalable Comet applications today, and clarify the future upgrade path for Servlet 3.0 standardization.