| Programming Flash Communication Server |
| Home | News | Sample Chapters | Source Code | Technotes & Articles | Errata |
I want every visitor to see the number of people in each chat room.
Create a special master application instance that keeps track of how many people are in each room in a shared object. Have each room instance connect to the master and get a proxy of the master's shared object.
Please try out a very simple Flash movie that shows how many people are in each room. But, don't forget to come back to this page.
Each room in a chat system is normally a separate FlashCom application instance. For example some Flash movies may have connected to an RTMP address such as rtmp://my.host.edu/chatApp/room1 and others to rtmp://my.host.edu/chatApp/room3. The people chatting in room1 are normally unaware of the people chatting in room3.
You can create a special master application instance that keeps track of how many people are in each chat room. Normally, when you use this recipe, you design your application so that Flash movies don't connect to the master instance - only room instances connect to the master in order to share information amongst themselves. A master instance is just another FlashCom application instance but at a different RTMP address. For example you might have all your room instances connect to an address like this: rtmp://my.host.edu/chatApp/master or you might use an address like this: rtmp://my.host.edu/chatApp/roomInfo.
Note: you can also create one or more lobby instances that are like chat rooms in that Flash movies connect to them (often before connecting to chat rooms). A lobby instance can also connect to the master instance but will probably provide different services to the Flash movies that connect to the lobby than is provided by the chat rooms.
The master instance creates a shared object. Let's call the shared object roomPopulationList. The roomPopulationList will have one property for each room. Each property will be named after the room and contain the current number of people in that room. The data in the roomPopulationList can be visualized using a table:
| Property Name | Property Value |
|---|---|
| room1 | 2 |
| room2 | 0 |
| room3 | 10 |
From the data in the table it looks like no one is using room0 and room3 is the most popular place to be.
So, how does the data about the number of people in each room get into the master instance's shared object and how do clients connected to each chat room get that information? Here's how it works:
Each room instance uses a server-side NetConnection object to make a network connection to the master instance using server-side ActionScript like this:
master_nc = new NetConnection();
master_nc.connect("rtmp://my.host.edu/chatApp/master", "room", roomName, password);
After making a network connection to the master instance, each room uses its connection to get a proxy of the master's roomPopulationList shared object:
roomPopulationList_so = SharedObject.get("roomPopulationList", false, master_nc);
In this case, the last parameter passed into SharedObject.get() is
the master_nc NetConnection.
Looking at the last statement you might think that the roomPopulationList_so
variable refers to a shared object like any other. However, because the last
parameter is a NetConnection, it really refers to a local representation (a
proxy) of the roomPopulationList in the master instance. When the roomPopulationList
is synched up with the master's shared object the room
instance can update it this way:
roomPopulationList_so.setProperty(roomName, application.clients.length);
The update is actually passed over the master_nc connection to
the master's version of the roomPopulationList where the shared object is updated.
Any rooms that have connected to the master instance and proxied its roomPopulationList
shared object will now have their own local version of the roomListSharedObject
updated. Any Flash movie connected to one of those rooms can connect to the
room's local copy of the master's shared object and get all the data from it.
In other words one application instance can connect to another instance and
setup a local copy of another instance's shared object. From then on it can
update the local copy and its updates will be carried over the network to the
master instance where the update actually takes place. In turn any other instance's
local copies will be updated automatically. Any Flash movie connected to an
instance with a local copy (proxy) of the mater's shared object can connect
to the shared object and display the data in it. And that's all that is needed
to show every visitor the number of people in each chat room. See the section
Proxied Shared Objects in chapter 8 of Programming
Flash Communication Server for more on proxied shared objects. Now, lets
get on to using them!
To keep the master instance, lobby instance, and chat room instances separate, I'll organize them using an instance naming convention within a single "chat_01" application. (In similar chap recipes I'll use similar application names like chat_02 and chat_3.) Here are some sample RTMP addresses:
I've bolded the instance part of the RTMP address. In these examples the instance part of the address has two parts. The first part "master", "lobby", or "room" is used to organize instances just like you would organize files in different directories. The last part of the instance name is used in the case of the rooms to provide separate room addresses. In this case I've named the master instance roomInformation and even though there will only be one lobby instance I've named it lobby1. Here's an illustration of some of the instances running on the server.
Figure 1. A Flash movie connected to the lobby and another Flash movie connected to room 1. Each movies uses a proxied shared object to display the number of visitors in each room. Each arrow represents a network connection.
You may have noticed that all the different RTMP addresses are on the same server and are part of the same application: chat_01. But the master, lobby, and rooms must all behave differently and therefore must execute different server-side ActionScript code. One way to create separate scripts for each type of intance (master, lobby, or room) would have been to create three application folders that contain their own unique main.asc file. However, it is possible to do everything in one folder by having one main.asc file that loads a master.asc, lobby.asc, or room.asc file depending on the RTMP address of the instance being started. Here's the complete listing of a main.asc file that does exactly that:
trace("Loading main.asc file.");
// The following onConnect method is a default that rejects all connections
// immediately. This onConnect method will be overwritten in the master.asc,
// lobby.asc, and room.asc in order to allow client connections when clients
// connecct to master, lobby, or room instances.
// This method must be defined before the other asc files are loaded.
application.onConnect = function(client){
return false;
}
// Split up the application.name string that will look like:
// lobbyChatRooms/room/room2 into an array of strings
// like: ["lobbyChatRooms", "room", "room2"]
application.nameArray = application.name.split("/");
// Now each part of the instance name can be checked separately.
if (application.nameArray.length == 3){
// Decide what other asc file to load based on the second part of the address:
// at nameArray[1].
switch (application.nameArray[1]){
case "master":
if (application.nameArray[2] == "roomInformation"){
load("roomInformation.asc");
}
break;
case "lobby":
if (application.nameArray[2] == "lobby1"){
load("lobby.asc");
}
break;
case "room":
if (application.nameArray[2].indexOf("room") == 0){
load("room.asc");
}
break;
}
}
Loading a second asc file does takes a little longer so if instance startup time is critical you may prefer to use separate application folders.
The roomInformation.asc file's job is to setup and maintain the roomPopulationList
shared object as long as any lobby or rooms are being used. Therefore as soon
as the roomInformation instance starts up, it creates a temporary roomPopulationList
shared object and keeps a reference to it as long as the roomInformation instance
is active. Lobby and room instances will connect to the roomInformation (master)
instance so the roomInformation instance's onConnect() method must
be able to check that a lobby or room is really connecting and accept their
connection. The master instance will start up as soon as the first lobby or
room tries to connect to it.
Another thing the roomInformation instance must be responsible for is making sure the data in the roomPopulationList is correct when a room or lobby disconnects from the master. If the lobby or room no longer exists the property representing that lobby or room in the roomPopulationList must be deleted. This is especially important if a room or lobby crashes so that it does not appear to still be in use. Here is code for the roomInformation.asc file:
trace("Loading roomInformation.asc file.");
// Object containing FlashCom server IP addresses and server passwords:
allowedServerIPs = {
"127.0.0.1" : "dj3-2ejka8+xz3",
"192.168.0.1" : "82jasd;cj2ws'q"
}
application.onAppStart = function(){
// On startup get a reference to the roomPopulationList shared object.
roomPopulationList_so = SharedObject.get("protected/roomPopulationList");
}
application.onConnect = function(client, roomType, roomName, password){
trace("Client referrer: " + client.referrer + " is connecting from " + client.ip);
// Check the client and the data it is passing in to make sure it is
// a valid instance connecting and not some attacker.
// If anything is wrong return false immediately disconnects the client
// and returns from onConnect.
if (!roomType || !roomName || !password){
return false; // Reject the connection attempt.
}
if (roomType != "room" && roomType != "lobby"){
return false;
}
var serverPassword = allowedServerIPs[client.ip];
if( ! serverPassword || serverPassword != password){
return false;
}
if (client.agent.indexOf("FlashCom") != 0){
return false;
}
// Add properties to the client so we know what room is disconnecting
// when onDisconnect is called at some time in the future.
// Note: roomType is not used here but is left in as a comment that in
// more complex systems you may need it.
// client.roomType = roomType;
client.roomName = roomName;
// Return true to accept the connection.
return true;
}
application.onDisconnect = function(client){
// If the client leaving has a room name delete the property
// in the roomPopulationList shared object for that room.
if(client.roomName){
roomPopulationList_so.setProperty(client.roomName, null);
}
}
In context of this recipe the lobby's responsibility is to connect to the roomInformation instance, get a proxy of the roomInformation's roomPopulationList shared object, and keep it updated. It must update the shared object whenever a Flash movie connects and whenever one disconnects from the lobby. By creating a proxy of the roomPopulationList shared object, the lobby makes it possible for Flash movies connected to the lobby to connect to the roomPopulationList shared object. That way each Flash movie can display how many people are in each room. To protect the contents of the roomPopulationList the lobby only provides clients read access to the protected folder the roomPopulationList is in. Here is the lobby.asc file:
trace("Loading lobby.asc file.");
// application.nameArray is setup in main.asc.
roomName = application.nameArray[2];
/**
* initResources is passed the nc to the main instance.
* In this case, all it has to do is:
* 1. create the proxied shared object using the nc;
* 2. initialize the shared object so that the first onSync updates
* the shared object with the current room population.
*/
function initResources(nc){
if (typeof roomPopulationList_so != "undefined") roomPopulationList_so.close();
// The roomPopulationList is a temporary SO so pass in false for persistance.
roomPopulationList_so = SharedObject.get("protected/roomPopulationList", false, nc);
// This server-side onSync method might be called many times but in this
// example it only has to be called the first time. Here's why:
// Between the time the network connection to the master was requested,
// and the time the shared object is actually synchronized, a client
// or two may have already connected to this lobby.
// To make sure all the clients are added to the room population list,
// the first time the shared object is synchronized the onSync method
// updates the list with the number of clients in the lobby. Since the
// onSync method is not needed any longer, it deletes itself.
roomPopulationList_so.onSync = function(list){
//trace("roomPopulationList_so.onSync> Time: " + new Date().getTime());
roomPopulationList_so.setProperty(roomName, application.clients.length);
delete this.onSync;
}
}
function doConnect(){
// Connect to the master instance. You can't use a relative path here. Use
// either localhost or the actual host name such as fcs1.flash-communications.net.
// Pass in "lobby" as the first parameter so the master in knows what type
// of instance is connecting. See the master.asc code's onConnect.
// Pass in the roomName too for good measure..
master_nc.connect("rtmp://localhost/chat_01/master/roomInformation", "lobby", roomName, "dj3-2ejka8+xz3");
// Note: we have to setup the roomPopulationList_so immediately so that
// it exists when the first onConnect is called because clients only have read
// access to it. We can't wait for the NetConnection onStatus method.
initResources(master_nc);
}
application.onAppStart = function(){
// Create a NetConnection object to connect to the master instance
master_nc = new NetConnection();
// Keep track of the state of the connection to help with error handling.
master_nc.state = "Default"; // 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 master disconnects the lobby will
// try to reconnect once.
master_nc.onStatus = function(info){
//trace("master_nc.onStatus> time: " + new Date().getTime());
//trace("master_nc.onStatus> info.description: " + info.description);
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 roomInformation instance is killed off or goes down we get a closed
// message so we can try again safely once:
if (this.state == "Connected"){
doConnect();
}
this.state = "Closed";
break;
case "NetConnection.Connect.Rejected":
this.state = "Rejected";
trace("Error: master instance rejected the connection attempt!");
break;
case "NetConnection.Connect.Failed":
this.state = "Failed";
trace("Error: Failed to connect to the master instance!");
break;
}
}
doConnect();
}
application.onConnect = function(client, userName){ // Only passing in user name here.
//trace("onConnect: userName: " + userName + " at: " + new Date().getTime());
//trace(client.referrer);
// Restrict the client to read-only access of the protected area
client.readAccess = "protected;public";
client.writeAccess = "public";
// If we had the user's full name we would do something here with it.
client.user = {userName: userName};
client.user.arrival = new Date();
// Accept the connection so the application.clients array is incremented.
application.acceptConnection(client);
roomPopulationList_so.setProperty(roomName, application.clients.length);
//trace("client count: " + application.clients.length);
return true;
}
application.onDisconnect = function(client){
roomPopulationList_so.setProperty(roomName, application.clients.length);
//trace("client count: " + application.clients.length);
}
application.onAppStop = function(){
//trace("onAppStop");
}
The room.asc file is identical to the lobby.asc file in this example because neither contains server-side code that relates to their function of being a lobby or a room. Consequently, the room.asc code is not shown here but is in the distribution file: chat_01.zip.
Note: when there is a single lobby it could also play the role of the master instance that owns the roomInformationList shared object. However, in this design a separate master instance named roomInformation is used in order to spread out the application load over both the lobby and master instances. In other designs where there is more than one lobby a separate master instance to hold room information is even more important for the stability of the application.
A Flash movie that connects to the lobby, or one of the rooms, can connect to the proxy of the protected/roomPopulationList shared object and display all the data in the shared object in a DataGrid. In the sample FLA the instance names for the lobby and some rooms are stored in a ComboBox component. When the user enters a user name and presses the connect button whatever room is showing in the changeRoomCombo is the one Flash will try to connect to:
nc.connect('rtmp:/chat_01/' + changeRoomCombo.value, userNameInput.text);
When the connection is accepted the Flash movie connects to the proxied shared object:
roomPopulation_so = SharedObject.getRemote("protected/roomPopulationList", nc.uri);
roomPopulation_so.owner = this;
roomPopulation_so.onSync = function(infoList){
this.owner.onSync(infoList);
}
roomPopulation_so.connect(nc);
Now whenever a visitor connects to or leaves a room the proxied shared object will be updated from the master shared object and onSync on each Flash movie will eventually be called. Here's the onSync method that updates a DataGrid named roomPopulationGrid.
function onSync(infoList){
var dp = roomPopulationGrid.dataProvider;
dp.length = 0;
var data = roomPopulation_so.data;
for (var p in data){
dp.push({Room: p, Visitors: data[p]});
}
dp.dispatchEvent({target:dp, type:"modelChanged", eventName: "updateAll"});
}
The onSync() method works by clearing the grid's data provider and then loops through every property in the shared object using a for in loop. In the loop the property name such as room1 or lobby1 will be in the p variable and the number of visitors will be the value in the property: data[p]. A new item is created and placed in the grid's data provider.The complete FLA including code is in the chat_01.zip file.
The code in this recipe is designed to be as simple as possible. The code is not designed to be secure and comunication components are not used. Finally, no fail-over is provided. Also, I recommend you consult Peldi's article at http://www.peldi.com/blog/archives/2005/01/test.html on scalability.
Document first posted April 02, 2005 by Brian Lesser