読者です 読者をやめる 読者になる 読者になる

AMF実装におけるハマリどころ

T2にてAMF実装を行っていますが、そこで調査してはまった事を書いておきます。

ハマリ1(we return null)

blazeDSのclientからメッセージ送信した際に、送信メッセージ内のclass名が空で飛んできたせいでうまく動作しないところが出てきた。
半泣きになって調べて、BlazeDSの実装を見て修正したがイマイチ納得出来てなかったところ
カタヤマンことid:c9katayama飯田橋の彼方からやってきてモヤモヤを開放してくれた。
これに関してはカタヤマンがエントリを書いてくれるはずなので割愛しますが、Adobeの熱い思いがBlazeDSのソースからは読み取れた。
私たちはNULLを返すのだと。

ハマリ2

JavaFXで独自実装で手軽にAMF通信したいと思いT2からAMF部だけ切り出してクライアントを作ってみた。
BlazeDS実装のサーバ側とT2実装のサーバ側と正常に通信出来たので、これはもうOKだろうと思ったので
調子に乗って他のAMF3実装でも動作確認してみようかなと軽いノリで思ったのが運の尽きでした。
結局、python実装、PHP実装、Ruby実装、Red5と次々にソースを確認するはめになってしまった。
他のAMF3実装で確認した代表的なものは以下。

PyAMF

http://pyamf.org/

最初に試したPyAMFは、ボディ部の先頭バイトが0x0a(AMF0配列)であるかどうかをチェックしていたために自作ったクライアントから
メッセージがうまく送れないのではまった。
自作が悪いのかと思い、BlazeDSのクライアントで試すがこれもNG。この時点でPyAMFが悪いのかと錯覚。
いろいろ調査した結果、flexからの送信しか受付ないんだという強い意思がPyAMF実装からは感じられた。
つまりPyAMFと通信するクライアントを書くには、flexが送るバイナリと同等なものを送らないといけないので注意。
先頭は0x0aにしないといけない。

  • PyAMFにおけるチェック(pyamf/remoting/__init__.py)
def _read_body(stream, decoder, strict=False):
    def _read_args():
        """
        @raise pyamf.DecodeError: Array type required for request body.
        """
        if stream.read(1) != '\x0a':
            raise pyamf.DecodeError("Array type required for request body")


読みこみだけでは無く、書き込み時もPyAMFに関しては同様。

  • PyAMFにおいてbody部を書き込む際には、Responseじゃない場合は、0x0aをwriteしている
    def _encode_body(message):
        if isinstance(message, Response):
            encoder.writeElement(message.body)

            return

        stream.write('\x0a')
        stream.write_ulong(len(message.body))
        for x in message.body:
            encoder.writeElement(x)
Zend-AMF(Zend/Amf/Request.php)

Zend-AMFだとコメントには、AMF0配列の中にAMF3メッセージだよとそれらしく書いてあるが特別チェックは行っていない。
条件を満たしてる時に、配列として扱ってるだけでPyAMFのようにはじいていない。
なのでBlazeDSのクライアントでもうまく通信は可能である。

<?php
    public function readBody()
    {
        $targetURI   = $this->_inputStream->readUTF();
        $responseURI = $this->_inputStream->readUTF();
        $length      = $this->_inputStream->readLong();

        try {
            $data = $this->_deserializer->readTypeMarker();
        } catch (Exception $e) {
            require_once 'Zend/Amf/Exception.php';
            throw new Zend_Amf_Exception('Unable to parse '.$targetURI.' body data '.$e->getMessage());
        }

        // Check for AMF3 objectEncoding
        if ($this->_deserializer->getObjectEncoding() == Zend_Amf_Constants::AMF3_OBJECT_ENCODING) {
            /*
             * When and AMF3 message is sent to the server it is nested inside
             * an AMF0 array called Content. The following code gets the object
             * out of the content array and sets it as the message data.
             */
            if(is_array($data) && $data[0] instanceof Zend_Amf_Value_Messaging_AbstractMessage){
                $data = $data[0];
            }
rubyamf

http://www.rubyamf.org/
rubyamfもZend-AMF同様、先頭バイトは特にチェックしていないのでBlazeDSのクライアントで通信可能。

amf_deserializer.rb

      def bodys
        @amf0_object_default_members_ignore = {}
        
        #find the total number of body elements
        body_count = read_int16_network
        
        #Loop over all the body elements
        0.upto(body_count - 1) do
          
          reset_referencables
          
          #The target method
          target = read_utf
          
          #The unique id that the client understands
          response = read_utf
          
          #Get the length of the body element
          length = read_word32_network
          
          #Grab the type of the element
          type = read_byte
          
          #Turn the argument elements into real data
          value = read(type)
          
          #new body
          body = AMFBody.new(target,response,value)
          
          #add the body to the amfobj 
          @amfobj.add_body(body)
        end    
      end
Red5

http://code.google.com/p/red5/

Red5も特別チェックは行っていないが、buildの際にivyを使うのでivyの勉強になった。
ivyは、ビルドして依存関係全て落としてきてからlib配下のjarにclasspathを通す必要がある。
Red5BlazeDSのクライアントで通信可能。

BlazeDS(AMFConnection)

BlazeDSのclient(AMFConnection)に関しては、flexから送信するバイナリと差異がある。
具体的には、ボディ部の先頭部に0x0aを入れるかどうか。
targetとresponseの後ろの1バイト(図の青部分)。flexは0x0aだが、AMFConnectionの場合はAMF3のデータタイプ(0x11)と分かる。


まとめ

AMF3通信においてボディ部の先頭は、0x0a(AMF0配列)でなければならないかどうかは仕様としてどこかに明確に提示されているのか見つからなかった。
確認した中でPyAMF以外は、そうじゃなくても動作する作りになっている。

これだけ確認することで、やっとどこでも動くクライアントが作れるが正直追いかけどころ間違えてます。


バイナリデータを解析することによってヘッダ部やボディ部も多少暗記できたが、この特技はどこにも使う用途は無いだろう。


id:shot6とカタヤマンは、夜になると延々とAMFのテストしかしないので何を話しかけても無視されます。
話しかけて無視されても気にしないで下さい。夜はAMFのテストの時間なんです。


とりあえず、独自実装でJavaFXからAMFでさくっとつなげるものは出来たが使う場面は無い。