Summary

This post shows how to combine cometd and wicket on a tomcat. Get the source here. Here is how the resulting wicket component is used:

// add a label that shows a number and can be updated via ajax
final Label clickedLabel ....

// add long running http connection to the page
final LongRunningConnectionNotifier notifier = new LongRunningConnectionNotifier("notifier", "myChannel" ){

  // this is called whenever notifier.notify( "myChannel" ) is called from any other request
  public void onNotify( List channels, AjaxRequestTarget target)
  {
    target.addComponent( clickedLabel );
  }

};

add( new AjaxLink("link"){
	public void onClick(AjaxRequestTarget target)
	{
		clicked++;
		// notify all listeners
		notifier.notify( "myChannel" );
		target.addComponent(clickedLabel);
	}
}) ;

The problem:

Sometimes web pages have to deal with input that comes from another source then the single user that uses the application. Chats for example need to show what other users have entered. Multiplayer games need to show the actions the other players make. Callcenter applications need to update what other call agents did with the calls. When using ajax, this is done by making frequent ajax-requests that rerender certain components if they have changed. Developers need to make a descision on how often these requests are made. The more often the page is updated the more up-to-date is what is presented to each user. At the same time each update-request generates server load. And as every user that has one of your pages open generates ajax-requests the number of requests can quickly become a problem. This problem is based on the fact that it is not possibleto send (push) information for a webserver to its clients. Its always the client that has to start the communication by making a http request. Even if there is nothing new that needs to be communicated.

Lets compare this with the situation where a family travels by car to visit the grand parents. The kids will ask „Are we there yet?“ all the time – even if has been just a few seconds ago. Http-Kids dont accept „I’ll tell you when we’re there. Stop asking.“ they just go on asking.

To solve this problem cometd has been invented. It makes use of long running http connections. The client (in this case the web site)  keeps a connection open to the server and thereby allows the server to act whenever there is a need to communicate. Cometd also specifies a communication protocol that allows clients to subscribe to certain channels. It requires the webserver to support long running connections, a servlet that handles the incoming connections and the subscriptions and a javascript part that runs on the client. Currently there is an implementation for jetty which can be integrated into tomcat. It uses the dojo java script toolkit. During a few hours of google-research I have seen other projects that involved cometd but nothing seemed both mature and still maintained. ( I may be terribly wrong here and are happy to be given pointers to what I have missed.) I wanted to use tomcat and jquery as I am using both in our a web game (weewar). Furthermore I am a big fan of wicket. So I’ve been thinking: It can’t be that complicated to have a lightweight implementation of long running http connections that just somehow make the client trigger a regular wicket request. After a Saturday of tinkering with the topic I came up with the code shown in the beginning of this post.

The recipie

- Add a Http11NioProtocol connector to the server.xml of your tomcat.

- Configure wicket to use the servlet instead of the filter

- Use jQuerys getScript to dynamically load a javascript snippet from port 8080.

- Use a 250 lines Cometd-Servlet that accepts and stores long running connections on port 8080. It writes a javascript  „onNotify( … );“ into the respons of a connection and closes it if this connection is to be notified.

- Add a onNotify javascript function to your page that triggers a regular wicketAjaxGet request

Lets go into more detail:

Tomcat supports long running connections with the Nio Protcolwith a specific connector, the Http11NioProtoco. So the first thing you do is to modify your server.xml to contain these two entries:

    <Connector port="80" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />

    <Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
               connectionTimeout="20000"
               redirectPort="8443" />

This enables you to access your server on port 8080 with long running connections. Its necessary to have two connectors because tomcat can only run one type of servlet (either the regular HttpServlet or the CometProcessor) on a certain type of connector.

Next thing you do is to setup a web application. Have it mapped to the root path „/“.  Your web.xml should look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC
   "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
   "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
	<servlet>
		<servlet-name>cometd</servlet-name>
		<servlet-class>cometd.test.ChatServlet</servlet-class>
	</servlet>

	<servlet-mapping>
		<servlet-name>cometd</servlet-name>
		<url-pattern>/chat</url-pattern>
	</servlet-mapping>

	<servlet>
		<servlet-name>wicket</servlet-name>
		<servlet-class>org.apache.wicket.protocol.http.WicketServlet</servlet-class>
	    <init-param>
	      <param-name>applicationClassName</param-name>
	      <param-value>cometd.test.WebApp</param-value>
	    </init-param>
	    <init-param>
	      <param-name>configuration</param-name>
	      <param-value>development</param-value>
	    </init-param>
	</servlet>

	<servlet-mapping>
		<servlet-name>wicket</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>

</web-app>

You may notice that I used the wicket servlet instead of the wicket filter. Its because I did not manage map the wicket filter to all urls but „/chat“ (web.xml does not allow to do that). And if the regular wicket http filter and the CometProcessor-Servlet run on the same url the two did not get along well.  If you want to map wicket to anything that is not the root you can use the filter as you can then separate the two filters easily.

When doing a regular http request to your tomcat, the wicket servlet will handle it and respond with a regular html page. Lets take a closer look at the javascript that is rendered into that page.

function onNotify( id, currentVersion )
{
	lastUpdate = currentVersion;
	wicketAjaxGet( callbackUrl+"&channels="+id );
}	

function connect()
{
	$.ajax({
		url: "http://localhost:8080/chat",
		dataType:'script',
		beforeSend: function(oXhr) { oXhr.setRequestHeader('Connection', 'Keep-Alive'); },
		data: "channels="+channels+"&lastUpdate="+lastUpdate+"&connectionId="+connectionId,
		complete: function(){ connect(); },
		error: function(){  connect(); }
	});
}
setTimeout( "connect();",100 );

As of version 1.2 jQuery is able to dynamically load a script from any server (and any port). This is done by setting the dataType to „script“. It appends what is set as „data“ to the url as query parameters. If such a connection is open, the server is able to answer with javascript and then close the connection. The received javascript is executed by jquery and basicly calls onNotify which then does a regular wicket ajax request . Thereafter a new long running connection is opened. callbackUrl, channels, lastUpdate and connectionId are rendered into global javascript context by the wicket component.

Lets have a closer look at the servlet:

public class ChatServlet extends HttpServlet implements CometProcessor
{

	protected static Map<String,Collection<Connection>> connections = new HashMap<String,Collection<Connection>>();
	protected static Map<String,Date> lastUpdate = new HashMap<String,Date>();
	protected static NotifierThread notifierThread = null;

	public void event(CometEvent event) throws IOException, ServletException {
		HttpServletRequest request = event.getHttpServletRequest();
		HttpServletResponse response = event.getHttpServletResponse();
		//System.out.println("Event:" + event.getEventType() );
		if (event.getEventType() == CometEvent.EventType.BEGIN) {
			synchronized (connections) {
				addConnection(request, response);
			}
		} else if (event.getEventType() == CometEvent.EventType.ERROR) {
			synchronized (connections) {
				removeConnection(request, response);
			}
			event.close();
		} else if (event.getEventType() == CometEvent.EventType.END) {
			synchronized (connections) {
				removeConnection(request, response);
			}
			event.close();
		} else if (event.getEventType() == CometEvent.EventType.READ) {
			// ignore input from stream
		}
	}

	public static void notify( long connectionId, String id )
	{
		notifierThread.notifyConnections( connectionId, id );
	}

// 200 lines omitted here that do administrative yet pretty straight forward stuff

}

The servlet reads the „channels“ parameter from each connection and stores it into a map. If all connections listening on a certain channel are to be notified it can iterate through the Connections write the javascript call to the response and close it.

These are the fundamental parts used. You can download the web here. Please let me know what you think of this solution. I’d be happy to improve this article if its of any help.


Jeder kennt es: Ab und an zieht man von einem Rechner auf den anderen um. Besonders schön ist es, wenn man einen neuen, schnelleren Rechner bekommen hat und von nun an auf dem arbeiten darf. Einer meiner Kollegen hier ist vorgestern von seinem MAC-G5 auf einen G6 umgezogen. Sein neuer Rechner hat – wie er stolz berichtete – 8 Prozessoren. Dualquadcore. Meine Freundin freut sich immer sehr, wenn sie von ihren „4 Matheschülern“ berichtet, die in ihrem Rechner für sie schuften. Er hat also nun doppelt so viele.  Einer um seinen HTML-Editor zu betreiben, einer der das Betriebssystem schaukelt, einer der einfach nur schöne Leuchteffekte macht und dabei die ganze Zeit leise „hmmm“ vor sich hin summt, einer der ein 3D Bild berechnet und die anderen Vier spielen ein MP3 ab. Eine lustige Rasselbande von Matheschülern eben.

Ich kann das  Gerede, wie einfach das alles auf dem Mac immer wäre ja echt nich leiden. „Guck mal wie einfach das geht … klick, klick … und alles läuft.“ Einerseits weil mir der jahrzentelange Religionskrieg zwischen (seinen) Macs und (meinen) PCs auf die Eier geht und andereseits weil er diesesmal auch noch so verdammt recht hat. Er hat seine alte Festplatte in den neuen Rechner eingebaut und dann das Profil mit wenigen Schritten ins neue Betriebsystem übernommen. Zitat: „Mit einem Klick“. Ich gebs ja zu: Man möchte mit einem Klick von einem Rechner auf den anderen umzuziehen. Auch wenn man bei Windows dieses Frühjahrsputz-artige Gefühl verspürt, wenn man eine neue Kiste einrichtet: Es macht einfach immer weniger Spaß Zeit mit der Wartung seiner Hardware zu verbringen.

Um für uns arme Windows Nutzer auch was zu tun hier eine Liste die hilft eine Putty Konfiguration von einem Rechner mit auf den anderen zu nehmen:

1) Start -> Ausführen -> regedt32

2) Strg-F und dann nach dem Schlüssel „Simon Tatham“ suchen.

3) Auf den gefunden Knoten rechtsklicken und „Exportieren“. Der inhalt wird dabei in eine zu wählende Datei geschrieben.

4) Die Datei auf den neuen Rechner kopieren.

5) Auf dem neuen Rechner die Datei rechtsklicken und „Zusammenführen“ auswählen.

6) Fertig

Funktioniert. Ich habs eben genauso gemacht.


Bloggen

13Apr08

Jeder bloggt. Zumindest kommt es einem so vor. Da ist viel dabei. Manches ist gut und maches nicht so – das ist ja ansich auch nicht anders zu erwarten. Weil ich in letztlich wieder ein bischen mehr Zeit habe dachte ich: Probierstes mal. Vieleicht machts ja Spaß und wenn nicht, dann weiß ich wenigstens nachher dass es nix für mich ist. In Deutsch und nicht in Englisch weil ich länger brauch um in Englisch zu formulieren und Zeit ist kostbar. Zudem kann ich mir irgendwie nicht recht vorstellen, dass „mich“ viele lesen werden. Also mal gucken was kommt.




Follow

Get every new post delivered to your Inbox.