Avg. Rating 5.0
Tags:



Problem

Managing distributed presence in a RTMFP group is a complex problem. An example of this would be maintaining a list of users who are participating in a global chat session, connected to Stratus, without maintaining the user list state on a 3rd party server. Stratus only serves as an introduction server so you do not have the ability to store additional information at the server. Put differently, how do you make everyone participating in a RTMFP group aware of one another without using an external service?

Solution

There is no perfect, end-all solution to this problem. However, there are methods that are better than others. Detailed here is one of the more reliable methods for maintaining a distributed presence in a RTMFP group that utilizes posting and directed routing.

Detailed explanation

RTMFP groups are designed such that in larger groups, not everyone will know about every single participant in a group.  Ideally, in most scenarios you do not want everyone aware of everyone else in the group because this does not scale well.  Imagine a million users participating in the same RTMFP group.  It can happen, but it is unrealistic to expect every user to know about every other user.  In smaller, more contained groups however, you may want for everyone to be aware of each other.  This is where the distributed presence problem comes in. 

When participating in RTMP groups connected to Stratus, the Stratus sever only services peer lookups and introduces peers to each other.  You cannot configure the server to store information such as friendly user names.  In a group, each user is described by their peerID (NetConnection.nearID in Flash Player).  This is the identifier that Stratus uses to identify each participant.  PeerID's are opaque strings and are not friendly to users and user interfaces as say, a user name would be.

Let's say that you have created a chat room using NetGroup posting that is connected to Stratus and you want to maintain a list of the users who are in that room.  You could maintain that list on a 3rd party server, but that is one extra resource to maintain.  Using posting and directed routing, you could maintain that user list within the group itself.

The solution works like this (see attached files for the full source):

Each user maintains an object containing information about each user.  This information includes the user name, a timestamp, and that persons peerID (their unique identifier).

/**
 * Internal object used for storing user information in the user list.
 *
 */
internal class UserObject extends Object
{
    
    public var id:String;
    public var name:String;
    public var stamp:Number;
    
} // UserObject

 

Periodically, each user will announce their self to the group using a NetGroup.post().

/**
         * Timer event handler to announce your presence to the group.
         * @param te
         *
         */        
        protected function announceSelf(te:TimerEvent = null):void
        {
           
            // create a new object to be sent to other participants
            // notifying them of your presence.
            var msg:KeepAlive = new KeepAlive();
            msg.seq = m_sequenceNumber++;
            msg.id = m_nearID;
            msg.name = m_userName;
           
            // NetGroup.post will return the messageID of the post if successful
            // otherwise it will return null on error
            // If this returns a null, then make sure you selected postingEnabled
            // on your GroupSpecifier object
            if (!post(msg))
            {
                throw new Error("Your GroupSpecifier must have posting enabled " +
                    "in order to use the UserList class.");
            }
           
            // update time stamp for self
            m_userList[m_nearID].stamp = getTimer();
        }

 

The other users in the group who receive this announcement will process it, and add that user's information to their user list if they do not already have an entry for that person.  Otherwise, the timestamp for that user's entry will be updated to a current timestamp.

/**
         * Updates the time stamp for a user when acknowledgement is
         * received of their presence.
         * @param user
         *
         */        
        protected function updateUser(user:Object):void
        {
            // grab the current timestamp
            user.stamp = getTimer();
           
            // no record of the user
            if (!m_userList[user.id])
            {
               
                // create a new user object
                // (so we're not wasting memory saving the sequence number)
                var uo:UserObject = new UserObject();
                uo.id = user.id;
                uo.name = user.name;
               
                m_userList[user.id] = uo;
               
                // dispatch user added event
                dispatchEvent(new UserListStatusEvent(
                    UserListStatusEvent.USER_ADDED, true, false, uo));
            }
           
            // update user list entry with a new timestamp
            m_userList[user.id].stamp = getTimer();
           
        }

 

Meanwhile, each user is also running a timer that inspects the timestamps in each user entry for their local snapshot of the user list.  If the time difference between the current time and time that user was last heard from exceeds a defined value, then that user will be purged from the local snapshot of the user list.

/**
         * Timer event handler to loop through the names and see who may no
         * longer be present in the group.
         * @param te
         *
         */        
        protected function expireNames(te:TimerEvent = null):void {
           
            // get current time stamp so we can calculate the time elapsed
            // since last post
            var currentStamp:uint = getTimer();
            var age:Number = 0;
           
            // loop through the user list object
            for (var p:String in m_userList)
            {
               
                // no need to check yourself
                if (p == m_nearID)
                {
                    continue;
                }
               
                // calculate age
                age = currentStamp - m_userList[p].stamp;
               
                // purge users who have expired
                if (age > m_expired)
                {
                    dispatchEvent(new UserListStatusEvent(
                        UserListStatusEvent.USER_REMOVED, true, false, m_userList[p]));
                    delete m_userList[p];
                    continue;
                }
            }
        }

 

For a user to get a snapshot of the group at the time they join, rather than waiting for each announce to trickle in, you can use directed routing.  When the user becomes aware if their first neighbor, they can ask that person for a copy of their user list. 

/**
         * Submits a request for a snapshot of the first neighbor's users list.
         * @param id
         *
         */
        protected function requestUsers(id:String):void
        {
            var request:ListRoutingObject = new ListRoutingObject();
            request.destination = id;
            request.sender = groupAddress;
            request.type = ListRoutingObject.REQEUST;
            sendToNearest(request, request.destination);
        }

 

The person who receives the request responds with a copy of their user list and the time of the current timestamp on their computer.  The new user then adds them to their local list and calculates new timestamps based on the age for each user object relative to their local timer.  An event will get dispatched for each user added.

//--------------------------------------------------------------------------
        // Directed Routing User List Request
        // This is the process by which a new joiner would ask a
        // neighbor for a copy of their users list.
        //--------------------------------------------------------------------------
       
        protected function processRouting(info:Object):void
        {
           
            // info properties...
            // message - object that was sent
            // from - group address of neighbor info was received from
            // fromLocal - if true, then from self and process, else pass along
           
            if (info.message.destination == groupAddress)
            {
                // neighbor has requested a copy of user list
                if (info.message.type == ListRoutingObject.REQEUST)
                {
                    var response:ListRoutingObject = new ListRoutingObject();
                    response.destination = info.message.sender;
                    response.time = getTimer();
                    response.users = m_userList;
                    response.type = ListRoutingObject.RESPONSE;
                   
                    // send the requester a copy of user list
                    sendToNearest(response, response.destination);
                }
               
                // neighbor has responded with a copy of their user list
                if (info.message.type == ListRoutingObject.RESPONSE)
                {
                    var users:Object = info.message.users;
                    var neighborsTime:Number = info.message.time;
                    var neighborsAge:Number = 0;
                    var localAge:Number = 0;
                   
                    // loop through and calculate new stamp relative to known
                    // age and this system's clock
                    for (var p:String in users)
                    {
                       
                        neighborsAge = neighborsTime - users[p].stamp + 1000;
                       
                        // dispatch new user added event
                        if (!m_userList[p])
                        {
                            dispatchEvent(new UserListStatusEvent(
                                UserListStatusEvent.USER_ADDED, true, false, users[p]));
                           
                            // update entry in user list
                            m_userList[p] = users[p];                    
                           
                            // calculate age, relevant to neighbors clock, and
                            // subtract age from this clock
                            // add one second to account for request and
                            // processing time
                            m_userList[p].stamp = getTimer() - neighborsAge;
                        }
                        else
                        {
                            // calculate the age relative to this instance's clock
                            localAge = getTimer() - m_userList[p].stamp;
                           
                            // if neighbor has a record with a more recent age,
                            // use their's
                            if (neighborsAge < localAge)
                            {
                                m_userList[p].stamp = getTimer() - neighborsAge;
                            }
                        }
                       
                    } // for
                   
                } // if RESPONSE
               
            }
            else if (!info.fromLocal)
            {
                // not from local, pass it along
                sendToNearest(info.message, info.message.destination);
            }
        }

 


Now the default times for the timers may appear high and a it may take a user up to 5 minutes before it disappears from a group and you get a User Removed event.  The announce time was set to 2 minutes, and the expire timeout to 5 minutes on purpose.

private const DEFAULT_ANNOUNCE_TIME:Number = 120000;    // every 2 minutes
        private const DEFAULT_EXPIRE_CHECK:Number = 60000;        // every minute
        private const DEFAULT_EXPIRE_TIMEOUT:Number = 300000;    // 5 minutes

 

As a developer, when implementing a solution like this, you need to be conscious of the bandwidth impact this solution can have.  The defaults here work reasonably for up to a couple hundred users.  Beyond that, this solution becomes impractical and the distributed presence problem takes on the form of a search problem, which isn't practical to try and solve in a group.  For each user, you need to be aware of the size of the keepalive sent to the group times the number of persons in the group averaged out by the frequency that you send it.  So, consider each keepalive is on average, 100 bytes and you have 200 participants.  At every 2 minutes you announce a keepalive to the group.  This averages out to around 167 bytes per second, or 1.3Kbps per second.  Simply participating in a group is a base of 2Kbps.  In this scenario you'll end up with about a sustained bandwidth of 4Kbps (accounting for overhead).  If you were to reduce the announce time to every 30 seconds, this would increase the keepalive traffic to about 5.4Kbps + base group traffic and overhead.

Posting + directed routing makes for a reasonable solution to this problem.  Using directed routing only can encounter a problem where users may be removed due to partitioning that can happen in the group.  Using Object Replication would require an arbiter to generate the index numbers and has other various factors that have to be considered.

In the attached files is a UserList class, an event class for the UserList, and a Flash Builder 4 example demonstrating a very minimal example of using the UserList.

In order to use the example, you will be required to obtain a Stratus Developer key if you have not already done so.  You can get a developer key here:  https://www.adobe.com/cfusion/entitlement/index.cfm?e=stratus

The list of the users within the UserList class is a primitive Object by design so that this class can be used with both Flash CS5 as well as Flash Builder 4.  The class can be modified to use an ArrayCollection or other dataprovider type so that you can bind directly to a Spark component.

Also, I did not illustrate it in the attached files, but you can set a timer in the app that owns the UserList object to check for clients that are "idle" but not yet expired and colorize their name in a List component on the stage.  To do this, you set a timer to periodically check the userList propery from the UserList class, then compare the age of the time stamp for each user object against a getTimer().  The method is similar to how the expire timer works.  If the age is past a certain threshhold you can change or fade the color of that user in the List component (using a custom ItemRenderer or other means).  Doing this inside the class is possible but you would need to add two events, one for IDLE and one for UNIDLE (when a user becomes active again).

RTMFPUserList.zip
[RTMFP UserList classes and example Flash Builder 4 MXML to demonstrate usage]

+
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. Permissions beyond the scope of this license, pertaining to the examples of code included within this work are available at Adobe.

Report abuse

Related recipes