AsyncHttpClientによるファイルのアップロード

前回に引き続き、Javaライブラリ「AsyncHttpClient」を用いた非同期HTTP通信について紹介する。ここまでの例ではGETリクエストによるデータの取得のみを扱っていたが、今回はファイルのアップロードについて考えてみよう。

AsyncHttpClientでは当然ながらGET以外のリクエストもサポートされている。それには、たとえばprepareGet()メソッドの代わりにpreparePost()やpreparePUT()などのメソッドを使用すればよい。さらに、ファイルの送信などを行う場合には、もっと便利な方法としてリクエストクエリを生成するcom.ning.http.client.RequestBuilderというクラスが用意されている。このクラスでは、リクエストに含める内容をsetXXXX()というメソッドに渡すことで、サーバに対するリクエストを自動で生成してくれる。

たとえば、ヘッダを追加する場合にはsetHeader()、body要素を追加する場合にはsetBody()といった具合である。これらのsetXXXX()メソッドは、戻り値として自分自身のRequestBuilderオブジェクトそのものを返す。最後に、build()メソッドを呼び出すことで、実際のリクエスト送信に使用するcom.ning.http.client.Requestオブジェクトが生成される。次のコードは、ヘッダとアップロードするファイルを指定してPOSTリクエストのRequestオブジェクトを生成する例である。対象のURLはsetUrl()メソッドで設定する。

リスト1

RequestBuilder builder = new RequestBuilder("POST");
Request requst = builder.setUrl("http://www.example.org/upload/")
    .setHeader("NAME", "VALUE")
    .setBody(new File("FILENAME"))
    .build();

実際のリクエストの送信は、AsyncHttpClientクラスのexecuteRequest()メソッドにRequestオブジェクトを渡すことで行う。executeRequest()メソッドでもprepareGet()などと同様にFutureベースの利用方法とHandlerベースの利用方法がある。ハンドラを指定しなかった場合にはFuture>Response<オブジェクトが返されるので、get()メソッドでレスポンスを取得すればよい。第2引数にハンドラを指定した場合には、サーバからのレスポンスに対して指定の処理が呼び出される。

次に示すのは、"upload.zip"というファイルをサーバにアップロードするプログラムの例である。このプログラムを試す場合には、URLおよびファイル名を任意のものに書き換えてほしい。

リスト2

public class AsyncHttpSample4 {
    public void fileUpload(String url, String filepath) {
        // Requestオブジェクトを生成
        RequestBuilder builder = new RequestBuilder("POST");
        Request requst = builder.setUrl(url)
        .setBody(new File(filepath))
        .build();

        // AsyncHttpClientオブジェクトを生成
        AsyncHttpClient httpClient = new AsyncHttpClient();
        try {
        // リクエストの送信
            Future<Response> future = httpClient.executeRequest(requst);

            // レスポンスの取得
            Response response = future.get();
            System.out.println("Response from " + response.getUri());

            // 切断
            httpClient.close();
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } catch (ExecutionException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        AsyncHttpSample4 asyncHttp = new AsyncHttpSample4();
        String url = "http://www.example.org/upload/";
        String filepath = "upload.zip";
        asyncHttp.fileUpload(url, filepath);
    }
}

ここではFutureベースの方法を利用しているので、get()メソッドが呼び出されるまでは、ファイルのアップロードが完了していなくてもプログラムの処理は中断されない。大きなサイズのファイルを送るような場合には有効な方法である。

ゼロコピー・アップロード

AsyncHttpClientはゼロコピー通信をサポートしている。ゼロコピー通信とは、余分なキャッシュなどを利用しないことでメモリやディスクに対するI/O処理を削減し、高速な通信を実現する方式である。まず、ゼロコピーによるファイルのアップロード方法について紹介する。

ゼロコピーのアップロードでも、通常と同様にRequestBuilderを利用する。異なるのは、アップロードするファイルを指定する際に、setBody()メソッドに対してFileオブジェクトではなく入力ストリーム・オブジェクトを渡す点だ。次に示すzeroCopyUpload()メソッドは、指定されたURLに対して指定のファイルをゼロコピーでアップロードするというものである。

リスト3

public void zeroCopyUpload(String url, String filepath) {
    try {
    // Requestオブジェクトを生成
    FileInputStream fio = new FileInputStream(filepath);
    RequestBuilder builder = new RequestBuilder("POST");
    Request requst = builder.setUrl(url)
        .setBody(fio)
        .build();

    // AsyncHttpClientオブジェクトを生成
    AsyncHttpClient httpClient = new AsyncHttpClient();

    // リクエストの送信
    Future<Response> future = httpClient.executeRequest(requst);

    // レスポンスの取得
    Response response = future.get();
    System.out.println("Response from " + response.getUri());

    // 切断
    httpClient.close();
    } catch (InterruptedException ex) {
    ex.printStackTrace();
    } catch (ExecutionException ex) {
    ex.printStackTrace();
    } catch (IOException ex) {
    ex.printStackTrace();
    }
}

RequestBuilderにはFileInputStreamを渡してRequestを生成している。この場合、リクエスト時にストリームから直接ファイルが読み込まれて送信されるようになる。その他の部分は通常とまったく同様に記述すればよい。

ゼロコピー・ダウンロード

続いて、ゼロコピーでのファイルのダウンロードについて考えてみる。ゼロコピーでのダウンロードとは、すなわちサーバから送られてきたファイルを直接ディスクに書き込むということだ。これにはcom.ning.http.client.AsyncHandlerインタフェースを利用する。AsyncHandlerは非同期HTTP通信用の汎用的なハンドラを定義したインタフェースであり、これをimplementsすることでサーバからのレスポンスに対応したさまざまな処理を実装することができる。

今回の場合、サーバから送られてきたbodyパートの内容をそのままディスクに書き込めばいい。AsyncHandlerには、bodyパートを受け取った際に呼び出されるonBodyPartReceived()というメソッドが定義されている。このメソッドを、次に示すように、受け取ったbodyの内容を出力ストリームに書き込むような処理を実装すればゼロコピーでのダウンロードが実現できる。なお、AsyncHandlerはインタフェースなので、onBodyPartReceived()以外のメソッドも実装する必要があることに注意してほしい。

リスト4

class MyAsyncHandler implements AsyncHandler<String> {
    OutputStream output;
    MyAsyncHandler(OutputStream outputStream) {
    output = outputStream;
    }

    @Override
    public STATE onBodyPartReceived(HttpResponseBodyPart bodyPart) {
    try {
        // BODYの内容をストリームに出力
        bodyPart.writeTo(output);
    } catch (IOException ex) {
        ex.printStackTrace();
    }
    return STATE.CONTINUE;
    }

    @Override
    public STATE onStatusReceived(HttpResponseStatus hrs) throws Exception {
    // ステータスを表示
    System.out.println(hrs.getStatusText());
    return STATE.CONTINUE;
    }

    @Override
    public STATE onHeadersReceived(HttpResponseHeaders hrh) throws Exception {
    // ヘッダを表示
    FluentCaseInsensitiveStringsMap headers = hrh.getHeaders();
    Set<String> keySet = headers.keySet();
    for (String key: keySet) {
        System.out.println(key + " = " + headers.get(key));
    }
    return STATE.CONTINUE;
    }

    @Override
    public String onCompleted() throws Exception {
    return "COMPLATED";
    }

    @Override
    public void onThrowable(Throwable t) {
    t.printStackTrace();
    }
}

リクエストの方法は、通常のHandlerベースの非同期通信と同様に行うことができる。すなわち、MyAsyncHandlerインスタンスを生成して、AsyncHttpClientクラスのprepareGet("URL").execute()メソッドに渡せばよい。以下に、前述のMyAsyncHandlerを利用して指定のファイルをゼロコピー・ダウンロードするプログラムの例を示す。

リスト5

public class AsyncHttpSample5 {
    public void zeroCopyDownload(String url, String filename) {
        // AsyncHttpClientオブジェクトを生成
        AsyncHttpClient httpClient = new AsyncHttpClient();

        try {
            // 保存用の出力ストリームを指定してGETリクエストを送信
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filename));
            Future<String> future = httpClient.prepareGet(url).execute(new MyAsyncHandler(bos));

            // 結果を表示
            System.out.println(future.get());

            // 切断
            bos.close();
            httpClient.close();
        } catch (ExecutionException ex) {
            ex.printStackTrace();
        } catch (InterruptedException ex) {
           ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        AsyncHttpSample5 asyncHttp = new AsyncHttpSample5();
        String url = "http://www.example.org/upload.zip";
        asyncHttp.zeroCopyDownload(url, "upload.zip");
    }
}

非同期でのHTTP通信は、パフォーマンスやユーザビリティを向上させるための有効な手段である。AsyncHttpClientはそのための方法を提供してくれるライブラリだ。非常にシンプルに構成されているため、HTTPの仕組みを強く意識ぜずに、違和感なく使うことができるだろう。