In order to run the CometD server implementation, you need to deploy a Java web application to a servlet container.
The web application needs to configure the CometD servlet (see here for configuration details) to interpret the Bayeux protocol.
Most of the times it is also needed to write one or more user-defined services that can act upon receiving messages on Bayeux channels.
The CometD servlet must be setup in web.xml.
If you followed the primer, then Maven has configured the web.xml file for you, but here we will detail its configuration.
This is a sample web.xml:
<?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">
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.cometd.server.continuation.ContinuationCometdServlet</servlet-class>
<init-param>
<param-name>timeout</param-name>
<param-value>60000</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>cometd</servlet-name>
<url-pattern>/cometd/*</url-pattern>
</servlet-mapping>
</web-app>
The org.cometd.server.continuation.ContinuationCometdServlet must be defined and mapped in web.xml, or otherwise the server will not be able to interpret the Bayeux protocol.
It is normally mapped on /cometd/*, but you can change the mapping url-pattern at your wish.
Here is the list of configuration init parameters accepted by the ContinuationCometdServlet:
| Parameter Name | Default Value | Parameter Description |
|---|---|---|
| timeout | 30000 | The time, in milliseconds, that a server will wait for a message before responding to a long poll with an empty response. |
| interval | 0 | The time, in milliseconds, that specifies how long the client must wait between the end of one long poll requests and the start of the next. |
| maxInterval | 10000 | The max period of time, in milliseconds, that the server will wait for a new long poll from a client before that client is considered invalid and is removed |
| logLevel | 0 | The log level; 0 = warn, 1 = info, 2 = debug |
| multiFrameInterval | -1 | The period of time, in milliseconds, that specifies the client normal polling period in case the server detects more than one tab/frame connected from the same browser. A non-positive value means that the second tab/frame will be disconnected. |
| requestAvailable | false | Whether or not the current HttpServletRequest should be returned by Bayeux.getCurrentRequest() |
| filters | The path of a JSON file, relative to the WEB-INF directory of the war, that specifies DataFilters to be installed |
|
| jsonDebug | false | Whether or not the full JSON input should be kept for debugging purposes |
| channelIdCacheLimit | 0 | The limit of the ChannelId cache. Set to -1 to disable caching, set to 0 for no limits, set to any positive value to clear the cache once the limit has been reached |
If you are using Jetty 7, you may want to configure also the CrossOriginFilter.
This filter implements the Cross-Origin Resource Sharing specification, and allows recent browsers that implements it (as of November 2009, Firefox 3.5.x, Chrome 3.x and Safari 4.x) to perform cross-domain JavaScript requests (see also the transport section).
Here is an example of web.xml configuration for the CrossOriginFilter:
<?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">
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.cometd.server.continuation.ContinuationCometdServlet</servlet-class>
<init-param>
<param-name>timeout</param-name>
<param-value>60000</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>cometd</servlet-name>
<url-pattern>/cometd/*</url-pattern>
</servlet-mapping>
<filter>
<filter-name>cross-origin</filter-name>
<filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cross-origin</filter-name>
<url-pattern>/cometd/*</url-pattern>
</filter-mapping>
</web-app>
Refer to this document for the filter configuration.
The Bayeux object can be configured with a org.cometd.SecurityPolicy object, which allows to control various steps of the Bayeux protocol such as handshake, subscription, publish, etc.
By default, the Bayeux object does not have a SecurityPolicy installed, which means that any operation is authorized.
The org.cometd.SecurityPolicy has a default implementation in org.cometd.server.AbstractBayeux$DefaultPolicy, that is useful as a base class in case of customization of the SecurityPolicy (see how authentication works for an example).
The org.cometd.SecurityPolicy methods are:
boolean canHandshake(Message message); boolean canCreate(Client client, String channel, Message message); boolean canSubscribe(Client client, String channel, Message message); boolean canPublish(Client client, String channel, Message message);
The methods are self-speaking and control, respectively, if an handshake, a channel creation, a subscription to a channel and a publish to a channel are to be authorized.
The default implementation org.cometd.server.AbstractBayeux.DefaultPolicy:
/** and /*To understand how to install your custom SecurityPolicy on the Bayeux object, see how it is done in the authentication howto.
A CometD service is a Java class that allow a developer to specify the code to run when Bayeux messages are received on Bayeux channels.
A CometD service is a Java class that extends the CometD class org.cometd.server.BayeuxService, that specifies the Bayeux channels the service is interested to, and that adheres to the contract required by the BayeuxService class:
public class EchoService extends BayeuxService (1)
{
public EchoService(Bayeux bayeux) (2)
{
super(bayeux, "echo"); (3)
subscribe("/echo", "processEcho"); (4)
}
public void processEcho(Client remote, Map<String, Object> data) (5)
{
remote.deliver(getClient(), "/echo", data, null); (6)
}
}
This is a simple echo service that returns the message sent by the remote client on channel "/echo" to the remote client itself.
Note the following:
org.cometd.server.BayeuxServiceorg.cometd.Bayeux objectorg.cometd.Client API to echo the message back to that particular clientThe contract that the BayeuxService class requires for callback methods is that the methods must have one of the following signatures:
// Obtains the remote client object and the message object public void processEcho(Client remote, Message message) // Obtains the remote client object and the message's data object // (additional message information, such as the channel or the id is lost) public void processEcho(Client remote, Map<String, Object> data) // Obtains the remote client object, the channel name, the message object and the message id public void processEcho(Client remote, String channelName, Message message, String messageId) // Obtains the remote client object, the channel name, the message's data object and the message id public void processEcho(Client remote, String channelName, Map<String, Object> data, String messageId)
Note that the channel name specified in the subscribe() method can be a wildcard, for example:
public class BaseballTeamService extends BayeuxService
{
public BaseballTeamService(Bayeux bayeux)
{
super(bayeux, "baseballTeam");
subscribe("/baseball/team/*", "processBaseballTeam");
}
public void processBaseballTeam(Client remote, String channelName, Map<String, Object> data, String messageId)
{
// Upon receiving a message on channel /baseball/team/*, forward to channel /events/baseball/team/*
getBayeux().getChannel("/events" + channelName, true).publish(getClient(), data, null);
}
}
Note also how in the first example we used Client.deliver() to send a message to a particular remote client, while in the second we used Channel.publish() to send a message to anyone who subscribed to channel "/events/baseball/team/*".
Once you have written your Bayeux services it is time to setup them in your web application, plain style or Spring style.
There are several ways to integrate your Bayeux services into your web application.
All of these ways are complicated by the fact that the Bayeux object is created by a servlet, and there is no easy way to detect, in general, when the Bayeux object has been created.
The simplest way to initialize your web application with your Bayeux services is to use a configuration servlet.
This configuration servlet will have no mapping, because its only scope is to initialize (or "wire" together) your services for your web application to work properly.
Following you can find a sample web.xml:
<?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">
<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>
<servlet>
<servlet-name>configuration</servlet-name>
<servlet-class>com.acme.cometd.ConfigurationServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
</web-app>
Note how we specified <load-on-startup> to be 1 for the Cometd servlet (so that the Bayeux object gets created and put in the ServletContext), and to be 2 for the configuration servlet, so that it will be initialized only after the Cometd servlet has been initialized and hence the Bayeux object be available.
This is the code for the ConfigurationServlet:
public class ConfigurationServlet extends GenericServlet
{
public void init() throws ServletException
{
// Grab the Bayeux object
Bayeux bayeux = (Bayeux)getServletContext().getAttribute(Bayeux.ATTRIBUTE);
new EchoService(bayeux);
// Create other services here
// This is also the place where you can configure the Bayeux object
// by adding extensions or specifying a SecurityPolicy
}
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException
{
throw new ServletException();
}
}
See here about the EchoService
Instead of using a configuration servlet, it is possible to use a configuration listener, by writing a class that implements ServletContextAttributeListener.
Following you can find the web.xml file:
<?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">
<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>
<listener>
<listener-class>com.acme.cometd.BayeuxInitializer</listener-class>
</listener>
</web-app>
This is the code for the BayeuxInitializer:
public class BayeuxInitializer implements ServletContextAttributeListener
{
public void attributeAdded(ServletContextAttributeEvent event)
{
if (Bayeux.ATTRIBUTE.equals(event.getName()))
{
// Grab the Bayeux object
Bayeux bayeux = (Bayeux)event.getValue();
new EchoService(bayeux);
// Create other services here
// This is also the place where you can configure the Bayeux object
// by adding extensions or specifying a SecurityPolicy
}
}
public void attributeRemoved(ServletContextAttributeEvent event)
{
}
public void attributeReplaced(ServletContextAttributeEvent event)
{
}
}
Integration of CometD services with Spring is particularly interesting, since most of the times your Bayeux services will require other beans to perform their service.
Not all Bayeux services are as simple as the EchoService, and having Spring's dependency injection (as well as other facilities) integrated greatly simplifies development.
Below you can find 3 proposed strategies to integrate with Spring.
This strategy delays Spring initialization until the CometD servlet is initialized.
This strategy is suited for those cases where Spring beans can be initialized late because no other services or frameworks need Spring to be initialized upfront.
This strategy requires a bit of coding to glue Spring and Bayeux services together.
The reason to require glue code is twofold:
So, in order to guarantee a portable initialization, a bit of code is required.
Below you can find the configuration files and the code needed to integrate with Spring.
First, the web.xml file:
<?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">
<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>
<listener>
<listener-class>com.acme.cometd.spring.LateSpringBayeuxInitializer</listener-class>
</listener>
</web-app>
Note how we use a listener to initialize Spring. This listener completely replaces Spring's org.springframework.web.context.ContextLoaderListener, but it is based on the same classes used by it to perform context initialization at web application startup.
This means that the LateSpringBayeuxInitializer looks up by default Spring beans located in /WEB-INF/applicationContext.xml, or in locations specified by the servlet context init param contextConfigLocation, as per usual Spring configuration.
Second, Spring's applicationContext.xml file, defining the EchoService we defined earlier:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="echoService" class="com.acme.cometd.EchoService">
<constructor-arg><ref bean="bayeux" /></constructor-arg>
</bean>
</beans>
Note how there is a reference to the "bayeux" bean, which is not defined in any Spring XML file, but setup by LateSpringBayeuxInitializer, see below.
Lastly, the glue code to integrate Spring, the LateSpringBayeuxInitializer class referenced in web.xml above:
public class LateSpringBayeuxInitializer implements ServletContextListener, ServletContextAttributeListener
{
private volatile ContextLoader loader;
public void contextInitialized(ServletContextEvent event)
{
}
public void contextDestroyed(ServletContextEvent event)
{
ContextLoader loader = this.loader;
if (loader != null)
loader.closeWebApplicationContext(event.getServletContext());
}
public void attributeAdded(ServletContextAttributeEvent event)
{
if (Bayeux.ATTRIBUTE.equals(event.getName()))
{
Bayeux bayeux = (Bayeux) event.getValue();
StaticListableBeanFactory factory = new StaticListableBeanFactory();
factory.addBean("bayeux", bayeux);
GenericApplicationContext bayeuxApplicationContext = new GenericApplicationContext(new DefaultListableBeanFactory(factory));
bayeuxApplicationContext.refresh();
loader = new BayeuxContextLoader(bayeuxApplicationContext);
ApplicationContext applicationContext = loader.initWebApplicationContext(event.getServletContext());
customizeBayeux(bayeux, applicationContext);
}
}
public void attributeRemoved(ServletContextAttributeEvent event)
{
}
public void attributeReplaced(ServletContextAttributeEvent event)
{
}
protected void customizeBayeux(Bayeux bayeux, ApplicationContext applicationContext)
{
}
private static class BayeuxContextLoader extends ContextLoader
{
private final ApplicationContext parentApplicationContext;
public BayeuxContextLoader(ApplicationContext parentApplicationContext)
{
this.parentApplicationContext = parentApplicationContext;
}
@Override
protected ApplicationContext loadParentContext(ServletContext servletContext) throws BeansException
{
return parentApplicationContext;
}
}
}
Note how you can override the customizeBayeux() method and further customize the Bayeux object by, for example, looking up a org.cometd.SecurityPolicy object from Spring's application context and set it on the Bayeux object. Or, in the same way, add your own Spring-configured Bayeux extensions to the Bayeux object.
This strategy allows for normal Spring initialization, but requires that Bayeux services are marked as lazy (and recursively all other beans/services that depend on Bayeux services), again to allow the CometD servlet to initialize properly.
This strategy is suited for those cases where other frameworks (for example, Struts2) require Spring to be initialized upfront.
This strategy requires a bit of coding to be able to access the Bayeux object from Spring configuration files.
First, the web.xml file:
<?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">
<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>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>com.acme.cometd.spring.LazySpringBayeuxInitializer</listener-class>
</listener>
</web-app>
Note how we use the normal Spring listener (ContextLoaderListener) to initialize Spring, but we also add another listener, LazySpringBayeuxInitializer.
The LazySpringBayeuxInitializer is the glue code that we need to make the Bayeux object accessible from Spring configuration files:
public class LazySpringBayeuxInitializer implements ServletContextAttributeListener
{
public void attributeAdded(ServletContextAttributeEvent event)
{
if (Bayeux.ATTRIBUTE.equals(event.getName()))
{
Bayeux bayeux = (Bayeux) event.getValue();
BayeuxHolder.setBayeux(bayeux);
}
}
public void attributeRemoved(ServletContextAttributeEvent event)
{
}
public void attributeReplaced(ServletContextAttributeEvent event)
{
}
}
The LazySpringBayeuxInitializer class makes use of another class, BayeuxHolder, that is very simple:
public class BayeuxHolder
{
private static volatile Bayeux bayeux;
public static void setBayeux(Bayeux bayeux)
{
BayeuxHolder.bayeux = bayeux;
}
public static Bayeux getBayeux()
{
return bayeux;
}
}
At this point, we just need to write Spring's applicationContext.xml file as follows:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="nonLazyService" class="com.acme..." />
<bean id="bayeux" class="com.acme.cometd.spring.BayeuxHolder" factory-method="getBayeux" lazy-init="true" />
<bean id="echoService" class="com.acme.cometd.EchoService" lazy-init="true">
<constructor-arg><ref local="bayeux" /></constructor-arg>
<constructor-arg><ref local="nonLazyService" /></constructor-arg>
</bean>
</beans>
Note how the Spring configuration file can have normal beans (such as nonLazyService), and how those beans can be injected in lazy beans such as our EchoService.
Also, note how we make use of the BayeuxHolder, which is lazy-initialized, to retrieve the Bayeux object stored by LazySpringBayeuxInitializer when the CometD servlet is initialized.
It is very important to note that the EchoService, which also is lazy-initialized, can be injected only in other lazy-initialized beans or in non-singleton beans (such as beans in scope prototype); if it is injected in singleton non-lazy beans, it will not be ready (because the CometD servlet is not yet initialized), and you will get an error.
This strategy initializes the Bayeux object directly in the Spring configuration file, and injects it in the servlet context, where it is picked up by the CometD servlet.
This strategy relies a bit more on CometD implementation details and may require changes if the implementation details change.
It may or may not require glue code, depending on other details of the application being developed (see below).
It requires also a bit more of discipline because the Bayeux object can now be configured in 2 places, the Spring configuration file and the web.xml file, and you don't want to split the Bayeux configuration in different places.
The web.xml file is as follows:
<?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">
<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>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
Spring's applicationContext.xml is as follows:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="nonLazyService" class="com.acme..." />
<bean id="bayeux" class="org.cometd.server.continuation.ContinuationBayeux">
<property name="timeout" value="15000" />
</bean>
<bean class="org.springframework.web.context.support.ServletContextAttributeExporter">
<property name="attributes">
<map>
<entry key="org.cometd.bayeux">
<ref local="bayeux" />
</entry>
</map>
</property>
</bean>
<bean id="echoService" class="com.acme.cometd.EchoService" lazy-init="true">
<constructor-arg><ref local="bayeux" /></constructor-arg>
<constructor-arg><ref local="nonLazyService" /></constructor-arg>
</bean>
</beans>
Note how the implementation class of the Bayeux object is now referenced in applicationContext.xml and how the Bayeux object is exported into the servlet context via Spring's ServletContextAttributeExporter.
This only creates an un-initialized Bayeux object; only after the CometD servlet has been initialized the Bayeux object is ready to be used.
Because of this, it is important that the echoService (and any other Bayeux service and, recursively, other dependent services) is lazy-initialized.
Since the echoService is lazy-initialized, there must be a way to tell Spring to actually instantiate and initialize it when the application first needs it.
If you are using another framework that rely on Spring for dependency injection, then this step is performed by the other framework. For example, if you use Struts2 with Spring support, you don't need to tell Spring to instantiate and initialize you CometD services, because Struts2 will recognize that there is the need to inject your CometD service (for example in a Struts2 action) and Struts2 will ask Spring to provide it.
Therefore, the configuration above is perfectly sufficient.
However, if you are not using other frameworks, you need to manually tell Spring to instantiate and initialize your CometD services.
The technique to do so is similar to what described for the non-Spring service integration: writing a configuration servlet or listener that will reference your lazy-initialized CometD services. Below an example:
public class ConfigurationServlet extends GenericServlet
{
public void init() throws ServletException
{
// Grab Spring's ApplicationContent
ApplicationContext context = WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext());
// Trigger CometD service initialization
context.getBean("echoService");
}
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException
{
throw new ServletException();
}
}
And of course this configuration servlet must be mapped with a higher value for the load-on-startup element in web.xml:
<?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">
<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>
<servlet>
<servlet-name>configuration</servlet-name>
<servlet-class>com.acme.cometd.ConfigurationServlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
The org.cometd.Bayeux interface is the central object that manages remote Cometd clients (or, better, their server-side counterpart) and channels.
One only of these objects exist for each web application, and it is created by the ContinuationCometdServlet, and put in the web application ServletContext to be accessed by other server side components.
See also the services section for how to retrieve and integrate the Bayeux object in your web application.
We have seen in the authorization section, how it is possible to use
Bayeux.setSecurityPolicy(SecurityPolicy policy)
to set the security policy for the Bayeux object, so that we can have full control on the authorization bits.
Another useful method is
Bayeux.getChannel(String channel, boolean create)
which allows to retrieve, and eventually create, a channel on which it is possible to publish messages, that will be received by all remote clients subscribed to that channel.
For example, imagine an online game where a player acts and we need to tell other players what that player did.
The acting player will communicate to the server its action, and the server will forward the action to other players:
public class GameService extends BayeuxService
{
...
public void processPlayerAction(Client remote, Message message)
{
// Forward the action to other players
Channel channel = getBayeux().getChannel("/dungeons/blue_cavern", false);
channel.publish(getClient(), message.getData(), null);
}
...
}
Sometimes, however, we need to send a message to a very specific client.
Imagine you're chatting with a friend, and you both logged in with your credentials at an imaginary site called www.bayeux-chat.org.
When you registered at www.bayeux-chat.org, the application stored your information in a database row with userId=100, and your friend was registered with userId=200.
When you connect to www.bayeux-chat.org to chat with your friend, you also have a Bayeux clientId=1234ABCD, while your friend has a Bayeux clientId=5678EFGH; you decide to start chatting privately with your friend.
Your chat message needs to contain an identifier of your friend (so the server will know to whom, of the thousands client connected, send your message), let's say it's the database's userId=200, but to deliver him your message, the server has to find the server-side Bayeux client object that represents your friend, and can do that using a mapping from userIds to clientId, and
Bayeux.getClient(String clientId)
in the following way:
public class ChatService extends BayeuxService
{
...
public void processPrivateChat(Client remote, Map<String, Object> data)
{
// Extract the userId
String friendUserId = (String)data.get("friendUserId");
// Do not forget to check if that friendUserId is really your friend !
// Map the database userId to a Bayeux clientId via some other service
String friendClientId = this.userToClientMapper.get(friendUserId);
Client remoteFriend = getBayeux().getClient(friendClientId);
remoteFriend.deliver(getClient(), "/chat-room/1", data, null);
}
...
}
How you map userIds to clientIds it's project dependent, but you can find some ideas in the authentication section.
Furthermore, it is possible to obtain the current HttpServletRequest from the Bayeux object via
Bayeux.getCurrentRequest()
This method returns null by default, unless the requestAvailable configuration parameter is set to true, see the configuration section.
From the HttpServletRequest it is possible to obtain the HttpSession, the ServletContext, etc. for any need you may have to interact with the servlet API.
Lastly, it is possible to add extensions to the Bayeux object, that will affect all clients, via:
Bayeux.addExtension(Extension) Bayeux.removeExtension(Extension)
For a discussion about extensions, see here.
The org.cometd.Channel class represent a named "topic" to which clients subscribe to receive messages.
The name of a channel resembles that of a filesystem directory, for example: /level_3/dungeons/diamonds_cavern or /nyse/stocks/JAVA.
You can find more information on channel in the Bayeux channel section.
The most common operation on channel is to publish a message on it, using:
Channel.publish(Client local, Object data, String messageId)
The first argument represent the server-side local client that sends the message, and most of the time can be null, as the implementation will use a default value.
The second argument represent the data that you want to send, and the third argument is the messageId, which can be null, as the implementation will generate a new value.
public class StockService extends BayeuxService
{
...
public void stockChanged(String stockName, double price)
{
Channel channel = getBayeux().getChannel("/nyse/stocks/" + stockName, false);
if (channel != null)
{
Map<String, Object> data = new HashMap<String, Object>();
data.put("price", price);
channel.publish(null, message, null);
}
}
...
}
A channel may have one or more org.cometd.DataFilters that are used to modify the incoming and outgoing messages (for example by replacing XML markups such as <foo> with <foo>). The data filters are added and removed via:
Channel.addDataFilter(DataFilter filter); Channel.removeDataFilter(DataFilter filter);
Channels may be set to be persistent, which means that when the last subscriber unsubscribes, the channel object will remain in memory and can be accessed via the Bayeux API.
Channel.isPersistent() Channel.setPersistent(boolean persistent)
Channels may also be set as lazy, which means that all messages sent to that channel will be marked as lazy. A lazy message is a message that does not wake up the long poll to be delivered immediately to the client, but it will be delivered on the first occasion (either a non-lazy message sent to the channel, or the long poll returning because it timed out).
Channel.isLazy() Channel.setLazy(boolean lazy)
Subscriptions to channels are normally not done directly on the channel, but via the Client API.
The org.cometd.Client class is the server-side representation of a remote Bayeux client, that is, a client that connected successfully to the server using the Bayeux protocol.
An Client is identified by a unique clientId, which can be obtained via
Client.getId()
and can be used in conjunction with Bayeux.getClient(String).
Similarly to the Bayeux API, it is possible to add extensions to a Client instance. Differently from the Bayeux case where the extension is applied to all clients, in this case the extension is applied to this particular client only.
Client.addExtension(Extension extension) Client.removeExtension(Extension extension)
For a discussion about extensions, see here.
Of particular interest are the APIs to add/remove listeners on a Client instance.
Client.addListener(ClientListener listener) Client.removeListener(ClientListener listener)
The org.cometd.ClientListener interface is empty, but its subinterfaces are more interesting.
org.cometd.RemoveListener is the subinterface upon which
RemoveListener.removed(String clientId, boolean timeout)
gets called when the Bayeux server detects that a client has disconnected, either orderly (i.e. the client called disconnect()) or because or network failures or client crashes.
The listener is notified after the server-side data structures have already been cleaned up so, for example, calling Bayeux.getClient(String) from RemoveListener.removed(String, boolean) would return null.
There is an example of usage of the RemoveListener in the authentication example.
org.cometd.MessageListener is the subinterface upon which
MessageListener.deliver(Client from, Client to, Message message)
gets called when the client receives a message.
While normally it is better to use a Bayeux service to implement your business logic, a MessageListener may be used to count the number of messages, as well as to measure the latency between when a message was sent and when it was received, for example in collaboration with the timesync extension.
For completeness, there are other subinterfaces of ClientListener, but they are somewhat less interesting and documented in their relative javadocs.
We have already seen how it is possible to send a message using the Client interface.
It is however possible send multiple messages in a single batch, for example:
public class BatchingService extends BayeuxService
{
....
public void processBatch(Client remote, Message message)
{
remote.startBatch();
Map<String, Object> externalData = new HashMap<String, Object>();
// Fill the external data
remote.deliver(getClient(), "/external", externalData, null);
Map<String, Object> emailData = new HashMap<String, Object>();
// Fill the email data
remote.deliver(getClient(), "/email", emailData, null);
remote.endBatch();
}
...
}
The HTTP protocol recommends a connection limit of 2 connections per domain.
Virtually all browsers adhere to this recommendation, thus any iframes, tabs or windows on the same browser connecting to the same host need to share the two connections.
If two iframes/tabs/windows initiate a Bayeux communication, both will start a long poll connect request and both connections will be consumed.
The CometD Server implementation implements the multiple-clients advice specified in the Bayeux specification.
The server uses "BAYEUX_BROWSER" cookie to detect multiple CometD clients from the same browser.
If multiple clients are detected, then only one long poll connection is allowed and subsequent long poll requests will not wait for messages before returning. They will return immediately with the multiple-clients field of the advice object set to true. The advice will also contain an interval field set to the value of the "multiFrameInterval" servlet init parameter (see here). This instructs the client not to send another long poll until that interval has passed.
The effect of this advice is that additional client connections will poll the server with a period of "multiFrameInterval". This avoids consume both HTTP connections at the cost of some latency for the additional iframes/tabs/windows.
It is recommended that the client application monitor the /meta/connect channel for multiple-clients field in the advice. If detected, the application may ask the user to close the additional tabs, or it could automatically close them or take some other action.