SitePen Support

Private Messages with cometD Chat

by Greg WilkinsOctober 14th, 2008

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.

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]
Webtide

8 Responses to “Private Messages with cometD Chat”

  1. Filip Hanik Says:

    Correct me if I am wrong, but this seems like a workaround around the publish/subscribe and the behavior of this would be implementation specific. Ie, in any other container implementing the spec, a client could subscribe to /service/privatechat and still receive the message, if not, then that would violate the protocol spec.

    The protocol doesn’t make the distinction on the channel name, hence this is not really a private chat, its a public chat that in your demo was implemented as private.

    If this is a desired feature, why not add a
    /meta/client-message

    or use something that makes the distinction in the protocol, not in a custom implementation.

    now, tell me that I’m wrong :)

  2. Peter Says:

    I am not sure what is going on, but this code:
    String peerId = membersMap.get(peerName);

    inside privateChat method throws an exception:

    Caused by: java.lang.NullPointerException
    at org.cometd.demo.ChatService.privateChat(ChatService.java:234)

    (peerName is not null but exception is thrown anyways)

    Any idea why would that be?

  3. Greg Wilkins Says:

    Filip.

    the /service/* name space is part of the spec, so this should not be implementation specific.

    cheers

  4. Greg Wilkins Says:

    From the spec:

    2.2.3 Service Channel

    The channels within the “/service/” channel segement 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 meta 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 9.

  5. Damian Says:

    You are wrong.

    Messages published on “/service” channels are not broadcast to all clients.

    From the Bayeux Protocol spec:

    “2.2.3 Service Channel
    The channels within the “/service/” channel segement 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 meta 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 9.”

    “9. Request / Response operation with service channels
    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 server side handlers.

    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 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 depedent fashion). The server side handler 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 application handler.”

  6. Ben Krembs Says:

    First off, great tutorial.

    Second, we frequently see a request to bundle authentication with a system like this. There’s an intriguing tidbit in the article:

    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.

    How would this work if one wanted to do username/password authentication? Googling uncovers painfully little on ClientBayeuxListener, and digging around in the source hasn’t yielded much for me yet. For example, even if I implemented clientAdded(Client client) — I only have access to the Client object. How would one even get a password string from that?

    Any suggestions? I could really use a push in the right direction..

  7. ray Says:

    hello,I have more requests from the same browser client and these requests subscribe the same channel(service/imservice) , shoud I need to subscribe for each request ? if only one request subscribe the channel, I find the browser client can’t receive the message sended to the channel ? How shoud I do ? Can you help me? thanks!

  8. Manoj Tiwari Says:

    http://cometd.org/documentation/cometd-java/server/services

    The contract that the BayeuxService class requires for callback methods is that the methods must have one of the following signatures:

    // Obtains the remote client object and the message object
    public void processEcho(Client remote, Message message)

    // Obtains the remote client object and the message’s data object
    // (additional message information, such as the channel or the id is lost)
    public void processEcho(Client remote, Map data)

    // Obtains the remote client object, the channel name, the message object and the message id
    public void processEcho(Client remote, String channelName, Message message, String messageId)

    // Obtains the remote client object, the channel name, the message’s data object and the message id
    public void processEcho(Client remote, String channelName, Map data, String messageId)

    Therefore, the method “public void trackMembers(final Client joiner, final String channelName, Map data)” doesn’t get invoked by Bayeux Server.

    Method signature should look like
    public void trackMembers(final Client joiner, final String channelName, Map data, String sId)

Leave a Reply



Copyright 2014 Comet Daily, LLC. All Rights Reserved