Using AMF3 for the cool shit
19 Jan 2009As we are now in the year 2009, for some reason you feel like there ought to be a way to take your cool shit, send it over the wire and get it back again, without having to resort to 99 searches on Google, a trip to Barnes & Noble for a stack of books with ugly green covers and stupid illustrations of birds and salamanders, and a spare weekend you'd be doing nothing better with anyway.
Your cool shit's in Flash, and it's not just text data, presumably, so by a process of elimination we conclude that your shit -- not of a lukewarm nature by any means -- is most appropriately represented by a series of ones and zeros -- in binary format. And that you want to get it on the database. 'Cuz you can't create the next viral sensation if you can't get your cool shit to the database, now can you?
And while many prosaic and generally annoying 3-to-4-letter acronyms exist for the transmission of information across the proverbial 'series of tubes,' unfortunately, most of them are founded on that most old school of information encoding conventions: the alphabet. (But if it's in UTF format, maybe we can be a little more forgiving and call it "Alphabet 2.0"). Sure, you could do something like MyCoolAssStaticUtilitiesClass.convertMyCoolShitToBase64($o:Object)
, but nah, fuckit, it's the year 2009 damnit, and you'd think there'd be an easy way to take your data - made on a computer - and store it - again, on another computer, without having to first convert it into "human-readable text," whatever the fuck that means.
Furthermore, if your shit's actually as cool as you say it is, we must be talking about something that's not just a single block of binary data like an bitmap, oh no, but a complex data object, full of mixed primitive types, maybe some untyped objects and an array or two, other value objects nested inside of them, and _then_ maybe an image or two. But the main thing is, it's stuff that you want to just dump into a field in a database record, and get back, without the extra drama of converting to and from XML with a couple extra hundred lines of code...
Anyway, presented here are the important points I learned about using AMFPHP with ByteArrays and VO's while starting on a new project/experiment thing involving user-generated character animations of 3D models, which naturally involves lots of binary data and complex data types.
Part 1: Moving binary data between Flash and the database
So I've drawn up a kind-of minimal example where some simple images are generated and saved to the database as binary. You can get the full project for both the Part 1 and Part 2 examples here. It's written in as flat and as non-object-oriented a manner as possible for your (my) convenience. The two important files to look at for Part 1 are "AmfphpBinaryExample.as" and "BaseService.php".
This won't be a literal step-by-step recipe, so use your working knowledge of the following to fill in the blanks: AS3, the AMFPHP Flash class package (v1.9 or whatever it ended up at), basic PHP5 using classes, and MySQL (tech stack of champions).
AMFPHP setup
First, a small point: After unzipping an undespoiled version of AMFPHP to your project/server environment, rename the file ".htaccess" in the unzipped "amfphp" directory to like ".htaccess_EXAMPLE". If you don't, it can make the directory inaccessible on Apache.
AMFPHP's 'encoding' property must be set to AMF3 for binary data to get properly sent to the Flash. Set $amfphp['encoding']
to 'amf3'
in the file "amfphp/core/shared/app/Globals.php", or in the file "amfphp/gateway.php" after the line include "core/amf/app/Gateway.php";
.
MySQL table setup
Create a table and add an "id" field that auto-increments as the primary key, as is customary. Also add a field in that table named "image". This is where our binary image data will go.
Most importantly, the data type for your binary field should be a blob, say, MEDIUMBLOB
(which can have a maximum size of 16mb).
If you're setting up the example project, use a SQL statement like this to set up your table if you're so inclined:
CREATE TABLE `[mydatabase]`.`binaryexample` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`data` MEDIUMBLOB NOT NULL
)
PHP service classes
When sending ByteArray to a PHP method, it comes in as on object with a property called "data", as it turns out. So if you do something like --
netConnection.call("MyService.saveData", myResponder, myByteArray);
-- and your PHP function looks like --
function saveData($argument) { ... }
-- you'd reference the binary data not with "$argument
", but "$argument->data
". Argh.
Once you have your variable with binary data in the PHP, before going to the database, you must use "mysql_real_escape_string" it first:
$binaryData = mysql_real_escape_string($binaryData);
If you're savvy with PHP/MySQL, you already do this as a matter of course, but if you're just a dabbler, like me, you may not know to. If you don't, your SQL operation will probably fail and worse, leaves an easy opening for the hacking of your database.
Finally, when you send the binary data back to the Flash, you have to put it in a ByteArray object:
return new ByteArray($myBinaryData);
AS3 setup
In AS3, AMF3 is the default encoding format for Flash remoting, but you might want to do this just to make your intentions explicit:
NetConnection.defaultObjectEncoding = ObjectEncoding.AMF3;
At this point, sending your binary shiz back and forth from Flash to PHP to the database and back... should just work. Hah!
Part II: Converting value objects to and from ByteArrays for database storage
In this example, the user manually draws on the screen to create a picture, but rather than save the end product as a static image, we're saving each 'stroke' in order to play back the entire process in a procedural fashion a la Pictaps. (The main AS source file to look at is "AmfphpVoBinaryExample.as"; the PHP service class is the same as in Part 1.)
This kind of 'use case' gives us an excuse to have to create a more complex data model than that of a bitmap or a WAV file, which can be treated as just a monolithic block of binary data. A conventional approach to this scenario might be to create an XML file like this:
<canvas>
<stroke fromX="5" fromY="4"
toX="6" toY="8" timecode="1"
thickness="2" color="#ff0000" />
<stroke ... />
<stroke ... />
etc.
</canvas>
Instead, we're gonna convert our data objects to ByteArray's for storage, and coming back, convert the ByteArrays back to ready-to-use instances of our custom classes. Thanks to AMF, this gets done with basically one line of code.
Creating the AS3 data classes
But first, let's explain the AS data structures used here. In this case, it makes sense to make a class called "Stroke", or "StrokeVO", if you will, which represents a line segment as drawn by the user, with the necessary variables for describing its properties:
package vos {
public class Stroke {
public var startX:Number;
public var startY:Number;
public var endX:Number;
public var endY:Number;
public var color:uint;
// etc.
}
}
The whole series of strokes will constitute a Canvas
:
package vos {
public class Canvas {
public var strokes:Array;
// etc.
}
}
Note, you might also add properties that are Strings, anonymous Objects, ByteArrays, etc., as well.
Converting classes to/from ByteArray
So let's say the user has created their drawing, and it's ready to be sent to the server. It'll be just a matter of:
var byteArray:ByteArray = new ByteArray();
byteArray.writeObject(canvas);
Going in the other direction, it looks like this:
canvas = byteArray.readObject() as Canvas;
The last critical piece of business is to register the class alias for each of the custom classes used for our data objects. Otherwise, Flash won't know how to properly cast the anonymous Object created from the ByteArray.
registerClassAlias( "vos.Canvas", Canvas );
registerClassAlias( "vos.Stroke", Stroke );
Oh yeah, if you're setting up this example for yourself, the database table is identical to the last one except for the table name:
CREATE TABLE `[mydatabase]`.`vobinaryexample` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`data` MEDIUMBLOB NOT NULL
)
Limitations
There are some pretty important limitations to this system in terms of deserialization. One is that your custom classes to be deserialized can't use references to other objects. That just doesn't work. Another, unfortunately, is that the constructors can't take arguments. This throws errors when you do "ByteArray.readObject()". Also, apparently, implicit getter/setters can introduce subtle or not-so-subtle side-effects when the setters do anything other than just store a value.
So in other words, your VO classes should be designed to be just that -- VO's: Strongly typed data structures storing values, or containing subobjects or sub-sub-objects that ultimately do the same, with minimal or no built-in functionality of their own. But oh well.
Part III: 'Class mapping', plus, a convenient service wrapper class
Finally, there's the issue of sending VO's from PHP to Flash, while having both PHP and Flash recognize the VO class for what it is. For this to work, you need to have matching class definitions for the VO in AS and PHP.
In this last example is my very own service 'wrapper' class for making remoting calls from Flash to PHP. It abstracts out NetConnection, Responder, and the "onResult"/"onFail" handler mechanics, making it easy to use. Here is the source.
In any case, in its Netconnection response handler, this class expects a VO from PHP called 'RemotingVO', which contains the 'payload' and any error information. A VO with the same name and the same properties exists on the Flash end. For this to work requires two steps:
(1) You have to "registerClassAlias" on the Flash side, as discussed earlier:
registerClassAlias("leelib.remoting.RemotingVo", RemotingVo);
(2) You have to add a line like this in your AMFPHP VO class:
var $_explicitType="leelib.remoting.RemotingVo";
Anyway, I get this info from this blog post, which does a much better job of explaining it than I'm doing now.
So now I'm done. That's everything I know about using AMFPHP. Too bad at my job, we use .NET on the backend, LOL.