CometD 2.9.1
Copyright © 2013 The Original Author(s)
19 October 2016
Table of Contents
List of Tables
CometD is a scalable web event routing bus that allows you to write low-latency, server-side, event-driven web applications. Typical examples of such applications are stock trading applications, web chat applications, online games, and monitoring consoles.
CometD makes use of an Ajax push technology pattern known as Comet, but also uses emerging web standards such as WebSocket for low latency communication.
The CometD project provides Java and JavaScript libraries that allow you to write low-latency, server-side, event-driven web applications in a simple and portable way. You can therefore concentrate on the business side of your application rather than worrying about low-level details such as the transport (HTTP or WebSocket), the scalability and the robustness. The CometD libraries provide these latter characteristics.
If you are new to CometD, you can follow this learning path:
Read Chapter 3, Installation to download, install CometD and to try out its demos.
Read Chapter 5, Primer to get your hands dirty with CometD with a step-by-step tutorial.
Read Chapter 6, Concepts & Architecture to understand the abstractions that CometD defines.
Study the CometD demos that ship with the CometD distribution .
Read the rest of this reference book for further details.
You can contribute to the CometD project and be involved in the CometD community, including:
Trying it out and reporting issues at http://bugs.cometd.org
Participating in the CometD Users and CometD Development mailing lists.
Helping out with the documentation by contacting the mailing lists or by reporting issues.
Spreading the word about CometD in your organization.
The services provided are provided directly by the CometD committers, ensuring the maximum result.
CometD training for your developers. Training includes tips and tricks, experience gained by developing CometD applications over the years and much more.
Development of proof-of-concept applications. We can develop these much quicker than developers not well experienced in CometD.
Development of an application framework tailored for your needs. Your developers can concentrate more on the implementation of the application rather than on CometD details.
Development of custom CometD features. Nothing like real-world use cases to improve an open source project like CometD.
CometD advices to your developers. Your development team has direct access to CometD experts to ask questions or opinions, so that mistakes are caught early when they cost less rather than later when they will cost a lot more.
CometD applications review. If you have already developed your CometD application, we can review it and suggest fixes or improvements to make it better.
CometD performance tuning. If your CometD application scales only up to a certain point, we can review its performance and suggest the best changes to apply to make it scale better.
CometD clustering review. If you need to scale horizontally, CometD provides a scalability cluster, see Section 8.4, “Scalability Clustering with Oort”. Clustering is never easy, but we can suggest the right approach to make your application clusterable.
Any other CometD-related service that you may need, we can provide it.
Webtide, part of Intalio, is the company that provides CometD commercial support.
Contact us for any information you need, no obligations on your part.
You can contribute to the CometD projects in several ways. The CometD project values your contribution, of any kind, and participating in mailing lists, submitting bugs or documentation enhancements is equally important as contributing code.
The simplest way to contribute to the CometD project is to subscribe to the CometD mailing lists. There are two mailing lists:
cometd-users@googlegroups.com, to be used for discussions about how to use CometD. This is a low traffic mailing list. To subscribe to this list, go to the mailing list home page and follow the instructions to apply for membership.
cometd-dev@googlegroups.com, to be used for discussions about the CometD implementation. This is a low traffic mailing list. To subscribe to this list, go to the mailing list home page and follow the instructions to apply for membership.
New members can join immediately the mailing list and receive messages, but their first post will be subject to moderation. If you post for the first time, please allow some hour to moderators to review the message and approve your post. This is done to reduce spam to the minimum possible.
The CometD project maintains a JIRA issue tracker that is used to submit issues about the CometD project, from bugs to feature requests, to documentation tasks, etc. Virtually every activity is tracked in this JIRA instance.
In order to submit an issue, you must have a valid Dojo Foundation login.
If you do not have one already, you can create one by visiting this link. Choose your username and password, follow the instructions, and once your account is created, do not try to login to the Dojo foundation, as you do not need to. Go back to JIRA and login to JIRA with the account you just created.
Once you are logged in into JIRA, you will be able to submit new issues and browse and comment existing ones. JIRA is a very nice issue tracking tool, and you can customize to your needs. For more information about JIRA, see the JIRA documentation.
The CometD project code is hosted at GitHub CometD Organization. The repository for the CometD libraries is https://github.com/cometd/cometd. You can build the CometD project yourself by following the instruction in Appendix A, Building.
In order to contribute code, you need to have a Contributor License Agreement (CLA) with the Dojo Foundation.
Don't worry ! It's easy and you can fill it online. Since contributing code normally requires also interaction with JIRA, it is best if you already have a Dojo Foundation account before filling the CLA. See the section called “Contributing by Reporting Issues” for how to create a Dojo Foundation account.
Go to the Dojo Foundation CLA online form, read it carefully and fill in the form at the bottom of the page. Once you have the CLA in place, you can contribute code via GitHub pull requests.
The repository for the CometD documentation is https://github.com/cometd/cometd-documentation, while the repository for the CometD tutorials is https://github.com/cometd/cometd-tutorials. The CometD documentation and tutorials are written in DocBook, and built using Maven.
There is no need for a Contributor License Agreement to contribute fixes or enhancements to the documentation or the tutorials, you can just contribute via GitHub pull requests.
You can download the CometD distribution from http://download.cometd.org.
Then unpack the distribution in a directory of your choice:
$ tar zxvf cometd-<version>-distribution.tgz $ cd cometd-<version>/
The CometD Demos contain:
Two full chat applications (one developed with Dojo, one with jQuery).
Examples of extensions such as message acknowledgement, reload, timesync and timestamp.
An example of how to echo private messages to a particular client only.
Clustered Auction demo (using the Oort clustering).
To try out the CometD demos shipped with the distribution, you need a Java Development Kit (JDK) - version 5.0 or later, and either Maven, version 3 or later, or a compliant servlet 2.5 or greater container such as Jetty.
Maven requires you to set up the JAVA_HOME
environment variable to point to your
JDK installation.
After that, running the CometD demos is very simple.
Assuming that $COMETD
is the CometD installation directory, and that you have the
mvn executable in your path:
$ cd $COMETD $ cd cometd-demo $ mvn jetty:deploy-war
The last command starts an embedded Jetty that listens on port 8080. Now point your browser to http://localhost:8080, to see the CometD Demos main page.
In the $COMETD/cometd-demo/target
directory you can find a deployable WAR
file that you can deploy in any Servlet (version 2.5 or greater) Container.
Refer to the Servlet Container configuration manual to learn how to deploy the WAR in your Servlet
Container. Refer also to Section 8.2.1.5, “Configuring Servlet 3 Asynchronous Features” for further
information and for specific instructions related to deployment on Servlet 3.0 (or greater) Containers.
When CometD does not work, the first thing that you want to do is to enable debug logging. This is helpful because by reading the debug logs you get a better understanding of what is going on in the system (and that alone may give you the answers you need to fix the problem), and because CometD developers will probably need the debug logs to help you.
To enable debug logging on the JavaScript client library (see Chapter 7, JavaScript Library) you must pass
the logLevel
field to the configuration of the cometd
object, with value
'debug'
(see also Section 7.1, “Configuring and Initializing” for other configuration options):
cometd.configure({ url: 'http://localhost:8080/cometd', logLevel: 'debug' });
The CometD JavaScript library uses the window.console
object to output log statements.
Once you have logging enabled on the JavaScript client library, you can see the debug logs in the browser JavaScript console. For example, in Firefox 24 you can open the JavaScript console by clicking on Tools → Web Developer → Web Console, while in Chrome 30 you can open the JavaScript console by clicking on Tools → Developer Tools.
Internet Explorer does not define the window.console
object as other browsers do,
resulting in CometD not being able to output logs in the JavaScript console, depending on the
Internet Explorer version.
To enable debug logging on the Java server library (see Section 8.2, “Server Library”), you must pass
the logLevel
option to the configuration, with the value 3
(see also
Section 8.2.1, “Configuring the Java Server” for further information):
<?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.CometdServlet</servlet-class> <init-param> <param-name>logLevel</param-name> <param-value>3</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>cometd</servlet-name> <url-pattern>/cometd/*</url-pattern> </servlet-mapping> </web-app>
The CometD Java libraries (both client and server) use SLF4J as logging framework.
By default, SLF4J comes with with a simple binding that does not log statements produced at debug level. Therefore you must configure SLF4J with a more advanced binding such as Log4J or Logback. Refer to the SLF4J documentation and the binding implementation documentation on how to configure logging when using the advanced bindings.
As a simple example, for Log4J it is usually enough to add the SLF4J-Log4J binding jar
(slf4j-log4j12-<slf4j-version>.jar
) and the Log4J jar
(log4j-<log4j-version>.jar
) in WEB-INF/lib
, and then add
a properly configured log4j.properties
file in WEB-INF/classes
.
You don't want to add these files manually; they are reported here just as a simple reference.
Usually these dependencies are configured in the build system that you use to create your web application.
Once you have logging enabled on the Java server library, you can see the debug logs in the terminal where you launched your Servlet Container (or in its log files, depending on how it is configured).
Working on a project that uses the CometD API requires preparation, especially regarding tools, that can save you a huge amount of time. One tool that should not be missing is Firebug (if you're using Firefox for development), or the equivalent for Internet Explorer 8, called Developer Tools.
The CometD project is built using Maven, and using Maven to also build your application is a natural fit. This Primer uses Maven as the basis for the setup, build and run of your application, but other build tools can apply the same concepts.
If you are working in the Windows OS, avoid at all costs using a path that contains spaces, such as "C:\Document And Settings\", as your base path. Use a base path such as "C:\CometD\" instead.
You can set up the project in two ways: using Section 5.1.1, “The Maven Way” or Section 5.1.2, “The Non-Maven Way”. For both, you can follow Section 5.1.3, “Setup Details” to see how some of the files of the project are set up.
Setting up a project based on the CometD libraries using Maven uses the Maven archetypes, which create the skeleton of the project, in a style very similar to Rails scaffolding.
Issue the following command from a directory that does not contain a
pom.xml
file (otherwise you will get a Maven error), for example an empty
directory:
$ cd /tmp $ mvn archetype:generate -DarchetypeCatalog=http://cometd.org ... Choose archetype: 1: http://cometd.org -> cometd-archetype-dojo-jetty6 2: http://cometd.org -> cometd-archetype-jquery-jetty6 3: http://cometd.org -> cometd-archetype-dojo-jetty7 4: http://cometd.org -> cometd-archetype-jquery-jetty7 5: http://cometd.org -> cometd-archetype-spring-jquery-jetty7 6: http://cometd.org -> cometd-archetype-spring-dojo-jetty7 Choose a number:
As you can see, there are six archetypes available that build a skeleton application using the Dojo or jQuery JavaScript toolkits, both with the choice of using Jetty 6 or Jetty 7 and Spring. Choose Dojo with Jetty 7, which is archetype number 3. The archetype generation requires that you define several properties and generates the application skeleton for you, for example:
Choose a number: : 3 Define value for property 'groupId': : org.cometd.primers Define value for property 'artifactId': : dojo-jetty7-primer Define value for property 'version': 1.0-SNAPSHOT: : Define value for property 'package': org.cometd.primers: : [INFO] Using property: cometdVersion = 2.3.1 [INFO] Using property: jettyVersion = 7.4.4.v20110707 Confirm properties configuration: groupId: org.cometd.primers artifactId: dojo-jetty7-primer version: 1.0-SNAPSHOT package: org.cometd.primers cometdVersion: 2.3.1 jettyVersion: 7.4.4.v20110707 Y: : ... [INFO] BUILD SUCCESS
Then:
$ cd dojo-jetty7-primer/
The skeleton project now exists as follows:
$ tree . . |-- pom.xml `-- src `-- main |-- java | `-- org | `-- cometd | `-- primers | |-- BayeuxInitializer.java | `-- HelloService.java `-- webapp |-- WEB-INF | `-- web.xml |-- application.js `-- index.jsp
The skeleton project is ready for you to run using the following command:
$ mvn install jetty:run ...
Now point your browser at http://localhost:8080/dojo-jetty7-primer, and you should see this message:
CometD Connection Succeeded Server Says: Hello, World
That's it. You have already written your first CometD application :-)
The first step is to configure your favorite JavaScript toolkit, in our example Dojo, that
the web container must serve. Using the Maven Way, this is obtained automatically by overlaying
the CometD Dojo bindings WAR file, cometd-javascript-dojo-[version].war
,
but here you must do it manually.
The cometd-javascript-dojo-[version].war
is located in the
cometd-javascript/dojo/target
directory of the CometD distribution.
Unpack the Dojo toolkit to a directory.
Delete the dojox/cometd
directory that Dojo provides and replace it
with the one the cometd-javascript-dojo-[version].war
contains.
The content of the dojox/cometd
directory should be the following:
dojox/cometd |-- ack.js |-- reload.js |-- timestamp.js `-- timesync.js
Delete the file dojox/cometd.js
that Dojo provides and replace it with
the one at the same path in the cometd-javascript-dojo-[version].war
.
Add the org
directory from the
cometd-javascript-dojo-[version].war
, and all its content, at the same
level of the dojox
directory.
The final content, equivalent to that produced by the Maven way, should be like this:
. |-- dijit |-- dojo |-- dojox | |-- cometd | | |-- ack.js | | |-- reload.js | | |-- timestamp.js | | `-- timesync.js | `-- cometd.js |-- org | |-- cometd | | |-- AckExtension.js | | |-- ReloadExtension.js | | |-- TimeStampExtension.js | | `-- TimeSyncExtension.js | `-- cometd.js |-- WEB-INF | |-- classes | | `-- org | | `-- cometd | | `-- primers | | |-- BayeuxInitializer.class | | `-- HelloService.class | |-- lib | | |-- bayeux-api-[version].jar | | |-- cometd-java-common-[version].jar | | |-- cometd-java-server-[version].jar | | |-- jetty-continuation-[version].jar | | |-- jetty-jmx-[version].jar | | |-- jetty-servlets-[version].jar | | `-- jetty-util-[version].jar | `-- web.xml |-- application.js `-- index.jsp
The org
directory contains the new shared CometD implementation and the
shared extensions, while the correspondent files in the dojox
directory are
the Dojo bindings.
Other bindings exist for the jQuery toolkit, but the shared CometD implementation is the same.
The second step is to configure the server side. If you use Java, this means that you have to set up the CometD servlet that responds to messages from clients. The details of the server side configuration and service development are explained in Section 8.2, “Server Library”.
The last step is to write a JSP (or HTML) file that downloads the JavaScript dependencies and the JavaScript application, as explained in the following section.
The JSP file, index.jsp
, contains the reference to the JavaScript toolkit
dependencies and to the JavaScript application file:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <script type="text/javascript" src="${pageContext.request.contextPath}/dojo/dojo.js.uncompressed.js"></script> <script type="text/javascript" src="application.js"></script> <script type="text/javascript"> var config = { contextPath: '${pageContext.request.contextPath}' }; </script> </head> <body> ... </body> </html>
It also configures a JavaScript configuration object, config
, with variables
that the JavaScript application might need. This is totally optional.
The JavaScript application, contained in the application.js
file,
configures the cometd
object and starts the application.
The archetypes provide:
require(['dojo/dom', 'dojo/_base/unload', 'dojox/cometd', 'dojo/domReady!'], function(dom, unloader, cometd) { function _connectionEstablished() { dom.byId('body').innerHTML += '<div>CometD Connection Established</div>'; } function _connectionBroken() { dom.byId('body').innerHTML += '<div>CometD Connection Broken</div>'; } function _connectionClosed() { dom.byId('body').innerHTML += '<div>CometD Connection Closed</div>'; } // Function that manages the connection status with the Bayeux server var _connected = false; function _metaConnect(message) { if (cometd.isDisconnected()) { _connected = false; _connectionClosed(); return; } var wasConnected = _connected; _connected = message.successful === true; if (!wasConnected && _connected) { _connectionEstablished(); } else if (wasConnected && !_connected) { _connectionBroken(); } } // Function invoked when first contacting the server and // when the server has lost the state of this client function _metaHandshake(handshake) { if (handshake.successful === true) { cometd.batch(function() { cometd.subscribe('/hello', function(message) { dom.byId('body').innerHTML += '<div>Server Says: ' + message.data.greeting + '</div>'; }); // Publish on a service channel since the message is for the server only cometd.publish('/service/hello', { name: 'World' }); }); } } // Disconnect when the page unloads unloader.addOnUnload(function() { cometd.disconnect(true); }); var cometURL = location.protocol + "//" + location.host + config.contextPath + "/cometd"; cometd.configure({ url: cometURL, logLevel: 'debug' }); cometd.addListener('/meta/handshake', _metaHandshake); cometd.addListener('/meta/connect', _metaConnect); cometd.handshake(); });
Notice the following:
The use of dojo.addOnLoad()
to wait for the document to load up before
executing the cometd
object initialization.
The use of dojo.addOnUnload()
to disconnect when the page is
refreshed or closed.
The use of the function _metaHandshake()
to set up the initial
configuration on first contact with the server (or when the server has lost client
information, for example because of a server restart).
This is totally optional, but highly recommended.
The use of the function _metaConnect()
to detect when the communication
has been successfully established (or re-established). This is totally optional,
but highly recommended.
Be warned that the use of the _metaConnect()
along with the
_connected
status variable can result in your code (that in this
simple example sets the innerHTML property) to be called more than once if,
for example, you experience temporary network failures or if the server restarts.
Therefore the code that you put in the
_connectionEstablished()
function must be
idempotent.
In other words, make sure that if the _connectionEstablished()
function is called more than one time, it will behave exactly as if it is
called only once.
The CometD project implements various Comet techniques to provide a scalable web messaging system, one that can run over HTTP or over other emerging web protocols such as WebSocket.
The client is the entity that initiates a connection, and the server is the entity that accepts the connection. The connection established is persistent – that is, it remains open until either side decides to close it.
Typical clients are browsers (after all, this is a web environment), but might also be other applications such as Java applications, browser plugin applications (such as Silverlight), or scripts in any scripting language.
Depending on the Comet technique employed, a client might open more than one physical connection to the server, but you can assume there exists only one logical conduit between one client and the server.
The CometD project uses the Bayeux protocol (see Appendix C, The Bayeux Protocol Specification v1.0) to exchange information between the client and the server. The unit of information exchanged is a Bayeux message formatted in JSON. A message contains several fields, some of which the Bayeux protocol mandates, and others that applications might add. A field is a key/value pair; saying that a message has a foo field means that the message has a field whose key is the string foo.
All messages the client and server exchange have a channel field. The channel field provides the characterization of messages in classes. The channel is a central concept in CometD: publishers publish messages to channels, and subscribers subscribe to channels to receive messages. This is strongly reflected in the CometD APIs.
A channel is a string that looks like a URL path such as /foo/bar
, /meta/connect
or /service/chat
.
The Bayeux specification defines three types of channels: meta channels, service channels and broadcast channels (see Appendix C, The Bayeux Protocol Specification v1.0).
A channel that starts with /meta/
is a meta channel, a channel that starts with
/service/
is a service channel, and all other channels are broadcast channels.
A message whose channel field is a meta channel is referred to as a meta message, and similarly there are service messages and broadcast messages.
The application creates service channels and broadcast channels; an application can create as many as it needs, and can do so at any time.
The CometD implementation creates meta channels; applications cannot create new meta channels. Meta channels provide to applications information about the Bayeux protocol (see Section 6.3.7, “Bayeux Protocol”), for example, whether handshakes have been successful or not, or whether the connection with the server is broken or has been re-established.
Applications create service channels, which are used in the case of request/response style of communication between client and server (as opposed to the publish/subscribe style of communication of broadcast channels, see below).
Applications also create broadcast channels, which have the semantic of a messaging topic and are used in the case of the publish/subscribe style of communication, where one sender wants to broadcast information to multiple recipients.
You can use wildcards to match multiple channels: channel /foo/*
matches /foo/bar
but not /foo/bar/baz
.
The latter is matched by /foo/**
.
You can use wildcards for any type of channel: /meta/*
matches all meta channels,
and /service/**
matches /service/bar
as well as /service/bar/baz
.
Channel /**
matches all channels.
You can specify the wildcards only as the last segment of the channel, so these are invalid channels:
/**/foo
or /foo/*/bar
.
CometD implements a web messaging system, in particular a web messaging system based on the publish/subscribe paradigm.
In a publish/subscribe messaging system publishers send messages, which are characterized in classes. Subscribers express their interest in one or more classes of messages, and receive only messages that match the interest they have subscribed to. Senders, in general, have no idea which or how many recipients receive the messages they publish.
CometD implements the hub-spoke topology. In the default configuration, this means that there is one central server (the hub) and all clients connect to that server via conduit links (the spokes).
In CometD, the server receives messages from publishers and, if the message's channel is a broadcast channel, re-routes the messages to interested subscribers. The CometD server treats meta messages and service messages in a special way; it does not re-route them to any subscriber (by default it is forbidden to subscribe to meta channels, and it is a no-operation to subscribe to service channels).
For example, imagine that clientAB
subscribes to channels /A
and
/B
, and clientB
subscribes to channel /B
.
If a publisher publishes a message on channel /A
, only clientAB
receives it.
On the other hand, if a publisher publishes a message on channel /B
, both clientAB
and clientB
receive the message. Furthermore, if a publisher publishes a message on channel
/C
, neither clientAB
nor clientB
receives the message, which ends its
journey on the server.
Re-routing broadcast messages is the default behavior of the server, and it does not need any application
code to perform the re-routing.
Looking from a high level then, you see messages flowing back and forth among clients and server through the conduits. A single broadcast message might arrive at the server and be re-routed to all clients; you can imagine that when it arrives on the server, the message is copied and that a copy is sent to each client (although, for efficiency reasons, this is not exactly what happens). If the sender also subscribes to the channel it published the message to, it receives a copy of the message back.
The following sections take a deeper look at how the CometD implementation works.
It should be clear by now that CometD, at its heart, is a client/server system that communicates via a protocol, the Bayeux protocol.
In the CometD implementation, the half-object plus protocol pattern captures the client/server communication: when a half-object on the client establishes a communication conduit with the server, its correspondent half-object is created on the server, and the two can – logically – communicate. CometD uses a variation of this pattern because there is the need to abstract the transport that carries messages to and from the server. The transport can be based on the HTTP protocol, but in recent CometD versions also on the WebSocket protocol (and you can plug in more transports).
In broad terms, the client is composed of the client half-object and the client transport, while the server is a more complex entity that groups server half-objects and server transports.
Sessions are a central concept in CometD. They are the representation of the half-objects involved in the protocol communication.
There are three types of sessions:
Client sessions – the client half-object on the remote client side.
Client sessions are represented by the
org.cometd.Cometd
object in JavaScript, and by the
org.cometd.bayeux.client.ClientSession
class (but more frequently by its subclass
org.cometd.bayeux.client.BayeuxClient
) in Java.
The client creates a client session to establish a Bayeux communication with the server,
and this allows the client to publish and receive messages.
Server sessions – the server half-object on the server side.
Server sessions are on the server, and are represented by the
org.cometd.bayeux.server.ServerSession
class; they are the counterpart of client sessions.
When a client creates a client session, it is not initially associated with a correspondent
server session.
Only when a client session establishes the Bayeux communication with the server does the server create its
correspondent server session, as well as the link between the two half-objects.
Each server session has a message queue. Messages publish to a channel and must be delivered
to remote client sessions that subscribe to the channel. They are first queued into the server session's
message queue, and then delivered to the correspondent client session.
Local sessions – the client half-object on the server side, represented by class
org.cometd.bayeux.server.LocalSession
.
Local sessions can be thought of as clients living in the server. They do not represent a remote client,
but instead a server-side client. Local sessions can subscribe to channels and publish messages like a client
session can, but they live on the server.
The server only knows about server sessions, and the only way to create a server session is to create
its correspondent client session first, and then make it establish the Bayeux communication with the server.
For this reason, on the server side, there is the additional concept of local session.
A local session is a client session that happens to live on the server, and hence is local to the server.
For example, imagine that a remote client publishes a message every time it changes its state. Other remote clients can just subscribe to the channel and receive those state update messages. But what if, upon receiving a remote client state update, you want to perform some activity on the server? Then you need the equivalent of a remote client, but living on the server, and that's what local sessions are.
Server-side services are associated with a local session. Upon creation of the server-side service, the local session handshakes and creates the correspondent server session half-object, so that the server can treat client sessions and local sessions in the same way (because it sees them both as server sessions). The server delivers messages sent to a channel to all server sessions that subscribe to that channel, no matter if they are remote client sessions or local sessions.
For further information on services, see Section 8.2.2, “Using Services”.
The server is represented by an instance of
org.cometd.bayeux.server.BayeuxServer
.
The BayeuxServer
object acts as a:
Repository for server sessions, see Section 6.3.1, “Sessions”.
Repository for server transports – represented by the
org.cometd.bayeux.server.ServerTransport
class.
A server transport is a server-side component that handles the details of the communication with the client.
There are HTTP server transports as well as a WebSocket server transport, and you can plug in other types as well.
Server transports abstract the communication details so that applications can work knowing only
Bayeux messages, no matter how they arrive on the server.
Repository for server channels – represented by the
org.cometd.bayeux.server.ServerChannel
class.
A server channel is the server-side representation of a channel; it can receive and publish Bayeux messages.
Repository for extensions – represented by the
org.cometd.bayeux.server.BayeuxServer.Extension
class.
Extensions allow applications to interact with the Bayeux protocol by modifying or even deleting
or replaying incoming and/or outgoing Bayeux messages.
For further information about extensions, see Chapter 9, Extensions.
Central authorization authority, via an instance of the security policy – represented by the
org.cometd.bayeux.server.SecurityPolicy
class.
CometD interrogates the security policy to authorize any sensible operation the server performs, such as handshakes,
channel creation, channel subscription and channel publishing. Applications can provide their own
security policy to implement their own authorization logic.
For further information about the security policy, see Section 8.2.3, “Authorization”.
Authorizers – represented by the org.cometd.bayeux.server.Authorizer
class allow you to apply more fine-grained authorization policies.
For further information on authorizers, see Section 8.2.4, “Server Channel Authorizers”.
Message processor, by coordinating the work of server transports, extensions and security policy, and by implementing a message flow algorithm (see Section 6.3.4, “Message Processing”) that allows applications to interact with messages and channels to implement their application logic.
Applications use listeners to interact with sessions, channels and the server. The Java and JavaScript APIs allow applications to register different kinds of listeners that receive notifications of the correspondent events. You can usefully think of extensions, security policies and authorizers as special types of listeners. The following sections treat them as such.
Examples of client session listeners include the following:
You can add extensions to a client session to interact with the incoming
and outgoing messages that arrive and that the session sends, via
ClientSession.addExtension(ClientSession.Extension)
.
A client session is a repository for channels; you can add message listeners to a channel to
notify you when a message arrives on that particular channel, via
ClientSession.getChannel(String).addListener(ClientSessionChannel.MessageListener)
.
On the server, the model is similar but much richer.
You can add extensions to a BayeuxServer
instance for all messages that flow through the server
via BayeuxServer.addExtension(BayeuxServer.Extension)
.
BayeuxServer
allows you to add listeners that it notifies when channels are created or destroyed
via BayeuxServer.addListener(BayeuxServer.ChannelListener)
,
and when server sessions are created or destroyed via
BayeuxServer.addListener(BayeuxServer.SessionListener)
.
ServerChannel
allows you to add authorizers via
ServerChannel.addAuthorizer(Authorizer)
, and listeners that get notified when a message arrives
on the channel via ServerChannel.addListener(ServerChannel.MessageListener)
, or when a client
subscribes or unsubscribes to the channel via
ServerChannel.addListener(ServerChannel.SubscriptionListener)
.
ServerSession
allows you to add extensions for messages that flow through the server session
via ServerSession.addExtension(ServerSession.Extension)
.
ServerSession
allows you to add listeners that get notified when the session is removed (for
example because the client disconnects, or because the client disappears and therefore the server expires the
correspondent server session) via ServerSession.addListener(ServerSession.RemoveListener)
.
ServerSession
allows you add listeners that can interact with the server session's message queue
for example to detect whether the queue is too big via
ServerSession.addListener(ServerSession.MaxQueueListener)
, or to add/remove/coalesce messages
just before they are sent via ServerSession.addListener(ServerSession.DeQueueListener)
.
ServerSession
allows you add listeners that get notified when a message is received by the
server session (no matter on which channel) via
ServerSession.addListener(ServerSession.MessageListener)
.
This section describes message processing on both the client and the server. Use the following image to understand the detailed components view that make up the client and the server.
When a client sends messages, it uses the client-side channel to publish them.
The client retrieves the client channel from the client session via
ClientSession.getChannel(String)
.
Messages first pass through the extensions, which process messages one by one;
if one extension denies processing of a message, it is deleted and it is not sent to the server.
At the end of extension processing, the messages pass to the client transport.
The client transport converts the messages to JSON (for the Java client, this is done by a
JSONContext.Client
instance, see Section 8.3.1, “JSONContext API”), establishes the
conduit with the server transport and then sends the JSON string over the conduit, as the payload
of a transport-specific envelope (for example, an HTTP request or a WebSocket message).
The envelope travels to the server, where the server transport receives it.
The server transport converts the messages from the JSON format back to message objects
(through a JSONContext.Server
instance, see Section 8.3.1, “JSONContext API”),
then passes them to the BayeuxServer
instance for processing.
The BayeuxServer
processes each message in the following steps:
It invokes BayeuxServer
extensions (methods rcv()
or rcvMeta()
);
if one extension denies processing, a reply is sent to the
client indicating that the message has been deleted, and no further processing is performed for the message.
It invokes ServerSession
extensions (methods rcv()
or rcvMeta()
,
only if a ServerSession
for that client exists);
if one extension denies processing, a reply is sent to the
client indicating that the message has been deleted, and no further processing is performed for the message.
It invokes authorization checks for both the security policy and the authorizers; if the authorization is denied, a reply is sent to the client indicating the failure, and no further processing is performed for the message.
If the message is a service or broadcast message, the message passes through
BayeuxServer
extensions (methods send()
or sendMeta()
).
It invokes server channel listeners; the application adds server channel listeners on the server, and offers the last chance to modify the message before it is eventually sent to all subscribers (if it is a broadcast message). All subscribers see any modification a server channel listener makes to the message, just as if the publisher has sent the message already modified. After the server channel listeners processing, the message is frozen and no further modifications should be made to the message. Applications should not worry about this freezing step, because the API clarifies whether the message is modifiable or not: the API has as a parameter a modifiable message interface or an unmodifiable one to represent the message object. This step is the last processing step for an incoming non-broadcast message, and it therefore ends its journey on the server. A reply is sent to publishers to confirm that the message made it to the server (see below), but the message is not broadcast to other server sessions.
If the message is a broadcast message, for each server session that subscribes to the channel,
the message passes through ServerSession
extensions (methods send()
or
sendMeta()
), then the server session queue
listeners are invoked and finally the message is added to the server session queue for delivery.
If the message is a lazy message (see Section 8.2.7, “Lazy Channels and Messages”), it is sent on first occasion. Otherwise the message is delivered immediately. If the server session onto which the message is queued corresponds to a remote client session, it is assigned a thread to deliver the messages in its queue through the server transport. The server transport drains the server session message queue, converts the messages to JSON and sends them on the conduit as the payloads of transport-specific envelopes (for example, an HTTP response or a WebSocket message). Otherwise, the server session onto which the message is queued corresponds to a local session, and the messages in its queue are delivered directly to the local session.
For both broadcast and non-broadcast messages, a reply message is created, passes through
BayeuxServer
extensions and ServerSession
extensions (methods send()
or
sendMeta()
). It then passes to the server
transport, which converts it to JSON through a JSONContext.Server
instance (see
Section 8.3.1, “JSONContext API”), and sends it on the conduit as the payload of a transport-specific envelope
(for example, an HTTP response or a WebSocket message).
The envelope travels back to the client, where the client transport receives it.
The client transport converts the messages from the JSON format back to message objects, for the Java client
via a JSONContext.Client
instance (see Section 8.3.1, “JSONContext API”).
Each message then passes through the extensions (methods send()
or sendMeta()
),
and channel listeners and subscribers are notified of the message.
The round trip from client to server back to client is now complete.
When Bayeux messages is received by the server, a thread is allocated to handle the messages, and server-side listeners are invoked in this thread. The CometD implementation does not spawn new threads to call server-side listeners; in this way the threading model is kept simple and very similar to the Servlet threading model.
This simple threading model implies that if a server-side listener takes a long time to process the message and to return control to the implementation, then the implementation cannot process the next messages that may arrive, most often halting the whole server processing.
This is due to the fact that a Bayeux client uses a limited number of connections to interact with the server. If a message sent to one connection takes a long time to be processed on the server, the client may send additional messages on that connection, but those will not be processed until the previous message processing ends.
It is therefore very important that if the application knows that a message may trigger a time consuming task (for example a database query), it does so in a separate thread.
Services (see Section 8.2.2, “Using Services”) are an easy way to setup server-side listeners but share the same threading model with normal server-side listeners: if they need to perform time consuming tasks, they need to do so in a separate thread, for example:
@Service public class MyService { @Inject private BayeuxServer bayeuxServer; @Session private LocalSession localSession; @Listener("/service/query") public void processQuery(final ServerSession remoteSession, final ServerMessage message) { new Thread() { public void run() { Map<String, Object> data = performTimeConsumingTask(message); // Send data to client once the time consuming task is finished remoteSession.deliver(localSession, message.getChannel(), responseData, null); } }.start(); } }
Now that you know that applications interact with CometD through listeners, and how both the client and the server process messages, you need to know what an application should do to interact with messages to perform its business logic.
For an application to interact with authentication, it must register a custom instance of a
SecurityPolicy
and override method SecurityPolicy.canHandshake(...)
.
The SecurityPolicy
can customize the handshake reply (for example, to give details about
an authentication failure) by retrieving the handshake reply from the handshake request:
public class MySecurityPolicy extends DefaultSecurityPolicy
{
public boolean canHandshake(BayeuxServer server, ServerSession session, ServerMessage message)
{
boolean authenticated = authenticate(session, message);
if (!authenticated)
{
ServerMessage.Mutable reply = message.getAssociated();
// Here you can customize the reply
}
return authenticated;
}
}
Meta messages and service messages end their journey on the server. An application can only interact with these kinds of messages via server channel listeners, and therefore must use such listeners to perform its business logic.
You can add server channel listeners in the following ways:
Directly via the API at initialization time (see Section 8.2.2.3, “Services Integration”).
Indirectly by using inherited services (see
Section 8.2.2.1, “Inherited Services”). You accomplish this by
calling AbstractService.addService(...),
or via annotated services (see Section 8.2.2.2, “Annotated Client-Side and Server-Side Services”) using @Listener
annotations.
Applications that need to perform time consuming tasks in server-side listeners should do so in a separate thread to avoid blocking the processing of other incoming messages (see Section 6.3.5, “Threading”).
Broadcast messages arrive to the server and are delivered to all ServerSessions
that subscribe
to the message's channel.
Applications can interact with broadcast messages via server channel listeners (in the same way as with
non-broadcast messages, see above), or by using a LocalSession
that subscribes to the message's
channel.
You can use this latter solution directly via the API at initialization time (see
Section 8.2.2.3, “Services Integration”), or indirectly via annotated services (see
Section 8.2.2.1, “Inherited Services”) using @Subscription
annotations.
Applications that need to perform time consuming tasks in server-side listeners should do so in a separate thread to avoid blocking the processing of other incoming messages (see Section 6.3.5, “Threading”).
Applications that want to deliver messages to a specific client can do so by looking up its correspondent
server session and delivering the message using ServerSession.deliver(...)
.
For example:
Remote client client1
wants to send a message to another remote client client2
.
Both clients are already connected and therefore have already performed the handshake with the server.
Their handshake contained additional information regarding their userId
, so that
client1
declared to be "Bob" and client2
declared to be "Alice".
The application could have used a SecurityPolicy
or a server channel message listener on channel
/meta/handshake
to perform a mapping between the userId
and the server session's id.
Now Bob wants to send a private message only to Alice.
The client1
can use a service channel for private messages (such as /service/private
),
so that messages are not broadcast, and the application is set up so that a server channel listener routes
messages arriving to /service/private
to the other remote client.
@Service public class PrivateMessageService { @Session private ServerSession session; @Listener("/service/private") public void handlePrivateMessage(ServerSession sender, ServerMessage message) { // Retrieve the userId from the message String userId = message.get("targetUserId"); // Use the mapping established during handshake to // retrieve the ServerSession for a given userId ServerSession recipient = findServerSessionFromUserId(userId); // Deliver the message to the other peer recipient.deliver(session, message.getChannel(), message.getData(), null); } }
Applications might need to broadcast messages on a particular channel in response to an external event.
Since BayeuxServer
is the repository for server channels, the external event handler just needs
a reference to BayeuxServer
to broadcast messages:
public class ExternalEventBroadcaster { private final BayeuxServer bayeuxServer; public ExternalEventBroadcaster(BayeuxServer bayeuxServer) { this.bayeuxServer = bayeuxServer; // Create a local session that will act as the "sender" this.session = bayeuxServer.newLocalSession("external"); this.session.handshake(); } public void onExternalEvent(ExternalEvent event) { // Retrieve the channel to broadcast to, for example // based on the "type" property of the external event ServerChannel channel = this.bayeuxServer.getChannel("/events/" + event.getType()); if (channel != null) { // Create the data to broadcast by converting the external event Map<String, Object> data = convertExternalEvent(event); // Broadcast the data channel.publish(this.session, data, null); } } }
A client communicates with the server by exchanging Bayeux messages.
The Bayeux protocol requires that the first message a new client sends be a
handshake message (a message sent on /meta/handshake
channel).
On the server, if the processing of the incoming handshake message is successful, BayeuxServer
creates the server-side half-object instance (ServerSession
) that
represents the client that initiated the handshake.
When the processing of the handshake completes, the server sends back a handshake reply to the client.
The client processes the handshake reply, and if it is successful, starts – under the covers – a
heartbeat mechanism with the server, by exchanging connect messages
(a message sent on a /meta/connect
channel).
The details of this heartbeat mechanism depend on the client transport used, but can be seen as the client
sending a connect message and expecting a reply after some time (when using HTTP transports, the heartbeat
mechanism is also known as long-polling).
The heartbeat mechanism allows a client to detect if the server is gone (the client does not receive the
connect message reply from the server), and allows the server to detect if the client is gone (the server
does not receive the connect message request from the client).
Connect messages continue to flow between client and server until either side decides to disconnect by sending a
disconnect message (a message sent on the /meta/disconnect
channel).
While connected to the server, a client can subscribe to channels by sending a
subscribe message (a message sent on a /meta/subscribe
channel).
Likewise, a client can unsubscribe from a channel by sending an
unsubscribe message (a message sent on a /meta/unsubscribe
channel).
A client can publish messages containing application-specific data at any time while it is connected,
and to any broadcast channel (even if it is not subscribed to that channel).
The CometD 2 JavaScript library is a portable JavaScript implementation with bindings for the
major JavaScript toolkits, currently Dojo and
jQuery.
This means that the CometD Bayeux JavaScript implementation is written in pure JavaScript with no dependencies on
the toolkits, and that the toolkit bindings add the syntactic sugar that makes the Bayeux APIs feel like they are
native to the toolkit.
For example, it is possible to refer to the standard cometd
object using the following notation:
// Dojo style var cometd = dojox.cometd; // jQuery style var cometd = $.cometd;
If you followed Chapter 5, Primer, you might have noticed that the skeleton project requires you
to reference both the portable implementation, under org/cometd.js
, and one binding - for example
Dojo's - under dojox/cometd.js
. For jQuery, the binding is under jquery/jquery.cometd.js
.
The use of the Bayeux APIs from the JavaScript toolkits is almost identical, and the following sections do not
refer to a particular toolkit.
Small differences surface only when passing callback functions to the Bayeux API, where Dojo users might use
dojo.hitch()
, while jQuery users might prefer an anonymous function approach.
The following sections present details about the JavaScript Bayeux APIs and their implementation secrets.
After you set up your skeleton project following Chapter 5, Primer, you probably want to fully understand how to customize and configure the parameters that govern the behavior of the CometD implementation.
The complete API is available through a single object prototype named org.cometd.Cometd
.
The Dojo toolkit has one instance of this object available under the name dojox.cometd
, while for
jQuery it is available under the name $.cometd
.
This default cometd
object has been instantiated and configured with the default values and it has
not yet started any Bayeux communication.
Before the cometd
object can start Bayeux communication it needs a mandatory parameter:
the URL of the Bayeux server.
The URL of the server must be absolute (and therefore include the scheme, host, optionally the port and the path
of the server).
The scheme of the URL must always be either "http" or "https". The CometD JavaScript implementation will
transparently take care of converting the scheme to "ws" or "wss" in case of usage of the WebSocket protocol.
One cometd
object connects to one Bayeux server. If you need to connect to multiple Bayeux
servers, see Section 7.1.1, “Configuring and Initializing Multiple Objects”
There are two ways of passing the URL parameter:
// First style: URL string cometd.configure('http://localhost:8080/cometd'); // Second style: configuration object cometd.configure({ url: 'http://localhost:8080/cometd' });
The first way is a shorthand for the second way. However, the second way allows you to pass other configuration parameters, currently:
Table 7.1. JavaScript Library Configuration Parameters
Parameter Name | Required | Default Value | Parameter Description |
---|---|---|---|
url | yes | The URL of the Bayeux server this client will connect to. | |
logLevel | no | info | The log level. Possible values are: "warn", "info", "debug". Output to window.console if available. |
maxConnections | no | 2 | The maximum number of connections used to connect to the Bayeux server. Change this value only if you know exactly the client's connection limit and what "request queued behind long poll" means. |
backoffIncrement | no | 1000 | The number of milliseconds that the backoff time increments every time a connection with the Bayeux server fails. CometD attempts to reconnect after the backoff time elapses. |
maxBackoff | no | 60000 | The maximum number of milliseconds of the backoff time after which the backoff time is not incremented further. |
reverseIncomingExtensions | no | true | Controls whether the incoming extensions are called in reverse order with respect to the registration order. |
maxNetworkDelay | no | 10000 | The maximum number of milliseconds to wait before considering a request to the Bayeux server failed. |
requestHeaders | no | {} |
An object containing the request headers to be sent for every Bayeux request
(for example, {"My-Custom-Header":"MyValue"} ).
|
appendMessageTypeToURL | no | true | Determines whether or not the Bayeux message type (handshake, connect, disconnect) is appended to the URL of the Bayeux server (see above). |
autoBatch | no | false | Determines whether multiple publishes that get queued are sent as a batch on the first occasion, without requiring explicit batching. |
connectTimeout | no | 0 | The maximum number of milliseconds to wait for a WebSocket connection to be opened. It does not apply to HTTP connections. A timeout value of 0 means to wait forever. |
stickyReconnect | no | true | Only applies to the WebSocket transport. Determines whether to stick using the WebSocket transport when a WebSocket transport failure has been detected after the WebSocket transport was able to successfully connect to the server. |
After you have configured the cometd
object, the Bayeux communication does not start until you call
handshake()
(see Section 7.2, “Handshaking”).
Previous users of the JavaScript CometD implementation called a method named init()
.
This method still exists, and it is a shorthand for calling configure()
followed by
handshake()
.
Follow the advice in Section 7.2, “Handshaking” as it applies as well to init()
.
Sometimes there is the need to connect to multiple Bayeux servers.
The default cometd
object available as dojox.cometd
or $.cometd
can only be configured to connect to one server.
However, it is easy to create other cometd
objects.
In Dojo, there is a dojox.Cometd
(note the capital 'C' of Cometd
)
constructor function that can be used to create new cometd
objects.
In jQuery, there is an equivalent $.Cometd
constructor function.
It can be used in this way:
// Dojo style var cometd1 = dojox.cometd; // The default cometd object var cometd2 = new dojox.Cometd(); // A second cometd object // jQuery style var cometd1 = $.cometd; // The default cometd object var cometd2 = new $.Cometd(); // A second cometd object // Configure and handshake cometd1.init('http://host1:8080/cometd'); cometd2.init('http://host2:9090/cometd');
Note how the two cometd
objects are initialized with different URLs.
Configuring extensions in the default cometd
object is covered in Chapter 9, Extensions.
To configure extensions for the additional cometd
objects must be done manually
in the following way:
// Dojo style var cometd1 = dojox.cometd; // The default cometd object var cometd2 = new dojox.Cometd(); // A second cometd object // jQuery style var cometd1 = $.cometd; // The default cometd object var cometd2 = new $.Cometd(); // A second cometd object // Configure extensions for the second object cometd2.registerExtension('ack', new org.cometd.AckExtension()); cometd2.registerExtension('timestamp', new org.cometd.TimeStampExtension()); cometd2.registerExtension('timesync', new org.cometd.TimeSyncExtension()); cometd2.registerExtension('reload', new org.cometd.ReloadExtension());
You should not configure the extensions for the default cometd
object in this way,
but instead follow Chapter 9, Extensions.
You should configure extension manually like shown above only for additional cometd
objects.
You can configure zero, one, or all the extensions for the additional cometd
objects,
depending on your application needs.
In order to initiate the communication with the Bayeux server, you must call either the
handshake()
or the init()
methods on the cometd
object.
The init()
function is a shorthand for a call to configure()
(see
Section 7.1, “Configuring and Initializing”) followed by a call to handshake()
.
Calling handshake()
effectively sends a handshake message request to the server,
and the Bayeux protocol requires that the server sends to the client a handshake message reply.
The Bayeux handshake creates a network communication with the Bayeux server, negotiates the type of transport to use, and negotiates a number of protocol parameters to be used in subsequent communications.
As with several methods of the JavaScript CometD API, handshake()
is an asynchronous function:
it returns immediately, well before the Bayeux handshake steps have completed.
Calling handshake()
does not mean that you have completed the
handshake with the server when handshake()
returns.
It is possible to invoke the handshake()
function passing as parameter a callback function that
will be invoked when the handshake message reply from the server arrives to the client (or, in case the
server does not reply, when the client detects a handshake failure):
// Configure cometd.configure({ url: 'http://localhost:8080/cometd' }); // Handshake with callback cometd.init(function(handshakeReply) { if (handshakeReply.successful) { // Successfully connected to the server. // Now it is possible to subscribe or send messages } else { // Cannot handshake with the server, alert user. } });
Passing a callback function to handshake()
is equivalent to register a
/meta/handshake
listener (see Section 7.3.4, “Subscribers versus Listeners”).
The handshake might fail for several reasons:
You mistyped the server URL.
The transport could not be negotiated successfully.
The server denied the handshake (for example, the authentication credentials were wrong).
The server crashed.
There was a network failure.
In case of a handshake failure, applications should not try to call handshake()
again:
the CometD library will do this on behalf of the application.
A corollary of this is that applications should usually only ever call handshake()
once in
their code.
Since the handshake()
call is asynchronous, it is not a good idea to write this code:
// WRONG CODE cometd.configure({ url: 'http://localhost:8080/cometd' }); // Handshake cometd.handshake(); // Publish to a channel cometd.publish('/foo', { foo: 'bar' });
It is not a good idea because there is no guarantee that the call to publish()
(see
Section 7.4, “Publishing”) can actually succeed in contacting the Bayeux server.
Since the API is asynchronous, you have no way of knowing synchronously (that is, by having
handshake()
function return an error code or by throwing an exception) that the handshake failed.
The right way is the following:
cometd.configure({ url: 'http://localhost:8080/cometd' }); // Handshake cometd.handshake(function(handshakeReply) { if (handshakeReply.successful) { // Publish to a channel cometd.publish('/foo', { foo: 'bar' }); } });
If you want to pass additional information to the handshake message (for example, authentication credentials)
you can pass an additional object to the handshake()
function:
cometd.configure({ url: 'http://localhost:8080/cometd' }); // Handshake with additional information. var additional = { com.acme.credentials: { user: 'cometd', token: 'xyzsecretabc' } }; cometd.handshake(additional, function(handshakeReply) { if (handshakeReply.successful) { // Your logic here. } });
The additional object will be merged into the handshake message, and the server
will be able to access the additional information in a /meta/handshake
listener, for example
using annotated services (see Section 8.2.2.2, “Annotated Client-Side and Server-Side Services”):
@Service public class MyService { @Listener(Channel.META_HANDSHAKE) public void metaHandshake(ServerSession remote, ServerMessage message) { Map<String, Object> credentials = (Map<String, Object>)message.get("com.acme.credentials"); // Verify credentials. } }
The additional object must not be used to tamper the handshake message by using reserved fields defined
by the Bayeux protocol (see Appendix C, The Bayeux Protocol Specification v1.0).
Instead, you should use field names that are unique to your application, better yet when fully qualified
like com.acme.credentials
.
The CometD JavaScript API offer an easy way to receive notifications about the details of the Bayeux protocol
message exchange: either by adding listeners to special channels (called meta channels),
explained in Section 7.3, “Subscribing and Unsubscribing”, or by passing callback functions to the API like you did
for handshake()
in the example above.
The following sections provide information about subscribing and unsubscribing with the JavaScript library.
Depending on the type of channel, subscribing and unsubscribing to a channel have different meanings. Refer to Section 6.1.1, “Channel Definitions” for the channel type definitions.
It is not possible to subscribe to meta channels: the server replies with an error message. It is possible to listen to meta channels (see Section 7.3.4, “Subscribers versus Listeners” for an explanation of the difference between subscribers and listeners). You cannot (and it makes no sense to) publish messages to meta channels: only the Bayeux protocol implementation creates and sends messages on meta channels. Meta channels are useful on the client to listen for error messages like handshake errors (for example, because the client did not provide the correct credentials) or network errors (for example, to know when the connection with the server has broken or when it has been re-established).
Service channels are used in the case of request/response style of communication between client and server (as opposed to the publish/subscribe style of communication on broadcast channels). While subscribing to service channels yields no errors, this is a no-operation for the server: the server ignores the subscription request. It is possible to publish to service channels, with the semantic of a communication between a specific client (the one that is publishing the message on the service channel) and the server. Service channels are useful to implement, for example, private chat messages: in a chat with userA, userB and userC, userA can publish a private message to userC (without userB knowing about it) using service channels.
Broadcast channels have the semantic of a messaging topic and are used in the case of publish/subscribe style of communication. Usually, it is possible to subscribe to broadcast channels and to publish to broadcast channels; this can only be forbidden using a security policy on the Bayeux server (see Section 8.2.3, “Authorization”) or by using authorizers (see Section 8.2.4, “Server Channel Authorizers”). Broadcast channels are useful to broadcast messages to all subscribed clients, for example in case of a stock price change.
The JavaScript CometD API has two APIs to work with channel subscriptions:
addListener()
and the correspondent removeListener()
subscribe()
and the correspondent unsubscribe()
The addListener()
method:
Must be used to listen to meta channel messages.
May be used to listen to service channel messages.
Should not be used to listen broadcast channel messages (use subscribe()
instead).
Does not involve any communication with the Bayeux server, and as such can be called before calling
handshake().
Is synchronous: when it returns, you are guaranteed that the listener has been added.
The subscribe()
method:
Must not be used to listen to meta channels messages (if attempted, the server returns an error).
May be used to listen to service channel messages.
Should be used to listen to broadcast channel messages.
Involves a communication with the Bayeux server and as such cannot be called before calling
handshake().
Is asynchronous: it returns immediately, well before the Bayeux server has received the subscription request.
Calling subscribe()
does not mean that you
have completed the subscription with the server when function returns.
If you want to be certain that the server received your subscription request (or not), you can either
register a /meta/subscribe
listener, or pass a callback function to subscribe()
:
cometd.subscribe('/foo', function(message) { ... }, function(subscribeReply) { if (subscribeReply.successful) { // The server successfully subscribed this client to the "/foo" channel. } });
Remember that subscriptions may fail on the server (for example, the client does not have enough
permissions to subscribe) or on the client (for example, there is a network failure).
In both cases /meta/subscribe
listener, or the callback function, will be invoked with
an unsuccessful subscribe message reply.
You can pass additional information to the subscribe message by passing an additional object to the
subscribe()
function:
var additional = { com.acme.priority: 10 }; cometd.subscribe('/foo', function(message) { ... }, additional, function(subscribeReply) { // Your logic here. });
Remember that the subscribe message is sent to the server only when subscribing to a channel for the first time. Additional subscriptions to the same channel will not result in a message being sent to the server. See also Section 7.2, “Handshaking” for further information about passing additional objects.
Both addListener()
and subscribe()
return a subscription object
that must be passed to, respectively, removeListener()
and unsubscribe()
:
// Some initialization code var subscription1 = cometd.addListener('/meta/connect', function() { ... }); var subscription2 = cometd.subscribe('/foo/bar/', function() { ... }); // Some de-initialization code cometd.unsubscribe(subscription2); cometd.removeListener(subscription1);
Function unsubscribe()
can too take an additional object and a callback function as parameters:
// Some initialization code var subscription1 = cometd.subscribe('/foo/bar/', function() { ... }); // Some de-initialization code var additional = { com.acme.discard: true } cometd.unsubscribe(subscription1, additional, function(unsubscribeReply) { // Your logic here. });
Similarly to subscribe()
, the unsubscribe message is sent to the server only when unsubscribing
from a channel for the last time.
See also Section 7.2, “Handshaking” for further information about passing additional objects.
The major difference between listeners and subscribers is that subscribers are automatically removed upon re-handshake, while listeners are not modified by a re-handshake. When a client subscribes to a channel, the server maintains a client-specific server-side subscription state. If the server requires a re-handshake, it means that it lost the state for that client, and therefore also the server-side subscription state. In order to maintain the client-side state consistent with that of the server, subscriptions - but not listeners - are automatically removed upon re-handshake.
A good place in the code to perform subscriptions is in a /meta/handshake
function.
Since /meta/handshake
listeners are invoked in both explicit handshakes the client performs
and in re-handshakes the server triggers, it is guaranteed that your subscriptions are always
performed properly and kept consistent with the server state.
Equivalently, a callback function passed to the handshake method behaves exactly like a
/meta/handshake
listener, and therefore can be used to perform subscriptions.
Applications do not need to unsubscribe in case of re-handshake; the CometD library takes care of removing
all subscriptions upon re-handshake, so that when the /meta/handshake
function executes again
the subscriptions are correctly restored (and not duplicated).
For the same reason, you should never add listeners inside a /meta/handshake
function,
because this will add another listener without removing the previous one, resulting in multiple notifications
of the same messages.
var _reportListener; cometd.addListener('/meta/handshake', function(message) { // Only subscribe if the handshake is successful if (message.successful) { // Batch all subscriptions together cometd.batch(function() { // Correct to subscribe to broadcast channels cometd.subscribe('/members', function(m) { ... }); // Correct to subscribe to service channels cometd.subscribe('/service/status', function(m) { ... }); // Messy to add listeners after removal, prefer using cometd.subscribe(...) if (_reportListener) { cometd.removeListener(_reportListener); _reportListener = cometd.addListener('/service/report', function(m) { ... }); } // Wrong to add listeners without removal cometd.addListener('/service/notification', function(m) { ... }); }); } });
In cases where the Bayeux server is not reachable (due to network failures or because the server crashed),
subscribe()
and unsubscribe()
behave as follows:
In subscribe()
CometD first adds the local listener to the list of subscribers for that channel, then
attempts the server communication. If the communication fails, the server does not know that it has to send
messages to this client and therefore on the client, the local listener (although present) is never invoked.
In unsubscribe()
, CometD first removes the local listener from the list of subscribers for that channel,
then attempts the server communication. If the communication fails, the server still sends the message to the
client, but there is no local listener to dispatch to.
Often times, applications need to perform dynamic subscriptions and unsubscriptions, for example when a user clicks on a user interface element, you want to subscribe to a certain channel. In this case the subscription object returned upon subscription is stored to be able to dynamically unsubscribe from the channel upon user demand:
var _subscription;
function Controller()
{
this.dynamicSubscribe = function()
{
_subscription = cometd.subscribe('/dynamic', this.onEvent);
};
this.onEvent = function(message)
{
...
};
this.dynamicUnsubscribe = function()
{
if (_subscription)
{
cometd.unsubscribe(_subscription);
_subscription = undefined;
}
}
}
In case of a re-handshake, dynamic subscriptions are cleared (like any other subscription) and the
application needs to figure out which dynamic subscription must be performed again.
This information is already known to CometD at the moment cometd.subscribe(...)
was called
(above in function dynamicSubscribe()
), so applications can just call resubscribe()
using the subscription object obtained from subscribe()
:
cometd.addListener('/meta/handshake', function(message) { if (message.successful) { cometd.batch(function() { // Static subscription, no need to remember the subscription handle cometd.subscribe('/static', staticFunction); // Dynamic re-subscription if (_subscription) { _subscription = cometd.resubscribe(_subscription); } }); } });
If a listener or subscriber function throws an exception (for example, calls a method on an undefined object), the error message is logged at level "debug". However, there is a way to intercept these errors by defining the global listener exception handler that is invoked every time a listener or subscriber throws an exception:
cometd.onListenerException = function(exception, subscriptionHandle, isListener, message) { // Uh-oh, something went wrong, disable this listener/subscriber // Object "this" points to the CometD object if (isListener) this.removeListener(subscriptionHandle); else this.unsubscribe(subscriptionHandle); }
It is possible to send messages to the server from the listener exception handler. If the listener exception handler itself throws an exception, this exception is logged at level "info" and the CometD implementation does not break. Notice that a similar mechanism exists for extensions, see Chapter 9, Extensions.
It is possible to subscribe to several channels simultaneously using wildcards:
cometd.subscribe("/chatrooms/*", function(message) { ... });
A single asterisk has the meaning of matching a single channel segment; in the example above it matches channels
/chatrooms/12
and /chatrooms/15
, but not /chatrooms/12/upload
.
To match multiple channel segments, use the double asterisk:
cometd.subscribe("/events/**", function(message) { ... });
With the double asterisk, the channels /events/stock/FOO
and /events/forex/EUR
match,
as well as /events/feed
and /events/feed/2009/08/03
.
The wildcard mechanism works also for listeners, so it is possible to listen to all meta channels as follows:
cometd.addListener("/meta/*", function(message) { ... });
By default, subscriptions to the global wildcards /*
and /**
result in an error, but
you can change this behavior by specifying a custom security policy on the Bayeux server.
These are the meta channels available in the JavaScript CometD implementation:
/meta/handshake
/meta/connect
/meta/disconnect
/meta/subscribe
/meta/unsubscribe
/meta/publish
/meta/unsuccessful
Each meta channel is notified when the JavaScript CometD implementation handles the correspondent Bayeux message.
The /meta/unsuccessful
channel is notified in case of any failure.
By far the most interesting meta channel to subscribe to is /meta/connect
because it gives the
status of the current connection with the Bayeux server. In combination with /meta/disconnect
,
you can use it, for example, to display a green connected icon or a red disconnected icon on the page,
depending on the connection status with the Bayeux server.
Here is a common pattern using the /meta/connect
and /meta/disconnect
channels:
var _connected = false; cometd.addListener('/meta/connect', function(message) { if (cometd.isDisconnected()) { return; } var wasConnected = _connected; _connected = message.successful; if (!wasConnected && _connected) { // Reconnected } else if (wasConnected && !_connected) { // Disconnected } }); cometd.addListener('/meta/disconnect', function(message) { if (message.successful) { _connected = false; } }
One small caveat with the /meta/connect
channel is that /meta/connect
is also used
for polling the server.
Therefore, if a disconnect is issued during an active poll, the server returns the active poll and this
triggers the /meta/connect
listener.
The initial check on the status verifies that is not the case before executing the connection logic.
Another interesting use of meta channels is when there is an authentication step during the handshake.
In this case the registration to the /meta/handshake
channel can give details about, for example,
authentication failures.
The publish()
method allows you to publish data onto a certain channel:
cometd.publish('/mychannel', { mydata: { foo: 'bar' } });
You cannot (and it makes no sense to) publish to a meta channel, but you can publish to a service or broadcast channel even if you are not subscribed to that channel. However, you have to handshake (see Section 7.2, “Handshaking”) before you can publish.
As with other JavaScript CometD API, publish()
involves a communication with the server and it is
asynchronous: it returns immediately, well before the Bayeux server has received the message.
When the message you published arrives to the server, the server replies to the client with a publish
acknowledgment; this allows clients to be sure that the message reached the server.
The publish acknowledgment arrives on the same channel the message was published to, with the same
message id
, with a successful
field.
If the message publish fails for any reason, for example because server cannot be reached, then a publish
failure will be emitted, similarly to publish acknowledgments.
For historical reasons, publish acknowledgments and failures are notified on the /meta/publish
channel (only in the JavaScript library), even if the /meta/publish
channel is not part of
Appendix C, The Bayeux Protocol Specification v1.0.
In order to be notified of publish acknowledgments or failures, is it recommended that you use this variant
of the publish()
function, passing a callback function:
cometd.publish('/mychannel', { mydata: { foo: 'bar' } }, function(publishReply) { if (publishReply.successful) { // The message reached the server } });
Calling publish()
does not mean that you have published
the message when publish()
returns.
If you need to publish several messages to different channels, you might want to use Section 7.6, “Message Batching”.
The JavaScript CometD implementation performs automatic reconnect in case of network or Bayeux server failures. Section 7.1, “Configuring and Initializing” describes the reconnect parameters.
Calling the JavaScript CometD API disconnect()
results in a message being sent to the Bayeux
server so that it can clean up any state associated with that client.
As with all methods that involve a communication with the Bayeux server, it is an asynchronous method: it
returns immediately, well before the Bayeux server has received the disconnect request.
If the server is unreachable (because it is down or because of network failures), the JavaScript CometD
implementation stops any reconnection attempt and cleans up any local state.
It is normally safe to ignore if the disconnect()
call has been successful or not: the client is in
any case disconnected, its local state cleaned up, and if the server has not been reached it eventually times out
the client and cleans up any server-side state for that client.
If you are debugging your application with Firebug, and you shut down the server, you see in the Firebug console
the attempts to reconnect.
To stop those attempts, type in the Firebug command line: dojox.cometd.disconnect()
(for Dojo) or
$.cometd.disconnect()
(for jQuery).
In case you really want to know whether the server received the disconnect request, you can pass a callback
function to the disconnect()
function:
cometd.disconnect(function(disconnectReply)
{
if (disconnectReply.successful)
{
// Server truly received the disconnect request
}
});
Like other APIs, also disconnect()
may take an additional object that is sent to the server:
var additional = {
com.acme.reset: false
};
cometd.disconnect(additional, function(disconnectReply)
{
// Your logic here.
});
See also Section 7.2, “Handshaking” for further information about passing additional objects.
In case of temporary network failures, the client is notified through the /meta/connect
channel
(see Section 7.3, “Subscribing and Unsubscribing” about meta channels) with messages that have the
successful
field set to false (see also the archetypes in Chapter 5, Primer as an example).
However, the Bayeux server might be able to keep the client's state, and when the network resumes the Bayeux server
might behave as if nothing happened.
The client in this case just re-establishes the long poll, but any message the client publishes during the network
failure is not automatically re-sent (though it is possible to be notified, through the /meta/publish
channel or better yet through callback functions, of the failed publishes).
If the network failure is long enough, the Bayeux server times out the lost client, and deletes the state associated with it. The same happens when the Bayeux server crashes (except that the state of all clients is lost). In this case, the reconnection mechanism on the client performs the following steps:
A long poll is re-attempted, but the server rejects it with a 402::Unknown client
error message.
A handshake is attempted, and the server normally accepts it and allocates a new client.
Upon the successful re-handshake, a long poll is re-established.
If you register meta channels listener, or if you use callback functions, be aware of these steps, since a reconnection might involve more than one message exchange with the server.
Often an application needs to send several messages to different channels. A naive way of doing it follows:
// Warning: non-optimal code cometd.publish('/channel1', { product: 'foo' }); cometd.publish('/channel2', { notificationType: 'all' }); cometd.publish('/channel3', { update: false });
You might think that the three publishes leave the client one after the other, but that is not the case.
Remember that publish()
is asynchronous (it returns immediately), so the three publish()
calls in sequence likely return well before a single byte reaches the network.
The first publish()
executes immediately, and the other two are in a queue, waiting for the first
publish()
to complete.
A publish()
is complete when the server receives it, sends back the meta response, and the client
receives the meta response for that publish.
When the first publish
completes, the second publish is executed and waits to complete. After that,
the third publish()
finally executes.
If you set the configuration parameter called autoBatch to true, the implementation
automatically batches messages that have been queued.
In the example above, the first publish()
executes immediately, and when it completes, the
implementation batches the second and third publish()
into one request to the server.
The autoBatch feature is interesting for those systems where events received asynchronously and
unpredictably – either at a fast rate or in bursts – end up generating a publish()
to the server: in
such cases, using the batching API is not effective (as each event would generate only one publish()
).
A burst of events on the client generates a burst of publish()
to the server, but the autobatch mechanism
batches them automatically, making the communication more efficient.
The queueing mechanism avoids queueing a publish()
behind a long poll. If not for this mechanism, the
browser would receive three publish requests but it has only two connections available, and one is already occupied
by the long poll request. Therefore, the browser might decide to round-robin the publish requests, so that the
first publish goes on the second connection, which is free, and it is actually sent over the network, (remember that
the first connection is already busy with the long poll request), schedule the second publish to the first
connection (after the long poll returns), and schedule the third publish again to the second connection, after the
first publish returns.
The result is that if you have a long poll timeout of five minutes, the second publish request might arrive at the
server five minutes later than the first and the third publish request.
You can optimize the three publishes using batching, which is a way to group messages together so that a single Bayeux message actually carries the three publish messages.
cometd.batch(function() { cometd.publish('/channel1', { product: 'foo' }); cometd.publish('/channel2', { notificationType: 'all' }); cometd.publish('/channel3', { update: false }); }); // Alternatively, but not recommended: cometd.startBatch() cometd.publish('/channel1', { product: 'foo' }); cometd.publish('/channel2', { notificationType: 'all' }); cometd.publish('/channel3', { update: false }); cometd.endBatch()
Notice how the three publish()
calls are now within a function passed to batch()
.
Alternatively, but less recommended, you can surround the three publish()
calls between
startBatch()
and endBatch()
.
Remember to call endBatch()
after calling startBatch()
.
If you don't - for example, because an exception is thrown in the middle of the batch - your messages continue to
queue, and your application does not work as expected.
If you still want to risk using the startBatch()
and endBatch()
calls, remember that
you must do so from the same context of execution; message batching has not been designed to span multiple user
interactions.
For example, it would be wrong to start a batch in functionA (triggered by user interaction), and ending the batch
in functionB (also triggered by user interaction and not called by functionA).
Similarly, it would be wrong to start a batch in functionA and then schedule (using setTimeout()
) the
execution of functionB to end the batch.
Function batch()
already does the correct batching for you (also in case of errors), so it is the
recommended way to do message batching.
When a batch starts, subsequent API calls are not sent to the server, but instead queued until the batch ends. The end of the batch packs up all the queued messages into one single Bayeux message and sends it over the network to the Bayeux server.
Message batching allows efficient use of the network: instead of making three request/response cycles, batching makes only one request/response cycle.
Batches can consist of different API calls:
var _subscription; cometd.batch(function() { cometd.unsubscribe(_subscription); _subscription = cometd.subscribe('/foo', function(message) { ... }); cometd.publish('/bar', { ... }); });
The Bayeux server processes batched messages in the order they are sent.
Appendix C, The Bayeux Protocol Specification v1.0 defines two mandatory
transports: long-polling
and callback-polling
.
The JavaScript CometD implementation implements these two transports and supports also the websocket
transport (based on HTML 5 WebSockets).
At the time of this writing, the IETF has finalized the WebSocket protocol into
RFC 6455.
However, most of the browsers still implement earlier drafts of the WebSocket protocol, so browser support
varies (most notably, Microsoft's Internet Explorer only supports WebSocket out of the box in version 10).
CometD falls back to long-polling
if the websocket
transport does not work.
The long-polling
transport is the default transport if the browser and the server do not support
WebSockets.
This transport is used when the communication with the Bayeux server happens on the same domain, and also in the
cross-domain mode for recent browsers, such as Firefox 3.5+ (see Section 7.7.5, “The cross-domain Mode”).
The data is sent to the server by means of a POST request with Content-Type application/json
via a plain XMLHttpRequest call.
The callback-polling
transport is used when the communication with the Bayeux server happens on a
different domain and when the cross-domain mode is not supported
(see Section 7.7.5, “The cross-domain Mode”).
It is well known that XMLHttpRequest calls have restrictions when the invocation is directed to a domain different
from the one to which the script has been downloaded (but, again, see Section 7.7.5, “The cross-domain Mode”
for an alternative solution).
To overcome XMLHttpRequest restrictions, this transport uses the JSONP script injection, which injects a
<script>
element whose src
attribute points to the Bayeux server.
The browser notices the script element injection and performs a GET request to the specified source URL.
The Bayeux server is aware that this is a JSONP request and replies with a JavaScript function that the browser
then executes, (and calls back into the JavaScript CometD implementation).
There are three main drawbacks in using this transport:
The transport is chatty. This is due to the fact that the browser executes the injected scripts sequentially,
and until a script has been completely "downloaded", it cannot be executed.
For example, imagine a communication that involves a script injection for the long poll, and a script injection
for a message publish. The browser injects the long poll script, a request is made to the Bayeux server, but the
Bayeux server holds the request waiting for server-side events (so the script is not downloaded). Then the
browser injects the publish script, the request is made to the Bayeux server, which replies (so the script is
downloaded). However, the browser does not execute the second script, because it has not executed the first yet
(since its download is not finished). In these conditions, the publish is executed only after the long poll
returns. To avoid this situation, the Bayeux server, in case of callback-polling
transport, resumes
the client's long poll for every message that arrives from that client, and that's why the transport is so chatty:
the long poll returns more often.
The message size is limited. This is necessary to support IE7, which has a 2083 character limit for GET requests.
The reaction to failures is slower.This is due to the fact that if the script injection points to a URL that returns an error (for example, the Bayeux server is down), the browser silently ignores the error.
The websocket
transport is available if the browser and the server support WebSocket
(Jetty 7 and later support the WebSocket protocol).
The WebSocket protocol is designed to be the bidirectional communication protocol for
the web, so it is a natural fit in the CometD project.
Since browser support has greatly improved, the websocket
transport is enabled by
default since CometD 2.4.0, but you can easily disable it if you experience problems.
The easiest way of enabling/disabling the websocket
transport is to set a boolean variable
before performing the initial CometD handshake:
var cometd = dojox.cometd; // Dojo style var cometd = $.cometd; // jQuery style // Disable the websocket transport cometd.websocketEnabled = false; // Initial handshake cometd.init('http://localhost:8080/cometd');
An alternative way of disabling the websocket
transport is to unregister its transport, see
Section 7.7.4, “Unregistering Transports”.
Remember that enabling the websocket
transport on the client is not enough: you must
also enable it on the server.
Follow Section 8.2.5, “Server Transports” for configuring websocket
on the server.
CometD JavaScript transports are added in the JavaScript toolkit bindings for the CometD JavaScript library.
The CometD JavaScript API allows you to unregister transports, and this can be useful to force the use of only one transport (for example, for testing purposes), or to disable certain transports that might be unreliable. For example, it is possible to unregister the WebSocket transport by unregistering it with the following code:
var cometd = dojox.cometd; // Dojo style var cometd = $.cometd; // jQuery style cometd.unregisterTransport('websocket');
Firefox 3.5 introduced the capability for XMLHttpRequest calls to be performed towards a different domain (see HTTP Access Control). The JavaScript CometD implementation also supports this, with no configuration necessary on the client (if the browser supports XMLHttpRequest cross-domain calls, CometD uses them) and with a bit of configuration for the server. See the Jetty Cross Origin Filter documentation for the server configuration.
To use the cross-domain mode, you need:
A cross-domain compliant browser (for example Firefox 3.5).
A compliant server (for example, Jetty configured with the CrossOriginFilter.
)
With this setup, even when the communication with the Bayeux server is cross-domain, CometD uses the
long-polling
transport, avoiding the drawbacks of the callback-polling
transport.
The CometD Java implementation is based on the popular Jetty Http Server and Servlet Container, for both the client and the server.
The CometD Java implementation, though based on Jetty, is portable on other Servlet 2.5 compliant servlet containers because it uses the portable Jetty Continuation API.
You can deploy the WAR file resulting from the development of your CometD web application to other Servlet 2.5 compliant servlet containers, but it does not scale as well because it is less integrated with other servlet containers. See Section 8.2.1.4, “Configuring Servlet 2.5 Containers” for further details.
When deployed to a Servlet 3.0 compliant servlet container, the CometD implementation makes use of the asynchronous features the servlet container offers, and is fully portable and scalable (the scalability is as good as the servlet container's implementation). See Section 8.2.1.5, “Configuring Servlet 3 Asynchronous Features” when deploying to Servlet 3.0 compliant servlet containers.
The CometD Java Implementation offers a client library and a server library, which the following sections document in detail.
You can use the CometD client implementation in any JSE™ or
JEE™ application.
It consists of one main class,org.cometd.client.BayeuxClient
, which
implements the org.cometd.bayeux.client.ClientSession
interface.
Typical uses of the CometD Java client include:
The transport for a rich thick Java UI (for example, Swing) to communicate to a Bayeux Server (also via firewalls).
A load generator to simulate thousands of CometD clients, for
example org.cometd.benchmark.BayeuxLoadClient
The following sections provide details about the Java
BayeuxClient
APIs and their implementation secrets.
To initiate the communication with the Bayeux server, you need to call:
BayeuxClient.handshake()
The following is a typical use:
// Create (and eventually set up) Jetty's HttpClient: HttpClient httpClient = new HttpClient(); // Here set up Jetty's HttpClient, for example: // httpClient.setMaxConnectionsPerAddress(2); httpClient.start(); // Prepare the transport Map<String, Object> options = new HashMap<String, Object>(); ClientTransport transport = new LongPollingTransport(options, httpClient); // Create the BayeuxClient ClientSession client = new BayeuxClient("http://localhost:8080/cometd", transport); // Here set up the BayeuxClient, for example: // client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener() { ... });
BayeuxClient
must be instantiated passing the absolute URL (and therefore including the scheme,
host, optionally the port and the path) of the Bayeux server.
The scheme of the URL must always be either "http" or "https". The CometD Java Client implementation will
transparently take care of converting the scheme to "ws" or "wss" in case of usage of the WebSocket protocol.
When BayeuxClient.handshake()
is called, the BayeuxClient
performs the handshake
with the Bayeux server and establishes the long poll connection asynchronously.
Calling handshake()
does not mean that you have completed the
handshake with the server when handshake()
returns.
To verify that the handshake is successful, you can pass a callback MessageListener
to
BayeuxClient.handshake()
:
ClientTransport transport = ... ClientSession client = new BayeuxClient("http://localhost:8080/cometd", transport); client.handshake(new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { if (message.isSuccessful()) { // Here handshake is successful } } });
An alternative, equivalent, way is to add a MessageListener
before calling
BayeuxClient.handshake()
:
ClientTransport transport = ... ClientSession client = new BayeuxClient("http://localhost:8080/cometd", transport); client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { if (message.isSuccessful()) { // Here handshake is successful } } }); client.handshake();
Another alternative is to use the built-in synchronous features of the BayeuxClient
and wait for the handshake to complete:
ClientTransport transport = ... BayeuxClient client = new BayeuxClient("http://localhost:8080/cometd", transport); client.handshake(); boolean handshaken = client.waitFor(1000, BayeuxClient.State.CONNECTED); if (handshaken) { // Here handshake is successful }
The BayeuxClient.waitFor()
method waits the given timeout (in milliseconds) for the
BayeuxClient
to reach the given state, and returns true if the state is reached before
the timeout expires.
The following sections provide information about subscribing and unsubscribing to channels.
Subscribing (or unsubscribing) involves first retrieving the channel you want to subscribe to
(or unsubscribe from) and then calling the subscribe()
(or unsubscribe()
) methods:
public class Example { private static final String CHANNEL = "/foo"; private final ClientSessionChannel.MessageListener fooListener = new FooListener(); public void attach() { ClientTransport transport = ... ClientSession client = new BayeuxClient("http://localhost:8080/cometd", transport); client.handshake(); boolean handshaken = client.waitFor(1000, BayeuxClient.State.CONNECTED); if (handshaken) { client.getChannel(CHANNEL).subscribe(fooListener); } } private static class FooListener implements ClientSessionChannel.MessageListener { public void onMessage(ClientSessionChannel channel, Message message) { // Here we received a message on the channel } } }
You can subscribe and unsubscribe only after the handshake is completed.
Calling subscribe()
(or unsubscribe()
does not
mean that you have completed the subscription (or unsubscription) with the server when the method returns.
Unsubscribing is straightforward: if you unsubscribe to a channel, CometD does not deliver messages
on that channel to message listeners.
Using the Example
class above:
public class Example { ... public void detach() { client.getChannel(CHANNEL).unsubscribe(fooListener); } }
If you need to know whether your subscription (or unsubscription) was received and processed by the server,
you can pass a callback MessageListener
to the subscribe()
(or
unsubscribe()
) methods:
final BayeuxClient client = ...; final ClientSessionChannel.MessageListener messageHandler = ...; client.handshake(new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { if (message.isSuccessful()) { // Subscribe client.getChannel("/foo").subscribe(messageHandler, new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { if (message.isSuccessful()) { // Subscription successful. } } } } } });
As in Section 7.3, “Subscribing and Unsubscribing”, a good place to perform subscriptions is a
handshake(...)
callback or a /meta/handshake
listener, because they are invoked
transparently if the server requests a new handshake.
Applications do not need to unsubscribe in case of re-handshake; the CometD library removes
the subscriptions upon re-handshake, so that when the /meta/handshake
listener executes again
the subscriptions are correctly restored (and not duplicated).
The internal implementation of the Bayeux protocol uses meta channels, and it does not make any sense to subscribe to them because they are not broadcast channels. It does make sense, however, to listen to messages that arrive on those channels.
public class Example
{
public void init()
{
ClientSession client = ...;
client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
// Here we received a handshake response message
}
});
}
}
The following is a typical example of publishing a message on a channel:
ClientTransport transport = ... ClientSession client = new BayeuxClient("http://localhost:8080/cometd", transport); client.handshake(); Map<String, Object> data = new HashMap<String, Object>(); // Fill in the data client.getChannel("/game/table/1").publish(data);
Publishing data on a channel is an asynchronous operation.
When the message you published arrives to the server, the server replies to the client with a publish
acknowledgment; this allows clients to be sure that the message reached the server.
The publish acknowledgment arrives on the same channel the message was published to, with the same
message id
, with a successful
field.
If the message publish fails for any reason, for example because server cannot be reached, then a publish
failure will be emitted, similarly to publish acknowledgments.
In order to be notified of publish acknowledgments or failures, you can use this variant of the
publish()
method:
Map<String, Object> data = new HashMap<String, Object>(); // Fill in the data client.getChannel("/game/table/1").publish(data, new ClientSessionChannel.MessageListener() { @Override public void onMessage(ClientSessionChannel channel, Message message) { if (message.isSuccessful()) { // The message reached the server } } });
Calling publish()
does not mean that you have published
the message when publish()
returns.
Message batching is also available:
final ClientSession client = ...; client.handshake(); client.batch (new Runnable() { public void run() { Map<String, Object> data1 = new HashMap<String, Object>(); // Fill in the data1 map object client.getChannel("/game/table/1").publish(data1); Map<String, Object> data2 = new HashMap<String, Object>(); // Fill in the data2 map object< client.getChannel("/game/chat/1").publish(data2); } });
The ClientSession
API also allows you to batch using startBatch()
and
endBatch()
, but remember to call endBatch()
after having called
startBatch()
, for example in a finally
block.
If you don't, your messages continue to queue up, and your application does not work as expected.
Disconnecting is straightforward:
BayeuxClient.disconnect();
Like the other APIs, you can pass a callback MessageListener
to be notified that the
server received and processed the disconnect request:
BayeuxClient client = ...;
client.disconnect(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
if (message.isSuccessful())
{
// Server processed the disconnect request.
}
}
});
Alternatively, you can wait for the disconnect to complete:
BayeuxClient client = ...;
client.disconnect();
client.waitFor(1000, BayeuxClient.State.DISCONNECTED);
You can configure org.cometd.client.BayeuxClient
class to use
multiple transports. It currently supports the long-polling
transport (that in turn depends on Jetty's
asynchronous HttpClient)
and the websocket
transport (that in turn depends on
Jetty's asynchronous WebSocket client.
You should configure BayeuxClient
with the websocket
transport
before the long-polling
transport, so that BayeuxClient
can fall back
to the long-polling
if the websocket
transport fails.
You do so by listing the WebSocket transport before the HTTP transport on the BayeuxClient
constructor,
for example:
// Prepare the WebSocket transport WebSocketClientFactory wsFactory = new WebSocketClientFactory(); wsFactory.start(); ClientTransport wsTransport = new WebSocketTransport(null, wsFactory, null); // Prepare the HTTP transport HttpClient httpClient = new HttpClient(); httpClient.start(); ClientTransport httpTransport = new LongPollingTransport(null, httpClient); // Configure the BayeuxClient, with the websocket transport listed before the http transport BayeuxClient client = new BayeuxClient("http://localhost:8080/cometd", wsTransport, httpTransport); // Handshake client.handshake();
The transports used by BayeuxClient
can be configured with a number of parameters.
Below you can find the parameters that are common to all transports, and those specific for each transport.
Table 8.1. Client Transports Common Parameters
Parameter Name | Required | Default Value | Parameter Description |
---|---|---|---|
jsonContext | no | org.cometd.common.JettyJSONContextClient | The JSONContext.Client class name (see Section 8.3, “JSON Libraries”)
|
Table 8.2. Long Polling Client Transport Parameters
Parameter Name | Required | Default Value | Parameter Description |
---|---|---|---|
maxNetworkDelay | no | HttpClient 's timeout |
The maximum number of milliseconds to wait before considering a request to the Bayeux server failed. |
Table 8.3. WebSocket Client Transport Parameters
Parameter Name | Required | Default Value | Parameter Description |
---|---|---|---|
maxNetworkDelay | no | 15000 | The maximum number of milliseconds to wait before considering a request to the Bayeux server failed. |
connectTimeout | no | 30000 | The maximum number of milliseconds to wait for a WebSocket connection to be opened. |
idleTimeout | no | 60000 | The maximum number of milliseconds a WebSocket connection is kept idle before being closed. |
maxMessageSize | no | 8192 | The maximum number of bytes allowed for each WebSocket message. |
stickyReconnect | no | true | Whether to stick using the WebSocket transport when a WebSocket transport failure has been detected after the WebSocket transport was able to successfully connect to the server. |
If you are building your application with Maven (the recommended way), your application just needs to declare dependencies for:
org.cometd.java:cometd-java-client
(Maven automatically pulls the Jetty dependencies
that the cometd-java-client
artifact needs).
an SLF4J
(the logging library) implementation such as org.slf4j:slf4j-simple
(we recommend
that you use org.slf4j:slf4j-log4j12
or ch.qos.logback:logback-classic
).
With these dependencies in place, you can use the long-polling
transport out of the box.
To use the websocket
transport, you declare the dependencies listed above for the
long-polling
transport, and you also need to declare dependencies for
org.cometd.java:cometd-websocket-jetty
(Maven automatically pulls the Jetty dependencies that
the cometd-websocket-jetty
artifact needs).
To run the CometD server implementation, you need to deploy a Java web application to a servlet container.
You configure the web application with the CometD servlet that interprets the Bayeux protocol (see Section 8.2.1, “Configuring the Java Server” for the CometD servlet configuration details).
While CometD interprets Bayeux messages, applications normally implement their own business logic, so they need to be able to interact with Bayeux messages – inspect them, modify them, publish more messages on different channels, and interact with external systems such as web services or persistent storage.
In order to implement their own business logic, applications need to write one or more user-defined services, see Section 8.2.2, “Using Services”, that can act upon receiving messages on Bayeux channels.
The following sections present detailed information about the Java Server APIs and their implementation.
You can specify BayeuxServer
and server transport parameters in web.xml
as init
parameters of the org.cometd.server.CometdServlet
.
If the CometD servlet creates the BayeuxServer
instance, the servlet init parameters are passed
to the BayeuxServer
instance, which in turn configures the server transports.
If you followed Chapter 5, Primer, Maven has configured the web.xml
file for you;
here are details its configuration, 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.CometdServlet</servlet-class> <init-param> <param-name>timeout</param-name> <param-value>60000</param-value> </init-param> <init-param> <param-name>logLevel</param-name> <param-value>3</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>cometd</servlet-name> <url-pattern>/cometd/*</url-pattern> </servlet-mapping> </web-app>
You must define and map the org.cometd.server.CometdServlet
in web.xml
to enable the server to interpret the Bayeux protocol.
It is normally mapped on /cometd/*
, but you can change the mapping url-pattern if you prefer.
Here is the list of configuration init parameters that the BayeuxServer
implementation accepts:
Table 8.4. BayeuxServer Configuration Parameters
Parameter Name | Default Value | Parameter Description |
---|---|---|
logLevel | 0 | The log level: 0 = off, 1 = config, 2 = info, 3 = debug. |
transports | empty string |
A comma-separated list of ServerTransport implementation class names (that take a
org.cometd.server.BayeuxServerImpl as only constructor parameter) to add to the
default server transports.
|
allowedTransports | empty string |
A comma-separated list of ServerTransport names allowed.
If not specified, the default server transports are allowed.
|
jsonContext | org.cometd.server.JettyJSONContextServer |
The full qualified name of a class implementing org.cometd.common.JSONContext.Server .
The class is loaded and instantiated using the default constructor.
|
validateMessageFields | true |
Whether message fields such as channel , id and
subscription should be validated to contain legal characters
as defined in Section C.3, “Protocol Elements”.
|
In CometD 2 the server transports are pluggable. You can configure them using parameters that may have a prefix that specifies the transport the parameter refers to.
For example, the parameter timeout
has no prefix, and hence it is valid for all transports;
the parameter callback-polling.jsonp.timeout
overrides the timeout
parameter for the
callback polling transport only, while ws.timeout
overrides it for the websocket transport
(see org.cometd.bayeux.Transport
javadocs for details).
Here is the list of configuration init parameters (to be specified in web.xml
)
that different server transports accept:
Table 8.5. Server Transports Common Configuration Parameters
Parameter Name | Default Value | Parameter Description |
---|---|---|
timeout | 30000 | The time, in milliseconds, that a server waits for a message before responding to a long poll with an empty response. |
interval | 0 | The time, in milliseconds, that the client must wait between the end of one long poll request and the start of the next. |
maxInterval | 10000 | The maximum period of time, in milliseconds, that the server waits for a new long poll from a client before that client is considered invalid and is removed. |
maxLazyTimeout | 5000 | The maximum period of time, in milliseconds, that the server waits before delivering or publishing lazy messages. |
metaConnectDeliverOnly | false |
Whether the transport should deliver the messages only via long poll (enables strict message ordering).
Enabling this option allows for strict message ordering at the cost of a slightly chattier protocol
(because delivery via /meta/connect requires waking up the long poll).
|
maxQueue | -1 |
The maximum size of the ServerSession queue. A value of -1 means no queue size limit.
A positive value triggers the invocation of
org.cometd.bayeux.server.ServerSession.MaxQueueListener s when the max queue
size is exceeded.
|
Table 8.6. Long Polling & Callback Polling Server Transports Configuration Parameters
Parameter Name | Default Value | Parameter Description |
---|---|---|
maxSessionsPerBrowser | 1 | The max number of sessions (tabs/frames) allowed to long poll from the same browser; a negative value allows unlimited sessions (see also Section 8.2.8, “Multiple Sessions”). |
allowMultiSessionsNoBrowser | false | Whether to allow multiple sessions (tabs/frames) in case the browser cannot be detected (see also Section 8.2.8, “Multiple Sessions”). |
multiSessionInterval | 2000 |
The period of time, in milliseconds, that specifies the client normal polling period in case the server detects
more sessions (tabs/frames) connected from the same browser than allowed by the
maxSessionsPerBrowser parameter.
A non-positive value means that additional sessions are disconnected.
|
browserCookieName | BAYEUX_BROWSER | The name of the cookie used to identify multiple sessions (see Section 8.2.8, “Multiple Sessions”). |
browserCookieDomain | The domain of the cookie used to identify multiple sessions (see Section 8.2.8, “Multiple Sessions”). By default there is no domain | |
browserCookiePath | / | The path of the cookie used to identify multiple sessions (see Section 8.2.8, “Multiple Sessions”). |
Table 8.7. WebSocket Server Transport Configuration Parameters
Parameter Name | Default Value | Parameter Description |
---|---|---|
ws.messagesPerFrame | 1 | How many Bayeux messages should be sent per WebSocket frame. Setting this parameter too high may result in WebSocket frames that may be rejected by the recipient because they are too big. |
ws.bufferSize | 65536 | The size, in bytes, of the buffer used to read and write WebSocket frames. |
ws.maxMessageSize | 65520 | The maximum size, in bytes, of an incoming WebSocket message. |
ws.idleTimeout | 300000 | The idle timeout, in milliseconds, for the WebSocket connection. |
If you are using Jetty 7, you might want to configure the
CrossOriginFilter
.
This filter implements the
Cross-Origin Resource Sharing specification,
and allows recent browsers that implement it (as of November 2009, Firefox 3.5.x, Chrome 3.x and Safari 4.x)
to perform cross-domain JavaScript requests (see Section 7.7, “JavaScript Transports”).
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.CometdServlet</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 the Jetty Cross Origin Filter documentation for the filter configuration.
CometD runs without additional configuration in Jetty 7 (a Servlet 2.5 Container) and in any Servlet 3.0 Container (see below). In order to run in other Servlet 2.5 (or greater) Containers, such as Tomcat, CometD requires additional configuration.
Non-Jetty Servlet 2.5 Containers need to install the ContinuationFilter
(see Section 8.2.1.3, “Configuring the CrossOriginFilter
”), mapping it to the same
path of the CometdServlet
(usual /cometd/*
), for example:
<?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.CometdServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>cometd</servlet-name> <url-pattern>/cometd/*</url-pattern> </servlet-mapping> <filter> <filter-name>continuation</filter-name> <filter-class>org.eclipse.jetty.continuation.ContinuationFilter</filter-class> </filter> <filter-mapping> <filter-name>continuation</filter-name> <url-pattern>/cometd/*</url-pattern> </filter-mapping> </web-app>
The CometD libraries are portable across Servlet Containers. When a CometD web application is deployed to a Servlet 3 compliant Container (such as Jetty 8 or greater, or Tomcat 7 or greater), CometD makes use of the Servlet 3 asynchronous features.
To enable the Servlet 3 asynchronous features, you need to:
Modify the web.xml
configuration by updating the version
attribute of the web-app
element to 3.0 (1).
Add the async-supported
element to filters that might execute before the
CometdServlet
and to the CometdServlet
itself (2).
For example:
<?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_3_0.xsd" version="3.0"> (1) <servlet> <servlet-name>cometd</servlet-name> <servlet-class>org.cometd.server.CometdServlet</servlet-class> <async-supported>true</async-supported> (2) </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> <async-supported>true</async-supported> (2) </filter> <filter-mapping> <filter-name>cross-origin</filter-name> <url-pattern>/cometd/*</url-pattern> </filter-mapping> </web-app>
The typical error that you get if you do not enable the Servlet 3 asynchronous features is the following:
IllegalStateException: the servlet does not support async operations for this request
While Jetty is configured by default with a non-blocking connector that allows CometD to run out of the box, Tomcat 7 is not, by default, configured with a non-blocking connector. You must first enable the non-blocking connector in Tomcat 7 in order for CometD to work properly. Please refer to the Tomcat documentation for how to configure a non-blocking connector in Tomcat.
Server channels are used to broadcast messages to multiple clients, and are a central concept of CometD
(see Chapter 6, Concepts & Architecture).
Class org.cometd.bayeux.server.ServerChannel
represents server channels; instances
of server channels can be obtained from a BayeuxServer
instance.
With the default security policy, server channels may be created simply by publishing to a channel: if the
channel does not exist, it is created on-the-fly.
This may open up for creation of a large number of server channel, for example when messages are published
to channels created with a random name, such as /topic/atyd9834o329
, and for race conditions
during channel creation (since the same server channel may be created
concurrently by two remote clients publishing to that channel at the same time).
To avoid that these transient server channels grow indefinitely and occupy a lot of memory, the CometD server aggressively sweeps server channels, by default every second, removing all channels that are not in use by the application anymore.
Given the above, we need to solve two problems:
how to atomically create and configure a server channel
how to avoid that channels that the application knows they will be used at a later time are swept prematurely
The solution offered by the CometD API for the first problem is to provide a method that atomically creates and initializes server channels:
BayeuxServer bayeuxServer = ...; boolean created = bayeuxServer.createIfAbsent("/my/channel", new ServerChannel.Initializer() { public void configureChannel(ConfigurableServerChannel channel) { // Here configure the channel } });
Method BayeuxServer.createIfAbsent(String channelName, Initializer... initializers)
atomically
creates the channel, returning true or false to indicate whether the channel was created or if it existed
already.
The Initializer
callback is called only if the channel is created by the invocation to
createIfAbsent(...)
.
The solution to the second problem is to configure the channel as persistent, so that the sweeper does not remove the channel:
BayeuxServer bayeuxServer = ...;
boolean created = bayeuxServer.createIfAbsent("/my/channel", new ServerChannel.Initializer()
{
public void configureChannel(ConfigurableServerChannel channel)
{
channel.setPersistent(true);
}
});
You can not only configure ServerChannel
instances to be persistent, but to be
lazy (see Section 8.2.7, “Lazy Channels and Messages”), you can add listeners, and you
can add Authorizer
s (see Section 8.2.4, “Server Channel Authorizers”).
Creating a server channel does not return you the channel that has been created. In order to obtain the
server channel reference, you need to call BayeuxServer.getChannel(String channelName)
:
BayeuxServer bayeuxServer = ...;
String channelName = "/my/channel";
bayeuxServer.createIfAbsent(channelName, new ServerChannel.Initializer()
{
public void configureChannel(ConfigurableServerChannel channel)
{
channel.setPersistent(true);
}
});
ServerChannel channel = bayeuxServer.getChannel(channelName);
The code above creates the channel, configures it to be persistent and then obtains a reference to it, that
is guaranteed to be non-null (if createIfAbsent(...)
returned true).
A typical error in CometD applications is to create the channel without making it persistent, and then trying to obtain a reference to it without checking if it's null:
BayeuxServer bayeuxServer = ...; String channelName = "/my/channel"; // Wrong, channel not marked as persistent, but used later bayeuxServer.createIfAbsent(channelName); // Other application code here ServerChannel channel = bayeuxServer.getChannel(channelName); channel.publish(...); // May throw NullPointerException
Between the createIfAbsent(...)
call and the getChannel(...)
call there is
application code that may take a while to complete (therefore allowing the sweeper to sweep the just created
server channel), so it is always safer to mark the channel as persistent, and when it is not needed anymore
mark the server channel as non persistent (by calling channel.setPersistent(false)
), to allow
the sweeper to sweep it.
The server channel sweeper will sweep channels that are non-persistent, have no subscribers, have no listeners, have no authorizers and have no children channels, and only after these conditions are met for three consecutive sweeper passes.
A CometD service is a Java class that allows a developer to specify the code to run when Bayeux channels receive Bayeux messages. When a message arrives on a channel to which the service instance subscribes, CometD invokes a callback method to execute user-specific code.
CometD 2 services can be of two kinds:
You can also integrate CometD 2 services with the Spring Framework as explained in Section 8.2.2.4, “Services Integration with Spring”.
The following sections present details about Java Server Services.
A CometD inherited service is a Java class that extends the CometD class
org.cometd.server.AbstractService
, which specifies the Bayeux channels of interest to
the service, and adheres to the contract the AbstractService
class requires:
public class EchoService extends AbstractService (1) { public EchoService(BayeuxServer bayeuxServer) (2) { super(bayeuxServer, "echo"); (3) addService("/echo", "processEcho"); (4) } public void processEcho(ServerSession remote, Map<String, Object> data) (5) { remote.deliver(getServerSession(), "/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.
Notice the following:
Extends from org.cometd.server.AbstractService.
Creates a constructor that takes a org.cometd.bayeux.server.BayeuxServer
object.
Calls the superclass constructor, passing the BayeuxServer object and an arbitrary name of the service, in this case "echo".
Subscribes to channel "/echo", and specifies the name of a method that must be called when
a message arrives to that channel, via addService(...)
.
Defines a method with the same name specified in (4), and with an appropriate signature (see below).
Uses the org.cometd.bayeux.server.ServerSession
API to echo the message back
to that particular client.
The contract that the BayeuxService
class requires for callback methods is that the methods must
have one of the following signatures:
// Obtains the remote session object and the message object public void processEcho(ServerSession remote, Message message) // Obtains the remote session object and the message's data object // (additional message information, such as the channel or the id is lost) public void processEcho(ServerSession remote, Map<String, Object> data) // Obtains the remote session object, the channel name, the message object and the message id public void processEcho(ServerSession remote, String channelName, Message message, String messageId) // Obtains the remote session object, the channel name, the message's data object and the message id public void processEcho(ServerSession remote, String channelName, Map<String, Object> data, String messageId)
Method signatures that take the message's data object (the second and fourth above) take a
Map<String, Object>
as parameter type because that is the default deserialization of a JSON
object in Java, but it is possible to specify a custom type, provided that the JSON object
has been converted to an object of the custom Java class, for example:
// Obtains the remote session object and a custom message's data object
public void processEcho(ServerSession remote, EchoInfo data)
See Section 8.3, “JSON Libraries” for further information about customizing serialization and deserialization of JSON objects.
Notice that the channel name specified in the addService()
method can be a wildcard, for example:
public class BaseballTeamService extends AbstractService { public BaseballTeamService(BayeuxServer bayeux) { super(bayeux, "baseballTeam"); addService("/baseball/team/*", "processBaseballTeam"); } public void processBaseballTeam(ServerSession 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).publish(getServerSession(), data, null); } }
Notice also how the first example uses ServerSession.deliver()
to send a message to a particular
remote client, while the second uses ServerChannel.publish()
to send a message to anyone who
subscribes to channel /events/baseball/team/*
.
Method addService(...)
is used to map a server-side channel listener with
a method that is invoked every time a message arrives on the channel.
It is not uncommon that a single service has multiple mappings, and mappings may be even added and removed
dynamically:
public class GameService extends AbstractService { public GameService(BayeuxServer bayeux) { super(bayeux, "game"); addService("/service/game/*", "processGameCommand"); addService("/game/event", "processGameEvent"); } public void processGameCommand(ServerSession remote, GameCommand command) { switch (command.getType()) { case GAME_START: { addService("/game/" + command.getGameId(), "processGame"); break; } case GAME_END: { removeService("/game/" + command.getGameId()); break; } ... } } public void processGameEvent(ServerSession remote, GameEvent event) { ... } }
Note how mappings can be removed using method removeService(...)
.
Each time a service instance is created, an associated LocalSession
(see
Section 6.3.1, “Sessions”) is created within the service itself: the service is a local client.
The LocalSession
has an associated ServerSession
and as such it is treated by the
server in the same way a remote client (that also creates a ServerSession
within the server) is.
The LocalSession
and ServerSession
are accessible via the AbstractService
methods getLocalSession()
and getServerSession()
respectively.
Once you have written your Bayeux services it is time to set them up in your web application: Section 8.2.2.3, “Services Integration” or Section 8.2.2.4, “Services Integration with Spring”.
Classes annotated with @Service
qualify as annotated services, both on the client-side and on
the server-side. Support for annotated services is available beginning with CometD 2.1.0.
Server-side services are usually written by extending the org.cometd.server.AbstractService
class, and instances of these classes normally have a singleton semantic and are created and configured at
web application startup.
The org.cometd.server.AbstractService
class provides (via inheritance) some facilities
that are useful when implementing a service, including access to the ServerSession
associated with
the service instance and registration of methods as a callbacks to receive messages from channels.
Services might depend on other services (for example, a data source to access the database), and might require
lifecycle management (that is, the services have start()
/stop()
methods that must be
invoked at appropriate times).
In services extending org.cometd.server.AbstractService
, dependency injection and
lifecycle management, previously you had to write the code by hand in a configuration servlet or configuration
listeners.
Now annotated server-side services offer full support for CometD features, and limited support for dependency
injection and lifecycle management via the org.cometd.annotation.ServerAnnotationProcessor
class.
Annotated service instances are stored in the servlet context under the key that correspond to their full qualified class name.
The CometD project offers limited support for dependency injection, since normally this is accomplished by other frameworks such as Spring or Guice.
In particular, it supports only the injection of the BayeuxServer object on fields and methods (not on constructors), and performs the injection only if the injection has not yet been performed.
The reason for this limited support is that the CometD project does not want to implement and support a generic dependency injection container, but instead offers a simple integration with existing dependency injection containers and a minimal support for required CometD objects (such as the BayeuxServer instance).
Annotated Style | Inherited Style |
---|---|
@org.cometd.annotation.Service("echoService") public class EchoService { @javax.inject.Inject private BayeuxServer bayeux; } |
public class EchoService extends AbstractService
{
public EchoService(BayeuxServer bayeux)
{
super(bayeux, "echoService");
}
}
|
The service class is annotated with @Service
and specifies the (optional) service name "echoService".
The BayeuxServer field is annotated with the standard
JSR 330 @Inject
annotation.
The @Inject
annotation is supported (for example, by Spring 3.x) for standard dependency injection
as specified by JSR 330.
The CometD project provides lifecycle management via the standard
JSR 250 @PostConstruct
and
@PreDestroy
annotations.
We offer this support for those that do not use a dependency injection container with lifecycle management
such as Spring.
To initialize channels before they can be actually referenced for subscriptions, the CometD API provides the
BayeuxServer.createIfAbsent(String channelId, ConfigurableServerChannel.Initializer... initializers)
method, which allows you to pass initializers that configure the given channel.
Furthermore, it is useful to have a configuration step for channels that happens before any subscription or
listener addition, for example, to configure authorizers on the channel
(see Section 8.2.4, “Server Channel Authorizers”.
In annotated services, you can use the @Configure
annotation on methods:
@Service("echoService") public class EchoService { @Inject private BayeuxServer bayeux; @Configure("/echo") public void configure(ConfigurableServerChannel channel) { channel.setLazy(true); channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH); } }
Services that extend org.cometd.server.AbstractService
have two facility methods to
access the LocalSession and the ServerSession, namely getLocalSession()
and
getServerSession()
.
In annotated services, this is accomplished using the @Session
annotation:
@Service("echoService") public class EchoService { @Inject private BayeuxServer bayeux; @org.cometd.annotation.Session private LocalSession localSession; @org.cometd.annotation.Session private ServerSession serverSession; }
Fields (or methods) annotated with the @Session
annotation are optional; you can just have the
LocalSession
field, or only the ServerSession
field, or both or none, depending on
whether you need them or not.
You cannot inject Session fields (or methods) with @Inject
. This is because the
LocalSession
object and the ServerSession
object are related, and tied to a
particular service instance. Using a generic injection mechanism could lead to confusion (for example, using
the same sessions in two different services).
For server-side services, methods annotated with @Listener
represent callbacks that are invoked
during the server-side processing of the message.
CometD passes a reference to Listener methods to both:
The ServerSession
half object that sent the message.
The ServerMessage
that the server is processing.
The callback method must have the following signature, or a covariant version of it:
Annotated Style | Inherited Style |
---|---|
@Service("echoService") public class EchoService { @Inject private BayeuxServer bayeux; @Session private ServerSession serverSession; @org.cometd.annotation.Listener("/echo") public void echo(ServerSession remote, ServerMessage.Mutable message) { String channel = message.getChannel(); Object data = message.getData(); remote.deliver(serverSession, channel, data, null); } } |
public class EchoService extends AbstractService { public EchoService(BayeuxServer bayeux) { super(bayeux, "echoService"); addService("/echo", "echo"); } public void echo(ServerSession remote, ServerMessage.Mutable message) { String channel = message.getChannel(); Object data = message.getData(); remote.deliver(getServerSession(), channel, data, null); } } |
The callback method can return false to indicate that the processing of subsequent listeners should not be performed and that the message should not be published.
For server-side services, methods annotated with @Subscription
represent callbacks that are
invoked during the local-side processing of the message.
The local-side processing is equivalent to the remote client-side processing, but it is local to the server.
The semantic is very similar to the remote client-side processing, in the sense that the message has completed
the server-side processing and has been published. When it arrives to the local side the information on the
publisher is not available anymore, and the message is a plain org.cometd.bayeux.Message
and not a org.cometd.bayeux.server.ServerMessage
, exactly as it would happen for a
remote client.
This is a rarer use case (most of the time user code must be triggered with @Listener
semantic),
but nonetheless is available.
The callback method must have the following signature:
@Service("echoService") public class EchoService { @Inject private BayeuxServer bayeux; @Session private ServerSession serverSession; @org.cometd.annotation.Subscription("/echo") public void echo(Message message) { System.out.println("Echo service published " + message); } }
The org.cometd.annotation.ServerAnnotationProcessor
class performs annotation processing.
BayeuxServer bayeux = ...; // Create the ServerAnnotationProcessor ServerAnnotationProcessor processor = new ServerAnnotationProcessor(bayeux); // Create the service instance EchoService service = new EchoService(); // Process the annotated service processor.process(service);
After the ServerAnnotationProcessor.process()
method returns, the service has been processed
by injecting the BayeuxServer
object and the sessions objects, by calling initialization lifecycle
methods, and by registering listeners and subscribers.
Symmetrically, ServerAnnotationProcessor.deprocess()
performs annotation deprocessing, which
deregisters listeners and subscribers, and then calls destruction lifecycle methods (but does not deinject
the BayeuxServer
object or session objects).
Client-side services have no equivalent in CometD releases prior 2.1.0.
Like their server-side counterpart, client-side services consist in classes annotated with @Service
.
We have introduced client-side services to reduce the boilerplate code required:
Annotated Style | Traditional Style |
---|---|
@Service public class Service { @Session private ClientSession bayeuxClient; @Listener(Channel.META_CONNECT) public void metaConnect(Message connect) { // Connect handling... } @Subscription("/foo") public void foo(Message message) { // Message handling... } } |
ClientSession bayeuxClient = ...; bayeuxClient.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { // Connect handling... } }); bayeuxClient.handshake(); bayeuxClient.waitFor(1000, BayeuxClient.State.CONNECTED); bayeuxClient.getChannel("/foo").subscribe(new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { // Message handling... } }); |
The CometD project does not offer dependency injection for client-side services, but supports lifecycle
management via the standard JSR 250
@PostConstruct
and @PreDestroy
annotations.
Client-side services usually have a shorter lifecycle than server-side services and their dependencies are
usually injected directly while creating the client-side service instance.
In client-side annotated services, the @Session
annotation allows the service instance to have the
ClientSession
object injected in a field or method.
Like server-side annotated services, the session field (or method) cannot be injected with @Inject
.
This is to allow the maximum configuration flexibility between service instances and ClientSession
instances.
@Service public class Service { @org.cometd.annotation.Session private ClientSession bayeuxClient; }
In client-side annotated services, methods annotated with @Listener
represent callbacks that
are called upon receipt of messages on meta channels.
Do not use listener callbacks to subscribe to broadcast channels.
Annotated Style | Traditional Style |
---|---|
@Service public class Service { @Listener(Channel.META_CONNECT) public void metaConnect(Message connect) { // Connect handling... } } |
bayeuxClient.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
// Connect handling...
}
});
|
In client-side annotated services, methods annotated with @Subscription
represent callbacks that are
called upon receipt of messages on broadcast channels.
Annotated Style | Traditional Style |
---|---|
@Service public class Service { @Listener("/foo/*") public void foos(Message message) { // Message handling... } } |
bayeuxClient.getChannel("/foo/*").subscribe(new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { // Message handling... } }); |
The org.cometd.annotation.ClientAnnotationProcessor
class does annotation processing.
ClientSession bayeuxClient = ...; // Create the ClientAnnotationProcessor ClientAnnotationProcessor processor = new ClientAnnotationProcessor(bayeuxClient); // Create the service instance Service service = new Service(); // Process the annotated service processor.process(service); bayeuxClient.handshake();
Listener callbacks are configured immediately on the ClientSession
object, while subscription
callbacks are automatically delayed until the handshake is successfully completed.
There are several ways to integrate your Bayeux services into your web application. This is complicated because a servlet creates the BayeuxServer object, and there is no easy way to detect, in general, when the BayeuxServer object has been created.
The simplest way to initialize your web application with your services is to use a configuration servlet. This configuration servlet has no URL mapping because its only scope is to initialize (or wire together) services for your web application to work properly.
Here 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.CometdServlet</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>
Notice that the web.xml
file specifies <load-on-startup>
to be:
1 for the CometD servlet – so that the Bayeux object gets created and put in the ServletContext
.
2 for the configuration servlet – so that it will be initialized only after the CometD servlet has been initialized and hence the BayeuxServer object is available.
This is the code for the ConfigurationServlet
:
public class ConfigurationServlet extends GenericServlet { public void init() throws ServletException { // Grab the Bayeux object BayeuxServer bayeux = (BayeuxServer)getServletContext().getAttribute(BayeuxServer.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 Section 8.2.2.1, “Inherited Services” about the EchoService
.
Instead of using a configuration servlet, you can use a configuration listener, by writing a class that
implements ServletContextAttributeListener
.
Here is a sample 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.CometdServlet</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 BayeuxServer bayeux = (BayeuxServer)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) { } }
If you prefer to use annotated services (see Section 8.2.2.1, “Inherited Services”, you still have to integrate them into your web application. The procedure is very similar to the procedures above, but it requires use of the annotation processor to process the annotations in your services.
For example, the ConfigurationServlet
becomes:
public class ConfigurationServlet extends GenericServlet { private final List<Object> services = new ArrayList<Object>(); private ServerAnnotationProcessor processor; public void init() throws ServletException { // Grab the BayeuxServer object BayeuxServer bayeux = (BayeuxServer)getServletContext().getAttribute(BayeuxServer.ATTRIBUTE); // Create the annotation processor processor = new ServerAnnotationProcessor(bayeux); // Create your annotated service instance and process it Object service = new EchoService(); processor.process(service); services.add(service); // 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 destroy() throws ServletException { // Deprocess the services that have been created for (Object service : services) processor.deprocess(service); } public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { throw new ServletException(); } }
The org.cometd.java.annotation.AnnotationCometdServlet
allows you to specify
a comma-separated list of class names to instantiate and process using a ServerAnnotationProcessor
.
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.java.annotation.AnnotationCometdServlet</servlet-class> <init-param> <param-name>services</param-name> <param-value>com.acme.cometd.FooService, com.acme.cometd.BarService</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>cometd</servlet-name> <url-pattern>/cometd/*</url-pattern> </servlet-mapping> </web-app>
In this example, the AnnotationCometdServlet
instantiates and processes the annotations
of one object of class com.acme.cometd.FooService
and of one object of class
com.acme.cometd.BarService
.
The service objects are stored as ServletContext
attributes under their own class name,
so that they can be easily retrieved by other components.
For example, FooService
can be retrieved using the following code:
public class AnotherServlet extends HttpServlet { protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { FooService service = (FooService)getServletContext().getAttribute("com.acme.cometd.FooService"); // Use the foo service here } }
The services created are deprocessed when AnnotationCometdServlet
is destroyed.
Integration of CometD services with Spring is particularly
interesting, since most of the time your Bayeux services require other beans to perform
their service.
Not all Bayeux services are as simple as the EchoService
(see Section 8.2.2.1, “Inherited Services”,
and having Spring's dependency injection (as well as other facilities) integrated greatly simplifies development.
The BayeuxServer
object is directly configured and initialized in the Spring configuration file,
which injects it in the servlet context, where the CometD servlet picks it up, performing no further
configuration or initialization.
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.CometdServlet</servlet-class> </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>
It is important to note that the web.xml
above does not
contain any configuration specified as <init-param>
.
This is because if you are using Spring, you want the configuration to be specified in just one place,
that is in the Spring configuration files.
Furthermore, the <load-on-startup>
directive is not present.
Since the Servlet Specification does not define the startup order between servlets and listeners
(in particular, <listener>
elements cannot be ordered with a
<load-on-startup>
element), you must make sure that the Spring
<listener>
runs before the CometdServlet
, otherwise
you risk that two BayeuxServer
objects are created (one by CometdServlet
and
the other by Spring), and your application will not work properly (likely, the connection with remote
clients will be handled by the CometdServlet
's BayeuxServer
, while your
services will be bound to Spring's BayeuxServer
).
By not specifying a <load-on-startup>
element in the CometdServlet
definition, the CometdServlet
will be initialized lazily after the
Spring <listener>
, ensuring that only the Spring's BayeuxServer
is
created and used also by CometdServlet
when it is initialized upon receiving the first
request from remote clients.
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="otherService" class="com.acme..." /> <bean id="bayeux" class="org.cometd.server.BayeuxServerImpl" init-method="start" destroy-method="stop"> <property name="options"> <map> <entry key="logLevel" value="3" /> <entry key="timeout" value="15000" /> </map> </property> </bean> <bean id="echoService" class="com.acme.cometd.EchoService"> <constructor-arg><ref local="bayeux" /></constructor-arg> <constructor-arg><ref local="otherService" /></constructor-arg> </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> </beans>
Spring now creates the BayeuxServer
object, configuring it via the options
property,
initializing via the start()
method, and exporting to the servlet context via Spring's
ServletContextAttributeExporter
.
This ensures that CometdServlet
will not create its own instance of
BayeuxServer
, but use the one that is already present in the servlet context, created by Spring.
Below you can find a Spring's applicationContext.xml
that configures the BayeuxServer
object with the WebSocket transport:
<?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="bayeux" class="org.cometd.server.BayeuxServerImpl" init-method="start" destroy-method="stop"> <property name="transports"> <list> <bean id="websocketTransport" class="org.cometd.websocket.server.WebSocketTransport"> <constructor-arg ref="bayeux" /> </bean> <bean id="jsonTransport" class="org.cometd.server.transport.JSONTransport"> <constructor-arg ref="bayeux" /> </bean> <bean id="jsonpTransport" class="org.cometd.server.transport.JSONPTransport"> <constructor-arg ref="bayeux" /> </bean> </list> </property> </bean> ... as before ... </beans>
When configuring the BayeuxServer
transports, you need to explicitly specify all the transports
you want, included the default transports that you do not need to specify when configuring a
BayeuxServer
using the CometdServlet
.
The order of the transports is important, and you want the WebSocketTransport
to be the first
of the list, followed by at least the JSONTransport
(also known as the "long-polling" transport)
as a fallback.
Spring 3.x supports annotation-based configuration, and Section 8.2.2.2, “Annotated Client-Side and Server-Side Services”
integrate nicely with Spring 3.x.
Spring 3.x is required over Spring 2.5.x because it supports injection via
JSR 330.
Prerequisite to making Spring 3.x work with CometD annotated services is to have JSR 330's
javax.inject
classes in the classpath along with
JSR 250's javax.annotation
classes
(these are included in JDK 6 and therefore only required if you use JDK 5).
Do not forget that Spring 3.x requires CGLIB classes in the classpath as well.
The web.xml
file is exactly the same as the one given as an example in the XML based configuration
above (see Section 8.2.2.4.1, “XML Based Spring Configuration”), and
the same important notes apply.
Spring's applicationContext.xml
is as follows:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" 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-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:component-scan base-package="com.acme..." /> </beans>
Spring scans the classpath for classes that qualify as Spring beans in the given base package.
The CometD annotated service needs some additional annotation to make it qualify as a Spring bean:
@javax.inject.Named // Tells Spring that this is a bean @javax.inject.Singleton // Tells Spring that this is a singleton @Service("echoService") public class EchoService { @Inject private BayeuxServer bayeux; @Session private ServerSession serverSession; @PostConstruct public void init() { System.out.println("Echo Service Initialized"); } @Listener("/echo") public void echo(ServerSession remote, ServerMessage.Mutable message) { String channel = message.getChannel(); Object data = message.getData(); remote.deliver(serverSession, channel, data, null); } }
The missing piece is that you need to tell Spring to perform the processing of the CometD annotations; do so using a Spring component:
@Component public class Configurer implements DestructionAwareBeanPostProcessor, ServletContextAware { private BayeuxServer bayeuxServer; private ServerAnnotationProcessor processor; @Inject private void setBayeuxServer(BayeuxServer bayeuxServer) { this.bayeuxServer = bayeuxServer; } @PostConstruct private void init() { this.processor = new ServerAnnotationProcessor(bayeuxServer); } public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { processor.processDependencies(bean); processor.processConfigurations(bean); processor.processCallbacks(bean); return bean; } public Object postProcessAfterInitialization(Object bean, String name) throws BeansException { return bean; } public void postProcessBeforeDestruction(Object bean, String name) throws BeansException { processor.deprocessCallbacks(bean); } @Bean(initMethod = "start", destroyMethod = "stop") public BayeuxServer bayeuxServer() { BayeuxServerImpl bean = new BayeuxServerImpl(); bean.setOption(BayeuxServerImpl.LOG_LEVEL, "3"); return bean; } public void setServletContext(ServletContext servletContext) { servletContext.setAttribute(BayeuxServer.ATTRIBUTE, bayeuxServer); } }
Summary:
This Spring component is the factory for the BayeuxServer object via the bayeuxServer()
method (annotated with Spring's @Bean
).
Creating CometD's ServerAnnotationProcessor
requires the BayeuxServer object, and therefore
it @Injects
it into a setter method.
The lifecycle callback init()
creates CometD's ServerAnnotationProcessor
,
which is then used during Spring's bean post processing phases.
Finally, the BayeuxServer object is exported into the servlet context for the CometD servlet to use.
You can configure the BayeuxServer
object with an
org.cometd.bayeux.server.SecurityPolicy
object, which allows you to control various steps of the Bayeux protocol such as handshake, subscription,
and publish.
By default, the BayeuxServer
object has a default SecurityPolicy
,
that allows almost any operation.
The org.cometd.bayeux.server.SecurityPolicy
has a default implementation in
org.cometd.server.DefaultSecurityPolicy
, that is useful as a base class to
customize the SecurityPolicy
(see how authentication
works for an example).
The org.cometd.bayeux.server.SecurityPolicy
methods are:
boolean canHandshake(BayeuxServer server, ServerSession session, ServerMessage message); boolean canCreate(BayeuxServer server, ServerSession session, String channelId, ServerMessage message); boolean canSubscribe(BayeuxServer server, ServerSession session, ServerChannel channel, ServerMessage messsage); boolean canPublish(BayeuxServer server, ServerSession session, ServerChannel channel, ServerMessage messsage);
Those methods control, respectively, whether a handshake, a channel creation, a subscription to a channel or a publish to a channel are to be authorized.
The SecurityPolicy
methods are invoked for ServerSession
s that correspond to both
local clients and remote clients (see Section 6.3.1, “Sessions”).
Application code can determine whether a ServerSession
correspond to a local client by
calling method ServerSession.isLocalSession()
.
In almost all cases, local clients should be authorized because they are created on the server by the application (for example, by services - see Section 8.2.2.1, “Inherited Services”) and therefore are trusted clients.
The default implementation org.cometd.server.DefaultSecurityPolicy
:
Allows any handshake.
Allows creation of a channel only from clients that performed a handshake, and only if the channel is not a meta channel.
Allows subscription from clients that performed a handshake, but not if the channel is a meta channel.
Allows publish from clients that performed a handshake to any channel that is not a meta channel.
A typical custom SecurityPolicy
may override the canHandshake(...)
method to control
authentication:
public class CustomSecurityPolicy extends DefaultSecurityPolicy { public boolean canHandshake(BayeuxServer server, ServerSession session, ServerMessage message) { // Always allow local clients if (session.isLocalSession()) return true; // Implement custom authentication logic boolean authenticated = authenticate(server, session, message); return authenticated; } }
To understand how to install your custom SecurityPolicy
on the BayeuxServer
object,
see how it is done in the
authentication howto.
Follow also the section on Section 8.2.4, “Server Channel Authorizers” for further information about authorization.
CometD 2.1.0 introduced authorizers, a feature that allows fine-grained control of authorization on channel operations.
Authorizers are objects that implement org.cometd.bayeux.server.Authorizer
;
you add them to server-side channels at setup time (before any operation on that channel can happen)
in the following way:
BayeuxServer bayeuxServer = ...;
bayeuxServer.createIfAbsent("/my/channel", new ConfigurableServerChannel.Initializer()
{
public void configureChannel(ConfigurableServerChannel channel)
{
channel.addAuthorizer(GrantAuthorizer.GRANT_PUBLISH);
}
});
The utility class org.cometd.server.authorizer.GrantAuthorizer
provides
some pre-made authorizers, but you can create your own at will.
Authorizers are particularly suited to control authorization on those channels that CometD creates at runtime, or for those channels whose ID CometD builds at runtime by concatenating strings that are unknown at application startup (see examples below).
Authorizers do not apply to meta channels, only to service channels and to broadcast channels.
You can add authorizers to wildcard channels (such as /chat/*
);
they impact all channels that match the wildcard channel on which you have added the authorizer.
An authorizer that you add to /**
impacts all non-meta channels.
For a non wildcard channel such as /chat/room/10
, the authorizer set is
the union of all authorizers on that channel, and of all authorizers on wildcard channels that match the
channel (in this case authorizers on channels /chat/room/*
, /chat/room/**
,
/chat/**
and /**
).
Authorizers control access to channels in collaboration with the
org.cometd.bayeux.server.SecurityPolicy
that is currently installed.
The org.cometd.bayeux.server.SecurityPolicy
class exposes three methods that you can use to control access to channels:
public boolean canCreate (BayeuxServer server, ServerSession session, String channelId, ServerMessage message); public boolean canSubscribe(BayeuxServer server, ServerSession session, ServerChannel channel, ServerMessage message); public boolean canPublish (BayeuxServer server, ServerSession session, ServerChannel channel, ServerMessage message);
These authorizers control, respectively, the operation to create a channel, the operation to subscribe to a channel, and the operation to publish to a channel.
The complete algorithm for the authorization follows:
If there is a security policy, and the security policy denies the request, then the request is denied.
Otherwise, if the authorizer set is empty, the request is granted.
Otherwise, if no authorizer explicitly grants the operation, the request is denied.
Otherwise, if at least one authorizer explicitly grants the operation, and no authorizer explicitly denies the operation, the request is granted.
Otherwise, if one authorizer explicitly denies the operation, remaining authorizers are not consulted, and the request is denied.
The order in which the authorizers are called is not important.
The following method of org.cometd.bayeux.server.Authorizer
must be implemented
with your authorization algorithm:
public Result authorize(Operation operation, ChannelId channel, ServerSession session, ServerMessage message);
One of three possible results must be returned:
public Result authorize(Operation operation, ChannelId channel, ServerSession session, ServerMessage message); { return Result.grant(); }
Result.grant()
explicitly grants permission to perform the operation passed in as a parameter
on the channel. At least one authorizer must grant the operation, otherwise the operation is denied.
The fact that one (or multiple) authorizers grant an operation does not imply that the operation is granted at the end of the authorization algorithm: it could be denied by another authorizer in the set of authorizers for that channel.
public Result authorize(Operation operation, ChannelId channel, ServerSession session, ServerMessage message); { return Result.ignore(); }
Result.ignore()
neither grants nor denies the permission to perform the operation passed in as a
parameter on the channel.
Ignoring the authorization request is the usual way to deny authorization if the conditions for which
the operation must be granted do not apply.
public Result authorize(Operation operation, ChannelId channel, ServerSession session, ServerMessage message);
{
return Result.deny("reason to deny");
}
Result.deny()
explicitly denies the permission to perform the operation passed in as a parameter
on the channel.
Denying the authorization request immediately results in the authorization being denied without even consulting
other authorizers in the authorizer set for that channel.
Typically, denying authorizers are used for cross-cutting concerns: where you have a sophisticated logic in authorizers to grant access to users for specific paid TV channels based on the user's contract (imagine that bronze, silver and gold contracts give access to different TV channels), you have a single authorizer that denies the operation if the user's balance is insufficient, no matter the contract or the TV channel being requested.
Similarly to the SecurityPolicy
(see Section 8.2.3, “Authorization”),
Authorizer
methods are invoked for any ServerSession
, even those generated by
local clients (such as services, see Section 8.2.2.1, “Inherited Services”).
Implementers should check whether the ServerSession
that is performing the operation is related
to a local client or to a remote client, and act accordingly (see example below).
The following example assumes that the security policy does not interfere with the authorizers, and that the code is executed before the channel exists (either at application startup or in places where the application logic ensures that the channel has not been created yet).
Imagine an application that allows you to watch and play games.
Typically, an ignoring authorizer is added on a root channel:
BayeuxServer bayeuxServer = ...; bayeuxServer.createIfAbsent("/game/**"); ServerChannel gameStarStar = bayeuxServer.getChannel("/game/**"); gameStarStar.addAuthorizer(GrantAuthorizer.GRANT_NONE);
This ensures that the authorizer set is not empty, and that by default (if no other authorizer grants or denies) the authorization is ignored and hence denied.
Only captains can start a new game, and to do so they create a new channel for that game, for example
/game/123
(where 123
is the gameId):
gameStarStar.addAuthorizer(new Authorizer() { public Result authorize(Operation operation, ChannelId channel, ServerSession session, ServerMessage message) { // Always grant authorization to local clients if (session.isLocalSession()) return Result.grant(); boolean isCaptain = isCaptain(session); boolean isGameChannel = !channel.isWild() && new ChannelId("/game").isParentOf(channel); if (operation == Operation.CREATE && isCaptain && isGameChannel) return Result.grant(); return Result.ignore(); } });
Everyone can watch the game:
gameStarStar.addAuthorizer(GrantAuthorizer.GRANT_SUBSCRIBE);
Only players can play:
ServerChannel gameChannel = bayeuxServer.getChannel("/game/" + gameId); gameChannel.addAuthorizer(new Authorizer() { public Result authorize(Operation operation, ChannelId channel, ServerSession session, ServerMessage message) { // Always grant authorization to local clients if (session.isLocalSession()) return Result.grant(); boolean isPlayer = isPlayer(session, channel); if (operation == Operation.PUBLISH && isPlayer) return Result.grant(); return Result.ignore(); } });
The authorizers are the following:
/game/** --> one authorizer that ignores everything --> one authorizer that grants captains authority to create games --> one authorizer that grants everyone the ability to watch games /game/123 --> one authorizer that grants players the ability to play
Imagine that later you want to forbid criminal supporters to watch games, so you can add another authorizer (instead of modifying the one that allows everyone to watch games):
gameStarStar.addAuthorizer(new Authorizer() { public Result authorize(Operation operation, ChannelId channel, ServerSession session, ServerMessage message) { // Always grant authorization to local clients if (session.isLocalSession()) return Result.grant(); boolean isCriminalSupporter = isCriminalSupporter(session); if (operation == Operation.SUBSCRIBE && isCriminalSupporter) return Result.deny("criminal_supporter"); return Result.ignore(); } });
The authorizers are now the following:
/game/** --> one authorizer that ignores everything --> one authorizer that grants captains the ability to create games --> one authorizer that grants everyone the ability to watch games --> one authorizer that denies criminal supporters the ability to watch games /game/123 --> one authorizer that grants players the ability to play
Notice how authorizers on /game/**
never grant Operation.PUBLISH
,
which authorizers only grant on specific game channels.
Also, the specific game channel does not need to grant Operation.SUBSCRIBE
,
because its authorizer ignores the subscribe operation that is authorizers therefore handle
on the /game/**
channel.
Appendix C, The Bayeux Protocol Specification v1.0 defines two mandatory
transports: long-polling
and callback-polling
.
These two transports are automatically configured in the BayeuxServer
instance, and there is no
need to explicitly specify them in the configuration.
For the websocket
transport things are different because there is no standard API for server-side
WebSocket yet (it is under consideration for the Servlet 3.1 specification), and therefore any implementation binds you to
the servlet container implementation.
The CometD project borrows many libraries from the Jetty
project.
While CometD itself is not bound to Jetty, and you can plug in other WebSocket implementations , the CometD
projects ships a websocket
server transport based on Jetty's WebSocket.
As such, if you want to use the CometD WebSocket server transport shipped by default, you
must run your web application in Jetty.
While for the websocket
server transport you are bound to run your
web application in a specific servlet container, it is possible to use the websocket
client transport on the server (for example to communicate with another WebSocket
CometD server) in any servlet container.
While Jetty's WebSocket server library works only in Jetty, Jetty's WebSocket client library is portable and does not bind you to any servlet container. For example, it is possible to have a client communicating via HTTP to a CometD web application running in Tomcat, which communicates via WebSocket to another CometD web application running in Jetty.
To configure the websocket
transport on the server, follow these instructions:
Change the CometdServlet
configuration in web.xml
(see also
Section 8.2.1, “Configuring the Java Server”) to specify the transports
parameter,
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.CometdServlet</servlet-class> <init-param> <param-name>transports</param-name> <param-value>org.cometd.websocket.server.WebSocketTransport</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>cometd</servlet-name> <url-pattern>/cometd/*</url-pattern> </servlet-mapping> </web-app>
You do not need to worry about the long-polling
and callback-polling
transports.
Those are present in any case, as the transports
parameter specifies
additional, non-required transports.
Be sure that you include the dependency org.cometd.java:cometd-websocket-jetty
.
This is very easy to do if you're using Maven: just declare the additional dependency in your application's
pom.xml
file.
If you are not using Maven, you need to make sure that
cometd-websocket-jetty-<version>.jar
is present in the WEB-INF/lib
directory of your web application .war file.
If you plan to use the websocket
client transport in your web application,
and you are not using Maven, make sure that
jetty-websocket-<version>.jar
, jetty-io-<version>.jar
and jetty-http-<version>.jar
are also present in the WEB-INF/lib
directory of your web application .war file.
If you have configured your web application to support cross-domain HTTP calls (see
Section 8.2.1.3, “Configuring the CrossOriginFilter
”), you do not need to worry because the
CrossOriginFilter
is by default disabled for the WebSocket protocol.
Server-side components such as services (see Section 8.2.2, “Using Services”), extensions (see Chapter 9, Extensions) or authorizers (see Section 8.2.4, “Server Channel Authorizers”) may need to access contextual information, that is information provided by the Servlet environment such as request attributes, HTTP session attributes, servlet context attributes or network address information.
While the Servlet model can easily provide such information, CometD can use also non-HTTP transports such as
WebSocket that may not have such information readily available. CometD abstracts the retrieval of
contextual information for any transport via the org.cometd.bayeux.server.BayeuxContext
class.
An instance of BayeuxContext
can be obtained from the BayeuxServer
instance:
BayeuxContext context = bayeuxServer.getContext();
A typical usage in a SecurityPolicy
(see Section 8.2.3, “Authorization”)
is the following:
public class OneClientPerAddressSecurityPolicy extends DefaultSecurityPolicy { private final Set<String> addresses = new HashSet<String>(); @Override public boolean canHandshake(BayeuxServer bayeuxServer, ServerSession session, ServerMessage message) { BayeuxContext context = bayeuxServer.getContext(); // Get the remote address of the client final String remoteAddress = context.getRemoteAddress().getHostName(); // Only allow clients from different remote addresses boolean notPresent = addresses.add(remoteAddress); // Avoid to leak addresses session.addListener(new ServerSession.RemoveListener() { public void removed(ServerSession session, boolean timeout); { addresses.remove(remoteAddress); } }); return notPresent ? true : false; } }
Refer to the Javadoc documentation
for further information about methods available in BayeuxContext
.
It is recommended to always call BayeuxServer.getContext()
to obtain the
BayeuxContext
instance; it should never be cached and then reused across messages.
This allows maximum portability of your code in case you're using a mix of WebSocket and HTTP transports.
For pure HTTP transports such as long-polling
and callback-polling
, there is
a direct link between the information contained in the BayeuxContext
and the current HTTP
request that carried the Bayeux message.
For these transports, the BayeuxContext
is created anew for every invocation of
BayeuxServer.getContext()
, wrapping the current HttpServletRequest
.
For the WebSocket transport, the BayeuxContext
instance is created only once, during the
upgrade from HTTP to WebSocket.
The information contained in the HTTP upgrade request is copied into the BayeuxContext
instance and never updated again, since after the upgrade the protocol is WebSocket, and such
contextual information is not available anymore.
Sometimes applications need to send non-urgent messages to clients; alert messages
such as "email received" or "server uptime", are typical examples, but chat messages sometimes also fit this definition.
While these messages need to reach the client(s) eventually, there is no need to deliver them immediately:
they are queued into the ServerSession
's message queue, but their delivery can be postponed
to the first occasion.
In CometD, "first occasion" means whichever of the following things occurs first (see Section 8.2.1, “Configuring the Java Server” for information about configuring the following parameters):
the channel's lazy timeout expires
the /meta/connect
timeout expires so that the /meta/connect
response is sent to the client
another non-lazy message triggered the ServerSession
's message queue to be sent to the client
To support this feature, CometD introduces the concepts of lazy channel and lazy messages.
An application can mark a message as lazy by calling ServerMessage.Mutable.setLazy(true)
on
the message instance itself, for example:
@Service public class LazyService { @Inject private BayeuxServer bayeuxServer; @Session private LocalSession session; public void receiveNewsFromExternalSystem(NewsInfo news) { ServerMessage.Mutable message = this.bayeuxServer.newMessage(); message.setChannel("/news"); message.setData(news); message.setLazy(true); this.bayeuxServer.getChannel("/news").publish(this.session, message); } }
In this example, an external system invokes method LazyService.receiveNewsFromExternalSystem(...)
every time there is news, and the service broadcasts
the news to all interested clients.
However, since the news need not be delivered immediately, the message is marked as lazy.
Each remote client possibly receives the message at a different time: some receive it
immediately (because, for example, they have other non-lazy messages to be delivered), some
receive it after few milliseconds (because, for example, their /meta/connect
timeout
expires in a few milliseconds), while others receive it only upon the whole maxLazyInterval
timeout.
In the same way you can mark a message as lazy, you can also mark server channels as lazy.
Every message that is published to a lazy channel becomes a lazy message which is then queued
into the ServerSession
's message queue for delivery on first occasion.
You can mark server channels as lazy at any time, but it is best to configure them as lazy at creation time, for example:
@Service public class LazyService { @Inject private BayeuxServer bayeuxServer; @Session private LocalSession session; @Configure("/news") public void setupNewsChannel(ConfigurableServerChannel channel) { channel.setLazy(true); } public void receiveNewsFromExternalSystem(NewsInfo news) { this.bayeuxServer.getChannel("/news").publish(this.session, news, null); } }
When a server channel is marked lazy, by default it has a lazy timeout specified by the global
maxLazyTimeout
parameter (see Section 8.2.1, “Configuring the Java Server”).
In more sophisticated cases, you may want to specify different lazy timeouts for different server channels, for example:
@Service public class LazyService { ... @Configure("/news") public void setupNewsChannel(ConfigurableServerChannel channel) { channel.setLazy(true); } @Configure("/chat") public void setupChatChannel(ConfigurableServerChannel channel) { channel.setLazyTimeout(2000); } ... }
In the example above, channel /news
inherits the default maxLazyTimeout
(5000 ms),
while the /chat
channel is configured with a specific lazy timeout of 2000 ms.
A server channel that has a non-zero, positive lazy timeout is automatically marked as lazy.
If a wildcard server channel such as /chat/*
is marked as lazy, then all messages sent to server
channels that match that wildcard server channel (such as /chat/1
) will be lazy.
Conversely, if a non-wildcard server channel such as /news
is lazy, then all messages sent to
children server channels of that non-wildcard server channel (such as /news/sport
) will not be lazy.
The HTTP protocol recommends a connection limit of two connections per domain. While modern browsers are configured by default with more than two connections per domain, it is not safe to make such an assumption; thus any iframes, tabs or windows on the same browser connecting to the same host need to share two connections.
If two iframes/tabs/windows initiate a Bayeux communication, both start a long poll connect request and both connections are consumed, making it impossible to send another Bayeux request (for example, a publish) until one of the two long polls returns.
The CometD Server implements the multiple-clients
advice (see
the section called “multiple-clients
advice field”).
The server uses BAYEUX_BROWSER
cookie to detect multiple CometD clients from the same browser.
If the CometD server detects multiple clients, only one long poll connection is allowed, and subsequent
long poll requests do not wait for messages before returning. They return immediately with the
multiple-clients
field of the advice object set to true. The advice also contains an
interval
field set to the value of the multiSessionInterval
servlet init parameter
(see Section 8.2.1, “Configuring the Java Server”). This instructs the client not to send another poll until
that interval has passed.
The effect of this advice is that additional client connections normally poll the server with a period of
multiSessionInterval
. This avoids consuming both HTTP connections at the cost of some latency
for the additional iframes/tabs/windows.
We recommend that the client application monitor the /meta/connect
channel for
multiple-clients
field in the advice. If detected, the application might ask the user to
close the additional tabs, or it could automatically close them, or take some other action.
Non-browser clients (or browsers with cookies disabled) must handle the BAYEUX_BROWSER
cookie
with the same semantic of browsers, or configure the server to allow multiple sessions even without
BAYEUX_BROWSER
information via the allowMultiSessionsNoBrowser
servlet init parameter
(see Section 8.2.1, “Configuring the Java Server”).
Server-side CometD components may be exposed as JMX MBeans and visible via monitoring tools such as JConsole or JVisualVM.
CometD MBeans build on Jetty's JMX support, and are portable across servlet containers.
If your CometD application is deployed in Jetty, then it is enough that you modify web.xml
by adding a Jetty-specific context parameter:
<?xml version="1.0" encoding="ISO-8859-1"?> <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"> <context-param> <param-name>org.eclipse.jetty.server.context.ManagedAttributes</param-name> <param-value>org.cometd.bayeux,org.cometd.oort.Oort</param-value> </context-param> <!-- The rest of your web.xml --> </webapp>
The value of the context parameter is a comma separated list of attribute names stored in the servlet
context.
The CometD implementation stores for you the BayeuxServer
instance in the servlet context
under the name defined by the constant BayeuxServer.ATTRIBUTE
which is indeed the string
org.cometd.bayeux
that you can find in the example above as the value of the context parameter.
Similarly, the CometD implementation stores the Oort
instance and Seti
instance
in the servlet context under the names defined - respectively - by the constants Oort.OORT_ATTRIBUTE
and Seti.SETI_ATTRIBUTE
, equivalent to the strings org.cometd.oort.Oort
and
org.cometd.oort.Seti
respectively.
Optionally, remember that annotated service instances (seeSection 8.2.2.2.1, “Server-Side Annotated Services”) are stored in the servlet context, and as such are candidate to use the same mechanism to be exposed as MBeans, provided that you define the right Jetty JMX MBean metadata descriptors.
This is all you need to do to export CometD MBeans when running within Jetty.
The context parameter configured above can be left in the web.xml
even if your application
is deployed in other Servlet containers, as it will only be detected by Jetty.
In order to leverage Jetty's JMX support in other Servlet containers, it is enough that you include the
jetty-jmx-<version>.jar
in your WEB-INF/lib
directory of your web
application.
This jar contains Jetty's JMX utility classes that can be used to easily export CometD MBeans.
Exporting CometD MBeans in other Servlet containers require a little bit more setup than what is needed in Jetty, but it is easily done with a small initializer class. Refer to Section 8.2.2.3, “Services Integration” for a broader discussion of how to initialize CometD components.
A simple example of such initializer class is the following:
import java.io.IOException; import java.lang.management.ManagementFactory; import javax.servlet.GenericServlet; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.cometd.bayeux.server.BayeuxServer; import org.eclipse.jetty.jmx.MBeanContainer; public class CometDJMXExporter extends GenericServlet { private volatile MBeanContainer mbeanContainer; @Override public void init() throws ServletException { try { mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); BayeuxServer bayeuxServer = (BayeuxServer)getServletContext().getAttribute(BayeuxServer.ATTRIBUTE); mbeanContainer.addBean(bayeuxServer); // Add other components mbeanContainer.start(); } catch (Exception x) { throw new ServletException(x); } } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { throw new ServletException(); } @Override public void destroy() { try { mbeanContainer.stop(); } catch (Exception ignored) { } } }
with the corresponding web.xml
configuration:
<?xml version="1.0" encoding="ISO-8859-1"?> <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"> <!-- The rest of your web.xml --> <servlet> <servlet-name>jmx-exporter</servlet-name> <servlet-class>com.acme.cometd.CometDJMXExporter</servlet-class> <!-- Make sure it's the last started --> <load-on-startup>2</load-on-startup> </servlet> </webapp>
In this example, CometDJMXExporter
is a configuration Servlet (and as such it is not
mapped to any path) that instantiates Jetty's JMX utility class MBeanContainer
,
extracts from the servlet context the CometD components that you want to expose as MBeans, and adds
them to the MBeanContainer
instance.
It is doing, programmatically, what Jetty does under the covers for you when it detects the
org.eclipse.jetty.server.context.ManagedAttributes
context parameter in web.xml
.
Since version 2.4.0, CometD allows you to customize the JSON library that it uses to convert incoming JSON into Bayeux messages and generate JSON from Bayeux messages.
Two implementations are available, one based on Jetty's
org.eclipse.jetty.util.ajax.JSON
class, and the other based on the
Jackson library.
The default implementation uses the Jetty library.
These two libraries are among the fastest available; it turns out that, for CometD,
the Jackson library is faster than Jetty's in both parsing and generating. Distinctions between them include:
The Jetty library allows you to plug in custom serializers and deserializers, to fine control the conversion
from object to JSON and vice versa, via a custom API.
Refer to the org.eclipse.jetty.util.ajax.JSON
javadocs
for further information.
The Jackson library offers a rich API based on annotations to customize JSON generation, but less so to customize JSON parsing and obtain objects of custom classes. Refer to the Jackson documentation for further details.
The CometD Java client implementation (see
Section 8.1, “Client Library”) uses the JSON library to generate JSON from and to parse JSON to
org.cometd.bayeux.Message
instances.
The JSON library class that performs this generation/parsing on the client must implement
org.cometd.common.JSONContext.Client
.
Similarly, on the server, a org.cometd.common.JSONContext.Server
implementation
generates JSON from and parses JSON to org.cometd.bayeux.server.ServerMessage
instances.
On the client, the org.cometd.common.JSONContext.Client
instance must be passed
directly into the transport configuration; if omitted, the default Jetty JSON library is used.
For example:
HttpClient httpClient = ...;
Map<String, Object> transportOptions = new HashMap<String, Object>();
// Use the Jackson implementation
JSONContext.Client jsonContext = new JacksonJSONContextClient();
transportOptions.put(ClientTransport.JSON_CONTEXT, jsonContext);
ClientTransport transport = new LongPollingTransport(transportOptions, httpClient);
BayeuxClient client = new BayeuxClient(cometdURL, transport);
All client transports can share the org.cometd.common.JSONContext.Client
instance
(since only one transport is used at any time).
You can customize the Jackson implementation and add your own serializers/deserializer in the following way:
public class MyJacksonJSONContextClient extends org.cometd.common.JacksonJSONContextClient
{
public MyJacksonJSONContextClient()
{
org.codehaus.jackson.map.ObjectMapper objectMapper = getObjectMapper();
objectMapper.registerModule(new MyModule());
}
private class MyModule extends org.codehaus.jackson.map.module.SimpleModule
{
public MyModule()
{
// Add your custom serializers/deserializers here
addSerializer(Foo.class, new FooSerializer());
}
}
}
The JSONContext.Server
and JSONContext.Client
classes also offer methods to obtain
a JSON parser (to deserialize JSON into objects) and a JSON generator (to generate JSON from objects),
so that the application does not need to hardcode the usage of a specific implementation library.
Class JSONContext.Parser
can be used to convert into objects any JSON that the application
needs to read for other purposes, for example from configuration files, and of course convert into custom
objects (see Section 8.3.3, “
Customizing Deserialization of JSON objects
”):
public EchoInfo readFromConfigFile(BayeuxServer bayeuxServer) throws ParseException { // JDK 7's try-with-resources try (FileReader reader = new FileReader("echo.json")) { JSONContext.Server jsonContext = (JSONContext.Server)bayeuxServer.getOption("jsonContext"); EchoInfo info = jsonContext.getParser().parse(reader, EchoInfo.class); return info; } }
Similarly, objects can be converted into JSON:
public EchoInfo writeToConfigFile(BayeuxServer bayeuxServer, EchoInfo info) throws IOException { // JDK 7's try-with-resources try (FileWriter writer = new FileWriter("echo.json")) { JSONContext.Server jsonContext = (JSONContext.Server)bayeuxServer.getOption("jsonContext"); String json = jsonContext.getGenerator().generate(info); writer.write(json); } }
On the server, you can specify the fully qualified name of a class implementing
org.cometd.common.JSONContext.Server
as init-parameter of the
CometdServlet
(see also Section 8.2.1, “Configuring the Java Server”);
if omitted, the default Jetty library is used.
For example:
<?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.CometdServlet</servlet-class> <!-- other parameters --> <init-param> <param-name>jsonContext</param-name> <param-value>org.cometd.server.JacksonJSONContextServer</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>cometd</servlet-name> <url-pattern>/cometd/*</url-pattern> </servlet-mapping> </web-app>
The class specified must be instantiable using the default parameterless constructor, and it must implement
org.cometd.common.JSONContext.Server
. You can customize it by adding
serializers/deserializers as explained above.
In the Oort clustering (see Section 8.4, “Scalability Clustering with Oort”), an Oort
instance need to have
both the server and the clientJSONContext
: the server one to deserialize messages that
other Oort comets send, and the client one to serialize messages to send to other Oort comets.
The Oort
instance will use the JSONContext.Server
already configured for the
server, as explained in Section 8.3.1.2, “Server Configuration”.
The Oort
instance will use of the JSONContext.Client
specified in the configuration
(see Section 8.4.3.1, “Common Configuration”).
It is possible to switch from one implementation of the JSON library to another – for example from the Jetty library to the Jackson library, provided that you write the application code carefully.
As of version 1.8.4, Jackson can only produce instances of java.util.List
when
deserializing JSON arrays.
The Jetty library, however, produces java.lang.Object[]
when deserializing JSON arrays.
Similarly, Jackson produces java.lang.Integer
where the Jetty library
produces java.lang.Long
.
To write portable application code, use the following code patterns:
Message message = ...; Map<String, Object> data = message.getDataAsMap(); // Expecting a JSON array // WRONG Object[] array = (Object[])data.get("array"); // CORRECT Object field = data.get("array"); Object[] array = field instanceof List ? ((List)field).toArray() : (Object[])field; // Expecting a long // WRONG long value = (Long)data.get("value"); // CORRECT long value = ((Number)data.get("value")).longValue();
Sometimes it is very useful to be able to obtain objects of application classes instead of just
Map<String, Object>
when calling message.getData()
.
You can easily achieve this with the Jetty JSON library. It is enough that the client formats the JSON object
adding an additional class
field whose value is the fully qualified class name that you want to
convert the JSON to:
cometd.publish('/echo', { class: 'org.cometd.example.EchoInfo', id: '42', echo: 'cometd' });
On the server, in the web.xml
file, you register the
org.cometd.server.JettyJSONContextServer
as jsonContext
(see
Section 8.3.1.2, “Server Configuration”), and at startup you add a custom converter for the
org.cometd.example.EchoInfo
class (see Section 8.2.2.3, “Services Integration”
for more details about configuring CometD at startup).
BayeuxServer bayeuxServer = ...;
JettyJSONContextServer jsonContext = (JettyJSONContextServer)bayeuxServer.getOption("jsonContext");
jsonContext.getJSON().addConvertor(EchoInfo.class, new EchoInfoConvertor());
Finally, these are the EchoInfoConvertor
and EchoInfo
classes:
public class EchoInfoConvertor implements JSON.Convertor { public void toJSON(Object obj, JSON.Output out) { EchoInfo echoInfo = (EchoInfo)obj; out.addClass(EchoInfo.class); out.add("id", echoInfo.getId()); out.add("echo", echoInfo.getEcho()); } public Object fromJSON(Map map) { String id = (String)map.get("id"); String echo = (String)map.get("echo"); return new EchoInfo(id, echo); } } public class EchoInfo { private final String id; private final String echo; public EchoInfo(String id, String echo) { this.id = id; this.echo = echo; } public String getId() { return id; } public String getEcho() { return echo; } }
If, instead of using the JavaScript client, you are using the Java client, it is possible to configure the Java client to perform the serialization/deserialization of JSON objects in the same way (see also Section 8.3.1.1, “Client Configuration”):
JettyJSONContextClient jsonContext = ...; jsonContext.getJSON().addConvertor(EchoInfo.class, new EchoInfoConvertor()); // Later in the application BayeuxClient bayeuxClient = ...; bayeuxClient.getChannel("/echo").subscribe(new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { // Receive directly EchoInfo objects EchoInfo data = (EchoInfo)message.getData(); } }); // Publish directly EchoInfo objects bayeuxClient.getChannel("/echo").publish(new EchoInfo("42", "wohoo"));
The CometD distribution ships a clustering solution called Oort that enhances the scalability of a CometD-based system. Instead of connecting to a single node (usually represented by a virtual or physical host), clients connect to multiple nodes so that the processing power required to cope with the load is spread among multiple nodes, giving the whole system more scalability than using a single node.
Oort clustering is not a high availability clustering solution: if one of the nodes crashes, then all the clients are disconnected and reconnect to other nodes (with a new handshake). All the information built by one client with its server up to that point (for example, the state of an online chess game) is generally lost (unless the application has implemented some other way to retrieve that information).
A typical, but not the only, infrastructure to set up an Oort cluster is to have a load balancer in front of Oort nodes, so that clients can connect transparently to any node. The load balancer should implement stickyness, which can be based on:
The remote IP address.
CometD's BAYEUX_BROWSER cookie (see Section C.9, “Multiple clients operation”).
Some other mechanism the load balancer supports.
You should configure DNS with a single host name/IP address pair (that of the load balancer), so that in case of a node crash, when clients attempt to reconnect to the same host name, the load balancer notices that the node has crashed and directs the connection to another node. The second node does not know about this client, and upon receiving the connect request sends to the client the advice to handshake.
The next sections use the following terminology:
An Oort cluster is also referred to as an Oort cloud; it follows that cloud is a synonym for cluster.)
An Oort node is also referred to an Oort comet; it follows that comet is a synonym for node).
Any CometD server can become an Oort comet by configuring an instance of
org.cometd.oort.Oort
.
The org.cometd.oort.Oort
instance is associated to the
org.cometd.bayeux.server.BayeuxServer
instance, and there can be only one
Oort
instance for each BayeuxServer
instance.
Oort comets need to know each others' URLs to connect and form a cloud. A new comet that wants to join the cloud needs to know at least one URL of another comet that is already part of the cloud. Once it has connected to one comet, the cloud informs the new comet of the other comets to which the new comet is not yet connected, and the new comet then connects to all the existing nodes.
There are two ways for a new comet to discover at least one other comet:
At runtime, via automatic discovery based on multicast.
At startup time, via static configuration.
For both static and automatic discovery there exists a set of parameters that you can use to configure
the Oort
instance.
The following is the list of common configuration parameters the automatic discovery
and static configuration servlets share:
Table 8.8. Oort Common Configuration Parameters
Parameter Name | Required | Default Value | Parameter Description |
---|---|---|---|
oort.url | Y | N/A | The unique URL of the Bayeux server associated to the Oort comet |
oort.secret | Y/N | random string | The pre-shared secret that authenticates connections from other Oort comets. It is mandatory when applications want to authenticate other Oort comets, see Section 8.4.3.6, “Authentication”. |
oort.channels | N | empty string | A comma-separated list of channels to observe at startup |
enableAckExtension | N | false |
Whether to enable the message acknowledgement extension (Section 9.5, “Message Acknowledgment Extension”)
in the BayeuxServer instance and in the OortComet instances
|
clientDebug | N | false | Whether to enable debug logging in the OortComet instances
|
jsonContext | N | empty string |
The full qualified name of a JSONContext.Client implementation used by the OortComet
instances used to connect to other Oort comets
|
You can configure the automatic discovery mechanism either via code, or by configuring a
org.cometd.oort.OortMulticastConfigServlet
in web.xml
,
for example:
<?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.CometdServlet</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>oort</servlet-name> <servlet-class>org.cometd.oort.OortMulticastConfigServlet</servlet-class> <load-on-startup>2</load-on-startup> <init-param> <param-name>oort.url</param-name> <param-value>http://host:port/context/cometd</param-value> </init-param> </servlet> </web-app>
Since Oort
depends on BayeuxServer
, the load-on-startup
parameter of the OortMulticastConfigServlet
must be greater than that of the
CometdServlet
.
The mandatory oort.url
init parameter must identify the URL at which this Oort comet
can be contacted, and it must be the URL the CometdServlet
of this node serves.
This URL is sent to other Oort comets, so it is important that the host part of the URL does
not point to "localhost", but to a resolvable host name or to an IP address, so that other comets
in the cluster can contact this comet.
Likewise, you must properly configure the context path part of the URL for this web application.
In addition to the common configuration init parameters, OortMulticastConfigServlet
supports the configuration of these additional init parameters:
Table 8.9. Oort Multicast Configuration Parameters
Parameter Name | Required | Default Value | Parameter Description |
---|---|---|---|
oort.multicast.bindAddress | N | the wildcard address | The bind address of the MulticastSocket that receives the advertisements |
oort.multicast.groupAddress | N | 239.255.0.1 | The multicast group address to join to receive the advertisements |
oort.multicast.groupPort | N | 5577 | The port over which advertisements are sent and received |
oort.multicast.timeToLive | N | 1 | The time to live of advertisement packets (1 = same subnet, 32 = same site, 255 = global) |
oort.multicast.advertiseInterval | N | 2000 | The interval in milliseconds at which advertisements are sent |
oort.multicast.connectTimeout | N | 1000 | The timeout in milliseconds to connect to other nodes |
oort.multicast.maxTransmissionLength | N | 1400 | The maximum length in bytes of multicast messages (the MTU of datagrams) - limits the length of the Oort URL advertised by the node |
Each comet that you configure with automatic discovery emits an advertisement (containing the
comet URL) every oort.multicast.advertiseInterval
milliseconds on the specified
multicast address and port (oort.multicast.groupAddress
and
oort.multicast.groupPort
) with the specified time-to-live
(oort.multicast.timeToLive
).
Advertisements continue until the web application is stopped, and only serve to advertise that a
new node has appeared.
Oort
has a built-in mechanism that takes care of membership organization
(see Section 8.4.3.4, “Membership Organization” for details).
When enabling the Oort automatic discovery mechanism, you must be sure that:
Multicast is enabled in the operating system of your choice.
The network interfaces have multicast enabled.
Multicast traffic routing is properly configured.
Linux is normally compiled with multicast support in the most common distributions. You can control network
interfaces with the ifconfig
command to check if they have
multicast enabled.
You can check multicast routing with the command route -n
, and the output should
contain a line similar to:
Destination Gateway Genmask Flags Metric Ref Use Iface 224.0.0.0 0.0.0.0 240.0.0.0 U 0 0 0 eth0
You might also want to force the JVM to prefer an IPv4 stack by setting the system property
-Djava.net.preferIPv4Stack=true
to facilitate multicast networking.
You can use the static discovery mechanism if multicast is not available on the system where CometD is deployed. It is only slightly more cumbersome to set up. It does not allow dynamic discovery of new nodes, but it is enough to configure each comet with the well-known URL of an existing, started, comet (often named "master"). The master comet should, of course, be started before all other comets.
You can accomplish the static discovery configuration either via code, or by configuring an
org.cometd.oort.OortStaticConfigServlet
in web.xml
,
for example:
<?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.CometdServlet</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>oort</servlet-name> <servlet-class>org.cometd.oort.OortStaticConfigServlet</servlet-class> <load-on-startup>2</load-on-startup> <init-param> <param-name>oort.url</param-name> <param-value>http://host:port/context/cometd</param-value> </init-param> </servlet> </web-app>
Just as for the automatic discovery, the load-on-startup
parameter of the
OortStaticConfigServlet
must be greater than that of the CometdServlet
.
OortStaticConfigServlet
supports the common init parameters listed in the previous
section, and the following additional init parameter:
Table 8.10. Oort Static Configuration Parameters
Parameter Name | Required | Default Value | Parameter Description |
---|---|---|---|
oort.cloud | N | empty string | A comma-separated list of URLs of other Oort comets to connect to at startup |
Configured in this way, the Oort comet is ready to be part of the Oort cloud, but it's not part
of the cloud yet, since it does not know the URLs of other comets (and there is no automatic
discovery).
To make the Oort comet part of the Oort cloud, you can configure the
oort.cloud
init parameter of the OortStaticConfigServlet
with one (or
a comma-separated list) of Oort comet URL(s) to connect to:
<?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.CometdServlet</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>oort</servlet-name> <servlet-class>org.cometd.oort.OortStaticConfigServlet</servlet-class> <load-on-startup>2</load-on-startup> <init-param> <param-name>oort.url</param-name> <param-value>http://host1:port/context/cometd</param-value> </init-param> <init-param> <param-name>oort.cloud</param-name> <param-value>http://host2:port/context/cometd,http://host3:port/context/cometd</param-value> </init-param> </servlet> </web-app>
Alternatively, it's possible to write custom initialization code (see the section on Section 8.2.2.3, “Services Integration” for suggestions on how to do so) that links the node to the Oort cloud (this might be useful if Oort comet URLs cannot be known a priori, but can be known at runtime), for example:
public class OortConfigurationServlet extends GenericServlet { public void init() throws ServletException { // Grab the Oort object Oort oort = (Oort)getServletContext().getAttribute(Oort.OORT_ATTRIBUTE); // Figure out the URLs to connect to, using other discovery means List<String> urls = ...; // Connect to the other Oort comets for (String url : urls) { OortComet oortComet = oort.observeComet(url); if (!oortComet.waitFor(1000, BayeuxClient.State.CONNECTED)) throw new ServletException("Cannot connect to Oort comet " + url); } } public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException { throw new ServletException(); } }
The OortComet
instance that Oort.observeComet(url)
returns is a
specialized version of BayeuxClient
, see Section 8.1, “Client Library”.
When an Oort comet connects to another Oort comet, a bidirectional communication
is established.
If cometA
connects to cometB
(for example, via
oortA.observeComet(urlB)
), then an OortComet
instance is created in
cometA
connected to cometB
, and another OortComet
instance
is created in cometB
connected to cometA
.
After this direct, bidirectional communication has been established, a special message broadcasts
across the whole Oort cloud (on channel /oort/cloud
) where the two comets broadcast their
known siblings.
Every node receiving this message that does not know about those siblings then establishes a
bidirectional communication with them.
For example, imagine that there are two simple Oort clouds, one made of comets A and B and the other made of comets C and D. When A and C connect, they broadcast their siblings (A broadcasts its siblings, now B and C, while C broadcasts its siblings, now A and D). All nodes connected, directly or indirectly, to the broadcaster receive this message. When C receives broadcasts from A's siblings it notices that one is itself (so it does nothing since it's already connected to A). The other is the unknown sibling B, and C establishes a bidirectional connection with B as well. Likewise, A receives the sibling broadcast message from C, and connects to D. Each new bidirectional connection triggers a sibling broadcast message on the whole cloud, until all comets are connected.
If a comet crashes, for example D, then all other comets detect that and disconnect from the faulty comet.
In this way, an Oort cloud is aware of its members, but it does not do anything useful for the application.
The next section covers broadcast message forwarding over the entire cloud.
Applications sometimes need to know when other comets join or leave the Oort cloud; they can do so by registering comet listeners that are notified when a new comet joins the cloud and when a comet leaves the cloud:
Oort oort = ...; oort.addCometListener(new Oort.CometListener() { public void cometJoined(Event event) { System.out.printf("Comet joined the cloud %s%n", event.getCometURL()); } public void cometLeft(Event event) { System.out.printf("Comet left the cloud %s%n", event.getCometURL()); } });
The comet joined event is notified only after the local Oort comet has allowed connection
from the remote comet (this may be denied by a SecurityPolicy
).
When a comet joined event is notified, it is possible to obtain the OortComet
connected
to the remote Oort via Oort.observeComet(String)
, and publish messages
(or subscribe to additional channels):
final Oort oort = ...; oort.addCometListener(new Oort.CometListener() { public void cometJoined(Event event) { String cometURL = event.getCometURL(); OortComet oortComet = oort.observeComet(cometURL); // Push information to the new comet oortComet.getChannel("/service/foo").publish("bar"); } public void cometLeft(Event event) { } });
Applications can use comet listeners to synchronize comets; a new comet can request (or be pushed)
application data that needs to be present in all comets (for example to warm up a cache).
Such activities occur in concert with a SecurityPolicy
that denies handshakes from
remote clients until the new comet is properly warmed up (clients retry the handshakes until the
new comet is ready).
When an Oort comet connects to another Oort comet, it sends a handshake message containing an extension field that is peculiar to Oort with the following format:
{ "channel": "/meta/handshake", ... /* other usual handshake fields */ "ext": { "org.cometd.oort": { "oortURL": "http://halley.cometd.org:8080/cometd", "cometURL": "http://halebopp.cometd.org:8080/cometd", "oortSecret": "cstw27r+l+XqE62IrNZdCDiUObA=" } } }
The oortURL
field is the URL of the comet that initiates the handshake; the
cometURL
field is the URL of the comet that receives the handshake; the
oortSecret
is the base64 encoding of the SHA-1 digested bytes of the pre-shared
secret of the initiating Oort comet (see the earlier section, Section 8.4.3.1, “Common Configuration”).
These extension fields provide a way for an Oort comet to distinguish a handshake of a remote
client (which might be subject to authentication checks) from a handshake performed by a remote comet.
For example, assume that remote clients always send an extension field containing an
authentication token; then it is possible to write an implementation of SecurityPolicy
as follows (see also the section about
authentication):
public class OortSecurityPolicy extends DefaultSecurityPolicy { private final Oort oort; private OortSecurityPolicy(Oort oort) { this.oort = oort; } @Override public boolean canHandshake(BayeuxServer server, ServerSession session, ServerMessage message) { // Local sessions can always handshake if (session.isLocalSession()) return true; // Remote Oort comets are allowed to handshake if (oort.isOortHandshake(message)) return true; // Remote clients must have a valid token Map<String, Object> ext = message.getExt(); return ext != null && isValid(ext.get("token")); } }
The Oort.isOortHandshake(Message)
method validates the handshake message and
returns true if it is a handshake from another Oort comet that has been configured with the
same pre-shared secret.
In this case, where you want to validate that the handshake attempt really comes from a valid
Oort comet (and not from an attacker that forged a message similar to what an Oort comet sends),
the pre-shared secret must be explicitly set to the same value for all Oort comets, because it
defaults to a random string that is different for each Oort comet.
Broadcast messages (that is, messages sent to non-meta and non-service channels, (see Section 7.3, “Subscribing and Unsubscribing” for further details) are by definition messages that all clients subscribing to the channel the message is being sent to should receive.
In an Oort cloud, you might have clients connected to different comets that subscribe to the same channel.
If clientA
connects to cometA
, clientB
connects
to cometB
and clientC
connects to cometC
, when
clientA
broadcasts a message you want clientB
and clientC
to receive, the Oort cloud must forward the message (sent by
clientA
and received by cometA
) to cometB
and cometC
.
You accomplish this by configuring the Oort configuration servlets to set the
oort.channels
init parameter to a comma-separated list of channels whose messages
are forwarded to the Oort cloud:
<?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.CometdServlet</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>oort</servlet-name> <servlet-class>org.cometd.oort.OortMulticastConfigServlet</servlet-class> <load-on-startup>2</load-on-startup> <init-param> <param-name>oort.url</param-name> <param-value>http://host1:port/context/cometd</param-value> </init-param> <init-param> <param-name>oort.channels</param-name> <param-value>/stock/**,/forex/*,/alerts</param-value> </init-param> </servlet> </web-app>
Alternatively, you can use Oort.observeChannel(String channelName)
to
instruct a comet to listen for messages on that channel published to one of the known comets
it is connected to.
When cometA
observes a channel, it means that messages sent on that channel, but
received by other comets, are automatically forwarded to cometA
.
Message forwarding is not bidirectional; if cometA
forwards messages to
other comets it is not automatic that the other comets forward messages to
cometA
.
However, in most cases you configure the Oort comets in the same way by the same
initialization code, and therefore all comets forward the same channels.
Forwarding of messages may be subject to temporary interruptions in case there is a temporary network connectivity failure between two comets. To overcome this problem, it is possible to configure the comets to enable the message acknowledgement extension (see Section 9.5, “Message Acknowledgment Extension”) so that, for short failures, the messages lost can be resent. Refer to Section 8.4.3.1, “Common Configuration” for the configuration details.
Remember that the message acknowledgement extension is not a fully persistent solution for lost messages (for example it does not guarantee message redelivery in case of long failures). CometD does not provide yet a fully persistent solution for messages in case of long failures.
Since it has the ability to observe messages published to broadcast channels, an Oort cloud can already
implement a simple chat application among users connected to different nodes.
In the example below, when clientA
publishes a message on channel /chat
(green arrow), it arrives on cometA
; since cometB
and cometC
have been configured to observe channel /chat
, they both receive the message
from cometA
(green arrows), and therefore they can deliver the chat message to
clientB
and clientC
respectively (green arrows).
If your application only needs to broadcast messages to clients connected to other comets, an
Oort
instance is all you need.
If you need to send messages directly to particular clients (for example, clientA
wants to send a message to clientC
but not to clientB
, then you need
to set up an additional component of the Oort clustering called Seti, see
Section 8.4.4, “Seti”.
Seti
is the Oort clustering component that tracks clients connected to any comet in the
cloud, and allows an application to send messages to particular client(s) in the cloud transparently,
as if they were in the local comet.
Keep the following points in mind when configuring Seti:
You must configure an org.cometd.oort.Seti
instance with an associated
org.cometd.oort.Oort
instance, either via code or by configuring an
org.cometd.oort.SetiServlet
in web.xml
.
There may be only one instance of Seti
for each Oort
.
The load-on-startup
parameter of the SetiServlet
must be greater
than that of the OortServlet
.
SetiServlet
does not have any configuration init parameter.
A configuration example 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.CometdServlet</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>oort</servlet-name> <servlet-class>org.cometd.oort.OortServlet</servlet-class> <load-on-startup>2</load-on-startup> <init-param> <param-name>oort.url</param-name> <param-value>http://host:port/context/cometd</param-value> </init-param> </servlet> <servlet> <servlet-name>seti</servlet-name> <servlet-class>org.cometd.oort.SetiServlet</servlet-class> <load-on-startup>3</load-on-startup> </servlet> </web-app>
Seti
allows you to associate a unique string representation of a user with one or more
ServerSessions
(see Chapter 6, Concepts & Architecture for more details on ServerSession
).
This normally occurs when the user first logs in to the application, and the unique string
representation of the user can be anything that the user provides to authenticate itself (a user
name, a token, a database ID). For brevity, this unique string representation of
the user is called userId
.
The same userId
may log in multiple times (for example from a desktop computer
and from a mobile device), so it is associated to multiple ServerSessions
.
In practice, the best way to associate a userId
with a ServerSession
is in a SecurityPolicy
during
authentication, for example:
public class MySecurityPolicy extends DefaultSecurityPolicy { private final Seti seti; public MySecurityPolicy(Seti seti) { this.seti = seti; } @Override public boolean canHandshake(BayeuxServer server, ServerSession session, ServerMessage message) { if (session.isLocalSession()) return true; // Authenticate String userId = performAuthentication(session, message); if (userId == null) return false; // Associate seti.associate(userId, session); return true; } }
Alternatively, you can perform the association in a BayeuxServer.Extension
or in a
CometD service (see Section 8.2.2, “Using Services”), in response to a specific message
that the client always sends after a successful handshake.
When a Seti
instance first associates a userId
with a session, it broadcasts a
presence message on the cloud (on channel /seti/all
, (see
Section 8.4.4.3, “
Listening for Presence Messages
”) that tells all
the other comets where this userId
is located.
In this way, all the comets in the cloud know where a particular userId
resides.
Further associations of the same userId
(with different sessions) on the same Seti
do not broadcast any presence message, because other Setis
already know that that
particular userId
resides in that Seti
.
The same userId
can be associated in different comets (for example, the desktop
computer logs in – and therefore is associated – in comet1
, while the mobile device
is associated in comet2
).
Similarly, you can disassociate a userId
at any time by calling
Seti.disassociate(userId, session)
.
If the user disconnects or "disappears" (for example, it crashed or its network dropped), the server removes or expires its
session and Seti
automatically
disassociates the userId
.
When the last disassociation of a particular userId
occurs on a Seti
instance, Seti
broadcasts a presence message on the cloud (on channel /seti/all
)
that tells all the other comets that userId
is no longer present on that
Seti
(although the same userId
might still be associated in other Setis
).
Applications can register presence listeners that are notified when a presence message arrives at a Seti
instance:
Seti seti = ...; seti.addPresenceListener(new Seti.PresenceListener() { public void presenceAdded(Event event) { System.out.printf("User ID %s is now present in comet %s%n", event.getUserId(), event.getURL()); } public void presenceRemoved(Event event) { System.out.printf("User ID %s is now absent in comet %s%n", event.getUserId(), event.getURL()); } });
The URL event.getURL()
returns is the URL of an Oort comet; you can use it to
retrieve the OortComet
instance connected to that comet, for example to publish messages
(or to subscribe to additional channels):
final Seti seti = ...; seti.addPresenceListener(new Seti.PresenceListener() { public void presenceAdded(Event event) { Oort oort = seti.getOort(); String oortURL = event.getURL(); OortComet oortComet = oort.getComet(oortURL); Map<String, Object> data = new HashMap<String, Object> data.put("action", "sync_request"); data.put("userId", event.getUserId()); oortComet.getChannel("/service/sync").publish(data); } public void presenceRemoved(Event event) { } });
After users have been associated,
Seti.sendMessage(String userId, String channel, Object data)
can send messages to a
particular user in the cloud.
@Service("seti_forwarder"); public class SetiForwarder { @Inject private Seti seti; @Listener("/service/forward") public void forward(ServerSession session, ServerMessage message) { Map<String,Object> data = message.getDataAsMap(); String targetUserId = (String)data.get("targetUserId"); seti.sendMessage(targetUserId, message.getChannel(), data); } }
In the example below, clientA
wants to send a message to clientC
but not to clientB
.
Therefore clientA
sends a message to the server it is connected to using a
service channel so that the message is not broadcast (see Section 7.3, “Subscribing and Unsubscribing”),
and then a specialized service (see Section 8.2.2, “Using Services”) routes
the message to the appropriate user using Seti
(see code snippet above).
The Seti
on cometA
knows that the target user is on cometC
(thanks to the association) and forwards the message to cometC
, which in turn
delivers the message to clientC
.
Section 8.4, “Scalability Clustering with Oort” described how it is possible to link nodes to form an Oort cloud. Oort nodes are all connected to each other so that they are aware of all the other peer nodes. Additionally, each node can forward messages that have been published to broadcast channels (see Section 6.1.1, “Channel Definitions”) to other nodes.
Your application may need to maintain information, on each node, that is distributed across all nodes. A typical example is the total number of users connected to all nodes. Each node can easily know how many users are connected to itself, but in this case you want to know the total number of all users connected to all nodes (for example to display the number in a user interface). The information that you want to distribute is the "data entity" - in this case the number of users connected to each node - and we name this feature "data distribution". Having each node distributing its own data entity allows each node to know the data entity of the other nodes, and compute the total number of users.
Furthermore, your application may need to perform certain actions on a specific node. For example, your application may need to access a database system that is only accessible from a specific node for security reasons. We name this feature "service forwarding".
Oort and Seti (see Section 8.4.4, “Seti”) alone do not offer data distribution or
service forwarding out of the box, but it is possible to build on Oort features to implement
them, and this is exactly what CometD offers, respectively, with OortObject
(Section 8.4.5.1, “OortObject”) and OortService
(Section 8.4.5.2, “OortService”).
An org.cometd.oort.OortObject
instance represents a named composite
data entity that is distributed in an Oort cluster.
Each node in the cluster has exactly one instance of an OortObject
with a specific
name, and the OortObject
contains N data entities, one of each node in the cluster.
There may be several Oort objects in the same node, provided they all have different names.
The data entity may be the number of users connected to each node, or the number of games played on each node, or the list of chat rooms created on each node, or the names of systems monitored by each node, etc., depending on your application's business domain.
In the image below, you can see 3 nodes (nodeA
, nodeB
and
nodeC
), each containing an Oort object named "users" (in orange) that stores
the names of the users connected to each Oort node.
The data entity in this case is a List<String>
representing the names of the
connected users for each node.
NodeA
has clients Ca1
and Ca2
connected, nodeB
has only client Cb1
connected, while nodeC
has 3 clients connected.
Oort objects are composites in that they store N data entities, also called parts,
where N is the number of nodes in the Oort cloud.
You can see that each Oort object is made of 3 parts (the innermost blue, green and red boxes);
each part is colored like the node it represents. The part that has the same color as the node it
lives in it's the local part.
Each Oort object can only update its local part: nodeA
can only add/remove user
names from its local (blue) part, and cannot add/remove from the remote parts (in green and red).
Likewise, nodeB
can only update the green part but not the blue and red parts,
and nodeC
can only update the red part, but not the blue and green ones.
If a new client connects to nodeB
, say Cb2
, then the application on
nodeB
takes the user name (B2
) that wants to share with other nodes,
and adds it to the Oort object on nodeB
.
The user name B2
will be added to the green part of nodeB
, and a message
will be broadcast to the other nodes, which will also modify the correspondent green parts on themselves,
adding a copy of B2
.
The remote parts of an Oort object can only be updated by messages internal to the OortObject
implementation; they cannot be updated by application code directly.
Each Oort object instance owns only its local part.
In the example, the user name A2
is present in all the nodes, but it is
owned only by the Oort object in nodeA
.
Anyone that wants to modify or remove A2
must perform this action in nodeA
.
Section 8.4.5.2, “OortService” shows how to forward service actions from one node
to another.
OortObject
allows applications to add/remove OortObject.Listener
s that are
notified of modification of a part, either local or remote.
Your application can implement these listeners to perform custom logic, see
Section 8.4.5.1.6, “OortObject Listeners”.
While OortObject
is a generic container of objects (like a List<String>
),
it may not be very efficient.
Imagine the case where the list contains thousands of names: the addition/removal of one name will
cause the whole list to be replicated to all other nodes, because the whole list is the data entity.
To avoid this inefficiency, CometD offers these specializations of OortObject
:
OortMap
, an OortObject
that contains a ConcurrentMap
OortStringMap
, an OortMap
with String
keys
OortLongMap
, an OortMap
with Long
keys
OortList
, an OortObject
that contains a List
Each specialization replicates single operations like the addition/removal of a key/value pair
in an OortMap
, or the addition/removal of an element in an OortList
.
OortMap
provides an OortMap.EntryListener
to notify applications of
map entry addition/removal, either local or remote.
OortList
provides an OortList.ElementListener
to notify applications of
element addition/removal, either local or remote. Applications can implement these listeners
to be notified of entry or element updates in order to perform custom logic, see
Section 8.4.5.1.6, “OortObject Listeners”.
OortObject
s are created by providing an OortObject.Factory
.
This factory is needed to create the data entity from its raw representation obtained from JSON.
This allows standard JDK containers such as java.util.concurrent.ConcurrentHashMap
to be used as data entities, but replicated among nodes using standard JSON.
CometD provides a number of predefined factories in class
org.cometd.oort.OortObjectFactories
, for example:
Oort oort = ...; // The factory for data entities OortObject.Factory<List<String>> factory = OortObjectFactories.forList(); // Create the OortObject OortObject<List<String>> users = new OortObject<List<String>>(oort, "users", factory); // Start it before using it users.start();
The code above will create an OortObject
named "users" whose data entity is a
List<String>
(this is an example; you may want to use the richer and more
powerful OortList<String>
instead).
Once you have created an OortObject
you must start it before using it.
OortObject
s are usually created at startup time, so that all the nodes have one instance
of the same OortObject
s with the same names.
Remember that the data entity is distributed among OortObject
s with the same name, so
if a node does not have that particular named OortObject
, then it will not receive
updates for that data entity.
It is possible to create OortObject
s on-the-fly in response to some event,
but the application must make sure that this event is broadcast to all nodes so that
each node can create its own OortObject
instance with the same name.
One OortObject
owns one data entity, which is its local part.
In the example above, the data entity is a whole List<String>
, so that's what we want
to share with other nodes:
OortObject.Factory<List<String>> factory = users.getFactory(); // Create a "default" data entity List<String> names = factory.newObject(null); // Fill it with data names.add("B1"); // Share the new list with the other nodes users.setAndShare(names);
Method setAndShare(...)
will replace the empty list (created internally when the
OortObject
was created) with the provided list, and broadcast this event to the cluster
so that other nodes can replace the part they have associated with this node with the new one.
Similarly, OortMap
has the putAndShare(...)
and removeAndShare(...)
methods to put/remove the map entries and share them:
OortStringMap<UserInfo> userInfos = ...; // Map user name "B1" with its metadata userInfos.putAndShare("B1", new UserInfo("B1", ...)); // In another place in the code // Remove the mapping for user "B1" userInfos.removeAndShare("B1");
OortList
has addAndShare(...)
and removeAndShare(...)
:
OortList<String> names = ...; // Add user name "B1" names.addAndShare("B1"); // In another place in the code // Remove user "B1" names.removeAndShare("B1");
Both OortMap
and OortList
inherit from OortObject
method setAndShare(...)
if you need to replace the whole map or list.
The OortObject
API will try to make it hard for you to interact directly with the
data entity, and this is by design.
If you can modify the data entity directly without using the above methods, then the local data
entity will be out of sync with the correspondent data entities in the other nodes.
Whenever you feel the need to access the data entity, and you cannot find an easy way to do it,
consider that you are probably taking the wrong approach.
For the same reasons mentioned above, it is highly recommended that the data that you store
in an Oort object is immutable. In the OortStringMap
example above, the UserInfo
object should be immutable, and if you need to change it, it is better to create a new UserInfo
instance with the new data and then call putAndShare(...)
to replace the old one, which will
ensure that all nodes will get the update.
The OortObject
implementation must be able to transmit and receive the data entity
to/from other nodes in the cluster, and recursively so for all objects contained
in the data entity that is being transmitted.
The data entity and the objects it contains are serialized to JSON using the standard CometD mechanism, and then transmitted. When a node receives the JSON representation of data entity and its contained objects, it deserializes it from JSON into an object graph.
In the OortStringMap
example above, the data entity is a
ConcurrentMap<String, Object>
and the values of this data entity are objects
of class UserInfo
.
While the OortObject
implementation is able to serialize a ConcurrentMap
to
JSON natively (because ConcurrentMap
is a Map
and therefore has a native
representation as a JSON object), it usually cannot serialize UserInfo
instances correctly
(by default, CometD just calls toString()
to convert such non natively representable objects
to JSON).
In order to serialize correctly instances of UserInfo
, you must configure Oort as explained
in Section 8.3.1.3, “Oort Configuration”.
This is done by creating a custom implementation of JSONContent.Client
:
package com.acme; import org.cometd.common.JettyJSONContextClient; public class MyCustomJSONContextClient extends JettyJSONContextClient { public MyCustomJSONContextClient() { getJSON().addConvertor(UserInfo.class, new UserInfoConvertor()); } }
In the example above the Jetty JSON library has been implicitly chosen by extending the CometD class
JettyJSONContextClient
. A similar class exist for the Jackson JSON library.
In the class above a convertor for the UserInfo
class is added to the root
org.eclipse.jetty.util.ajax.JSON
object retrieved via getJSON()
.
This root JSON
object is the one responsible for CometD message serialization.
A typical implementation of the convertor could be (assuming that your UserInfo
class
has an id
property):
import java.util.Map; import org.eclipse.jetty.util.ajax.JSON; public class UserInfoConvertor implements JSON.Convertor { @Override public void toJSON(Object obj, JSON.Output out) { UserInfo userInfo = (UserInfo)obj; out.addClass(UserInfo.class); out.add("id", userInfo.getId()); } @Override public Object fromJSON(Map object) { String id = (String)object.get("id"); return new UserInfo(id); } }
Class UserInfoConvertor
depends on the Jetty JSON library; a similar class can be written
for the Jackson library (refer to Section 8.3, “JSON Libraries” for further information).
Finally, you must specify class MyCustomJSONContextClient
as the jsonContext
parameter of the Oort configuration (as explained in Section 8.4.3.1, “Common Configuration”)
in the web.xml
file, for example:
<web-app ... >
...
<servlet>
<servlet-name>oort-config</servlet-name>
<servlet-class>org.cometd.oort.OortMulticastConfigServlet</servlet-class>
<init-param>
<param-name>oort.url</param-name>
<param-value>http://localhost:8080/cometd</param-value>
</init-param>
<init-param>
<param-name>oort.secret</param-name>
<param-value>oort_secret</param-value>
</init-param>
<init-param>
<param-name>jsonContext</param-name>
<param-value>com.acme.MyCustomJSONContextClient</param-value>
</init-param>
<load-on-startup>2</load-on-startup>
</servlet>
...
</web-app>
Similarly, in order to deserialize correctly instances of UserInfo
, you must configure CometD,
again as explained in Section 8.3.1.3, “Oort Configuration”.
This is done by creating a custom implementation of JSONContext.Server
:
package com.acme; import org.cometd.server.JettyJSONContextServer; public class MyCustomJSONContextServer extends JettyJSONContextServer { public MyCustomJSONContextServer() { getJSON().addConvertor(UserInfo.class, new UserInfoConvertor()); } }
Like before, the Jetty JSON library has been implicitly chosen by extending the CometD class
JettyJSONContextServer
. A similar class exist for the Jackson JSON library.
Class UserInfoConvertor
is the same class we defined above and it is therefore
used for both serialization and deserialization.
You must specify class MyCustomJSONContextServer
as the jsonContext
parameter of the CometD configuration (as explained in Section 8.2.1, “Configuring the Java Server”)
in the web.xml
file, for example:
<web-app ... >
...
<servlet>
<servlet-name>cometd</servlet-name>
<servlet-class>org.cometd.annotation.AnnotationCometdServlet</servlet-class>
<init-param>
<param-name>jsonContext</param-name>
<param-value>com.acme.MyCustomJSONContextServer</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
...
</web-app>
To summarize, the serialization of the ConcurrentMap
data entity of a OortStringMap
will happen in the following way: the ConcurrentMap
is a Map
and is natively
represented as a JSON object; the UserInfo
values will be converted to JSON as specified by the
UserInfoConvertor.toJSON(...)
method.
The JSON obtained after the serialization is transmitted to other nodes. The node that receive it will
deserialize the received JSON into a plain Map
containing UserInfo
value objects
converted as specified by the UserInfoConvertor.fromJSON(...)
method.
Finally the plain Map
object will be passed to the Oort object factory (see
Section 8.4.5.1.2, “OortObject Creation”) to be converted into a ConcurrentMap
.
OortObject
s are made of parts, and applications may need to access the data contained
in all parts. In the examples above, an application may want to be able to access all the user names
from all nodes.
In order to access the data from all parts, OortObject
provides the
merge(OortObject.Merger merger)
method.
Applications can use mergers provided by org.cometd.oort.OortObjectMergers
or implement their own, for example:
OortList<String> names = ...;
// Merge all the names from all the nodes
List<String> allNames = names.merge(OortObjectMergers.listUnion());
Merging is a local operation that does not involve network communication: it is just merging all the
data entity parts contained in the OortObject
.
When one node updates the data entity it owns, CometD notifies the other nodes so that they can keep in sync the data entity part correspondent to the node that performed the update. Applications can register listeners to be notified of such events, and perform their custom logic.
A typical example is when an application needs to show the total number of currently logged in users.
Every time a user connects and logs in, say, in NodeA, then NodeB needs to be notified to update
the total number in the user interface of the users connected to NodeB.
The Oort object we use in this example is an OortObject<Long>
, but you want to
use CometD's built-in org.cometd.oort.OortLong
in your application.
Since the application already updates the OortObject<Long>
in NodeA, the
correspondent OortObject<Long>
in NodeB is updated too.
The application can register a listener for such events, and update the user interface:
// At initialization time, create the OortObject and add the listener final OortObject<Long> userCount = new ...; userCount.addListener(new OortObject.Listener() { public void onUpdated(OortObject.Info<T> oldInfo, OortObject.Info<T> newInfo) { // The user count changed somewhere, broadcast the new value long count = userCount.merge(OortObjectMergers.longSum()); broadcastUserCount(count); } public void onRemoved(OortObject.Info<T> info); { // A node disappeared, broadcast the new user count long count = userCount.merge(OortObjectMergers.longSum()); broadcastUserCount(count); } private void broadcastUserCount(long count) { // Publish a message on "/user/count" to update the remote clients connected to this node BayeuxServer bayeuxServer = userCount.getOort().getBayeuxServer(); bayeuxServer.getChannel("/user/count").publish(userCount.getLocalSession(), count, null); } });
Class org.cometd.oort.OortObject.Info
represents a data entity part
of an OortObject
and contains the data entity and the Oort URL correspondent to
the node that it represent.
For this particular example, the Info
objects are not important, since we are
only interested in the total user count, that can be obtained by merging (see
Section 8.4.5.1.5, “OortObject Data Entity Merging”).
They can be used, however, to compute the difference before and after the update if needed.
Similarly, OortMap
supports registration of OortMap.EntryListener
s
that are notified when OortMap
entries change due to calls to
putAndShare(...)
or removeAndShare(...)
.
OortMap.EntryListener
are notified only when map entries are updated.
To be notified when the whole map changes due to calls to setAndShare(...)
,
you can use an OortMap.Listener
(inherited from OortObject
) as
described above.
In some cases, the whole map is updated but you want to be notified as if single entries are
changed; in this case you can use an OortMap.DeltaListener
, that converts
whole map updates into map entry updates.
OortList
supports registration of OortList.ElementListener
s
that are notified when OortList
elements change due to calls to
addAndShare(...)
or removeAndShare(...)
.
OortList.ElementListener
are notified only when list elements are updated.
To be notified when the whole list changes due to calls to setAndShare(...)
,
you can use an OortList.Listener
(inherited from OortObject
) as
described above.
In some cases, the whole list is updated but you want to be notified as if single elements
are changed; in this case you can use an OortList.DeltaListener
, that converts
whole list updates into list element updates.
An org.cometd.oort.OortService
is a named CometD service (see
Section 8.2.2, “Using Services”) that forwards actions from a
requesting node to the node in the cluster that
owns the data onto which the action must be performed, called the
owner node, and receives the action result back.
OortService
builds on the concept introduced by OortObject
that the ownership of a particular data entity belongs to one node only.
Any node can read the data entity, but only the owner can create/modify/delete it.
In order to perform actions that modify the data entity, a node has to know
what is the node that owns the data entity, and then forward the action to
the owner node.
OortService
provides the facilities to forward the action to the
owner node and receive the result of the action, or its failure.
Class org.cometd.oort.OortService
must be subclassed by the application
to implement key methods that perform the action logic.
The typical workflow of an OortService
is the following:
The OortService
receives a message from a remote client. The message contains
enough information for the OortService
to determine which node owns the data
onto which the action must be applied, in the form of the owner node's Oort URL.
Once the owner node Oort URL is known, the OortService
can forward the action
by calling method forward(...)
, passing in action information and an opaque context.
The owner node may be the node that received the message from the remote client itself, and
applications do not need to do anything different from the case where the owner node is a
different one.
The action arrives to the owner node and CometD invokes method onForward(...)
on the OortService
that resides on the owner node, passing in the action information
sent from the second step.
Method onForward(...)
is implemented by application to perform the custom logic and
may return a successful result or a failure.
The successful action result returned by onForward(...)
is sent back by CometD to the
requesting node, and when it arrives there, CometD invokes method onForwardSucceeded(...)
,
passing in the result of the action returned by onForward(...)
and the opaque context
passed to the forward(...)
method in the second step.
Method onForwardSucceeded(...)
is implemented by the application.
The action failure returned by onForward(...)
is sent back by CometD to the
requesting node, and when it arrives there, CometD invokes method onForwardFailed(...)
,
passing in the failure returned by onForward(...)
and the opaque context passed to the
forward(...)
method in the second step.
Method onForwardFailed(...)
is implemented by the application.
OortService
s are uniquely named across the cluster, and are usually created at
startup by subclassing them.
Since they are CometD services, they are usually annotated to listen for messages on certain
channels (see Section 8.2.2.2, “Annotated Client-Side and Server-Side Services” for further details on service
annotations):
ServerAnnotationProcessor processor = ...;
Oort oort = ...;
// Create the service instance and process its annotations
NameEditService nameEditService = new NameEditService(oort);
processor.process(nameEditService);
where the NameEditService
is defined as follows:
@Service(NameEditService.NAME) public class NameEditService extends OortService<String, Boolean> { public static final String NAME = "name_edit"; public NameEditService(Oort oort) { super(oort, NAME); } // Lifecycle methods triggered by standard lifecycle annotations @PostConstruct public void construct() throws Exception { start(); } @PreDestroy public void destroy() throws Exception { stop(); } // CometD's listener method on channel "/service/edit" @org.cometd.annotation.Listener("/service/edit") public void editName(final ServerSession remote, final ServerMessage message) { // Step #1: remote client sends an action request. // This runs in the requesting node. // Find the owner Oort URL from the message. // Applications must implement this method with their logic. String ownerURL = findOwnerURL(message); // Prepare the action data. String oldName = (String)remote.getAttribute("name"); String newName = (String)message.getDataAsMap().get("name"); Map<String, Object> actionData = new HashMap<String, Object>(); actionData.put("oldName", oldName); actionData.put("newName", newName); // Step #2: forward to the owner node. // Method forward(...) is inherited from OortService forward(ownerURL, actionData, new ServerContext(remote, message)); } @Override protected Result<Boolean> onForward(Request request) { // Step #3: perform the action. // This runs in the owner node. try { Map<String, Object> actionData = request.getDataAsMap(); // Edit the name. // Applications must implement this method. boolean result = editName(actionData); // Return the action result. return Result.success(result); } catch (Exception x) { // Return the action failure. return Result.failure("Could not edit name, reason: " + x.getMessage()); } } @Override protected void onForwardSucceeded(Boolean result, ServerContext context) { // Step #4: successful result. // This runs in the requesting node. // Notify the remote client of the result of the edit. context.getServerSession().deliver(getLocalSession(), result, null); } @Override protected void onForwardFailed(Object failure, ServerContext context) { // Step #5: failure result. // This runs in the requesting node. // Notify the remote client of the failure. context.getServerSession().deliver(getLocalSession(), failure, null); } }
Applications may have data entities that are naturally owned by any node. For example, in a chat application a chat room may be created by a user in any node, and be owned by the node the user that created it is connected to.
There are cases, however, where entities cannot be owned by any node, but instead must be owned by one node only, usually referred to as the master node. A typical example of such an entity is a unique (across the cluster) ID generator that produces unique number values, or a service that accesses a storage for archiving purposes (such as a file system or a database) that is only available on a particular node, or a service that must perform the atomic creation of certain entities (for example, unique user names), etc.
CometD provides org.cometd.oort.OortMasterService
that can be subclasses by
applications to write services that perform actions on data entities that must be owned by a single
node only.
There is one instance of OortMasterService
with the same name in each
node (like for other OortService
s), but only one of them is the master.
CometD provides an out-of-the-box implementation of OortMasterService
,
org.cometd.oort.OortMasterLong
, that can be used as a unique-across-the-cluster
number generator.
The implementation of an OortMasterService
subclass is similar to that of
OortService
(see Section 8.4.5.2.1, “OortService Creation”),
but this time the forward(...)
is always called with the same Oort URL (that of the
master node) that can be obtained by calling method
OortMasterService.getMasterOortURL()
.
Decide whether or not a node is a master node can be done by reading system properties passed to the command line, or via configuration files, or other similar means.
In general, applications can be seen as programs that create data and operate on that data.
Given a certain node, the application may need to access data stored on a remote node.
For modify/delete operations on the data, use an OortService
and forward the action
to the owner node.
The read operation, however, can be performed either using an OortObject
or using an
OortObject
.
When using an OortObject
, you trade more memory usage for smaller latencies to read the
data, since the data is replicated to all nodes and therefore the read operation is local and does
not involve network communication.
When using an OortService
, you trade less memory usage for bigger latencies to read the
data, since reading the data requires to forward the action to the node that owns the data and have
the owner node to send it back to the requesting node.
Whether to use one solution or the other depends heavily on the application, the server machine specification (especially available memory), and may even change over time.
For example, an application that is able to handle user information for a user base of 500 users
using OortObject
s may not be able to do so when it grows to 500,000 users.
Similarly, if the nodes are colocated in the same data center connected via a fast network, it may
be worth using OortService
(as the network time will be negligible), but if the nodes
and geographically distributed (for example, one in America, one in Europe, one in Asia), then
the network time may become an issue and data replication through OortObject
a better
solution to minimize latencies.
The CometD implementation includes the ability to add/remove extensions. An extension is a function that CometD calls; it allows you to modify a message just before sending it (an outgoing extension) or just after receiving it (an incoming extension).
An extension normally adds fields to the message being sent or received in the
ext
object
that the Bayeux specification defines.
An extension is not a way to add business fields to a message, but rather a way to process all messages, including the meta messages the Bayeux protocol uses, and to extend the Bayeux protocol itself. An extension should address concerns that are orthogonal to the business, but that provide value to the application. Typical examples of such concerns is to guarantee message ordering, to guarantee message delivery, to make sure that if the user navigates to another CometD page or reloads the same CometD page in the browser, the same CometD session is used without having to go through a disconnect/handshake cycle, to add to every message metadata fields such as the timestamp, to detect whether the client and the server have a time offset (for example when only one of them is synchronized with NTP), etc.
If you do not have such concerns or requirements, you should not use the extensions, as they add a minimal overhead without bringing value. On the other hand, if you have such orthogonal concerns for your business (for example, to cryptographically sign every message), extensions are the right way to do it.
You should look at the available extensions, understand the features they provide, and figure out whether they are needed for your use cases or not. If you truly have an orthogonal concern and the extension is not available out of the box, you can write your own, following the indications that follow.
Normally you set up extensions on both the client and the server, since fields the client adds usually need a special processing by the server, or viceversa; it is possible that an extension is only client-side or only server-side, but most of the time both client and server need them. When an extension does not behave as expected, it's often because the extension is missing on one of the two sides.
The next sections describe the JavaScript CometD Extensions; they follow the same pattern the portable JavaScript CometD implementation uses: a portable implementation of the extension with bindings for the specific JavaScript toolkit, currently Dojo and jQuery.
An extension is a JavaScript object with four optional methods:
outgoing(message)
–called just before a message is sent
incoming(message)
–called just after a message is received
registered(name, cometd)
–called when the extension is registered
unregistered()
–called when the extension is unregistered
These methods are optional; you can use only one, or maybe two, three, or all of them. If they are present, CometD invokes them at the proper time.
Writing an extension that logs and counts the long polls is quite easy: you need a reference
to the cometd
object that has the logging methods, and you need only the
outgoing extension method:
var LoggerExt = function() { var _cometd; var _counter; this.registered = function(name, cometd) { // Store the cometd object reference _cometd = cometd; }; this.outgoing(message) { if (message.channel == '/meta/connect') { // Log the long poll _cometd._info('bayeux connect'); // Count the long polls if (!message.ext) message.ext = {}; if (!message.ext.logger) message.ext.logger = {}; if (!message.ext.logger.counter) message.ext.logger.counter = 0; message.ext.logger.counter = ++_counter; } }; };
Notice that meta messages are also passed to the extension methods; you normally have to filter the messages that the extension method receives by looking at the channel or at some other message value.
Notice also that you can modify the message by adding fields, normally in the ext
field.
Be careful not to overwrite the ext
field, which other extensions might have set:
check whether it's present first.
It is also a good practice to group extension fields so that there is no clash with other extensions
(in the example above, the only field – counter – is grouped in the
message.ext.logger
object).
The outgoing()
and incoming()
methods can avoid returning something,
or returning the message itself (or another message object).
This means that the extension has processed the message and therefore other extensions, if present,
can process it, or the implementation can process the message (either by sending it to the server –
for outgoing extensions – or by notifying listeners – for incoming extensions).
If the extension method returns null
, the processing should stop: other extensions do not
process the message, and CometD does not further process it.
CometD does not send the message to the server, nor notify listeners.
The JavaScript CometD API defines three methods to manage extensions:
registerExtension(name, extension)
– registers an extension with the given name
unregisterExtension(name)
– unregisters the extension previously registered with the given name
getExtension(name)
– obtains a reference to the extension previously registered with the given name
Following the example above, you can register the extension like this:
cometd.registerExtension('loggerExt', new LoggerExt());
From now on, the meta connect messages are modified to carry the counter from the example extension above.
Unregistering the extension is similar:
cometd.unregisterExtension('loggerExt');
It is not possible to register two extensions under the same name.
You can register more than one extension, and CometD supplies them in registration order:
outgoing extensions methods are called in registration order and, by default, incoming registration
methods are called in reverse registration order. See also the reverseIncomingExtensions
configuration parameter in Section 7.1, “Configuring and Initializing”.
For example, if you register extA
and extB
, for outgoing messages the
methods called are: extA.outgoing()
and then extB.outgoing()
,
while for incoming messages the methods called are extB.incoming()
and then
extA.incoming()
.
While it is normally good practice to catch exceptions within extension functions, sometimes this is tedious to code, or there is no control about the quality of the extension (for example, it's a third party extension). The JavaScript CometD API provides a way to define the global extension exception handler that is invoked every time an extension throws an exception (for example, calling a method on an undefined object):
cometd.onExtensionException = function(exception, extensionName, outgoing, message) { // Uh-oh, something went wrong, disable this extension // Object "this" points to the CometD object this.unregisterExtension(extensionName); // If the message is going to the server, add the error to the message if (outgoing) { // Assume we have created the message structure below var badExtension = message.ext.badExtensions[extensionName]; badExtension.exception = exception; } }
Be very careful to use the CometD object to publish messages within the extension exception handler, or you might end up in an infinite loop (the extensions process the publish message, which might fail and call again the extension exception handler). If the extension exception handler itself throws an exception, this exception is logged at level "info" and the CometD implementation does not break.
To learn about a similar mechanism for listeners and subscribers, see Section 7.1, “Configuring and Initializing”.
The next sections explain in detail the use of the extensions CometD provides.
The activity extension monitors the activity of server sessions to disconnect them after a configurable
period of inactivity.
This is a server-side only extension implemented by class
org.cometd.server.ext.ActivityExtension
.
This extension is useful because the Bayeux Protocol (see Appendix C, The Bayeux Protocol Specification v1.0) uses a heart-beat mechanism
(over the channel /meta/connect
) that prevents servers to declare resources such as connections
or transport sessions (for example, Servlet's HttpSession
s) as idle, and therefore prevents
servers to close or destroy resources when they are idle for a long time.
A specific example is the following: when the long-polling
CometD transport is used, the
heart-beat mechanism consist of a HTTP POST request to the server.
Even if the CometD client is completely idle (for example, the user went to lunch), the heart-beat
mechanism continues to send POSTs at a regular interval to the server.
If the server's web.xml
is configured with a <session-timeout>
element,
that destroys the HttpSession
after 30 minutes of inactivity, then HttpSession
s will
never be destroyed because the heart-beat mechanism looks like a legitimate HTTP request to the server,
and it never stops. The server cannot know that those POSTs are just heart-beats.
The ActivityExtension
solves this problem.
The ActivityExtension
defines two types of inactivity:
client-only inactivity, where the client sends periodic heart-beat messages but no other messages, while the server may send normal messages to clients
client-server inactivity, where the client and the server sends only periodic heart-beat messages and no other messages.
To enable the ActivityExtension
, you must add the extension to the BayeuxServer
during initialization, specifying the type of activity you want to monitor, and the max inactivity period
in milliseconds:
bayeuxServer.addExtension(new ActivityExtension(ActivityExtension.Activity.CLIENT, 15 * 60 * 1000L));
The example above configures the ActivityExtension
to monitor client-only activity,
and disconnects inactive clients after 15 minutes (or 15 * 60 * 1000 ms) of inactivity.
Similarly, to monitor client-server activity:
bayeuxServer.addExtension(new ActivityExtension(ActivityExtension.Activity.CLIENT_SERVER, 15 * 60 * 1000L));
The example above configures the ActivityExtension
to monitor client-server activity,
and disconnects inactive clients after 15 minutes of inactivity.
The org.cometd.server.ext.ActivityExtension
can be installed if you want to
monitor the inactivity of all clients in the same way.
It is possible to monitor the inactivity of particular clients by not installing the
ActivityExtension
on the BayeuxServer
, and by installing a
org.cometd.server.ext.ActivityExtension.SessionExtension
on the specific server session for which you want to monitor inactivity, for example:
public class MyPolicy extends DefaultSecurityPolicy { @Override public boolean canHandshake(BayeuxServer server, ServerSession session, ServerMessage message) { if (!isAdminUser(session, message)) session.addExtension(new ActivityExtension.SessionExtension(ActivityExtension.Activity.CLIENT, 10 * 60 * 1000L)); return true; } }
In the example above, a custom SecurityPolicy
(see Section 8.2.3, “Authorization”)
checks whether the handshake is that of an administrator user, and installs the activity extension only for
non-administrators, with an inactivity timeout of 10 minutes.
Alternatively, you can write a BayeuxServer
extension that installs the
ActivityExtension.SessionExtension
selectively:
public class MyExtension extends BayeuxServer.Extension.Adapter { @Override public boolean sendMeta(ServerSession session, ServerMessage.Mutable message) { if (Channel.META_HANDSHAKE.equals(message.getChannel()) && message.isSuccessful()) { if (!isAdminUser(session, message)) session.addExtension(new ActivityExtension.SessionExtension(ActivityExtension.Activity.CLIENT, 10 * 60 * 1000L)); } return true; } }
In the example above, a custom BayeuxServer
extension checks whether the handshake has been
successful, and installs the activity extension only for non-administrators, with an inactivity timeout
of 10 minutes.
By default, CometD does not enforce a strict order on server-to-client message delivery, nor it provides the guarantee that messages sent by the server are received by the clients.
The message acknowledgment extension provides message ordering and message reliability to the Bayeux protocol for messages sent from server to client. This extension requires both a client-side extension and a server-side extension. The server-side extension is available in Java. If you are interested only in message ordering (and not reliability), see Section 9.5.4, “Message Ordering”.
To enable support for acknowledged messages, you must add the extension to the
org.cometd.bayeux.server.BayeuxServer
instance during initialization:
bayeuxServer.addExtension(new org.cometd.server.ext.AcknowledgedMessagesExtension());
The AcknowledgedMessageExtension
is a per-server extension that monitors handshakes from new
remote clients, looking for those that also support the acknowledged message extension, and then adds the
AcknowledgedMessagesClientExtension
to the ServerSession
correspondent to the
remote client, during the handshake processing.
Once added to a ServerSession
, the AcknowledgedMessagesClientExtension
guarantees ordered delivery of messages, and resend of unacknowledged messages, from server to client.
The extension also maintains a list of unacknowledged messages and intercepts the traffic on the
/meta/connect
channel to insert and check acknowledge IDs.
The dojox/cometd/ack.js
provides the client-side extension binding for Dojo, and it is sufficient
to use Dojo's dojo.require
mechanism:
dojo.require("dojox.cometd.ack");
The file jquery.cometd-ack.js
provides the client-side extension binding for jQuery.
You must include this file in the HTML page via the <script>
tag:
<script type="text/javascript" src="AckExtension.js"></script> <script type="text/javascript" src="jquery.cometd-ack.js"></script>
In both Dojo and jQuery extension bindings, the extension is registered on the default cometd
object under the name ack.
Furthermore, you can programmatically disable/enable the extension before initialization by setting the
ackEnabled
boolean field on the cometd
object:
// Disables the ack extension during handshake
cometd.ackEnabled = false;
cometd.init(cometdURL);
To enable message acknowledgement, both client and server must indicate that they support message acknowledgement.
This is negotiated during handshake. On handshake, the client sends {"ext":{"ack": "true"}}
to
indicate that it supports message acknowledgement.
If the server also supports message acknowledgement, it likewise replies with {"ext":{"ack": "true"}}
.
The extension does not insert ack IDs in every message, as this would impose a significant burden on the server
for messages sent to multiple clients (which would need to be reserialized to JSON for each client).
Instead the ack ID is inserted in the ext
field of the /meta/connect
messages that are
associated with message delivery.
Each /meta/connect
request contains the ack ID of the last received ack response:
"ext":{"ack": 42}
.
Similarly, each /meta/connect
response contains an ext ack ID that uniquely identifies the batch
of responses sent.
If a /meta/connect
message is received with an ack ID lower that any unacknowledged messages
held by the extension, then these messages are requeued prior to any more recently queued messages and the
/meta/connect
response sent with a new ack ID.
It is important to note that message acknowledgement is guaranteed from server to client only, and not from client to server. This means that the ack extension guarantees that messages sent by the server to the clients will be resent in case of temporary network failures that produce loss of messages. It does not guarantee however, that messages sent by the client will reach the server.
Message ordering is not enforced by default by CometD.
A CometD server has two ways to deliver the messages present
in the ServerSession
queue to its correspondent remote client:
through /meta/connect
responses
through direct responses
Delivery through a /meta/connect
response means that the server will deliver the messages
present in the ServerSession
queue along with a /meta/connect
response, so
that the messages delivered to the remote client are:
[{/meta/connect response message}, {queued message 1}, {queued message 2}, ...]
.
Direct delivery depends on the transport.
For polling transports it means that the server will deliver the messages present in the
ServerSession
queue along with some other response message that is being processed in that moment.
For example, let's assume that clientA
is already subscribed to channel /foo
,
and that it wants to subscribe also to channel /bar
. Then clientA
sends a
subscription request for channel /bar
to the server, and just before processing the subscription
request for channel/bar
, the server receives a message on channel /foo
, that
therefore needs to be delivered to clientA
(for example, clientB
published a
message to channel/foo
, or an external system produced a message on channel /foo
).
The message on channel /foo
gets queued on clientA
's ServerSession
.
However, the server notices that it has to reply to subscription request for /bar
, so
it includes the message on channel /foo
in that response, thus avoiding to wake up
the /meta/connect
, so that the messages delivered to the remote client are:
[{subscription response message for /bar}, {queued message on /foo}]
.
For non-polling transports such as websocket
the server will just deliver the messages
present in the ServerSession
queue without waking up the /meta/connect
,
because non-polling transports have a way to send server-to-client messages without the need to have a
pending response onto which the ServerSession
's queued messages are piggybacked.
These two ways of delivering messages compete each other to deliver messages with the smallest latency.
Therefore it is possible that a server receives from an external system two messages to be delivered for
the same client, say message1
first and then message2
; message1
is
queued and immediately dequeued by a direct delivery, while message2
is queued and then
dequeued by a /meta/connect
delivery.
The client may see message2
arriving before message1
, for example because the
thread scheduling on the server favored message2
's processing or because the TCP communication
for message1
was somehow slowed down (not to mention that browsers could as well be source of
uncertainty).
To enable just server-to-client message ordering (but not reliability), you need to configure the server
with the metaConnectDeliverOnly
parameter, as explained in
Section 8.2.1, “Configuring the Java Server”.
When server-to-client message ordering is enabled, all messages will be delivered through
meta/connect
responses.
In the example above, message1
will be delivered to the client, and message2
will wait on the server until another meta/connect
is issued by the client (which happens
immediately after message1
has been received by the client).
When the server receives the second meta/connect
request, it will respond to it immediately
and deliver message2
to the client.
It is clear that server-to-client message ordering comes at the small price of slightly increased latencies
(message2
has to wait the next meta/connect
before being delivered), and slightly
more activity for the server (since meta/connect
s will be resumed more frequently than normal).
There is an example of acknowledged messages in the Dojo chat demo that comes bundled with the CometD distribution, and instruction on how to run the CometD demos in Section 3.1, “Running the Demos”.
To run the acknowledgement demo, follow these steps:
$ cd cometd-demo $ mvn jetty:run
Start CometD
Point your browser to http://localhost:8080/dojo-examples/chat/ and make sure to check Enable reliable messaging.
Use two different browser instances to begin a chat session, then briefly disconnect one browser from the network (you can do this by setting the work offline feature).
While one browser is disconnected, type some chat in the other browser, which is received when the disconnected browser reconnects to the network.
Notice that if the disconnected browser is disconnected in excess of maxInterval (default 10s), the client times out and the unacknowledged queue is discarded.
The reload extension allows CometD to load or reload a page without having to re-handshake in the new (or reloaded) page, thereby resuming the existing CometD connection. This extension requires only the client-side extension.
The dojox/cometd/reload.js
provides the client side extension binding for Dojo, and it is
sufficient to use Dojo's dojo.require
mechanism:
dojo.require("dojox.cometd.reload");
The file jquery.cometd-reload.js
provides the client-side extension binding for jQuery.
This file must be included in the HTML page via the <script>
tag, along with the
jQuery cookie plugin and the reload
extension implementation:
<script type="text/javascript" src="jquery.cookie.js"></script> <script type="text/javascript" src="ReloadExtension.js"></script> <script type="text/javascript" src="jquery.cometd-reload.js"></script>
In both Dojo and jQuery extension bindings, the extension is registered on the default
cometd
object under the name "reload".
The reload extension accepts the following configuration parameters:
Table 9.1. Reload Extension Configuration Parameters
Parameter Name | Default Value | Parameter Description |
---|---|---|
cookieName | org.cometd.reload | The name of the short-lived cookie the reload extension uses to save the connection state details |
cookiePath | / | The path of the short-lived cookie the reload extension uses |
cookieMaxAge | 5 | The max age, in seconds, of the short-lived cookie the reload extension uses |
The JavaScript cometd
object is normally already configured with the default reload extension
configuration.
To reconfigure the reload extension:
Reconfigure the extension at startup:
var cometd = dojox.cometd; // Use $.cometd if using jquery cometd.getExtension('reload').configure({ cookieMaxAge: 15 });
Reconfigure the extension every time the cometd.reload()
method is invoked:
var cometd = dojox.cometd; // Use $.cometd if using jquery ... cometd.reload({ cookieMaxAge: 15 });
If you increase the maxCookieAge
, be aware that you should probably increase
the value of maxInterval
too (see Section 8.2.1, “Configuring the Java Server”), to avoid
the server expiring the session while the page is reloading.
The reload extension is useful to allow users to reload CometD pages, or to navigate to other CometD pages, without going through a disconnect and handshake cycle, thus resuming an existing CometD session on the reloaded or on the new page.
When reloading or navigating away from a page, browsers will destroy the JavaScript context associated to that page, and interrupt the connection to the server too. On the reloaded or on the new page, the JavaScript context is recreated anew by the browser, but the CometD JavaScript library has lost all the CometD session details that were established in the previous page. In absence of the reload extension, application need to go through another handshake step to recreate the CometD session details needed.
The reload extension gives the ability to resume the CometD session in the new page, by re-establishing the previously successful CometD session. This is useful especially when the server builds a stateful context for the CometD session that is not to be lost just because the user refreshed the page, or navigated to another part of the same CometD web application. A typical example of this stateful context is when the server needs to guarantee message delivery (see Section 9.5, “Message Acknowledgment Extension”). In this case, the server has a list of messages that have not been acknowledged by the client, and if the client reloads the page, without the reload extension this list of messages will be lost, causing the client to potentially loose important messages. With the reload extension, instead, the CometD session is resumed and it will appear to the server as if it was never interrupted, thus allowing the server to deliver to the client the unacknowledged messages.
The reload extension works in this way: on page load, the application configures
the CometD object, registers channel listeners and finally calls cometd.handshake()
.
This handshake normally contacts the server and establishes a new CometD session, and the reload
extension tracks this successful handshake.
On page reload, or when the page is navigated to another CometD page, the application code must call
cometd.reload()
(for example, on the page unload event handler).
When cometd.reload()
is called, the reload extension saves a short-lived cookie with the
CometD session state details.
When the new page loads up, it will execute the same code executed on the first page load, namely the code
that configured CometD, that registered channel listeners, and that finally called
cometd.handshake()
.
The reload extension is invoked upon the new handshake, sees that the short lived cookie saved
previously is there, and re-establishes the CometD session with the information stored in the
short lived cookie.
A simple example follows:
<html> <head> <script type="text/javascript" src="dojo/dojo.js"></script> <script type="text/javascript"> dojo.require("dojox.cometd"); dojo.require("dojox.cometd.reload"); dojox.cometd.configure({ url: "http://localhost:8080/context/cometd", logLevel: "info" }); // Always subscribe to channels from successful handshake listeners dojox.cometd.addListener("/meta/handshake", new function(m) { if (m.successful) { dojox.cometd.subscribe("/some/channel", function() { ... }); dojox.cometd.subscribe("/some/other/channel", function() { ... }); } }); // On page unload, call dojox.cometd.reload() dojo.addOnUnload(dojox.cometd, "reload"); // Finally, handshake dojox.cometd.handshake(); </script> </head> <body> ... </body> </html>
The timestamp extension adds a timestamp
to the message object for every message the client
and/or server sends.
It is a non-standard extension because it does not add the additional fields to the ext
field,
but to the message object itself.
This extension requires both a client-side extension and a server-side extension. The server-side extension
is available in Java.
To enable support for time-stamped messages, you must add the extension to the
org.cometd.bayeux.server.BayeuxServer
instance during initialization:
bayeuxServer.addExtension(new org.cometd.server.ext.TimestampExtension());
The dojox/cometd/timestamp.js
provides the client-side extension binding for Dojo,
and it is sufficient to use Dojo's dojo.require
mechanism:
dojo.require("dojox.cometd.timestamp");
The file jquery.cometd-timestamp.js
provides the client-side extension binding for jQuery.
You must include in the HTML page the implementation file and the jQuery extension binding, in order,
via <script>
tags:
<script type="text/javascript" src="TimeStampExtension.js"></script> <script type="text/javascript" src="jquery.cometd-timestamp.js"></script>
In both Dojo and jQuery extension bindings, the extension is registered on the default cometd
object under the name "timestamp".
The timesync extension uses the messages exchanged between a client and a server to calculate the offset between the client's clock and the server's clock. This is independent from Section 9.7, “Timestamp Extension”, which uses the local clock for all timestamps. This extension requires both a client-side extension and a server-side extension. The server-side extension is available in Java.
To enable support for time synchronization, you must add the extension to the
org.cometd.bayeux.server.BayeuxServer
instance during initialization:
bayeuxServer.addExtension(new org.cometd.server.ext.TimesyncExtension());
The dojox/cometd/timesync.js
provides the client-side extension binding for Dojo,
and it is sufficient to use Dojo's dojo.require
mechanism:
dojo.require("dojox.cometd.timesync");
The file jquery.cometd-timesync.js
provides the client-side extension binding for jQuery.
You must include in the HTML page the implementation file and the jQuery extension binding, in order,
via <script>
tags:
<script type="text/javascript" src="TimeSyncExtension.js"></script> <script type="text/javascript" src="jquery.cometd-timesync.js"></script>
In both Dojo and jQuery extension bindings, the extension is registered on the default cometd
object under the name "timesync".
The timesync extension allows the client and server to exchange time information on every handshake and connect message so that the client can calculate an approximate offset from its own clock epoch to that of the server. The algorithm used is very similar to the NTP algorithm.
With each handshake or connect, the extension sends timestamps within the ext field, for example:
{ext:{timesync:{tc:12345567890,l:23,o:4567},...},...}
where:
tc is the client timestamp in ms since 1970 of when the message was sent
l is the network lag that the client has calculated
o is the clock offset that the client has calculated
You can calculate the accuracy of the offset and lag with tc-now-l-o
, which should be zero if
the calculated offset and lag are perfectly accurate.
A Bayeux server that supports timesync should respond only if the measured accuracy value is greater than
accuracy target.
The response is an ext
field, for example:
{ext:{timesync:{tc:12345567890,ts:1234567900,p:123,a:3},...},...}
where:
tc is the client timestamp of when the message was sent
ts is the server timestamp of when the message was received
p is the poll duration in ms - ie the time the server took before sending the response
a is the measured accuracy of the calculated offset and lag sent by the client
On receipt of the response, the client is able to use current time to determine the total trip time, from which it subtracts p to determine an approximate two way network traversal time. Thus:
lag = (now-tc-p)/2
offset = ts-tc-lag
To smooth over any transient fluctuations, the extension keeps a sliding average of the offsets received. By default this is over ten messages, but you can change this value by passing a configuration object during the creation of the extension:
// Unregister the default timesync extension cometd.unregisterExtension('timesync'); // Re-register with different configuration cometd.registerExtension('timesync', new org.cometd.TimeSyncExtension({ maxSamples: 20 }));
The client-side timesync extension also exposes several methods to deal with the result of the time synchronization:
getNetworkLag()
–to obtain the calculated network latency between client and server
getTimeOffset()
–to obtain the offset between the client's clock and the server's clock in ms
getServerTime()
–to obtain the server's time
setTimeout()
–to schedule a function to be executed at a certain server time
Building the CometD project has 2 minimum requirements:
Make sure that the mvn executable is in your path, and that your JAVA_HOME
environment variable points to the directory where Java is installed.
You can obtain the source code from either the distribution tarball or by checking out the source from the GitHub repository.
If you want to use the distribution tarball, download it from here, then unpack it with the following commands:
$ tar zxvf cometd-[version]-distribution.tar.gz $ cd cometd-[version]
If you want to use the latest code, clone the GitHub repository with the following commands:
$ git clone git://github.com/cometd/cometd.git cometd $ cd cometd
Once you have obtained the source code, you need to issue the following command to build it:
$ mvn clean install
If you want to save some time, you can skip the execution of the test suite using the following command:
$ mvn clean install -DskipTests=true
In general the API classes and interfaces moved from org.cometd.*
to
org.cometd.
bayeux
.*
.
Server-side implementation classes remained in org.cometd.server.*
,
and client-side implementation classes remained in org.cometd.client.*
.
CometD 1.x | CometD 2.x |
---|---|
org.cometd.*
|
org.cometd.
bayeux
.*
|
org.cometd.
Bayeux
|
org.cometd.bayeux.
server.BayeuxServer
|
org.cometd.
Client
|
org.cometd.bayeux.
server.ServerSession
|
org.cometd.
Channel
|
org.cometd.bayeux.
server.ServerChannel
|
org.cometd.server.
BayeuxService
|
org.cometd.server.
AbstractService
|
CometD 1.x | CometD 2.x |
---|---|
org.cometd.java:
cometd-api
|
org.cometd.java:
bayeux-api
|
CometD 1.x | CometD 2.x |
---|---|
org.cometd.server.
continuation.ContinuationCometdServlet
|
org.cometd.server.
CometdServlet
|
CometD 1.x | CometD 2.x |
---|---|
multiFrameInterval
|
long-polling.multiSessionInterval
|
requestAvailable
|
N/A (always true)
|
CometD 1.x | CometD 2.x |
---|---|
org.cometd.server.BayeuxService.
subscribe()
|
org.cometd.server.AbstractService.
addService()
|
CometD 1.x | CometD 2.x |
---|---|
public void method(
Client c, Message m
)
|
public void method(
ServerSession c, ServerMessage m
)
|
CometD 1.x | CometD 2.x |
---|---|
Channel c = bayeux.getChannel(
"/foo", true
)
|
bayeuxServer.
createIfAbsent("/foo");
Channel c = bayeuxServer.getChannel(
"/foo"
);
|
CometD 1.x | CometD 2.x |
---|---|
org.cometd.
Bayeux.ATTRIBUTE
|
org.cometd.bayeux.
server.BayeuxServer.ATTRIBUTE
|
org.cometd.
Bayeux.META_HANDSHAKE
|
org.cometd.bayeux.
Channel.META_HANDSHAKE
|
org.cometd.
Bayeux.META_CONNECT
|
org.cometd.bayeux.
Channel.META_CONNECT
|
org.cometd.
Bayeux.META_SUBSCRIBE
|
org.cometd.bayeux.
Channel.META_SUBSCRIBE
|
org.cometd.
Bayeux.META_UNSUBSCRIBE
|
org.cometd.bayeux.
Channel.META_UNSUBSCRIBE
|
org.cometd.
Bayeux.META_DISCONNECT
|
org.cometd.bayeux.
Channel.META_DISCONNECT
|
This document specifies a protocol for the Internet community, and requests discussion and suggestions for improvement. This document is written in the style and spirit of an IETF RFC but is not, as of yet, an official IETF RFC. Distribution of this document is unlimited.
Bayeux is a protocol for transporting asynchronous messages (primarily over web protocols such as HTTP and WebSocket), with low latency between a web server and web clients.
The primary purpose of Bayeux is to support responsive bidirectional interactions between web clients, for example using using AJAX, and the web server.
Bayeux is a protocol for transporting asynchronous messages (primarily over HTTP), with low latency between a web server and a web client. The messages are routed via named channels and can be delivered:
server to client
client to server
client to client (via the server)
By default, publish/subscribe routing semantics are applied to the channels.
Delivery of asynchronous messages from the server to a web client is often described as server push. The combination of server push techniques with an AJAX web application has been called Comet. CometD is a project by the Dojo Foundation to provide multiple implementation of the Bayeux protocol in several programming languages.
Bayeux seeks to reduce the complexity of developing Comet web applications by allowing implementers to more easily interoperate, to solve common message distribution and routing problems, and to provide mechanisms for incremental improvements and extensions.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC2119. An implementation is not compliant if it fails to satisfy one or more of the MUST or REQUIRED level requirements for the protocols it implements. An implementation that satisfies all the MUST or REQUIRED level and all the SHOULD level requirements for its protocols is said to be "unconditionally compliant"; one that satisfies all the MUST level requirements but not all the SHOULD level requirements for its protocols is said to be "conditionally compliant."
This specification uses a number of terms to refer to the roles played by participants in, and objects of, Bayeux communication:
A program that initiates the communication.
A web client (for example, a HTTP client, but also a WebSocket client) is a program that initiates TCP/IP connections for the purpose of sending web requests. A Bayeux client initiates the Bayeux message exchange and will typically execute within a web client, but it is likely to have also Bayeux clients that execute within web servers.
Implementations may distinguish between
Bayeux clients running within a web client and Bayeux clients running
within the web server. Specifically server-side Bayeux clients MAY be
privileged clients with access to private information about other clients
(see the section called “clientId
”) and subscriptions.
An application program that accepts communications from clients. A web server accepts TCP/IP connections in order to service web requests (HTTP requests or WebSocket requests) by sending back web responses. A Bayeux server accepts and responds to the message exchanges initiated by a Bayeux client.
For the HTTP protocol, an HTTP request message as defined by section 5 of RFC 2616.
For the HTTP protocol, an HTTP response message as defined by section 6 of RFC 2616.
A message is a JSON encoded object exchanged between client and server for the purpose of implementing the Bayeux protocol as defined by Section C.4, “Common Message Field Definitions”, Section C.5, “Meta Message Field Definitions” and Section C.6, “Event Message Field Definitions”,
Application specific data that is sent over the Bayeux protocol.
The transport specific message format that wraps a standard Bayeux message.
A named destination and/or source of events. Events are published to channels and subscribers to channels receive published events.
A communication link that is established either permanently or transiently, for the purposes of messages exchange. A client is connected if a link is established with the server, over which asynchronous events can be received.
JavaScript Object Notation (JSON) is a lightweight data-interchange format. It is easy for humans to read and write. It is easy for machines to parse and generate. It is based on a subset of the JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999. JSON is described at http://www.json.org/.
The HTTP protocol is a request/response protocol. A client sends a request to the server in the form of a request method, URI, and protocol version, followed by a MIME-like message containing request modifiers, client information, and optional body content over a connection with a server. The server responds with a status line, including the message's protocol version and a success or error code, followed by a MIME-like message containing server information, entity metainformation, and possible entity-body content.
The server may not initiate a connection with a client nor send an unrequested response to the client, thus asynchronous events cannot be delivered from server to client unless a previously issued request exists. In order to allow two way asynchronous communication, Bayeux supports the use of multiple HTTP connections between a client and server, so that previously issued requests are available to transport server to client messages.
The recommendation of section 8.1.4 of RFC 2616 is that a single client SHOULD NOT maintain more than 2 connection with any server, thus the Bayeux protocol MUST NOT require any more than 2 HTTP requests to be simultaneously handled by a server in order to handle all application (Bayeux based or otherwise) requests from a client.
While HTTP is the predominant transport protocol used on the internet, it is not intended that it will be the only transport for Bayeux. Other transports that support a request/response paradigm may be used (for example, WebSocket is not a request/response protocol, but supports a request/response paradigm). However this document assumes HTTP for reasons of clarity. When non-HTTP connection-level transport mechanisms are employed, conforming Bayeux servers and clients MUST still conform to the semantics of the JSON encoded messages outlined in this document.
Several of the "transport types" described in this document are distinguished primarily by how they wrap messages for delivery over HTTP and the sequence and content of the HTTP connections initiated by clients. While this may seem like a set of implementation concerns to observant readers, the difficulties of creating interoperable implementations without specifying these semantics fully is a primary motivation for the development of this specification. Were the deployed universe of servers and clients more flexible, it may not have been necessary to develop Bayeux.
Regardless, care has been taken in the development of this specification to ensure that future clients and servers which implement differing connection-level strategies and encodings may still evolve and continue to be conforming Bayeux implementations so long as they implement the JSON-based public/subscribe semantics outlined herein.
The rest of this document speaks as though HTTP will be used for message transport.
Bayeux clients implemented in JavaScript that run within the security framework of a browser MUST adhere to the restrictions imposed by the browser, such as the same origin policy or the CORS specification, or the threading model. These restrictions are normally enforced by the browser itself, but nonetheless the client implementation must be aware of these restrictions and adhere to them.
Bayeux clients implemented in JavaScript but not running within a browser MAY relax the restrictions imposed by browsers.
A Bayeux event is sent from the client to the server via a HTTP request initiated by a client and transmitted to the origin server via a chain of zero or more intermediaries (proxy, gateway or tunnel):
BC ---------- U ---------- P ------------ O ---------- BS | --M0(E)--> | | | | | | ---HTTP request(M0(E))--> | | | | | | --M0(E)--> | | | | | <---M1---- | | | <---HTTP response(M1)---- | | | <---M1--- | | | | | | | | |
The figure above represents a Bayeux event E encapsulated in a Bayeux message M0 being sent from a Bayeux client BC to a Bayeux server BS via a HTTP request transmitted from a User Agent U to to an Origin server O via a proxy P. The HTTP response contains another Bayeux message M1 that will at least contain the protocol response to M0, but may contain other Bayeux events initiated on the server or on other clients.
A Bayeux event is sent from the server to the client via a HTTP response to a HTTP request sent in anticipation by a client and transmitted to an origin server via a chain of zero or more intermediaries (proxy, gateway or tunnel):
BC ---------- U ---------- P ------------ O ---------- BS | ---M0---> | | | | | | --- HTTP request(M0) ---> | | | | | | ----M0---> | ~ ~ ~ ~ ~ wait | | | | <--M1(E)-- | | | <--HTTP response(M1(E))-- | | | <--M1(E)-- | | | | ~ ~ ~ ~ ~
The figure above represents a Bayeux message M0 being sent from a Bayeux client BC to a Bayeux server BS via a HTTP request transmitted from a User Agent U to to an Origin server O via a proxy P. The message M0 is sent in anticipation of a Bayeux event to be delivered from server to client and the Bayeux server waits for such an event before sending a response. A Bayeux event E is shown being delivered via Bayeux message M1 in the HTTP response. M1 may contain zero, one or more Bayeux events destined for the Bayeux client.
The transport used to send events from the server to the client may terminate the HTTP response (which does not imply that the connection is closed) after delivery of M1 or use techniques to leave the HTTP response uncompleted and stream additional messages to the client.
Polling transports will always terminate the HTTP response after sending all available Bayeux messages.
BC ---------- U ---------- P ------------ O ---------- BS | ---M0---> | | | | | | --- HTTP request(M0) ---> | | | | | | ----M0---> | ~ ~ ~ ~ ~ wait | | | | <--M1(E)-- | | | <--HTTP response(M1(E))-- | | | <--M1(E)-- | | | | | ---M2---> | | | | | | --- HTTP request(M2) ---> | | | | | | ----M2---> | ~ ~ ~ ~ ~ wait
On receipt of the HTTP response containing M1, the Bayeux client issues a new
Bayeux message M2 either immediately or after an interval in anticipation of
more events to be delivered from server to client.
Bayeux implementations MUST support a specific style of polling
transport called long polling (see Section C.7.1, “The long-polling
Transport”).
Some Bayeux transports use the streaming technique (also called the forever response) that allows multiple messages to be sent within the same HTTP response:
BC ---------- U ---------- P ------------ O ---------- BS | ---M0---> | | | | | | --- HTTP request(M0) ---> | | | | | | ----M0---> | ~ ~ ~ ~ ~ wait | | | | <--M1(E0)- | | | <--HTTP response(M1(E0))- | | | <--M1(E0)- | | | | ~ ~ ~ ~ ~ wait | | | | <--M1(E1)- | | | <----(M1(E1))------------ | | | <--M1(E1)- | | | | ~ ~ ~ ~ ~ wait
Streaming techniques avoid the latency and extra messaging of anticipatory requests, but are subject to the implementation of user agents and proxies as they requires incomplete HTTP responses to be delivered to the Bayeux client.
In order to achieve bidirectional communication, a Bayeux client uses 2 HTTP connections (see the section called “HTTP Transport”) to a Bayeux server so that both server to client and client to server messaging may occur asynchronously:
BC ---------- U ---------- P ------------ O ---------- BS | ---M0---> | | | | | | ------ req0(M0) --------> | | | | | | ----M0---> | ~ ~ ~ ~ ~ wait | --M1(E1)-> | | | | | | ----- req1(M1(E1))------> | | | | | | --M1(E1)-> | | | | | <---M2---- | | | <---- resp1(M2)---------- | | | <---M2--- | | | | ~ ~ ~ ~ ~ wait | | | | <-M3(E2)-- | | | <-----resp2(M3(E2))------ | | | <-M3(E2)-- | | | | | ---M4---> | | | | | | ------req3(M4)----------> | | | | | | ----M4---> | ~ ~ ~ ~ ~ wait
HTTP requests req0 and req1 are sent on different TCP/IP connections, so that the response to req1 may be sent before the response to req0. Implementations MUST control HTTP pipelining so that req1 does not get queued behind req0 and thus enforce an ordering of responses.
Bayeux connections are negotiated between client and server with handshake messages that allow the connection type, authentication and other parameters to be agreed upon between the client and the server.
BC ----------------------------------------- BS | ------------------ handshake request ---> | | <---- handshake response ---------------- | | -------------------- connect request ---> | ~ ~ wait | <------ connect response ---------------- |
Bayeux connection negotiation may be iterative and several handshake messages may be exchanged before a successful connection is obtained. Servers may also request Bayeux connection renegotiation by sending an unsuccessful connect response with advice to reconnect with a handshake message.
BC ----------------------------------------- BS | ------------------ handshake request ---> | | <-- unsuccessful handshake response ----- | | ------------------ handshake request ---> | | <-- successful handshake response ------- | | -------------------- connect request ---> | ~ ~ wait | <------ connect response ---------------- | | -------------------- connect request ---> | | <---- unsucessful connect response ------ | | ------------------ handshake request ---> | | <-- successful handshake response ------- | | -------------------- connect request ---> | ~ ~ wait | <------ connect response ---------------- |
OPTIONALLY, messages can be sent without a prior handshake (see Section C.6.1, “Publish”).
BC ----------------------------------------- BS | ------------------- message request ----> | | <---- message response ------------------ |
This pattern is often useful when implementing non-browser clients for Bayeux servers. These clients often simply wish to address messages to other clients which the Bayeux server may be servicing, but do not wish to listen for events themselves.
Bayeux servers MAY support messages sent without a prior handshake, but in any case MUST respond to such messages (eventually with an error message).
-------------++------------+-------------+------------+------------ State/Event || handshake | Timeout | Successful | Disconnect || request | | connect | request || sent | | response | sent -------------++------------+-------------+----------- +------------ UNCONNECTED || CONNECTING | UNCONNECTED | | CONNECTING || | UNCONNECTED | CONNECTED | UNCONNECTED CONNECTED || | UNCONNECTED | | UNCONNECTED -------------++------------+-------------+------------+------------
The characters used for Bayeux names and identifiers are defined by the BNF definitions:
alpha = lowalpha | upalpha lowalpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" upalpha = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" alphanum = alpha | digit mark = "-" | "_" | "!" | "~" | "(" | ")" | "$" | "@" string = *( alphanum | mark | " " | "/" | "*" | "." ) token = ( alphanum | mark ) *( alphanum | mark ) integer = digit *( digit )
Channels are identified by names that are styled as the absolute path component of a URI without parameters as defined by RFC2396.
channel_name = "/" channel_segments channel_segments = channel_segment *( "/" channel_segment ) channel_segment = token
The channel name consists of an initial "/" followed by an optional sequence of path segments separated by a single slash "/" character. Within a path segment, the character "/" is reserved.
Channel names commencing with "/meta/" are reserved for the Bayeux protocol (see the section called “Meta Channels”). Channel names commencing with "/service/" have a special meaning for the Bayeux protocol (see the section called “Service Channels”). Example non-meta channel names are:
/foo
/foo/bar
/foo-bar/(foobar)
A set of channels may be specified with a channel globbing pattern:
channel_pattern = *( "/" channel_segment ) "/" wild_card wild_card = "*" | "**"
The channel patterns support only trailing wildcards of either "*" to match a single segment or "**" to match multiple segments. Example channel patterns are:
/foo/*
Matches /foo/bar
and /foo/boo
. Does not match /foo
,
/foobar
or /foo/bar/boo
.
/foo/**
Matches /foo/bar
, /foo/boo
and /foo/bar/boo
.
Does not match /foo
, /foobar
or /foobar/boo
.
The channels commencing with the "/meta/" segment are the channels used by the Bayeux protocol itself. Local server-side Bayeux clients MAY, and remote Bayeux clients SHOULD NOT, subscribe (see Section C.5.4, “Subscribe”) to meta channels. Messages published to meta channels MUST NOT be distributed to remote clients by Bayeux servers. A server side handler of a meta channel MAY publish response messages that are delivered only to the client that sent the original request message. If a message published to a meta channel contains an id field, then any response messages delivered to the client MUST contain an id field with the same value.
The channels commencing with the "/service/" channel segment are special channels designed to assist request/response style messaging. Messages published to service channels are not distributed to any remote Bayeux clients. Handlers of service channels MAY deliver response messages to the client that published the request message. Servers SHOULD NOT record any subscriptions they receive for service channels. If a message published to a service channel contains an id field, then any response messages SHOULD contain an id field with the same value or a value derived from the request id. Request/response operations are described in detail in Section C.10, “Request / Response operation with service channels”
A protocol version is a integer followed by an optional "." separated sequence of alphanumeric elements:
version = integer *( "." version_element ) version_element = alphanum *( alphanum | "-" | "_" )
Versions are compared element by element, applying normal alphanumeric comparison to each element.
A client ID is an random, non predictable sequence of alpha numeric characters:
clientId = alphanum *( alphanum )
Client IDs are generated by the server and SHOULD be created with a strong
random algorithm that contains at least 128 truly random bits. Servers MUST
ensure that client IDs are unique and SHOULD attempt to avoid reuse of client
IDs. Client IDs are encoded for delivery as strings.
See also the section called “clientId
”.
Bayeux messages are JSON encoded objects that contain an unordered sequence of name value pairs representing fields and values. Values may be a simple strings, numbers, boolean values, or complex JSON encoded objects or arrays. A Bayeux message MUST contain one and only one channel field which determines the type of the message and the allowable fields.
All Bayeux messages SHOULD be encapsulated in a JSON encoded array so that multiple messages may be transported together. A Bayeux client or server MUST accept either array of messages and MAY accept a single message. The JSON encoded message or array of messages is itself often encapsulated in transport specific formatting and encodings. Below is an example Bayeux message in a JSON encoded array representing an event sent from a client to a server:
[ { "channel": "/some/name", "clientId": "83js73jsh29sjd92", "data": { "myapp" : "specific data", value: 100 } } ]
The channel
message field MUST be included in every Bayeux message to specify
the source or destination of the message. In a request, the channel specifies
the destination of the message, and in a response it specifies the source of
the message.
The version
message field MUST be included in messages to/from the
/meta/handshake
channel to indicate the protocol version expected by the
client/server.
The minimumVersion
message field MAY be included in messages to/from the
/meta/handshake
channel to indicate the oldest protocol version that can be
handled by the client/server.
The supportedConnectionTypes
field is included in messages to/from the
/meta/handshake
channel to allow clients and servers to reveal the transports
that are supported. The value is an array of strings, with each string
representing a transport name. Defined connection types include:
long-polling
This transport is defined in Section C.7.1, “The long-polling
Transport”.
callback-polling
This transport is defined in Section C.7.2, “The callback-polling
Transport”
iframe
OPTIONAL transport using the document content of a hidden iframe element.
flash
OPTIONAL transport using the capabilities of a browser flash plugin.
All server and client implementations MUST support the long-polling
connection type and SHOULD support callback-polling
.
All other connection types are OPTIONAL.
The clientId
message field uniquely identifies a client to the Bayeux server.
The clientId
message field MUST be included in every message sent to the server
except for messages sent to the /meta/handshake
channel and MAY be omitted in
a publish message (see Section C.6, “Event Message Field Definitions”).
The clientId
message field MAY be returned in message responses except for
failed handshake requests and for publish message responses that were sent without
clientId
.
However, care must be taken to not leak the clientId
to other clients when broadcasting messages, because that would allow any other client to
impersonate the client whose clientId
was leaked.
The advice
message field provides a way for servers to inform clients of their
preferred mode of client operation so that in conjunction with server-enforced
limits, Bayeux implementations can prevent resource exhaustion and inelegant
failure modes.
Furthermore, the advice
message field provides a way for clients to inform
servers of their preferred mode of operation so that they can better inform client-side
applications of state changes (for example, connection state changes) that are relevant for applications.
The advice
field is a JSON encoded object containing general and transport specific values
that indicate modes of operation, timeouts and other potential transport
specific parameters. Advice fields may occur either in the top level of an advice object or
within a transport specific section of the advice object.
Unless otherwise specified in Section C.6, “Event Message Field Definitions” and Section C.7, “Transports”, any Bayeux response message may contain an advice field. Advice received always supersedes any previous received advice.
An example advice field sent by the server is:
"advice": { "reconnect": "retry", "timeout": 30000, "interval": 1000, "callback-polling": { "reconnect": "handshake" } }
An example advice field sent by the client is:
"advice": { "timeout": 0 }
The reconnect
advice field is a string that indicates how the client should
act in the case of a failure to connect. Defined reconnect
advice field values are:
retry
a client MAY attempt to reconnect with a /meta/connect
message after the interval
(as defined by interval
advice field or client-default backoff), and with the same credentials.
handshake
the server has terminated any prior connection status and the client MUST
reconnect with a /meta/handshake
message.
A client MUST NOT automatically retry when a reconnect advice handshake
has been received.
none
indicates a hard failure for the connect attempt.
A client MUST respect reconnect advice none
and MUST NOT automatically
retry or handshake.
Any client that does not implement all defined values of reconnect MUST NOT automatically retry or handshake.
An integer representing the period of time, in milliseconds, for the server to delay
responses to the /meta/connect
channel.
This value is merely informative for clients. Bayeux servers SHOULD honor timeout advices sent by clients.
An integer representing the minimum period of time, in milliseconds, for a client to delay
subsequent requests to the /meta/connect
channel.
A negative period indicates that the message should not be retried.
A client MUST implement interval support, but a client MAY exceed the interval provided by the server. A client SHOULD implement a backoff strategy to increase the interval if requests to the server fail without new advice being received from the server.
This is a boolean field, which if true indicates that the server has detected multiple Bayeux client instances running within the same web client.
This is an array of strings field, which if present indicates a list of host names or IP addresses that MAY be used as alternate servers with which the client may connect. If a client receives advice to re-handshake and the current server is not included in a supplied hosts list, then the client SHOULD try the hosts in order until a successful connection is establish. Advice received during handshakes with hosts in the list supersedes any previously received advice.
The connectionType
message field specifies the type of transport the client
requires for communication. The connectionType
message field MUST be included
in request messages to the /meta/connect
channel. Connection types are
listed in the section called “supportedConnectionTypes
”.
An id
message field MAY be included in any Bayeux message with an alpha numeric value:
id = alphanum *( alphanum )
Generation of IDs is implementation specific and may be provided by the
application. Messages published to /meta/**
and /service/**
SHOULD have
id
fields that are unique within the connection.
Messages sent in response to messages delivered to /meta/**
channels MUST use
the same message id as the request message.
Messages sent in response to messages delivered to /service/**
channels SHOULD
use the same message id as the request message or an id derived from the
request message id.
The timestamp
message field SHOULD be specified in the following ISO 8601
profile (all times SHOULD be sent in GMT time):
YYYY-MM-DDThh:mm:ss.ss
A timestamp message field is OPTIONAL in all Bayeux messages.
The data
message field is an arbitrary JSON encoded object that contains event
information. The data
message field MUST be included in publish messages, and a Bayeux
server MUST include the data
message field in an event delivery message.
The boolean successful
message field is used to indicate success or failure and
MUST be included in responses to the /meta/handshake
, /meta/connect
,
/meta/subscribe
, /meta/unsubscribe
, /meta/disconnect
, and publish
channels.
The subscription
message field specifies the channels the client wishes to
subscribe to or unsubscribe from. The subscription
message field MUST be
included in requests and responses to/from the /meta/subscribe
or
/meta/unsubscribe
channels.
The error
message field is OPTIONAL in any Bayeux response.
The error
message field MAY indicate the type of error that occurred when a
request returns with a false successful message. The error message field should
be sent as a string in the following format:
error = error_code ":" error_args ":" error_message | error_code ":" ":" error_message error_code = digit digit digit error_args = string *( "," string ) error_message = string
Example error strings are:
401::No client ID 402:xj3sjdsjdsjad:Unknown Client ID 403:xj3sjdsjdsjad,/foo/bar:Subscription denied 404:/foo/bar:Unknown Channel
An ext
message field MAY be included in any Bayeux message. Its value SHOULD be a
JSON encoded object with top level names distinguished by implementation names (for example
"com.acme.ext.auth").
The contents of ext
message field may be arbitrary values that allow extensions to be
negotiated and implemented between server and client implementations.
The connectionId
message field was used during development of the Bayeux protocol
and its use is now deprecated and SHOULD not be used.
A Bayeux client initiates a connection negotiation by sending a message to the
/meta/handshake
channel.
In case of HTTP same domain connections, the handshake requests MUST be sent to the server
using the long-polling
transport, while for cross domain connections the handshake
request MAY be sent with the long-polling
transport and failing that with the
callback-polling
transport.
A handshake request MUST contain the following message fields:
channel
value MUST be "/meta/handshake"
version
The version of the protocol supported by the client
supportedConnectionTypes
An array of the connection types supported by the client for the purposes
of the connection being negotiated (see the section called “supportedConnectionTypes
”).
This list MAY be a subset of the connection types actually supported if
the client wishes to negotiate a specific connection type.
A handshake request MAY contain the message fields:
minimumVersion
ext
id
A client SHOULD NOT send any other message in the request with a handshake message. A server MUST ignore any other message sent in the same request as a handshake message. An example handshake request is:
[ { "channel": "/meta/handshake", "version": "1.0", "minimumVersion": "1.0beta", "supportedConnectionTypes": ["long-polling", "callback-polling", "iframe"] } ]
A Bayeux server MUST respond to a handshake request with a handshake response message. How the handshake response is formatted depends on the transport that has been agreed between client and server.
A successful handshake response MUST contain the message fields:
channel
value MUST be "/meta/handshake"
version
supportedConnectionTypes
The connection types supported by the server for the purposes of the connection being negotiated. This list MAY be a subset of the connection types actually supported if the server wishes to negotiate a specific connection type. This list MUST contain at least one element in common with the supportedConnectionType provided in the handshake request. If there are no connectionTypes in common, the handshake response MUST be unsuccessful.
clientId
A newly generated unique ID string.
successful
value true
A successful handshake response MAY contain the message fields:
minimumVersion
advice
ext
id
same value as request message id
authSuccessful
value true
, this field MAY be included to support prototype client
implementations that required the authSuccessful
field
An example successful handshake response is:
[ { "channel": "/meta/handshake", "version": "1.0", "minimumVersion": "1.0beta", "supportedConnectionTypes": ["long-polling","callback-polling"], "clientId": "Un1q31d3nt1f13r", "successful": true, "authSuccessful": true, "advice": { "reconnect": "retry" } } ]
An unsuccessful handshake response MUST contain the message fields:
channel
value MUST be "/meta/handshake"
successful
value false
error
a string with the description of the reason for the failure.
An unsuccessful handshake response MAY contain the message fields:
supportedConnectionTypes
The connection types supported by the server for the purposes of the connection being negotiated. This list MAY be a subset of the connection types actually supported if the server wishes to negotiate a specific connection type.
advice
version
minimumVersion
ext
id
same value as request message id
An example unsuccessful handshake response is:
[ { "channel": "/meta/handshake", "version": "1.0", "minimumVersion": "1.0beta", "supportedConnectionTypes": ["long-polling","callback-polling"], "successful": false, "error": "Authentication failed", "advice": { "reconnect": "none" } } ]
For complex connection negotiations, multiple handshake messages may be
exchanged between the Bayeux client and server. The handshake response will
set the successful
message field to false until the handshake process is complete.
The advice
and ext
message fields may be used to communicate additional information
needed to complete the handshake process. An unsuccessful handshake response
with reconnect
advice field of handshake
is used to continue the connection
negotiation. An unsuccessful handshake response with reconnect
advice field of
none
is used to terminate connection negotiations.
After a Bayeux client has discovered the server's capabilities with a
handshake exchange, a connection is established by sending a message to the
/meta/connect
channel. This message may be transported over any of the
transports indicated as supported by the server in the handshake response.
A connect request MUST contain the message fields:
channel
value MUST be "/meta/connect"
clientId
The client ID returned in the handshake response
connectionType
The connection type used by the client for the purposes of this connection.
A connect request MAY contain the message fields:
ext
id
A client MAY send other messages in the same HTTP request with a connection message. A server MUST handle any other message sent in the same request as a connect message after the handling of the connect message is complete.
An example connect request is:
[ { "channel": "/meta/connect", "clientId": "Un1q31d3nt1f13r", "connectionType": "long-polling" } ]
A transport MUST maintain one and only one outstanding connect message. When a
HTTP response that contains a /meta/connect
response terminates, the client
MUST wait at least the interval
specified in the last received advice
before
following the advice to reestablish the connection.
A Bayeux server MUST respond to a connect request with a connect response message over the same transport used for the request.
A Bayeux server MAY wait to respond until there are event messages available in the subscribed channels for the client that need to be delivered to the client.
A connect responses MUST contain the message fields:
channel
value MUST be "/meta/connect"
successful
boolean indicating the success or failure of the connection
A connect response MAY contain the message fields:
error
advice
ext
clientId
The client ID returned in the handshake response
id
same value as request message id
timestamp
An example connect response is:
[ { "channel": "/meta/connect", "successful": true, "error": "", "clientId": "Un1q31d3nt1f13r", "timestamp": "12:00:00 1970", "advice": { "reconnect": "retry" } } ]
The client MUST maintain only a single outstanding connect message. If the server does not have a current outstanding connect and a connect is not received within a configured timeout, then the server SHOULD act as if a disconnect message has been received.
When a connected client wishes to cease operation it should send a request to
the /meta/disconnect
channel for the server to remove any client-related state. The
server SHOULD release any waiting meta message handlers. Bayeux client
applications SHOULD send a disconnect request when the user shuts down a
browser window or leaves the current page. A Bayeux server SHOULD NOT rely
solely on the client sending a disconnect message to remove client-related state
information because a disconnect message might not be sent from the client or
the disconnect request might not reach the server.
A disconnect request MUST contain the message fields:
channel
value MUST be "/meta/disconnect"
clientId
The client ID returned in the handshake response
A disconnect request MAY contain the message fields:
ext
id
An example disconnect request is:
[ { "channel": "/meta/disconnect", "clientId": "Un1q31d3nt1f13r" } ]
A Bayeux server MUST respond to a disconnect request with a disconnect response.
A disconnect response MUST contain the message fields:
channel
value MUST be "/meta/disconnect"
successful
boolean value indicated the success or failure of the disconnect request
A disconnect response MAY contain the message fields:
clientId
The client ID returned in the handshake response
error
ext
id
same value as request message id
An example disconnect response is:
[ { "channel": "/meta/disconnect", "successful": true } ]
A connected Bayeux client may send subscribe messages to register interest in a channel and to request that messages published to that channel are delivered to itself.
A subscribe request MUST contain the message fields:
channel
value MUST be "/meta/subscribe"
clientId
The client ID returned in the handshake response
subscription
a channel name or a channel pattern or an array of channel names and channel patterns.
A subscribe request MAY contain the message fields:
ext
id
An example subscribe request is:
[ { "channel": "/meta/subscribe", "clientId": "Un1q31d3nt1f13r", "subscription": "/foo/**" } ]
A Bayeux server MUST respond to a subscribe request with a subscribe response message.
A Bayeux server MAY send event messages for the client in the same HTTP response as the subscribe response, including events for the channels just subscribed to.
A subscribe response MUST contain the message fields:
channel
value MUST be "/meta/subscribe"
successful
boolean indicating the success or failure of the subscribe
subscription
a channel name or a channel pattern or an array of channel names and channel patterns.
A subscribe response MAY contain the message fields:
error
advice
ext
clientId
The client ID returned in the handshake response
id
same value as request message id
timestamp
An example successful subscribe response is:
[ { "channel": "/meta/subscribe", "clientId": "Un1q31d3nt1f13r", "subscription": "/foo/**", "successful": true, "error": "" } ]
An example failed subscribe response is:
[ { "channel": "/meta/subscribe", "clientId": "Un1q31d3nt1f13r", "subscription": "/bar/baz", "successful": false, "error": "403:/bar/baz:Permission Denied" } ]
A connected Bayeux client may send unsubscribe messages to cancel interest in a channel and to request that messages published to that channel are not delivered to itself.
An unsubscribe request MUST contain the message fields:
channel
value MUST be "/meta/unsubscribe"
clientId
The client ID returned in the handshake response
subscription
a channel name or a channel pattern or an array of channel names and channel patterns.
An unsubscribe request MAY contain the message fields:
ext
id
An example unsubscribe request is:
[ { "channel": "/meta/unsubscribe", "clientId": "Un1q31d3nt1f13r", "subscription": "/foo/**" } ]
A Bayeux server MUST respond to a unsubscribe request with a unsubscribe response message.
A Bayeux server MAY send event messages for the client in the same HTTP response as the unsubscribe response, including events for the channels just unsubscribed to as long as the event was processed before the unsubscribe request.
An unsubscribe response MUST contain the message fields:
channel
value MUST be "/meta/unsubscribe"
successful
boolean indicating the success or failure of the unsubscribe operation
subscription
a channel name or a channel pattern or an array of channel names and channel patterns.
A unsubscribe response MAY contain the message fields:
error
advice
ext
clientId
The client ID returned in the handshake response
id
same value as request message id
timestamp
An example unsubscribe response is:
[ { "channel": "/meta/unsubscribe", "clientId": "Un1q31d3nt1f13r", "subscription": "/foo/**", "successful": true, "error": "" } ]
Application events are published in event messages sent from a Bayeux client to a Bayeux server and are delivered in event messages sent from a Bayeux server to a Bayeux client.
A Bayeux client can publish events on a channel by sending event messages. An event message MAY be sent in new HTTP request or it MAY be sent in the same HTTP request as any message other than a handshake meta message.
A publish message MAY be sent from an unconnected client (that has not performed handshaking and thus does not have a client ID). It is OPTIONAL for a server to accept unconnected publish requests and they should apply server specific authentication and authorization before doing so.
A publish event message MUST contain the message fields:
channel
The channel to which the message is published
data
The message data as an arbitrary JSON encoded object
A publish event message MAY contain the message fields:
clientId
The client ID returned in the handshake response
id
A unique ID for the message generated by the client
ext
An example event message is:
[ { "channel": "/some/channel", "clientId": "Un1q31d3nt1f13r", "data": "some application string or JSON encoded object", "id": "some unique message id" } ]
A Bayeux server MAY respond to a publish event message with a publish event acknowledgement.
A publish event message response MUST contain the message fields:
channel
The channel to which the message was published
successful
boolean indicating the success or failure of the publish
A publish event response MAY contain the message fields:
id
error
ext
An example event reponse message is:
[ { "channel": "/some/channel", "successful": true, "id": "some unique message id" } ]
Event messages MUST be delivered to clients if the client is subscribed to the channel of the event message.
Event messages MAY be sent to the client in the same HTTP response as any other message other than a
/meta/handshake
response.
If a Bayeux server has multiple HTTP requests from the same client, the server SHOULD deliver all available messages
in the HTTP response that will be sent immediately in preference to waking a waiting connect meta message request.
Event message delivery MAY not acknowledged by the client.
A deliver event message MUST contain the message fields:
channel
The channel to which the message was published
data
The message as an arbitrary JSON encoded object
A deliver event response MAY contain the message fields:
id
Unique message ID from the publisher
ext
advice
An example event deliver message is:
[ { "channel": "/some/channel", "data": "some application string or JSON encoded object", "id": "some unique message id" } ]
The "long-polling" transport is a polling transport that attempts to minimize both latency in server-client message delivery, and the processing/network resources required for the connection. In "traditional" polling, servers send and terminate responses to requests immediately, even when there are no events to deliver, and worst-case latency is the polling delay between each client request. Long-polling server implementations attempt to hold open each request until there are events to deliver; the goal is to always have a pending request available to use for delivering events as they occur, thereby minimizing the latency in message delivery. Increased server load and resource starvation are addressed by using the reconnect and interval advice fields to throttle clients, which in the worst-case degenerate to traditional polling behaviour.
Messages SHOULD be sent to the server as the body of an application/json
HTTP POST request with UTF-8 encoding.
Alternatively, messages MAY be sent to the server as the message
parameter of a
application/x-www-form-urlencoded
encoded POST request.
If sent as form encoded, the Bayeux messages are sent as the message
parameter in one of
the following forms as:
Single valued and contain a single Bayeux message
Single valued and contain an array of Bayeux message
Multi valued and contain a several individual Bayeux message
Multi valued and contain a several arrays of Bayeux message
Multi valued and contain a mix of individual Bayeux messages and arrays of Bayeux message
Messages SHOULD be sent to the client as unencapsulated body content of a
HTTP POST response with content type application/json
with UTF-8 encoding.
A long-polling
response message may contain an advice field containing transport-specific
fields to indicate the mode of operation of the transport.
For the long-polling
transport, the advice field MAY contain the following fields:
timeout
the number of milliseconds the server will hold the long poll request
interval
the number of milliseconds the client SHOULD wait before issuing another long poll request
Messages SHOULD be sent to the server as the message
parameter of a url encoded
HTTP GET request.
Responses are sent wrapped in a JavaScript callback in order to facilitate delivery.
As specified by the JSONP pseudo-protocol,
the name of the callback to be triggered
is passed to the server via the jsonp
HTTP GET parameter.
In the absence of such a parameter, the name of the callback defaults to jsonpcallback
.
The called function will be passed a JSON encoded array of Bayeux messages.
A callback-polling
response message may contain an advice field containing transport-specific
fields to indicate the mode of operation of the transport.
For the callback-polling
transport, the advice field MAY contain the following fields:
timeout
the number of milliseconds the server will hold the long poll request
interval
the number of milliseconds the client SHOULD wait before issuing another long poll request
The Bayeux protocol may be used with:
No authentication
Container supplied authentication (e.g. BASIC authentication or cookie managed session based authentication)
Bayeux extension authentication that exchanges authentication credentials
and tokens within Bayeux messages ext
fields
For Bayeux authentication, no algorithm is specified for generating or
validating security credentials or token. This version of the protocol only
defines that the ext
field may be used to exchange authentication challenges,
credentials, and tokens and that the advice
field may be used to control
multiple iterations of the exchange.
The connection negotiation mechanism may be used to negotiate authentication or request re-authentication.
The AJAX hijacking vulnerability is when an attacking web site uses a script tag to execute JSON encoded content obtained from an AJAX server. The Bayeux protocol is not vulnerable to this style of attack when cookies are not used for authentication and a valid client ID is needed before private client data is returned. The use of POST by some transports further protects against this style of attack.
Current HTTP client implementations are RECOMMENDED to allow only 2 connections between a client and a server. This presents a problem when multiple instances of the Bayeux client are operating in multiple tabs or windows of the same browser instance. The 2 connection limit can be consumed by outstanding connect meta messages from each tab or window and thus prevent other messages from being delivered in a timely fashion.
It is RECOMMENDED that Bayeux server implementations use the cookie "BAYEUX_BROWSER" to identify a HTTP client and to thus detect multiple Bayeux clients running within the same HTTP client. Once detected, the server SHOULD not wait for messages in connect and SHOULD use the advice interval mechanism to establish traditional polling.
It is RECOMMENDED that Bayeux client implementations use client side persistence or cookies to detect multiple intances of Bayeux clients running within the same HTTP client. Once detected, the user MAY be offered the option to disconnect all but one of the clients. It MAY be possible for client implementations to use client side persistence to share a Bayeux client instance.
The publish/subscribe paradigm that is directly supported by the Bayeux
protocol is difficult to use to efficiently implement the request/response
paradigm between a client and a server. The /service/**
channel space has been
designated as a special channel space to allow efficient transport of
application request and responses over Bayeux channels. Messages published to
service channels are not distributed to other Bayeux clients so these channels
can be used for private requests between a Bayeux client and a Bayeux server.
A trivial example would be an echo service, that sent any message received from
a client back to that client unaltered. Bayeux clients would subscribe the
/service/echo
channel, but the Bayeux server would not need to record this
subscription. When a client publishes a message to the /service/echo
channel,
it will be delivered only to server-side subscribers (in an implementation
dependent fashion). The server side subscriber for the echo service would handle
each message received by publishing a response directly to the client
regardless of any subscription. As the client has subscribed to /service/echo
,
the response message will be routed correctly within the client to the
appropriate subscription handler.
These instructions are only for CometD committers that want to perform a CometD release.
$ git clone git://github.com/cometd/cometd.git release/cometd ... $ cd release/cometd $ mvn clean install ... $ mvn release:prepare # Specify the SVN tag to be just the version number ... $ mvn release:perform -Darguments=-Dgpg.passphrase=... ... $ git push origin master
Login to Sonatype OSS.
Click on "Staging Repositories" and you should see the staged project just uploaded by the
mvn release:perform
command issued above, with status "open".
Tick the checkbox correspondent to the open staged project, choose "Close" from the toolbar,
enter a description such as "CometD Release 1.0.0", then click on the "Close" button.
This will make the staged project downloadable for testing, but not yet published to central.
Tick again the checkbox correspondent to the closed staged project, choose "Release" from the toolbar, enter a description such as "CometD Release 1.0.0", then click on the "Release" button. This will publish the project to the Maven Central Repository.
$ cd $COMETD $ git checkout <version> ... $ mvn clean install -DskipTests=true ... $ cd cometd-distribution $ mvn assembly:assembly
This creates the distribution tarball in the cometd-distribution/target
directory.
Then upload it.
$ cd $COMETD/cometd-distribution/target $ scp cometd-<version>-distribution.tar.gz <user>@download.cometd.org:/srv/www/vhosts.d/download.cometd.org/
Make sure the archetype catalog $HOME/.m2/archetype-catalog.xml
refers to the release just prepared, then upload it.
$ scp $HOME/.m2/archetype-catalog.xml <user>@cometd.org:/srv/www/vhosts.d/cometd.org/
$ cd $COMETD/cometd-java $ mvn javadoc:aggregate-jar $ scp target/cometd-java-<version>-javadoc.jar <user>@docs.cometd.org:/srv/www/vhosts.d/docs.cometd.org/
Then ssh
into the host, unzip the javadocs jar file into the apidocs
directory.
Perform a release in JIRA.
$ git clone https://github.com/cometd/cometd-dojo.git $ cd cometd-dojo
Make sure that the JavaScript files are in sync with the main CometD distribution.
Edit the package.json
file, update the CometD version and the Dojo version, then commit it.
$ git push $ git tag -a "<version>" -m "CometD Release <version>." $ git push --tags