AMF と Perl について

| | Save This Page to del.icio.us このエントリーを含むはてなブックマーク

Flex や Flash で言うところの「Remoting」を行いたい場合、
サーバー側を Perl で実装しようとすると、どんな手段があるでしょうか。

古くから CPAN に存在するのは、AMF::Perl というモジュールです。
これは、AMFPHP からの移植されたモジュールらしいのですが、
残念ながら 2004 年でメンテナンスが止まってしまっていることで、
なかなか使いにくいという状況でした。

そんななか、先日 Data::AMF という新しいモジュールが CPAN
登録されている気付いたので、これを試してみることにしました。

そして試してみる中で色々わかったことがあったのでまとめてみようと思います。


AMF (Action Message Format) とは

そもそも AMF とはなんでしょうか。

名前の中にフォーマットとあるように、
これはただのデータ形式です。

ActionScript における、Object や Array や String や XML などのデータを
バイト列に変換する時にどんな仕様でバイトを並べるか、
ということを定めたものです。

ところで AMF は 2 種類存在しています。
古くからある AMF0 と、新しい AMF3 です。

どちらの仕様も、Adobe から公開されています。
http://opensource.adobe.com/wiki/display/blazeds/Developer+Documentation

この仕様に従って、バイト列を読み書きさえすれば、
バイト列を扱えるものであればどんな言語で AMF を扱うことが出来ます。


Remoting とは

AMF が何となくわかったところで、続いて Remoting です。

Remoting は、クライアントサイドのコードからサーバーサイドのコードに
書いてあるメソッドを読んで、その処理結果を受け取るための仕組みです。

と書くと何だかわかったような気になりますが、
実際にどんな通信が行われているかこれだといまいちわかりません。

実際に行われているのはただのバイト列のやりとりです。

クライアントからサーバーにバイト列を POST し、
サーバーはクライアントにバイト列を返す、ただそれだけのことをしています。

それでなんでサーバーサイドのメソッドを呼べるのか、というと
AMF の時と似ていて、バイト列を生成するためのルールがあるからです。

Remoting で扱われるバイト列の中には、
サーバーサイドで呼んで欲しいメソッドの名前や引数、返り値などが含まれます。

さらに、引数や返り値として扱われるデータは AMF になっているため、
AMF のバージョン情報も含まれています。

Remoting の仕様は AMF0 PDF の「4.1 AMF パケットと NetConnection」
という箇所に書かれています。


サーバー側の Remoting の実装

何もないところから始めると、Remoting を実現するためには
AMF と Remoting の両方の仕様に従った実装が必要です。

Data::AMF の現在のバージョンでは、
AMF0 の実装と、Remoting の実装だけされています。

AMF3 のデータを含んだ Remoting のバイナリは扱えません。
AMF3 を扱いたい場合には、自前で AMF3 のデータを扱うための
I/O クラスを実装する必要がありそうです。


クライアント側の Remoting の実装

ActionScript で Remoting のコードを書こうとする場合、
通常であれば NetConnection クラスを使用します。

Flex のライブラリも実際には NetConnection を使っています。

しかし、Remoting の仕様さえ理解していれば、
URLLoader クラスで書くことも出来ます。

Remoting の仕様に従った ByteArray を作り、
URLRequest クラスの data プロパティに渡して、
URLLoader クラスの load メソッドを呼べば良いだけです。


実際に Remoting してみる

仕組みがわかったところで、実際にテストしてみます。

AMF でやりとりするメリットには、XML によるやりとりに比べて
速度面で優位になる、という点がありますので、
両者を比較するための簡単なアプリケーションを用意してみました。

http://seagirl.jp/labs/amf/index.html

このアプリケーションでは、Data::AMF を使った CGI とのやりとりに加え、
CGI からの Remoting のレスポンスをファイルにダンプした
スタティックなファイルを用意しておくことで、
単にスタティックなファイルを読むというテストも含めています。

スタティックな Remoting のデータが書かれたファイルと、
スタティックな XML ファイルを読んで、速度比べることで、
純粋に XML AMF の差を調べられるようにしました。

その結果、AMF と XML のデータフォーマットの違いによる
通信コストの差は XML AMF のだいたい 1.5 倍ということがわかりました。

さらに、CGI で Perl を使って動的にデータ(Perl のオブジェクト) を生成し、
XML::Simple でシリアライズしたものと、Data::AMF でシリアライズしたもの
をそれぞれ用意し、速度を比べてみました。

すると、スタティックなファイルで比べた時とは違い、
ほとんど同じか、あるいは Data::AMF の方が遅い、というような
結果になりました。

ということは、Data::AMF によるシリアライズが、
XML::Simple によるシリアライズに比べて遅いということになります。

これでは本末転倒になってしまい、
Remoting をする意味がなくなってしまいます。

AMF なバイト列を作るのはそんなにコストがかかるのか
ということになりますが、Data::AMF のソースを見てみたところ、
このモジュールは Pure Perl で書かれていました。

これを XS で書き直すことで改善するのではないか
ということで、Data::AMF を XS にしてみたいと思いました。

最後に今回の実験用に作ったアプリケーションの Flex のコードを公開します。

 

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
	xmlns:mx="http://www.adobe.com/2006/mxml"
	layout="vertical"
	horizontalAlign="left"
	verticalGap="10">
	<mx:Script>
		<![CDATA[
			import mx.utils.ObjectUtil;
			import mx.controls.Alert;
			import mx.collections.XMLListCollection;
			import flash.utils.getTimer;
			import mx.collections.ArrayCollection;
			
			public static const BASE_URI:String = initializeBaseURL();
				
			private static function initializeBaseURL():String
			{
				if ((Security.sandboxType.indexOf('local') != -1))
					return 'http://localhost/~yoshizu/labs/AMF/htdocs/';
				else
					return 'http://seagirl.jp/labs/amf/';
			}
				
			private var startTime:int = 0;
			
			private function now():int
			{
				return getTimer() - startTime;
			}
			
			private function log(... rest):void
			{	
				var args:Array = rest;
				if (args[0] is Function)
				{
					var func:Function = args.shift();
					func(args);
				}
				
				var str:String = '';
				for each (var argument:String in args)
				{
					str += argument + ' ';
				}
				
				if (result.text != '')
					result.text += "\n";
				
				result.text += str;
			}
			
			private function reset():void
			{
				result.text = '';
				dataGrid.dataProvider = null;
			}
				
			private function getData():void
			{
				log(trace, '--------------------------------------------------', new Date());
				
				var buffer:String = 'Mode: ';
				buffer += mode.selectedValue ? 'Static File' : 'CGI';
				buffer += '\n';
				buffer += 'Data Type: ';
				buffer += dataType.selectedValue == 1 ? 'AMF' : 'XML';
				buffer += '\n';
				log(buffer);
				
				submit.enabled = false;
				
				if (!mode.selectedValue && dataType.selectedValue == 1)
					getDataWithNetConnection();
				else
					getDataWithURLLoader();	
			}
			
			private function getDataWithNetConnection():void
			{				
				startTime = getTimer();
				log('Start loading: ', now());
				
				var nc:NetConnection = new NetConnection();
				nc.objectEncoding = ObjectEncoding.AMF0;
				nc.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler);
				nc.addEventListener(AsyncErrorEvent.ASYNC_ERROR, errorHandler);
				nc.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
				nc.addEventListener(SecurityErrorEvent.SECURITY_ERROR, errorHandler);
				nc.connect(BASE_URI + 'amf.cgi');
				nc.call('get_data', new Responder(callback, null), 1, 2);
			}
			
			private function callback(res:Object):void
			{
				log('Finish loading:', now());
				
				var array:Array = res.data;
				dataGrid.dataProvider = new ArrayCollection(array);
				
				log('Finish display:', now(), '\n');
				
				submit.enabled = true;
			}
			
			private function getDataWithURLLoader():void
			{
				startTime = getTimer();
				log('Start loading:', now());
				
				var name:String = dataType.selectedValue == 1 ? 'amf' : 'xml';
				var extension:String = mode.selectedValue ? 'txt' : 'cgi';
				
				var v:URLVariables = new URLVariables();
				v.action = 'get_data';
				
				var r:URLRequest = new URLRequest();
				r.url = BASE_URI + name + '.' + extension;
				r.data = v;
				
				if (!mode.selectedValue)
					r.method = URLRequestMethod.POST;
				
				var l:URLLoader = new URLLoader();
				l.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
				l.addEventListener(SecurityErrorEvent.SECURITY_ERROR, errorHandler);
				
				if (dataType.selectedValue == 1)
				{
					l.dataFormat = URLLoaderDataFormat.BINARY;
					l.addEventListener(Event.COMPLETE, amfCompleteHandler);
				}
				else
				{
					l.addEventListener(Event.COMPLETE, xmlCompleteHandler);
				}
				
				l.load(r);
			}
			
			private function amfCompleteHandler(event:Event):void
			{
				var bytes:ByteArray = ByteArray(event.currentTarget.data);
				bytes.position = 0;
				
				var version:uint = bytes.readUnsignedShort();
				var headerLength:uint = bytes.readUnsignedShort();
				var messageLength:uint = bytes.readUnsignedShort();
				
				trace('AMF Version:', version);
				trace('Header Length:', headerLength);
				trace('Message Length:', messageLength);
				
				// ヘッダをパース
				var headers:Array = [];
				new Array(headerLength).forEach(
					function (element:Object, index:int, array:Array):void
					{
						var name:String = bytes.readUTF();
						var must:uint = bytes.readUnsignedInt();
						var length:uint = bytes.readUnsignedInt();
						
						var data:ByteArray = new ByteArray();
						data.objectEncoding = ObjectEncoding.AMF0;
						
						bytes.readBytes(data, 0, length);
						
						var value:Object = data.readObject();
						headers.push(value);
					}
				);
				
				// メッセージをパース
				var messages:Array = [];
				new Array(messageLength).forEach(
					function (element:Object, index:int, array:Array):void
					{
						var targetURI:String = bytes.readUTF();
						var responseURI:String = bytes.readUTF();
						var length:uint = bytes.readUnsignedInt();
						
						trace('Target URI:', targetURI);
						trace('Response URI:', responseURI);
						trace('Length:', length);
						
						var data:ByteArray = new ByteArray();
						data.objectEncoding = ObjectEncoding.AMF0;
						
						bytes.readBytes(data, 0, length);
						trace('Message Data Length:', data.length);
						
						var value:Object = data.readObject();
						messages.push(value);
					}
				);
				
				// 取得したデータをコールバック関数に渡す
				messages.forEach(
					function (element:Object, index:int, array:Array):void
					{
						callback(element);
					}
				);
			}
			
			private function xmlCompleteHandler(event:Event):void
			{
				var result:XML = XML(event.currentTarget.data);
				
				log('Finish loading:', now());
				
				var items:XMLList = result.data.item;
				dataGrid.dataProvider = new XMLListCollection(items);
				
				log('Finish display:', now(), '\n');
				
				submit.enabled = true;
			}
			
			private function netStatusHandler(event:NetStatusEvent):void
			{
				if (event.info.code == "NetConnection.Call.Failed")
				{
					Alert.show(ObjectUtil.toString(event.info), 'エラー');
					
					submit.enabled = true;
				}
			}
			
			private function errorHandler(event:ErrorEvent):void
			{
				Alert.show(event.text, 'エラー');
				
				submit.enabled = true;
			}
			
		]]>
	</mx:Script>
	<mx:Text text="Comparing AMF with XML" fontWeight="bold" fontSize="18"/>
	
	<mx:Spacer/>
	
	<mx:Grid>
		<mx:GridRow>
			<mx:RadioButtonGroup id="mode"/>
			<mx:GridItem>
				<mx:Text text="Mode:"/>
			</mx:GridItem>
			<mx:GridItem>
				<mx:RadioButton groupName="mode" value="true" label="Static File"/>
			</mx:GridItem>
			<mx:GridItem>
				<mx:RadioButton groupName="mode" value="false" label="CGI (Using Perl and Data::AMF)" selected="true"/>
			</mx:GridItem>
		</mx:GridRow>
		<mx:GridRow>
			<mx:RadioButtonGroup id="dataType"/>
			<mx:GridItem>
				<mx:Text text="Data Type:"/>
			</mx:GridItem>
			<mx:GridItem>
				<mx:RadioButton groupName="dataType" value="1" label="AMF" selected="true"/>
			</mx:GridItem>
			<mx:GridItem>
				<mx:RadioButton groupName="dataType" value="2" label="XML"/>
			</mx:GridItem>
		</mx:GridRow>
	</mx:Grid>
	
	<mx:HBox>
		<mx:Button id="submit" label="Submit" click="getData()"/>
		<mx:Button label="Reset" click="reset()"/>
	</mx:HBox>
	
	<mx:Spacer/>
	
	<mx:VDividedBox width="100%" height="100%">
		<mx:TextArea id="result" width="100%" height="70%"/>
		<mx:DataGrid id="dataGrid" width="100%" height="30%">
			<mx:columns>
				<mx:DataGridColumn dataField="id"/>
				<mx:DataGridColumn dataField="name"/>
				<mx:DataGridColumn dataField="description"/>
			</mx:columns>
		</mx:DataGrid>
	</mx:VDividedBox>
		
</mx:Application>

TRACKBACK

このブログ記事を参照しているブログ一覧: AMF と Perl について

このブログ記事に対するトラックバックURL: http://blog.s2factory.co.jp/MT/mt-tb.cgi/67