東のレガリア

EM,GCDAsyncSocketでおかしやすい間違い

EMやGCDAsyncSocketを使うと非同期ノンブロッキングのデータ通信が割りとお手軽にできるが、受信時のパケット長と状態について注意する必要がある。

  • GCDAsyncSocket

GCDAsyncSocketで注意したいのは、一つのGCDAsyncSocketは必ず一つの状態をもつということ。

一例として、writeDataやreadDataは非同期処理のためハンドラが必要。デリゲート先にハンドラを書き、そのハンドラで次の状態にうつる。こうして、デリゲートをチェインしていくことで、状態を進めていくという使い方をする。

TCPパケットの受信は、長さがわかっていれば、readToLengthで長さを指定すれば、受信ハンドラに遷移する。が、ここで注意しなければいけないのは、readToLengthに指定する長さが1バイトでも大きいと、ずっと待ちが続いてしまい、次の状態にうつらない。

TCPパケットの長さを知るには、パケットの先頭にパケット長をつけるのが一般的だが、サーバも、正しくこれを実装し、きちんとデータを送信しなければいけない。

RESTfulなhttpのようなプロトコルに慣れていると間違いをおかしやすい。 この方式の弱点は、異常が発生したときのリカバリ、状態を適切に遷移しなければ全くおかしな動きをするということ。

たとえば、状態AからBに正しくうつるはずが、中途半端な長さのパケットしかこないからまだ状態Aになっている。サーバはもうBになったと思って続きのパケットを送ったら、もうおしまいである。

それでも、回避策としてtimeoutをつけて、timeout時にサーバにtimeoutしたパケットを投げて、それぞれの状態の同期をとるか、チェックサムを入れるということが必要。

真面目に開発するのであれば、 プロトコル・スタック設計としてステートマシンとかを書くものだと思う。

  • EM

EMのreceive_dataは、パケットを非同期ノンブロッキングで受信する。しかし、GCDAsyncSocketのように長さは指定できないため、やはりパケット内で長さを示す必要がある。

一度の受信で得たパケットを、チャンクというらしい。チャンクの組立は、実装者に任されるという話だ。 http://htn.to/o9p748U

例えば、先頭4バイトが長さということがわかっていれば、receive_dataで4バイトを読み、長さに達するまで、

    def receive_data(data)
            if @@maxlen == 0
                    @@maxlen = @@buf[0..3].pack('V')
            end
            @@buf << data
            if @@maxlen > @@buf.length
                    return
            end
            #ここにくればチャンク組立が完了している

という感じで受信を続けていればOKだ。