Flash Lobby - creating a lobby and rooms application using a proxied shared object.

A regular question on the flashcomm mailing list at chattyfig.figleaf.com has been "how do I show what rooms are in use and who is in each room from within my lobby?" A good way to let the lobby know what is going on in each room is to open a NetConnection between each room and the lobby and then let each room update a shared object belonging to the lobby.

This short article is based on a series of posts I made on the flashcomm mailing list and includes a simple demonstration program you can download and experiment with.

-Brian Lesser

Sample Files:

Download Zip chLobbyRooms.zip (186KB ZIP)

Approaches to the Problem

There are three ways to let the lobby know what is going on in each room:

  1. Use Flash Remoting to connect to a database. Whenever a client connects to a room or the lobby each application instance updates the database. The drawback to this is that the lobby instance must poll the database constantly to see what has changed.
  2. Use remote methods to send information to the lobby - connect each room instance to the lobby instance using a NetConnection object and then have the room use remote method calls to send user and room information to the lobby. The Macromedia sample applications sample_lobby and sample_room use this method. This method works fine unless each room has to know something about who is in the other rooms. In that case additional method calls or a shared object have to be used to redistribute information in the lobby to the rooms.
  3. Use a proxied shared object. Connect each room instance to the lobby instance using a NetConnection object and then have the room update a shared object belonging to the lobby with room and user information. This method has the advantage of being relatively simple to implement and automatically makes the information about what rooms are active and who is in them available not only to the lobby but to each room.

Creating a Global User List

There are many different approaches to using proxied shared objects. In one, each room could access its own shared object in the lobby or in another, all the rooms could use one shared object.Each shared object could be directly updated by each room or each room can call a method in the lobby to update the shared object. Complicating things further is the fact that different applications will need different authentication mechanisms. Some will require authentication of each user against a database or user directory. Other applications will allow anonymous logins but force each user to use a unique user name. Other applications will let users connect with any user name.

To keep things simple this application makes the following assumptions:

Since each user has a unique ID or username and can only be in one place at once,  we can safely use each user's id as a unique way to identify each slot in one shared object. So the lobby can own a shared object that acts as a kind of global user list - so that's what I'll call it. The data in it might look something like this if it were a simple ActionScript object:

globalUserList = {
  blesser: {fullName: "Brian Lesser", room: "roomForAnArgument", arrivalTime: "2003:02:02:12:01:01"},
  charper: {fullName: "Chris Harper", room: "roomForAnArgument", arrivalTime: "2003:02:02:12:01:02"},
  peldi: {fullName: "Giacomo Guilizzoni", room: "meaningOfLife", arrivalTime: "2003:02:02:12:02:22"}
}
Each user in the shared object gets their own unique slot based on a user name and the slot contains the name of the room they have connected to. For good measure we might throw in their arrival time in the room. The code above is useless of course because it is just a simple object to designed only to show how the data might be arranged.

When a room connects to the lobby's globalUserList shared object it creates a local stand-in or "proxy" of the globalUserList that the room's clients can connect to just as though the globalUserList shared object belonged to the room. So every Flash movie can call SharedObject.getRemote("globalUserList"...) and watch as people connect and disconnect from all the rooms and the lobby. Whenever a change occurs in the shared object, the Flash movies will have to look at the changed slot, get the user's name and room and put the information in some list or other component.

The Server-Side Lobby Code

The listing below is a simple script that provides the bare essentials of setting up a lobby application instance. It could be the main.asc script in a separate lobby application. ie applications/lobby/main.asc or it could be a lobby.asc file loaded in and executed whenever someone visits the default instance of an application. But, I'll describe that scenario later. The main thing is that this script is what is loaded and run on the server when the lobby instance first starts up.

When it starts it gets a temporary shared object named readOnly/globalUserList. From then on when a client connects to the lobby it adds an item in the global user list to represent the client. Each entry contains an object that includes a room property. Since this is the lobby instance the value of room is always "Lobby". When a client leaves the lobby their entry is deleted from the global user list. If they connect to a room an entry for that user will be added back into the global user list by the room at that time.

The only unusual thing in this code is that both room instances and Flash movies will connect to the lobby. Each time a room or movie connects application.onConnect() will be called and a client object representing either a movie or room instance will be passed in to onConnect(). The lobby needs to treat rooms and movies differently. When a movie connects the lobby has to update the global user list with user information. But, when a room connects it shouldn't do that. See the comments in the onConnect() method to see how the lobby distinguishes if a room or movie is trying to connect. When a room connects the connection is accepted and the room is not restricted to reading or writing any resources in the lobby. When a movie connects it is restricted to only reading the contents of the global user list as a security precaution. You can take that out if you want to, but it is the reason the path name is so long: readOnly/globalUserList. Finally, when a movie connects the so.setProperty() call updates the shared object. Here's the code - it is extensively commented:

/** When the application starts created a temporary shared object. */
application.onAppStart = function(){
   globalUserList_so = SharedObject.get("readOnly/globalUserList");
}

/**
 * When a client connects determine if it is a room instance or a
 * a swf connecting. If it is a swf add an entry in the global user list.
 */
application.onConnect = function(client, userName, roomName) {

   // Test to see if a room is trying to connect.
   // The IP when making a local connection should be 127.0.0.1
   // regardless of the public IP the server is using.
   // client.agent will always start with FlashCom.
   // Pass in an extra string as a room indentifier.

   if (client.ip == "127.0.0.1" &&
       client.agent.indexOf("FlashCom") == 0 &&
       userName == "room"){
     // It's a room connecting so remember it:
     client.type = "room";
     client.roomName = roomName;
     // By default the room client can read and write anything
     // so we just accept it's connection by returning true.
     return true;
   }
   else {
     // It's not a room connecting so assume the client is a swf
     // and remember it.
     client.type = "client";
     // For safety restrict the swf to having read-only access
     // in the name space where the global user list is kept:
     //     "readOnly/globalUserList"
     client.readAccess  = "readOnly;public";
     client.writeAccess = "public";
     client.userName = userName;
     // In the next line an entry for the client is created in the
     // global user list.
     // The entry is an object that could contain other data than
     // the room name - in this case "Lobby" - and arrival time.
     // For example the user's full name and E-mail address.
     globalUserList_so.setProperty(userName, {room: "Lobby", arrival: new Date()});
     return true;
   }
}

/**
 * When a room or swf disconnect either remove all the user entries for the
 * room, if there are any, or remove the entry for the swf to show it is
 * no longer in the lobby
 */
application.onDisconnect = function(client) {
  if (client.type == "client"){
    // Delete the entry for this user in the global user list.
    globalUserList_so.setProperty(client.userName, null);
  }
  else{
    // Get all the user names into the names array.
    var names = globalUserList_so.getPropertyNames();
    // Get the name of the room that is disconnecting.
    var roomName = client.roomName;
    var user;
    // Check every name to see if it is in the room that is disconnecting.
    for (var i in names){
       user = globalUserList_so.getProperty(names[i]);
       // If a user is listed as being in the room that is disconnecting,
       // delete the user's entry in the global user list.
       if (user.room == roomName) globalUserList_so.setProperty(names[i], null);
    }
  }
}

In summary: the lobby.asc file accepts connections from both Flash movies and room instances that connect to the lobby. Both rooms and Flash movies are represented by the client object that is passed into the application.onConnect() method. The lobby keeps track of what kind of client has connected by setting a property on the client named type to either room or client. When a Flash movie connects an entry is created for the user in the global user list and when the movie disconnects the entry is deleted. When a room connects the lobby keeps track of the room name by setting a property named roomName of the client object to the room's name that was passed into the onConnect() method as an extra connection parameter by the room. When the room disconnects the lobby makes sure all the entries for every user in the room are removed from the global user list. In theory the lobby should not have to do that. However, if the room instance crashes the extra checking by the lobby makes sure no "zombie" user entries remain in the global user list.

The Server-Side Room Code

The server-side code to implement a room instance follows. In this case the room must make a network connection to the lobby instance using a NetConnection object:

lobby_nc = new NetConnection();
lobby_nc.connect("rtmp://localhost/chLobbyRooms", "room"); 

Then the room connects to the readOnly/globalUserList shared object something like this:

globalUserList_so = SharedObject.get("readOnly/globalUserList", false, lobby_nc);  

Passing the lobby_nc NetConnection object into the SharedObject.get() method means if the call is successful the room instance will be able to user the lobby's shared object. Also, Flash movies that connect to this room will also be able to connect to the global user list shared object because when the room connects to the lobby's shared object it creates a proxy of the lobby's shared object. Movies connected to this room instance can connect to the proxy. So a movie connected to a room could do this:

globalUserList_so = SharedObject.getRemote("readOnly/globalUserList", nc.uri);
globalUserList_so.owner = this;
globalUserList_so.onSync = function(list){
  this.owner.globalUserList_lb.removeAll();
  for (var p in this.data){
    this.owner.globalUserList_lb.addItem(p, this.data[p]);
  }
}
globalUserList_so.connect(nc); 

Which is the same as what a movie connected to the lobby could do to display a list of users.

Unfortunately, there is a complication. When the first client tries to connect to a room, the room instance starts up and tries to connect to the lobby. Before the network connection can be established the application.onConnect() method is called. In this trace output onConnect() is called before the network connection is completed and before the shared object connects:

onConnect:   1070060739891
nc.onStatus: 1070060739942
so.onSync:   1070060739942 

Note: the numbers are millisecond values.

Since the shared object hasn't been setup we can't add the client information into it yet - or if we try the attempt will be ignored. So when the shared object is finally synchronized for the first time we have to make sure all the clients that have already connected to the room are have entries in the global user list. See the code in the onSync() method below to see how this is done.

/**
 * initLobbyResources is passed the nc to the lobby.
 * In this case all it has to do is setup the global user list shared object.
 */
function initLobbyResources(nc){
  // The global user list is a temporary SO so pass in false for persistence.
  globalUserList_so = SharedObject.get("readOnly/globalUserList", false, nc);
  // Set ready flag to false to indicate the shared object has not connected yet.
  globalUserList_so.ready = false;
  // The onSync method will be called many times but in this case on the server
  //  we only care about the first time.
  // Between the time the network connection to the lobby was requested and the
  // time the shared object is actually synchronized a client or two may
  // have already connected to this room. To make sure all the clients are
  // added to the global user list, the first time the shared object is
  // synchronized the onSync method loops through the application.clients
  // array and adds all the clients into the global user list.
  globalUserList_so.onSync = function(list){
    trace("so.onSync: " + new Date().getTime());
    if (! this.ready){
      this.ready = true;
      for (var i = 0; i < application.clients.length; i++){
        var client = application.clients[i];
        globalUserList_so.setProperty(client.userName, {room: roomName, arrival: client.arrival});
      }
    }
  }
}

application.onAppStart = function(){

  // Let's take the room name as the name of the app instance
  // without the app name. For example if this is chLobbyRooms/room1
  // we only want the room1 part:

  roomName = application.name.substring(application.name.indexOf("/") + 1);

  // Create a NetConnection object to connect to the lobby instance
  lobby_nc = new NetConnection();
  // Keep track of the state of the connection to help with error handling.
  lobby_nc.state = "Closed"; // remember the last thing that happend.
  // The onStatus method of the NetConnection object doesn't do much here
  // so I added the feature that if the lobby disconnects the room will
  // try to reconnect once.
  lobby_nc.onStatus = function(info){
  trace("nc.onStatus: " + new Date().getTime());
    switch(info.code){
      case "NetConnection.Connect.Success":
        this.state = "Connected";
        break;
      case "NetConnection.Connect.Closed":
        // If the connection was rejected there isn't much we can do about it.
        // If the connection failed something is seriously wrong.
        // If the lobby instance is killed off or goes down we get a closed
        // message so we could try again safely once:
        if (this.state == "Connected"){
          this.connect("rtmp://localhost/chLobbyRooms", "room", roomName);
          initLobbyResources(this);
        }
        this.state = "Closed";
        break;
      case "NetConnection.Connect.Rejected":
        this.state = "Rejected";
        trace("Error: Lobby rejected the connection attempt!");
        break;
      case "NetConnection.Connect.Failed":
        this.state = "Failed";
        trace("Error: Failed to connect to the lobby!");
        break;
    }
  }
  // Connect to the lobby. You can't use a relative path here. Use
  // either localhost or the actual host name.
  // Pass in "room" as a user name because that's what the lobby expects.
  // Pass in the roomName too so the lobby knows the name of the room
  // trying to connect.
  lobby_nc.connect("rtmp://localhost/chLobbyRooms", "room", roomName);
  // Note: we have to setup the globalUserList_so immediately so that
  // it exists when the first onConnect is called. We can't wait for
  // the NetConnection onStatus method.
  initLobbyResources(lobby_nc);
}

application.onConnect = function(client, userName){
  trace("onConnect: " + new Date().getTime());
  // If we had the user's full name and other user info we would do something here with it.
  client.userName = userName;
  client.arrival = new Date();
  // Try to add an entry for this user in the global user list.
  globalUserList_so.setProperty(userName, {room: roomName, arrival: client.arrival});
  return true;
}

application.onDisconnect = function(client){
  // When a client disconnects remove the user's entry from the global user list.
  globalUserList_so.setProperty(client.userName, null);
}

Deployment

One question you may ask yourself is how to deploy the two scripts listed above. One way is to create two separate application subdirectories and name each file main.asc in each of the directories. For example:

.../applications/lobby/main.asc
.../applications/rooms/main.asc

If you did that you would have to adapt the room script by fixing lines like this one to connect to the lobby properly:

this.connect("rtmp://localhost/chLobbyRooms", "room", roomName);

If the lobby instance is located at .../applications/lobby/main.asc you might need to do something like this:

this.connect("rtmp://localhost/lobby", "room", roomName);   

However, you really don't need to create two separate applications subfolders to use the two separate asc files (or derivatives of them). For arguments sake, lets say you wanted to only use one application folder. As an example lets say you want to call your folder, and therefore the application, "chLobbyRooms". (The ch is a reference to Chris Harper who asked a question on the chattyfig flashcomm mailing list.) So here is how that might work:

When a movie connects to rtmp:/chLobbyRooms  it is actually connecting to the default instance at

rtmp:/chLobbyRooms/_ definist _

So lets make that the lobby instance. When a user connects to the default instance they connect to the lobby. When a user connects to any other instance they connect to a room. To make that work we want the lobby code to run in the default instance and the room code to run in all the other instances. It turns out it is not difficult to do this using two features of server-side ActionScript:

  1. the application.name property provides  the path to the application instance without the host name. So for example if the instance running is at rtmp://myhost.mydomain.xx/chLobbyRooms/room23 then application.name will contain the string: chLobbyRooms/room23. The application name has two parts. The part before the first slash is the name of the application and the part after the first slash is the instance name.
  2. the load function in server-side ActionScript allows you to load an asc file at runtime. The file will be loaded, compiled, and executed.

These two features mean that you can create a single main.asc file that loads either a lobby.asc file or a rooom.asc file depending on what instance of the application is running. If the default instance named _ definist _ is running you can load the lobby.asc file and if anything else is running you can load the room.asc file.  Here is a very short main.asc file that does exactly that:

application.instanceName    = application.name.replace(/.*?\//, "");
application.applicationName = application.name.replace(/\/.*/, "");

if (application.instanceName == "_definst_"){
  load("lobby.asc");
}
else{
  load("room.asc");
}

The first line of the script uses a regular expression to extract everything after the first slash in the application.name and puts it into a new property of the application. Actually it creates a copy of the application.name string and deletes everything before the first slash and the first slash - which amounts to the same thing. The second line grabs everything before the first slash and puts it into the applicationName property.

The if statement completes everything. If the instance running is the default instance the lobby.asc file is loaded. If not it must be a room so the rom.asc file is loaded. If you want to try this save the first script as lobby.asc, the second script as room.asc and the script above as main.asc and put them in one directory for example chLobbyRooms:

...applications/chLobbyRooms/main.asc
...applications/chLobbyRooms/lobby.asc
...applications/chLobbyRooms/room.asc

Download the zip file and try it out.

Flash Lobby Installation Instructions

The sample files provided here are designed to be run on a test system and are not designed for real production use. These instructions assume you are running the Flash Communication Server version 1.5.1 and the Flash player on the same system so that a Web server is not required. If you do try to run the sample files on a service provider's system make sure the service provider allows inter-instance communications. You may also have to replace localhost with the host name of your FCS server.

Server/Application Files

Within the root directory of the zip file is the chLobbyRooms subdirectory. The chLobbyRooms directory must be unzipped and placed in the Flash Communication Server's applications directory. The three asc files in the chLobbyRooms directory should have the following paths:

.../applications/chLobbyRooms/main.asc
.../applications/chLobbyRooms/lobby.asc
.../applications/chLobbyRooms/room.asc

where ... is the path to your Flash Communication Server installation directory. On some Windows systems the Flash Communication Server directory will be C:\Program Files\Macromedia\Flash Communication Server MX.

Client/Browser Files

All the client-side code, swf, and fla files are in the root directory of the chLobbyRooms.zip file. They can be unzipped anywhere and either the chLobbyRoomsTestClient.html or chLobbyRoomsTestClient.swf files can be run. The files are:

You should have version 6.r65 of the Flash player installed or later.

Using the Test Client

After installing the application load the chLobbyTestClient.html file, enter a unique user name in the text field and press the Login button. Assuming all goes well a screen will appear with a global user list and room list combo box as well as other UI components. Click on your user name in the global user list to see more information about it. Then select a room in the room list combo box and press the Go button. The Current Location field at the top of the swf will show you what room you are in. Click on your user name again and it should also show you are in a different room. The chat is local to the room you are in.


Post or read comments for this page.