Saturday, October 24, 2009

simple cometd / jetty private client messaging

Recently I've been researching comet servers and protocols. Based primarily on the Comet Daily server maturity guide / feature matrix, I was split between Jetty/java-cometd & Orbited. Since Orbited's released version did not support cross-domain connections, I went with Jetty/java-cometd.

I needed:
  • Cross-browser support
  • No need for broadcast messages
  • Ability to send messages to specific clients
The application I am writing for is not Java based. At the moment Java does not play any role in our environment. What extremely limited documentation there already is seems to assume familiarity with Java servlets and Maven. I have no prior Maven experience, and the last time I seriously wrote any Java code was 2002. This will hopefully help others in the same situation. I don't claim this to be "right" just that it works for me.

I tried to follow the primer on the cometd project site. It didn't work, bit gave me a good starting point. I ended up with a directory layout like:
jetty7-test
|-- pom.xml
\-- src
\-- main
|-- java
| \-- test
| \-- cometd
| \-- PrivateServlet.java
\-- webapp
|-- index.html
\-- WEB-INF
|-- web.xml

pom.xml

The pom.xml looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>test.cometd</groupId>
<artifactId>jetty7-test</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>

<build>
<defaultGoal>install</defaultGoal>
<plugins>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<overlays>
<overlay />
</overlays>
</configuration>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>7.0.0.v20091005</version>
<configuration>
<!-- uncommet to scan for updates while running -->
<!-- <scanIntervalSeconds>10</scanIntervalSeconds> -->
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- needed since for me it defaults to java 1.3 compiling -->
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<!-- get Jetty and cometd-java -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.cometd.java</groupId>
<artifactId>cometd-java-server</artifactId>
<version>1.0.0rc0</version>
</dependency>
</dependencies>

</project>

It doesn't fetch the jquery or dojo components since that wasn't working. And it doesn't matter for me, since that is hosted separately.

web.xml

The web.xml looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">

<!-- Request that Jetty create an MBean to manage the Bayeux instance -->
<context-param>
<param-name>org.eclipse.jetty.server.context.ManagedAttributes</param-name>
<param-value>org.cometd.bayeux</param-value>
</context-param>


<!-- Load the cometd service -->
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.cometd.server.continuation.ContinuationCometdServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>cometd</servlet-name>
<url-pattern>/cometd/*</url-pattern>
</servlet-mapping>

<!-- Load our extension -->
<servlet>
<servlet-name>privatemsg</servlet-name>
<!-- the package name (test.cometd) and class name of the servlet to load -->
<servlet-class>test.cometd.PrivateServlet</servlet-class>
<!-- IMPORTANT!! Must be set to load after conetd -->
<load-on-startup>2</load-on-startup>
</servlet>

</web-app>
The load-on-startup values indicate the order in which modules are loaded. They must be set so your code loads after the cometd server

PrivateServlet.java

The PrivateServlet.java looks like this:
package test.cometd;



import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServlet;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServletResponse;

import org.cometd.Bayeux;
import org.cometd.Client;
import org.cometd.Message;
import org.cometd.server.AbstractBayeux;
import org.cometd.server.BayeuxService;
import org.eclipse.jetty.util.log.Log;

public class PrivateServlet extends HttpServlet
{
public PrivateServlet()
{
}


@Override
public void init(ServletConfig config) throws ServletException
{

super.init(config);

//get a reference to the previously instatiated bayeux server
final Bayeux bayeux=(Bayeux)getServletContext().getAttribute(Bayeux.ATTRIBUTE);
if (bayeux==null) {
config.getServletContext().log("No "+Bayeux.ATTRIBUTE+" initialized");
throw new UnavailableException(Bayeux.ATTRIBUTE);
}

//this makes it so the user cannot subscribe to anything,
//limiting the service to direct messages only.
bayeux.setSecurityPolicy(new AbstractBayeux.DefaultPolicy()
{
public boolean canSubscribe(org.cometd.Client client, String channel, org.cometd.Message message)
{
return false;
}
});


new PrivateService(bayeux);

}

/**
* Simple private message system
*/
public static class PrivateService extends BayeuxService
{
public PrivateService(Bayeux bayeux)
{
super(bayeux, "private");
subscribe("/service/sendprivate/*", "directMessage");
}

public Object directMessage(Client source, String channel, Object data, String messageId)
{
Client dest = getBayeux().getClient(channel.substring(21));
if (dest!=null) {
//Log.info("relay "+data+" to "+dest);
send(dest,"/service/recvprivate",data,null);
return "OK";
} else {
//Log.info("DEST NOT FOUND "+channel.substring(21));
return "Failed";
}
}
}


@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException
{
((HttpServletResponse)res).sendError(503);
}
}

Ready to Run


To start the Jetty server, and run with the specified configuration, just type
mvn clean jetty:run

that will automatically download dependencies, compile the java code, and start the server.

To use this Bayeux extension, the javascript on the client side must tell the server what clientid the user has. Then the server, via phomet from PHP or similar, can send a message to "/service/sendprivate/", which the client will pick up by listening to "/service/recvprivate"

No comments:

Post a Comment