4.5. A More Realistic Example
Writing and testing a simple
main.asc script for
helloWorld is an important first step in
becoming familiar with any new environment. However, while the
helloWorld application introduces the
application, Client, and
info objects, it's not very
useful. Although not a full-featured implementation, Example 4-3 is a bit more realistic
main.asc script in that it shows many of the
methods already discussed working in a more typical manner. The code
in Example 4-3 demonstrates:
Accepting or rejecting client connections based on a username and
password Adding properties and methods to the Client
object more simply, by adding an object property instead of
individual properties and methods Limiting guest clients but not other types of clients when a maximum
number of users is reached Using the server-side setInterval( ) function to
periodically update information about connected clients
The code listed in Example 4-3 is not commented to
save space, but the version on the web site is. Take a moment to read
through the code. It is described immediately after the listing.
Example 4-3. A more realistic main.asc
users = { Brian: {password: "secretPassword1", role: "author"},
Robert: {password: "secretPassword2", role: "author"},
Justin: {password: "secretPassword3", role: "author"},
Joey: {password: "secretPassword4", role: "author"},
Peldi: {password: "secretPassword5", role: "author"},
Guest: {password: "Guest", role: "guest"}
};
access = {author: {readAccess: "/", writeAccess: "/"},
guest: {readAccess: "/public", writeAccess: ""}
};
MAXCONNECTIONS = 5;
function User (client, userName, role) {
this.client = client;
this.userName = userName;
this.role = role;
this.connectTime = new Date( ).getTime( );
}
User.prototype.getTimeConnected = function ( ) {
var now = new Date( ).getTime( );
return (now - this.connectTime)/1000; // seconds
};
User.prototype.getPingTime = function ( ) {
var client = this.client;
if (client.ping( )) {
return client.getStats( ).ping_rtt / 2;
}
else{
return "Client is not connected."
}
};
User.prototype.getConnectionInfo = function ( ) {
return " IP: " + this.client.ip +
", user name: " + this.userName +
", connection time: " + this.getTimeConnected( ) +
", ping time: " + this.getPingTime( );
};
function listCurrentUsers ( ) {
trace("-------------- Current Users --------------");
var i, user;
var total = application.clients.length;
trace("There are " + total + " current users.");
for (i = 0; i < total; i++) {
user = application.clients[i].user;
trace(user.getConnectionInfo( ));
}
trace("-------------------------------------------");
}
application.onAppStart = function ( ) {
trace(application.name + " is starting at " + new Date( ));
setInterval(listCurrentUsers, 60000);
};
application.onConnect = function (client, userName, password) {
trace("Connection attempt from IP: " + client.ip + " userName: " + userName);
var user = users[userName];
if (!user || !password || user.password != password) {
application.rejectConnection(client, {msg: "Invalid user name or password."});
return;
}
if (application.clients.length >= MAXCONNECTIONS && user.role == "guest") {
application.rejectConnection(client, {msg: "Chat is already full."});
return;
}
client.user = new User(client, userName, user.role);
client.writeAccess = access[user.role].writeAccess;
client.readAccess = access[user.role].readAccess;
application.acceptConnection(client);
trace("Connection accepted at " + new Date( ));
};
application.onDisconnect = function (client) {
trace(client.user.userName + " is disconnecting at: " + new Date( ));
trace(client.user.userName + " was connected for " +
client.user.getTimeConnected( ) + " seconds.");
};
application.onAppStop = function ( ) {
trace(application.name + " is stopping at " + new Date( ));
};
The following sections examine the major functionality in the code
example.
4.5.1. Authenticating and Customizing Clients
Clients that connect to an application instance can be
authenticated in a number of ways. Chapter 11
provides an example of authenticating users using Flash Remoting and
ColdFusion; Chapter 18 describes a number of
other approaches and their strengths and weaknesses.
Example 4-3 uses a simplistic approach of providing
a hardcoded users object, an associative array of
objects. The objects it contains can be accessed using property names
like Brian, Robert, and
Justin. The objects contain
password and role properties
for each user. For example users.Brian contains an
object with Brian's password and role. Accessing
Brian's password in the users
object could be done using dot notation or using the square bracket
operator. In Example 4-3s
onConnect( ) method, an object is retrieved from
the users object this way:
var user = users[userName];
If userName contains a string that matches a
property name in the users object, the
user variable is assigned an object reference, and
it will have a password and
role property. For example, if
userName is
"Brian",
user.password will be
secretPassword1. If userName
does not contain a property name that exists in
users, the value of user will
be undefined. All of this is taken into account in
this if statement:
if (!user || !password || user.password != password) {
application.rejectConnection(client, {msg: "Invalid user name or password."});
return;
}
Each part of the condition is necessary to prevent errors and
validate the user. For example, if no user were found, the
user variable does not contain an object reference
and will evaluate to false in the Boolean
expression. In that case, the client connection will be rejected. If
the user passed in an empty or undefined password string, in the
Server-Side ActionScript, password will evaluate
to false and again the client will be rejected.
Assuming user is an object and
password is a non-empty string, the
user.password property can be safely accessed and
compared to the password the client provided.
 |
Be sure that a variable contains an object before attempting to
access any of its properties. Otherwise, a type error will occur on
the server, and execution of the script will stop until the
instance's script is allocated another thread by
FlashCom when an event handler or other callback function must be
called. See "try/catch/finally
Statements" earlier in this chapter for another way
to avoid errors halting script execution.
|
|
If the user is authenticated, the client object
can be customized to store information about the user, control access
to resources, and be extended with additional methods. A good way to
package related information and methods together is to add an object
as a new property of the client object:
client.user = new User(client, userName, user.role);
The User constructor can also be used to
initialize the client further without cluttering the
onConnect( ) method too much. In Example 4-3, the User constructor
also gets the connection time and stores it in the
user object along with
userName, role, and a copy of
the client reference.
Read and write access to shared objects and streams is controlled by
looking up the readAccess and
writeAccess properties of objects in the
access object:
client.writeAccess = access[user.role].writeAccess;
client.readAccess = access[user.role].readAccess;
For example, if the user.role property is
"guest", the previous statements
will have this effect:
client.writeAccess = "";
client.readAccess = "/public";
The empty string means that the client cannot write to or create any
shared objects or publish any streams to this instance. However, the
client can read data and play streams in the
public directory or any of its subdirectories.
If the user.role were
"author", the statements would have
this effect:
client.writeAccess = "/";
client.readAccess = "/";
The "/" string indicates that the
client has access to any resource.
4.5.1.1 Using the Client.prototype object
In Example 4-3, every client
object is given a user object property before its
connection is accepted. That way, every client will contain common
information about each user such as his username and will contain
common methods such as user.getConnectionInfo(
). In some application designs, where a common object is
not added to each client, the most efficient way to add common
methods to every object is to attach them to
Client.prototype. Example 4-4
shows part of a script that uses the
prototype object to make available a method for
all client objects.
Example 4-4. Using the Client.prototype object to extend all clients
Client.prototype.getTimeConnected = function ( ) {
var now = new Date( ).getTime( );
return (now - this.connectTime)/1000; // seconds
};
application.onConnect = function (client, userName, password) {
client.connectTime = new Date( ).getTime( );
application.acceptConnection(client);
};
application.onDisconnect = function (client) {
trace("Client connected for: " + client.getTimeConnected( ));
};
The Client class cannot be subclassed because
the Client constructor is never called directly
in a script. Whenever it is necessary to significantly extend the
Client class, you should use composition. Adding
the user object to each client and then delegating
work, such as getting the client's connection
information, to the user object is one example of
composition. Other objects can also be added in order to take on
other responsibilities on behalf of the client. For detailed
discussions on attaching methods to a class's
prototype, see ActionScript for Flash MX: The Definitive
Guide. For thorough discussions of composition versus
inheritance, see Essential ActionScript
2.0.
4.5.2. Limiting the Number of Client Connections
The number of currently accepted client
connections is always available via the length
property of the application.clients array.
Restricting the number of clients that can connect at any one time is
a simple matter of checking the length property
and rejecting clients when it reaches a certain size. In some
applications, it may be necessary to allow unlimited connections from
users of one type and restrict connections of another type. In Example 4-3, guest connections are not accepted when the
number of accepted clients reaches a certain point:
if (application.clients.length >= MAXCONNECTIONS && user.role == "guest") {
application.rejectConnection(client, {msg: "Chat is already full."});
return;
}
This example shows how to reject a connection to prevent it from
being established. You can use application.disconnect(
) at any time to disconnect a client that has already been
accepted.
4.5.3. Performing Periodic Updates with setInterval( )
You'll often want to perform some action
periodically. On the client side, this may be achieved in several
ways, such as using timelines or enterFrame
events. However, neither of these is available on the server. In
server-side code, you can use setInterval( )
to call a function or method after a
certain amount of time. The function or method can be called once or
many times at regular intervals. In the onAppStart( )
method in Example 4-3,
setInterval( ), is called and passed a reference
to the listCurrentUsers( ) function:
setInterval(listCurrentUsers, 60000);
In this example, listCurrentUsers( ) is called
once every minute (60,000 milliseconds). In turn,
listCurrentUsers( ) iterates over the
application.clients array and calls each
client's user.getConnectionInfo(
) method. This method uses the properties of the
client object, its user object,
and User class methods to output text about the
client. See Macromedia's Server-Side Communication
ActionScript Dictionary (available in PDF format from the URL cited
earlier and also in LiveDocs format from http://livedocs.macromedia.com/flashcom/mx2004)
for a listing of all the properties and methods of the
Client class.
Various Client properties can be output to
illustrate their values. The ping( ) and
getStats( ) methods of the
Client class do more than provide static
information about the client. Calling ping( )
immediately sends a test message that makes the round-trip from the
server to the Flash client and back again. The message may take some
time to return, so the ping(
)
method returns TRue if the previous ping message
was returned by the client (or the server believes the client is
still connected) and false if the client was not
connected:
if (client.ping( )) {
return client.getStats( ).ping_rtt / 2;
}
else {
return "Client is not connected."
}
If ping( ) returns true,
calling the getStats( ) method returns an
information object whose ping_rtt property
contains the round-trip time in milliseconds from the last available
ping( ) call. The ping( )
method should be used with caution because it sends each ping message
at the highest available prioritypotentially delaying other
RTMP messages. When ping( ) is used to determine
network latency, it should not be called too frequently. As a
guideline, Macromedia's
ConnectionLight
component's default ping interval is 2
seconds.
|