AS3SX入門4 後編。
やること
AS3SX 入門4 PersistentObjectについて(前編)
insertボタンを押すとサーバーサイドでデータが挿入される。
データには順番に1ずつ増えていく一意のインデックスが割り当てられ、挿入日時とあわせて保存される。
insertを呼んでからコールバックのinsertCompleteが呼ばれるまでの時間(ミリ秒)を計り、レスポンスとしてインデックスとともに返す。
queryボタンを押すとテキスト入力の数字と一致するインデックスを持つデータを検索し、データの有無と総データ数、コールバックまでの時間(ミリ秒)と見つかったのなら挿入日時をレスポンスとして返す。
indexedにチェックを入れるとインデックスが張られたフィールドで検索する。
最初にリクエストを送ってからサーバーサイドswfが起動する時間が少しかかるかもしれない。
うーん。DBへのアクセス時間はどうなんだろ。インデックス張られたフィールドで検索しても違いはないように見える。というか結構なばらつきが。
もっとデータ件数が増えれば恩恵を受けられるのかな?
QueryRequestクラス
をもつだけのクラス。検索のリクエスト用。
DBdataクラス
をもつだけのPersistentObject継承クラス。
CustomDataのインデックスは0から連番にしたいのだが、そのためにはその時点でドキュメントがいくつあるかの情報が必要だ。サーバーサイドのインスタンス変数にいれておけばいいように思えるが、それだとサーバーサイドswfがシャットダウンしてしまえばリセットされてしまう。
なので必然的にDBから取得することになるのだが、ドキュメント数を得るメソッドは見当たらない。
get~Array系で全ドキュメント取得し、返り値のArrayのlengthを調べるという手もあるのだがなんかぜんぶ持ってくるって明らかに無駄な処理でいやだ。
仕方ないから他のコレクションを作成してドキュメントを1つだけ挿入し、そこにいれとく、そのためのクラス。
さらにswfがシャットダウンするタイミングが分からないためCustomDataをinsertするごとに毎回アップデートする羽目に。
CustomDataクラス
package { import com.as3sx.persitency.PersistentObject; import flash.utils.getTimer; /** * ... * @author */ public class CustomData extends PersistentObject { public static var iComplete:Function; public var indexedId:uint; public var nonIndexedId:uint; public var date:String; public function CustomData(){ } override public function insertComplete():void { iComplete(getTimer()); } override public function getIndexedFields():Array { return ["indexedId"]; } } }
実際のデータを入れておくPersistentObject。
保存するフィールドは挿入日時のdate:StringとドキュメントのインデックスのindexedId:uintとnonIndexedId:unit。
この2つのuintには同じ値を入れる。
なんのためにあるかというと、片方をインデックスの張られたフィールドにしてみてそれぞれのフィールドからの検索時に速度の違いが見られるのかな、って思って。
なのでgetIndexedFields():Arrayをoverrideして["indexedId"]を返すようにしておく。これでindexedIdでの検索時にはインデックスが張られているので速い・・・はず。
挿入が完了するまで時間も計りたいのでinsert完了時に自動で呼びされるinsertComlete():voidをoverrideしてgetTimer()しておく。
getTimer()したはいいがその結果をどうすればいいのだろう。
外部に結果を伝えるにはインスタンス変数として持って外部から読むか、外部の参照に結果を渡すしかない。
外部から読めるインスタンス変数というとpublicにする必要があるがpublicインスタンス変数を持たせるとDBに無駄に保存してしまう。
一度にinsert出来るのは1つのPersistentObjectだけだろうからstatic変数にいれることで解決はできるが、外部からpublic static変数を読もうにもいつ読んでいいのかわからない。外部にinsertの完了を知らせることが必要だ。
そこでiComlete:Functionというstatic変数を持たせてコールバックのinsertComlete内でそれを呼ぶことにする。
あらかじめstatic public var iComplete:Functionにメソッドを入れておけば完了時にそのメソッド呼ばれる仕組みだ。
ついでにgetTimer()の結果も伝えたいので引数にintをとることにしよう。
package { import com.as3sx.server.PersistentObjectManager; import flash.display.Sprite; import flash.utils.getTimer; /** * ... * @author */ public class ServerMain extends Sprite { private var persistent:PersistentObjectManager; private var ready:Boolean; private var dbData:DBdata; private var time:int; public function ServerMain():void { // entry point ready = false; CustomData.iComplete = this.iComplete; ServerAS3SX.setRequestHandler(Boolean, onInsert); ServerAS3SX.setRequestHandler(QueryRequest, onQuery); persistent = ServerAS3SX.persistentObject; if (ServerAS3SX.isRunningOnAS3SX()){ persistent.getOnePersitentObjectBy("___type", "DBdata", DBdata, onDBdataLoad); } } private function onDBdataLoad(data:DBdata):void { if (data == null){ dbData = new DBdata(); dbData.numDocuments = 0; dbData.insert(); } else { dbData = data; } ready = true; } private function onInsert(e:Boolean):void { if (!ready){ return; } var po:CustomData = new CustomData(); po.indexedId = dbData.numDocuments; po.nonIndexedId = dbData.numDocuments; po.date = new Date().toString(); time = getTimer(); po.insert(); } private function iComplete(endTime:int):void { ServerAS3SX.sendResponse("[insert] index : " + dbData.numDocuments + ", time : " + (endTime - time) + " ms"); dbData.numDocuments++; dbData.update(); } private function onQuery(e:QueryRequest):void { if (!ready){ return; } var field:String; if (e.isIndexed){ field = "indexedId"; } else { field = "nonIndexedId"; } time = getTimer(); persistent.getOnePersitentObjectBy(field, e.index, CustomData, onCustomDataLoad); } private function onCustomDataLoad(data:CustomData):void { time = getTimer() - time; if (data == null){ ServerAS3SX.sendResponse("[not found] total data : " + dbData.numDocuments + ", time : " + time + " ms"); } else { ServerAS3SX.sendResponse("[found] total data : " + dbData.numDocuments + ", time : " + time + " ms, inserted : " + data.date); } } } }
コンストラクタ
サーバーサイドswf起動時にやりたいことはなにか。
次に挿入すべきCustomDataコレクションのインデックスがどこから始まるのかを知るためにDBdataからドキュメントを読み込んでおかなくてはならない。
なのでpersistent.getOnePersitentObjectBy()をコンストラクタで呼ぶ。
ドキュメントは1つしかないので検索条件は適当にコレクション名にした。(___typeフィールドはコレクション名(PersistentObject継承クラス名))
しかしこれだとコンパイル時(はクライアントサイドなので、コンパイルと同時にswfを実行する設定にしてあると)にエラーが出てしまう。
ちょっと嫌なので、確かにサーバーサイドで実行されていますよ、という確認が取れた場合のみ呼ぶことにする。
ServerAS3SX.isRunningOnAS3SX():Booleanにはそんな機能があるので使ってみよう。実行がサーバーサイドならtrueが返される。
DBdataの読み込みが完了しないとCustomDataの操作はできないが、サーバーサイドswfが起動されるのは最初にクライアントサイドからリクエストがあったときだ
なのでサーバーサイドのコンストラクタで非同期処理を行う場合、コンストラクタでServerAS3SX.setRequestHandlerしてあると非同期処理が終わる前にリクエストハンドラが実行されてしまう可能性がある。
準備完了してからServerAS3SX.setRequestHandlerする方法もあるが、それだと最初のリクエストに対して「そんなリクエストは想定されてないよ」というCautionが出るので気持ちのいいものではない。
なので準備完了かどうかのフラグを作ってリクエストハンドラの頭で準備完了かどうかのチェックをしてまだなら無視するようにした。
この方法だと、サーバーサイドswf起動時の最初のリクエストが無視されることになるので、クライアントサイド側で再送の仕組みが必要だ。
そしてCustomData.iCompleteにthis.iCompleteをいれておく。これでCustomDataのinsertComplete時にiCompleteが呼ばれる。
onDBdataLoad
DBdataの読み込みが完了したらまずドキュメントがあったかどうかを確認しよう。検索の結果見つからなかった場合nullが返ってくるので引数がnullかを確認する。
nullだった場合(本当に最初の一回だけだが)まだコレクションが作られてないのでDBdataをnewしてinsertしておく。
このDBdataのinsertは特に待つ必要はない。
nullじゃなかった場合は引数をそのままインスタンス変数に入れる。
これでdbData.numDocumentsでCustomDataのドキュメント数が取得できるようになった。
準備完了したのでフラグをたてておく。
onInsert
Booleanがリクエストとして送られてきたらCustomDataを新しく作ってinsertする。
indexedIdとnonIndexedIdには同じ値(dbData.numDocuments)を入れて、現在時刻もStringにして保存。
getTimer()で挿入開始時刻を保持しておいてinsert開始だ。
insertが完了するとCustomDataのinsertCompleteでgetTimer()してiCompleteの引数として返ってくる。
iComplete
getTimer同士の値を引くことでinsert()からコールバックのinsertComleteが呼ばれるまでの時間が計れる。これをついでに挿入したドキュメントのインデックス番号と一緒にレスポンスにして返そう。
CustomDataのドキュメントが増えたのでdbData.numDocumentsを+1してupdateいておく。
このupdateはいつswfがシャットダウンしてもいいようになので、特に完了を待つ必要はない。
onQuery
QueryRequestクラスのリクエストがきたらisIndexed:BooleanでindexedIdとnonIndexedIdどちらのフィールドで検索するかを選んでindexの値と一致するものを検索する。
検索時間を計るためにgetTimer()しとく。
onCustomDataLoad
CustomDataの検索が完了したらコールバックが呼ばれるまでの時間を計算する。
検索結果が見つかった場合(QueryRequestのindexがdbData.numDocumentsより小さいときは見つかるはず)、見つかった旨と合計いくつのデータから検索したのか、検索時間、ついでに見つかったデータがinsertされた日時をStringにしてレスポンスする。
見つからなかった場合はinsert日時以外をレスポンス。
package { import com.as3sx.connection.SocketConnection; import com.bit101.components.CheckBox; import com.bit101.components.InputText; import com.bit101.components.PushButton; import flash.display.Sprite; import flash.events.Event; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.utils.Timer; /** * ... * @author */ public class ClientMain extends Sprite { /// change endpoint private static const END_POINT:String = "http://as3sx.com/Apps/いあいあ/"; private var tf:TextField; private var timer:Timer; private var object:*; private var insertButton:PushButton; private var queryButton:PushButton; private var indexed:CheckBox; private var index:InputText; public function ClientMain():void { if (stage) init(); else addEventListener(Event.ADDED_TO_STAGE, init); } private function init(e:Event = null):void { removeEventListener(Event.ADDED_TO_STAGE, init); // entry points ClientAS3SX.setEndPoint(END_POINT); addEventListener(Event.ENTER_FRAME, connectionCheck); } private function connectionCheck(e:Event):void { if (SocketConnection.instance.connected){ removeEventListener(Event.ENTER_FRAME, connectionCheck); // connected tf = new TextField(); tf.autoSize = TextFieldAutoSize.LEFT; tf.x = 20; tf.y = 200; addChild(tf); insertButton = new PushButton(this, 20, 20, "insert", onInsert); queryButton = new PushButton(this, 20, 110, "query", onQuery); indexed = new CheckBox(this, 20, 80, "indexed"); index = new InputText(this, 100, 80); index.restrict = "0-9"; index.text = "0"; // timer = new Timer(5000); timer.addEventListener(TimerEvent.TIMER, onTimer); ClientAS3SX.setResponseHandler(String, onString); } } private function onInsert(e:MouseEvent):void { tf.text = ""; insertButton.enabled = false; queryButton.enabled = false; object = true; timer.start(); ClientAS3SX.sendRequest(object); } private function onQuery(e:MouseEvent):void { tf.text = ""; insertButton.enabled = false; queryButton.enabled = false; var query:QueryRequest = new QueryRequest(); query.isIndexed = indexed.selected; if (index.text == ""){ query.index = 0; } else { query.index = uint(index.text); } object = query; timer.start(); ClientAS3SX.sendRequest(object); } private function onTimer(e:TimerEvent):void { //try to request again ClientAS3SX.sendRequest(object); } private function onString(e:String):void { object = null; timer.reset(); tf.text = e; insertButton.enabled = true; queryButton.enabled = true; } } }
リクエストを送る際にTimerインスタンスで5秒間計り、それまでにレスポンスがない場合リクエストを再送するようにしてある。
これでサーバーサイドの、最初のリクエストが無視される問題を無理矢理?解決できる。
あとはレスポンスが返る前にまたリクエストを送るとめんどくさそうなので禁止してあるけど、正直多人数が同時に送ってきたらどうなるかはまったく考えていない。
今回のプロジェクトのソースファイルはこちら。
PersistentObjectを使えば簡単なデータ保存くらいはできるようになるかもね。
データベースとしてはちょっとどうなの?ってかんじ。
少なくともバグらしきものは直してくれないと。
以上入門終わり。
AS3SXの主要な機能はこれで使えるようになったはずだからあとはアイディア次第かなあ。
とりあえずできるようになってほしいこと。技術的に無理かは知らん。
AS3SX Admin Center
API
あと地味に送信できるデータの上限(約2kByte)もきついかな。分割して送ることはできるけど。
Feedbackに対するチーム?からのレスポンスがないのと、外人(日本人もだけど)の反応が薄いのがどうもなあ。
Author:9ballsyn
ActionScriptについて
最近はMolehill