Androidフレームワークに対する疑問の一つに HttpClient が有る。

通信は非同期で行えと口をすっぱくして言うくせに標準の HttpClient は同期式しか用意されていない。 しかもこいつは connect 処理に対してはキャンセルが効かない。 タイムアウトしか出来ないと言う腐った実装だったりする。

非同期通信ライブラリくらい有るだろうと思って調べてみたら3つほど出てきた。

Apache HttpAsyncClient 4.0

Android で動かすとエラーになる。
Android に入っている Apache HttpCore が古いため。
最新の HttpCore を使うにはパッケージ名を変更して別のライブラリとしてインストールする必要が有るらしい。

ボツ。

loopj Android Asynchronous Http Client

定番ぽい雰囲気。
でも標準の HttpClient をラップしているだけで NIO は使って無い。
つまりスレッドの占有状態は変わらない。
しかも API がいまいちダサい。

俺的にちょっと違う。

koush AndroidAsync

NIO 使って1スレッドで動くと書いてある。おぉ求めていた物。
でもサンプルコードがちょこっと有るだけでドキュメントが無い。
ソースも見てみたがコメントすら書いてない。orz

作りかけなのかドキュメント書くのが嫌いなのかは分からんけどいずれにせよライブラリとしては使えない。

だったら自分で書いてやらぁ

腹が立って来たので自分で実装してみることにした。

  • NIO + Selector で通信スレッドを1つで済ませる。
  • API は極力標準の HttpClient をパクって。
  • レスポンスは InputStream と ByteBuffer を選べる用に。
  • Keep-Alive と Chunked-Transfer をサポート。

ぐらいで。

分かってたけど簡単じゃ無いね NIO は。
全部のクラスが状態遷移マシンですわ。

何とか最低限のテストコードを通せました。

InputStream を受け取る POST のサンプルコード。

クラスが Async~ になるだけでほぼ標準のHttpClientと同じように使える。
InputStream は全文をオンメモリに持つので大きいデータには使えない。 PipedInputStream を使うオプションが有ってもいいかもしれない。
ちなみに execute() は UIスレッドで実行してもエラーにならない。

AsyncHttpClient mClient = new AsyncHttpClient();
private void doSend() {
    AsyncHttpPost request = new AsyncHttpPost("http://192.168.0.2/cgi-bin/log.sh");
    HttpEntity httpEntity = new StringEntity("Test data");
    request.setHttpEntity(httpEntity);

    mClient.execute(request, new AsyncHttpListenerBase() {
        @Override
        public void onResponseBody(HttpResponse httpResponse) {
            if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                // サーバからエラー
            }
            try {
                InputStream is = httpResponse.getEntity().getContent();
                BufferedReader br = new BufferedReader(new InputStreamReader(is));
                String line;
                while ((line = br.readLine()) != null) {
                    Log.i("DEBUG", "->" + line);
                }
                br.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

ByteBuffer を受け取る GET のサンプルコード。

ByteBuffer を直接で無く ByteBufferReader で受け取るのは使い終わった ByteBuffer を返して貰うため。(再利用する)
この辺り利便性と実行効率を両立させるのが難しいです。

Google さんは Chunked で応答して来ますがアプリに渡すときには平文に変換しています。ここが実装上の一番面倒なところ。

それと FileChannel って Selector 使えないのね。はじめて知ったわ。

private void doSend2() {
    AsyncHttpGet request = new AsyncHttpGet("http://www.google.co.jp/");
    mClient.execute(request, new AsyncHttpListenerBase() {
        FileChannel mFileChannel;

        @Override
        public boolean isResponseBodyPart() {
            return true;
        }
        @Override
        public void onResponseBodyPart(ByteBufferReader transporter) {
            try {
                if (mFileChannel == null) {
                    @SuppressWarnings("resource")
                    FileOutputStream file = new FileOutputStream(getFilesDir()+"/index.html");
                    mFileChannel = file.getChannel();
                }

                ByteBuffer buffer = transporter.read();
                if (buffer != null) {
                    while (buffer.hasRemaining()) {
                        if (mFileChannel.write(buffer) == -1) break;
                    }
                } else {
                    mFileChannel.close();
                }
                transporter.release(buffer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onResponseBody(HttpResponse httpResponse) {
            // not called.
        }
    });
}

まとめ

結局現状では loopj で妥協するくらいしか無さそう。

今回作ったライブラリはちゃんとテストを行えば実用になりそうな気がする。
興味のある人はこちらからどうぞ

追記

HTTPSも欲しいなと思って作り始めたらどつぼに嵌ったよ。
JSSE のドキュメントがアバウト過ぎて訳分かんねー。
でも何とか Selector + SSLEngine の組み合わせで動かすことに成功。

需要は無いと思うけどソースはこちら