非同期通信を利用する

前々回・前回と2回にわたり、Java 11から正式に追加されたHTTPクライアントAPIの使い方を解説した。

  • HTTPクライアントAPIを使ったHTTP通信の例

    HTTPクライアントAPIを使ったHTTP通信の例

HttpURLConnectionクラスを利用した従来のHTTP通信と、この新しいHTTPクライアントAPIのもっとも大きな違いは、非同期通信やWebSocketといったHTTP/2プロトコルで追加された要素をサポートしている点だと言える。

通常の同期通信は、リクエストはHttpClientクラスのsend()メソッドを使用して行う。それに対して非同期通信を行いたい場合は、次のようにsendAsync()メソッドを呼び出せばよい。

// HttpClientインスタンスを作成
HttpClient client = HttpClient.newHttpClient();

// HttpRequestインスタンスを作成
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("https://httpbin.org/delay/5")) // 5秒の遅延でデータをリクエスト
        .build();

// 非同期でリクエストを送信
CompletableFuture<HttpResponse<String>> future =
        client.sendAsync(request,
            HttpResponse.BodyHandlers.ofString());

System.out.println("Receiving..."); // ここはレスポンスを待たずに実行される

// レスポンスボディを出力
try {
        System.out.println(future.get().body());
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

ここで指定しているリクエスト先は、リクエストを受けてから5秒間待ったあとでレスポンスを返す。send()メソッドであればレスポンスを受け取るまで処理がブロックされるが、sendAsync()メソッドの場合はリクエストを送った時点で処理が戻って、次のprintln()が実行されるはずだ。

sendAsync()に渡す引数は、リクエスト用のHttpRequestとHttpResponse.BodyHandlerの2つで、これはsend()の場合と同様だ。ただし、戻り値の型はCompletableFuture<HttpResponse<T>>となっており、非同期処理なのでConcurrency APIに準拠していることがわかる。このCompletableFutureからはget()メソッドでHttpResponseが取得できる。あとは同期通信の場合と同様にbody()やheadear()でレスポンスの中身が取り出せる。

ちなみに、sendAsync()の戻り値はCompletableFutureオブジェクトなので、レスポンスの取得後に任意の処理を追加することもできる。例えば、次の例ではthenApply()でレスポンス取得後に本文を取り出し、それをprintln()で出力するように設定している。間にexceptionally()で、例外が発生した場合の出力内容には「Error:」の文字が付け加えられる。通信の完了後にこの一連の処理が実行されるので、future.get()はその結果(println()の出力)が返される。

// レスポンス取得後の処理を追加
future.thenApply(HttpResponse::body)
        .exceptionally(e -> "Error: " + e.getMessage())
        .thenAccept(System.out::println);

// レスポンスボディを出力
try {
    future.get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

複数のURIに同時にリクエストを送る

非同期通信を利用すれば、複数のURIに対して同時にリクエストを送信するようなこともできる。次のコードは、2つの異なるURIに対して同時にリクエストを送信し、それぞれのレスポンスの本文を出力する例である。

// HttpClientインスタンスを作成
HttpClient client = HttpClient.newHttpClient();

try {
    // 2つの異なるリクエストURIを設定
    List<URI> uris = Arrays.asList(
            new URI("https://httpbin.org/get?param=123"),
            new URI("https://httpbin.org/get?param=456"));

    // 複数のURIに同時にリクエストを送信
    List<CompletableFuture<String>> futures = uris.stream()
            .map(uri -> client
                .sendAsync(
                    HttpRequest.newBuilder(uri).build(),
                    HttpResponse.BodyHandlers.ofString())
                .thenApply(response -> response.body()))
            .collect(Collectors.toList());

    System.out.println("Receiving..."); // ここはレスポンスを待たずに実行される

    // レスポンスを表示
    for (CompletableFuture<String> future : futures) {
        System.out.println(future.get());
    }
} catch (URISyntaxException e) {
    e.printStackTrace();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

まず、対象となるURIをListに格納した上で、すべてのURIに対してsendAsync()を呼び出している。レスポンスとして取得したHttpResponseに対しては、thenApply()で通信完了後にレスポンス本文だけを取り出すように設定している。そして、最終的に得られたCompletableFutureを再度Listに格納する。

sendAsync()を利用することで、1つのリクエストの結果を待つことなく次のリクエストが開始されるため、大きなファイルの分割ダウンロードなどといった用途に活用することができる。

WebSocketを使った双方向通信を試す

続いて、WebSocketを使った通信を試してみよう。WebSocketは、Webアプリケーションにおいてサーバとクライアント間でソケットを利用した双方向通信を行うための規格である。通常のHTTP通信は、クライアントからのリクエストに対してサーバがレスポンスを返すという形式が基本であり、1回の通信がそれ単体で完結するためにヘッダなどの情報量が多いという特徴がある。それに対してWebSocketは、1度サーバとクライアントがコネクションを確立したら、その後の通信は専用のプロトコルを用いて行うため、オーバーヘッドの小さい双方向通信が実現できる。

HTTPクライアントAPIでは、java.net.http.WebSocketクラスがWebSocket通信のためのソケットの役割を果たす。W次のコードは、WebSocketを使ってサーバ/クライアント間でメッセージの送受信を行う例になる。

// HttpClientインスタンスを作成
HttpClient client = HttpClient.newHttpClient();

// WebSocketのビルダを作成
WebSocket.Builder websocketBuilder = client.newWebSocketBuilder();

// WebSocketイベントを定義
WebSocket.Listener listener = new WebSocket.Listener() {
    /* 接続開始時の処理 */
    @Override
    public void onOpen(WebSocket webSocket){
        System.out.println("Socket opened...");
        // 許容するメッセージ受信回数(2回)を設定する
        webSocket.request(2);
    }

    /* テキストメッセージ受診時の処理 */
    @Override
    public CompletionStage<?> onText(WebSocket webSocket,
            CharSequence data, boolean last) {
        System.out.println("RECEIVE: " + data);
        return null;
    }

    /* 切断時の処理 */
    @Override
    public CompletionStage<?> onClose(WebSocket webSocket, 
            int statusCode, String reason) {
        System.out.println("Socket closed...");
        return null;
    }
};

// 接続開始
CompletableFuture<WebSocket> future = 
        websocketBuilder.buildAsync(URI.create("ws://echo.websocket.org"), listener);

try {
    // ソケットを取得
    WebSocket webSocket = future.get();

    // メッセージを送信
    webSocket.sendText("Hello.", true);
    webSocket.sendText("Rock it with WebSocket.", true);

    Thread.sleep(1000);

    // 切断
    CompletableFuture<WebSocket> end = 
            webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Finished");
        end.get();
}catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

WebSocketクラスを利用するには2つの準備が必要となる。1つはWebSocketインスタンスのビルダとなるWebSocket.Builderインスタンスの作成で、これはHttpClientクラスのnewWebSocketBuilder()メソッドによって行える。

もう1つの準備が、コネクション確立後の各種イベントを処理するリスナーを定義することだ。リスナーはWebSocket.Listenerインタフェースをimplementsして定義する。上の例では、接続が確立した際に呼び出されるonOpen()と、テキストメッセージを受信した際に呼び出されるonText()、そしてソケット切断時のonClose()の3つのメソッドを実装している。

準備ができたら、WebSocket.Builderインスタンスに対してbuildAsync()メソッドを実行すれば、第1引数のURIに接続要求が送られる。第2引数にはさきほど用意したWebSocket.Listenerを渡す。名前からもわかるように、このメソッドはsendAsycn()と同様に非同期に動作するため、接続の成否に関わらずすぐに終了し、戻り値としてCompletableFuture<WebSocket>インスタンスを返す。正常に接続が確立できた場合には、このCompletableFutureからget()メソッドで通信のためのWebSocketインスタンスが取得できる。

この例では省略しているが、もしbuildAsync()の実行時に何らかのエラーによって接続に失敗した場合は、戻り値のCompletableFutureは各種例外で終了する。したがって、接続失敗を考慮したプログラムにしたい場合はexceptionally()メソッドなどを利用してエラー処理を追加すればよい。

テキストメッセージの送信はsendText()メソッドで行う。ほかに、バイナリメッセージを送信するsendBinary()メソッドや、Pingを送信するsendPing()メソッドなどがある。今回の接続先に指定している ws://echo.websocket.org はWebSocketのテストを行うために用意されたサービスで、送信したメッセージがそのまま送り返されるようになっている。したがって、2回送ったテキストはそれぞれそのまま返信されてきて、Webocket.ListenerのonText()が呼び出されるはずだ。

ソケットのクローズはsendClose()メソッドで行う。このメソッドも非同期に動作して、戻り値としてクローズ済みのWebSocketインスタンスを持つCompletableFuture<WebSocket>を返す。

以上、3回にわたってJava 11で導入されたHTTPクライアントAPIの基本的な使い方を解説してきた。JavaでHTTP通信を行うには、Apache HttpComponentのような外部ライブラリを使うという選択肢もある。しかし、HTTPクライアントAPIの登場によって標準APIだけでモダンな方式によるHTTP通信が実装できるというのはやはり大きな強みと言えるだろう。