Section 9.8. Advanced Topics

9.8. Advanced Topics

Now that you know how remote methods in FlashCom work, you're ready for the fun stuff! This section covers some advanced topics and offers tips on how to really use RMI in FlashCom to its fullest potential.

9.8.1. Passing Variable Numbers of Arguments

One common trick to achieve polymorphism (calling the same function with a variable number of arguments) in ActionScript is the use of the arguments object to create methods that can have a variable number of arguments passed in.

For example, you can invoke the following addNumbers( ) function with differing numbers of parameters:

 function addNumbers( ) {   var res = 0;   for (var i = 0; i < arguments.length; i++)     res += arguments[i];   return res; } 

For example:

 var sum = addNumbers(1, 2); var sum = addNumbers(10, 9, 8, 7, 123, 42, 456, 76, 3453, 56, 0, 345); 

In client-side ActionScript (including methods attached to NetConnection ), the arguments property can be treated like an array. It contains the parameters passed into the function.

In SSAS, arguments is a hash table with a special .length property, not an array, so it must be looped through manually if you want to create an array out of it.

A different kind of polymorphism can be reached in client-to-server calls by writing a simple "proxy" method to other methods. The following server-side code example shows how to create a method on the Client object that calls any method on the application instance, using the apply( ) method:

 Client.prototype.makeCall = function (methodName /* , p1, p2, ...pn*/) {   var params = new Array(  );   params.push(this);   for (var i = 1; i < arguments.length; i++)     params.push(arguments[i]);  return application[methodName].apply(application, params); }; 

Notice how the arguments are transformed in a proper array by looping through them and how a reference to the calling client ( this ) is inserted as a first parameter to the call.

So, for example, if your application has the following two methods:

 application.getClientStats = function (clientObj) {   return clientObj.getStats( ); }; application.setUserInfo = function (clientObj, name, age, height) {   clientObj.name   = name;   clientObj.age    = age;   clientObj.height = height; }; 

you can call them like this:

 nc.call("makeCall", res, "getClientStats"); nc.call("makeCall", null, "setUsername", "MyName", 29, "185 cm"); 

Notice the client-to-server polymorphismyou are executing the same call ( makeCall( ) ), but with a variable number of arguments; makeCall( ) , in turn , passes the arguments to the method you want to call.

Inserting a reference to the calling client as a first parameter allows the called method to read client-specific variables or make a server-to-client call on the reference.

For a more elaborate example of how to use the arguments object on the server side, search for "arguments" in some of the communication component framework's server-side files: framework.asc , facade.asc , and application.asc . Of particular interest is the __toArray__( ) method, which converts the arguments object into a real array, so that other array functions such as concat( ) can be used with it.

9.8.2. Making Calls Without Waiting for connect( )

There is a little-known feature in the Flash Player that allows you to make client-to-server calls on the NetConnection even before the NetConnection is established. For example, the following code will work:

 nc = new NetConnection(  ); nc.connect("rtmp:/test"); nc.call("myCall", null); 

This code makes the asynchronous operation of connecting to the server look synchronous. It creates the NetConnection , connects, and makes a call, all without waiting. The Flash Player does the queuing up for us, delivering the call on the server side only after the connection handshake has completed.

While this feature makes programming for FlashCom more approachable to beginning client/server coders, it is not a best practice. What if the server decides to reject our connection? Our calls will have failed by the time we find out about it (in the onStatus( ) handler, if we took the trouble to define one). To work around it, we would have to define a queue of calls invoked before the connection was established, which takes us back to square one.

The best practice is to wait to make additional calls from onStatus( ) after checking whether the connect( ) call succeeded.


The following code shows how to implement the operation consistent with best practices:

 nc = new NetConnection(  ); nc.onStatus = function (info) {   if (info.code == "NetConnection.Connect.Success") {     setUp( );   }   // ...other code to handle failure modes. }; nc.connect("rtmp:/test"); function setUp ( ) {   nc.call("myCall",null);   // ...any other code that should run only if the connection is successful. } 

The code first creates a NetConnection object, then defines an onStatus( ) handler on it. The onStatus( ) handler calls a global setUp( ) function only once and only if the connection is established. Calling connect( ) on the NetConnection triggers onStatus( ) when the call completes. Therefore, setUp( ) isn't called until after the connect( ) call succeeds.

As Chapter 5 and Chapter 8 discussed, you can make calls on streams and shared objects before the connection is established as well, though we wouldn't recommend it as a best practice.

This feature is most commonly used on shared objects with occasionally connected applications such as Macromedia Central.

9.8.3. Reusing Result Objects

A good way to save memory is to reuse the same result object for more than one call. After defining it once, you can use it multiple times. Consider the following example in which the result object is defined only once:

 var res = new Object(  ); res.onResult = function (val) {   trace("Returned:" + val); }; function doCall ( ) {   nc.call("doCall", res, _global.index++); } doCall( ); doCall( ); doCall( ); 

Contrast the preceding code with the following. They do the same thing, but this code creates a new result object inside doCall( ) :

 function doCall (  ) {   var res = new Object(  );   res.onResult = function (val) {     trace("Returned:" + val);   };   nc.call("doCall", res, _global.index++); } doCall( ); doCall( ); doCall( ); 

The first bit of code is more efficient because it defines the result object only once, while the second example creates a new result object for every call we make to doCall( ) .

9.8.4. Calling obj1/obj2/method and obj1.obj2.method

A very handy feature of FlashCom is the automatic resolution of complex method names .

As we have seen before, calling nc.call( " someMethod " ) on the client side will invoke someMethod( ) on the instance of the Client object in the server-side script. Consider this server-side code in which we define a UserPreferences object; the code creates an instance ( userPrefs ) and attaches it to the instance of the Client object for the client connecting to the application:

 function UserPreferences (client) {   this.client = client; } UserPreferences.prototype.setName = function (name) {   this.name = name; }; UserPreferences.prototype.getName = function ( ) {   return this.name; }; application.onConnect = function (client) {   client.userPrefs = new UserPreferences(client);   application.acceptConnection(client); }; 

Thanks to the automatic resolution of method names, we can invoke this code from the client side as follows :

 nc.call("userPrefs.setName", null, "Dave"); nc.call("userPrefs/getName", res); 

and everything will magically work! There is no need to build wrapper methods to relay the invocation; the server automatically finds the userPrefs object attached to the calling client and invokes setName( ) and getName( ) on it. Using the dot (.) or the slash ( / ) is equivalent in client-to-server calls.

This also works for server-to-client calls, but the dot (.) syntax is not allowed. You have to use the slash ( / ) to delimit objects when making a server-to-client call.

Nesting works too, so you can call "userPrefs/memberObj1/memberObj2/functionName". There is no set limit to how many levels of nesting you can have, but your machine's RAM will eventually run out! Also, if you need that many levels of nesting, you may want to consider restructuring your code.

Consider again the server-side code for the earlier lobby/rooms example. This code:

 // Attach only the methods we want to this client. client.getRoomInfo = asc_getRoomInfo; client.personJoined = asc_personJoined; client.personLeft = asc_personLeft; 

would be a lot cleaner if we encapsulated those three methods in a RoomHandlers object, saved in a separate .asc file, like this:

 RoomHandlers = function (  ) { }; RoomHandlers.prototype.getRoomInfo  = function (  ) { /* same code as before */ }; RoomHandlers.prototype.personJoined = function (  ) { /* same code as before */ }; RoomHandlers.prototype.personLeft   = function ( ) { /* same code as before */ }; 

This would make the lobby.asc file a lot cleaner to read. Then all we'd have to do to attach the three methods to the client object is:

 client.roomHandlers = new RoomHandlers(  ); 

Then whenever room.asc wants to call any of these methods, it would just have to prepend the roomHandlers name to the call, like this:

 application.toLobby_nc.call("roomHandlers/personJoined"); 

9.8.5. Routing Remote Calls Through a Common Function

Sometimes it's useful to define one big method that operates as a "bottleneck" for function calls that have something in common. Imagine wanting to localize your application and have all the strings accessed through the same function or wanting to have all your trace( ) statements pass through one function, so that they can all be turned off by commenting out one line of code. You can put the common code in the "router" function and write smaller, cleaner subfunctions. Here's an example of a client-side router function, which gets invoked every time something goes wrong on the server side:

 nc.serverSideError = function (code, title, msg) {   hideRegularUI(  );   showErrorUI(  );   switch (code) {     case 100: // Remoting error       showErrorMessage(title, msg, "Retry");       break;     case 200: // Script error       showErrorMessage(title, msg, "Quit");       nc.close( );       break;     case 300: // Disconnect yourself       showErrorMessage("You have been ejected",            "User " + msg + " has removed you from the room", "");       break;     default:       trace("Unknown server-side error code:" + code);   } }; 

This function could be called by the server in any of the following ways:

 client.call("serverSideError", null, 100, "Remoting Error 103",              "There was an error executing xyz call"); System.onStatus = function ( ) {   for (var i = 0; i < application.clients.length; i++)     application.clients[i].call("serverSideError", null, 200,           "Script Error!", info.code); }; client.call("serverSideError", null, 300, "", "Peldi"); 

Here's a similar example of a server-side router method, called by clients to perform some operation on another user:

 Client.prototype.updateUser = function (command, userID, param2) {   switch (command) {     case "delete":       application.users_so.setProperty(userID, null);       break;     case "change-name":       var userObj = application.users_so.getProperty(userID);       userObj.name = param2;       application.users_so.setProperty(userID, userObj);       break;     case "promote":       application.updateRole(userID, param2);       break;     default:       trace("Unknown command " + command);   }   application.clientsByID[userID].isDirty = true; }; 

which could be invoked by clients in a number of ways:

 nc.call("updateUser", null, "delete", 3); nc.call("updateUser", null, "change-name", 1, "Sam"); nc.call("updateUser", null, "promote", 2, "admin"); 

You get the idea. For more information about design patterns in FlashCom applications, refer to Chapter 15.

9.8.6. Using a Fa §ade Method on the Server-Side

A facade pattern allows you to group method calls within one or more simpler method calls so that you do not have to make repeated RMI calls to do a complex transaction. In other words, you call one function across the network that calls several functions on the server one at a time, processing intermediate results if necessary.

A fa §ade method on the server side can be very helpful. Imagine the following examples:

  • In a complex, componentized application, such as Breeze Live, a user promotes another user to "presenter" status. Here a single client-to-server call to an updateRole(id, role) fa §ade method could do the trick. The updateRole( ) method would then notify the server side of each component that the role of user ID has changed, without having each client-side portion of each component make a setRole( ) client-to-server call itself.

  • A connection component is charged with setting up all other components in an application. A simple approach would be to have each component set up its server-side portion with a connect( ) call. A fa §ade approach would call a setUp(componentsArray) function on the server side only once, which would then call connect( ) on the server-side portion of each component in the array.

Because RMI calls are so slow compared to code execution, you should strive to minimize their number. Fa §ade methods on the server side are often the solution.


9.8.7. __resolve( ) and apply( )

Perhaps the most powerful feature of Server-Side ActionScript is the __resolve( ) method, which is invoked every time a non-defined method is called or an undefined property is accessed on the server side (by a server-side script or a client call). It's a "last chance" that the server gives us before failing, either with a script error or with a "Method not found" error. Here's an example that will make it clear. On the client side, we have this code, which attempts to invoke myCall( ) on the server:

 nc = new NetConnection(  ); nc.connect("rtmp:/test"); nc.call("myCall", null, "test", true); nc.call("myCall", null, "two", false); 

Assume that on the server side we don't have a myCall( ) method defined. If we run this code, we would get a "Method not found (myCall)" error on the server side and a "NetConnection.Call.Failed" error on the client side.

Now let's add the following __resolve( ) handler to the server-side code Client prototype:

 Client.prototype.__resolve = function (name) {   trace("__resolve " + name);   Client.prototype[name] = function (a, b) {      trace(name + "! " + a + "," + b);   };   return Client.prototype[name]; }; 

Reload the application, restart the client-side movie, and you will see the following server-side traces:

 __resolve myCall myCall! test,true myCall! two,false 

What's happening? The first time we call myCall( ) from the client, the method is not found on the server, so __resolve( ) is invoked on the Client object. The parameter automatically passed to the __resolve( ) function is the name of the undefined method. We trace it, then we define a function with that name on the fly, attaching it to the Client.prototype object. In the __resolve( ) method, you should always return something that has the same type of what you were looking for. In this case, __resolve( ) was invoked because of a missing method, so we return the method we just created.

The next time we call myCall( ) , the __resolve( ) method doesn't even get triggered, because now Client.prototype already has a method called myCall( ) it's the one we created in the __resolve( ) method!so it just gets invoked.

This feature, combined with the arguments object, can be used to create very powerful automatic call routing based on naming schemes. Perhaps the best-known example of code that does just this is the facade.asc file that is part of the communication components server-side framework, which you can find in the scriptlib folder of your FlashCom installation.

9.8.8. Using call( ) and send( ) together

Sometimes server-to-client calls are more easily achieved using SharedObject.send( ) . Look at the sendMessage( ) method in chat.asc in your scriptlib/components folder, for example. This method is used to broadcast a new chat message from a client to all the other clients:

 // Send a message to all others participating in the chat session. FCChat.prototype.sendMessage = function (client, mesg) {   mesg = this.hiliteURLs(mesg);   // ...some other code.   this.message_so.send("message", mesg); };Whenever a client hits 

Whenever a client hits the Send button in the chat session, sendMessage( ) is invoked through a client-to-server call. The message is checked for URLs, appended to the chat history, and broadcast to all the clients connected to the application, using:

 this.message_so.send("message", mesg); 

If you wanted to use server-to-client calls instead, you would have to loop through the application.clients array, like this:

 for (var i = 0; i < application.clients.length; i++)   application.clients[i].call("message", null, mesg); 

which would require each client to have a message( ) method attached to its NetConnection object. Using SharedObject.send( ) is simpler. Because we know that all the clients looking at the chat are subscribed to the message_so shared object, we can just invoke send( ) on it and define a message( ) method on it on the client side.

This saves us a loop, but doesn't give us the same fine granularity. We might, for example, want to send a message to a subset of clients (private messages or messages to presenters). In that case, looping through the application.clients array is the way to go (so that you can perform checks on each client object):

 for (var i = 0; i < application.clients.length; i++) {   if (application.clients[i].role == "presenter") {    application.clients[i].call("message", null, mesg);   } } 

9.8.9. Using application.registerProxy( )

Here's a problem. Let's suppose you are designing a large application distributed across multiple servers and the client isn't connected directly to a given application instance on another server. How do you make a client-to-server call to your server-side script, have it make a server-to-server call to another application instance, and return the result to the client?

Well, with the knowledge you have gained in this chapter, this shouldn't be too hard to solve. Here's the client-side code:

 nc = new NetConnection(  ); nc.owner = this; nc.onStatus = function (info:Object):Void {   if (info.code == "NetConnection.Connect.Success")     this.owner.setUp( ); }; nc.connect("rtmp://127.0.0.1/myApp"); this.setUp = function ( ) {   nc.regularAddResult = function (val) {     trace("regularAddResult:" + val);   };   nc.call("regularAdd", null, 4, 5); }; 

Here's the server-side script for the first application, myApp :

 application.onAppStart = function (  ) {   this.nc = new NetConnection(  );   this.nc.connect("rtmp://127.0.0.1/otherApp/"); }; Client.prototype.regularAdd = function (a, b) {   res = new Object( );   res.client = this;   res.onResult = function (val) {     this.client.call("regularAddResult", null, val);   };   application.nc.call("regularAdd", res, a, b); }; 

And here's the server-side script for otherApp :

 Client.prototype.regularAdd = function (a, b) {   return (a + b); }; 

The client calls regularAdd( ) on myApp , which in turn calls regularAdd( ) on otherApp , grabbing the result. Then, in order to pass the result to the client, myApp needs to make a server-to-client call to regularAddResult( ) .

One very little known feature of FlashCom named application.registerProxy( ) allows you to solve the problem with a lot less code.

Here's the revised client-side code:

 nc = new NetConnection(  ); nc.owner = this; nc.onStatus = function (info:Object):Void {   if (info.code == "NetConnection.Connect.Success")     this.owner.setUp( ); }; nc.connect("rtmp://127.0.0.1/myApp"); this.setUp = function ( ) {   res = new Object( );   res.onResult = function (val) {     trace("onResult:" + val);   };   nc.call("doAdd", res, 2, 3); }; 

Here's the myApp server-side code:

 application.onAppStart = function (  ) {   this.nc = new NetConnection(  );   this.nc.onStatus = function (info) {     if (info.code == "NetConnection.Connect.Success")       application.registerProxy("doAdd", this);   };   this.nc.connect("rtmp://127.0.0.1/otherApp/"); }; 

And here's the otherApp server-side code:

 Client.prototype.doAdd = function (a, b) {   return a + b; }; 

In this revised version, when the client calls doAdd( ) , it can specify a result object, and the result of doAdd( ) on otherApp will be automatically proxied from otherApp to myApp , all thanks to application.registerProxy( ) .

The registerProxy( ) method also takes a third parameter, so that your methods can have different names in different application instances. For example, we could change the call to registerProxy( ) as follows:

 application.registerProxy("doAdd", this, "addTwoNumbers"); 

and have the code of otherApp be:

 Client.prototype.addTwoNumbers = function (a, b) {   return a + b; }; 

The second parameter passed to registerProxy( ) can be either a reference to a NetConnection connected to another application instance or a reference to a client connected to the application. This way you could have one client execute some methods, having other clients thinking that they are executing methods on the server side.

This technique is useful to offload call processing to another client or instance. For example, you could choose to do the script processing of all database- related methods in an instance specifically set up to handle that task. You could do the same thing in Server-Side ActionScript, but using registerProxy( ) on a client is convenient and sometimes faster to develop (you might already have a standalone client-side application that talks to a database). Simply modify the code to connect to FlashCom and use registerProxy( ) to register the database-related functions for other clients to use.

9.8.10. Passing Objects as Parameters

An extremely rarely used feature of FlashCom allows you to pass full-fledged typed objects as parameters to a client-to-server or server-to-client call.

To understand this feature, let's build an example of the command design pattern , in which commands are sent to a dispatcher that executes them blindly. Usually in the command pattern, there is a Command interface like this:

 interface Command {   public function execute(Void):Void; } 

and command classes that implement it. Here's an example:

 class SetUpGlobalsCommand implements Command {   private var m_name:String;   private var m_id:Number;   // Constructor   function SetUpGlobalsCommand (name:String, id:Number) {     m_name = name;     m_id = id;   }   public function execute (Void):Void {     // Do something   } } 

Another example command could be the following:

 class ResetAllColorsCommand implements Command {  // Constructor   function ResetAllColorsCommand (Void) {   }   public function execute (Void):Void {     // Do something   } } 

Now imagine wanting to implement the command pattern with the preceding commands coming from the server side and going to a client. First, let's build it using the methods already described in this chapter.

This could be your server-to-client call to trigger SetUpGlobalsCommand( ) :

 aClient.call("commandDispatcher", null, {type:"setUpGlobals", name:"Brian", id:0}); 

The ResetAllColorsCommand( ) method could be invoked like this:

 aClient.call("commandDispatcher", null, {type:"resetAllColors"}); 

The client-side handler for both calls would look like something like this:

 nc.commandDispatcher = function (cmdRecord) {   switch (cmdRecord.type) {     case "setUpGlobals":     var newCommand = SetUpGlobalsCommand(cmdRecord.name, cmdRecord.id);     newCommand.execute( );     break;   case "resetAllColors":     var newCommand = ResetAllColorsCommand( );     newCommand.execute( );     break;   } } 

As you can see, the type property of the record passed as a parameter is used to determine which command to instantiate and execute.

Passing typed objects as parameters offers another way to solve the same problem.

Look at this server-side script:

 function SetUpGlobalsCommand (name, id) {   this.m_name = name;   this.m_id = id; } application.registerClass("SetUpGlobalsCommand", SetUpGlobalsCommand); function ResetAllColorsCommand ( ) { } application.registerClass("ResetAllColorsCommand", ResetAllColorsCommand); 

Basically, the code creates two objects and uses application.registerClass( ) to tell FlashCom to serialize the constructor of these classes when passing instances of them as parameters to remote calls.

Only the constructor gets serialized, so there is no point in defining an execute( ) method on these classes here (unless of course you want to use the commands on the server side as well). Remember that no methods get passed through, only the constructor and the class members that are assigned in it.


Here's what the server-to-client calls would look like now:

 aClient.call("commandDispatcher", new SetUpGlobalsCommand("Brian", 0)); aClient.call("commandDispatcher", new ResetAllColorsCommand(  )); 

Notice how we pass class instances instead of simple objects. This is what the client-side code would look like now:

 nc.commandDispatcher = function (cmdInstance) {   cmdInstance.execute(  ); } 

The Flash Player will automatically create an instance of the appropriate class when deserializing the parameter, so all that's left to do is call execute( ) on it.

See how much simpler the handler gets? Okay, admittedly, not that much simpler. We told you this feature was used extremely rarely.

You can pass typed objects in a similar fashion in client-to-server calls. For an example, refer to the Server-Side Communication ActionScript Dictionary documentation that comes with FlashCom.



Programming Flash Communication Server
Programming Flash Communication Server
ISBN: 0596005040
EAN: 2147483647
Year: 2003
Pages: 203

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net