One of the common misconceptions regarding cometD, is that it can only do publish-subscribe messaging. While this misconception may be encouraged by the protocol design, it is definitely possible to do private messaging with cometD. In this article, I’ll look as some of the recent additions to the cometD chat demo and how they use private messages to implement member lists and private chat.
The basics of the cometd chat demo is definitely publish subscribe. All clients subscribe to the “/chat/demo” channel to receive chat, and publish to the same channel to send chat:
dojox.cometd.subscribe("/chat/demo", room, "_chat"); dojox.cometd.publish("/chat/demo", { user: room._username, join: true, chat: room._username + " has joined" });
The cometD server is able to support this style of chat without any server-side chat specific components. But a chat room without a members list is a pretty basic chat room, and for that we need to introduce some server-side services to track members. For this demo, tracking members is a little harder than normal, because we are not running behind any authentication, so we cannot easily identify the user. If we had a real authentication mechanism in place, tracking users would simply be a matter of creating a ClientBayeuxListener
instance and implementing the clientAdded
and clientRemoved
methods. So without authentication, the demo trusts the clients to tell us who they are in a join message.
So on the server, we create a ChatService
that extends BayeuxService
and registers to listen for all messages to chat channels:
public class ChatService extends BayeuxService { private final ConcurrentMap<String, Map<String, String>> _members = new ConcurrentHashMap<String, Map<String, String>>(); public ChatService(Bayeux bayeux) { super(bayeux, "chat"); subscribe("/chat/**", "trackMembers"); }
This requests that the trackMembers method be called for all messages to “/chat/**”. This method is implemented to trigger on the join messages and to find/create a map of username to cometD clientId for each room encountered:
public void trackMembers(final Client joiner, final String channelName, Map<String, Object> data) { if (Boolean.TRUE.equals(data.get("join"))) { Map<String, String> membersMap = _members.get(channelName); if (membersMap == null) { Map<String, String> newMembersMap = new ConcurrentHashMap<String, String>(); membersMap = _members.putIfAbsent(channelName, newMembersMap); if (membersMap == null) membersMap = newMembersMap; }
The joining user is then added to the map of all users in the chat room and the updated set of all user names is published to the channel so that all clients receive the list:
final String userName = (String)data.get("user"); members.put(userName, joiner.getId()); getBayeux().getChannel(channelName, false) .publish(getClient(), members.keySet(), null);
As well as joining the chat room, we need to track the leaving the chat room. The most reliable way to do this is to register a RemoveListener
against the client, which is called if the client is removed from cometD for any reason:
final Map<String, String> members = membersMap; joiner.addListener(new RemoveListener() { public void removed(String clientId, boolean timeout) { members.values().remove(clientId); Channel channel = getBayeux().getChannel(channelName, false); if (channel != null) channel.publish(getClient(), members.keySet(), null); } }); } }
So that was a little more involved that if we had an authentication mechanism, but it’s simple enough and we now have the server side tracking our users and maintaining a map between username and cometD clientID. This makes it relatively simple to add a service for private messages between users. We start by adding another subscription to the ChatService for private messages:
public ChatService(Bayeux bayeux) { super(bayeux, "chat"); subscribe("/chat/**", "trackMembers"); subscribe("/service/privatechat", "privateChat"); }
This subscribes the privateChat method to the channel “/service/privatechat”. Any channel in “/service/**” is special, in that it is not a broadcast publish/subscribe channel and any messages published is delivered only to the server or to clients that are explicitly called. In this case, clients publish to the privatechat channel and the messages are sent only to the server. The client JavaScript is updated to handle name::text as a way of sending a private message:
chat: function(text){ var priv = text.indexOf("::"); if (priv > 0) { dojox.cometd.publish("/service/privatechat", { room: "/chat/demo", user: room._username, chat: text.substring(priv + 2), peer: text.substring(0, priv) }); } else { dojox.cometd.publish("/chat/demo", { user: room._username, chat: text }); } },
The privateChat service method is implemented to create a private message from the data passed and deliver it to the identified peer client and echo it back to the talking client:
public void privateChat(Client source, String channel, Map<String, Object> data,String id) { String room = (String)data.get("room"); Map<String, String> membersMap = _members.get(room); String peerName = (String)data.get("peer"); String peerId = membersMap.get(peerName); if (peerId!=null) { Client peer = getBayeux().getClient(peerId); if (peer!=null) { Map<String, Object> message = new HashMap<String, Object>(); message.put("chat", data.get("chat")); message.put("user", data.get("user")); message.put("scope", "private"); peer.deliver(getClient(), roomName, message, id); source.deliver(getClient(), roomName, message, id); return; } } }
The key code here are the calls to deliver on the source and peer Client
instances. Unlike a call to Channel.publish(...)
, which will broadcast a message to all subscribers for a channel, a call to Client.deliver(...)
will deliver the message to the channel handler only for that client. Thus both the source and the peer clients will receive the private message on the “/chat/demo” channel, but no other subscribers to the “/chat/demo” channel will receive that message.
It is the distinction between Channel.publish(...)
and Client.deliver(...)
that is the key to private messaging in cometD. Both use the channel to identify which handler(s) on the client will receive the message, but only the publish method uses the list of subscribers maintained by the server to determine which clients to deliver a published message to.