CLOVER🍀

That was when it all began.

Reactor Netty 0.7のHttpClient/HttpServerで遊ぶ

Reactor Netty 0.7.2を使って、今度はHttpClientやHttpServerを使って遊んでみようと思います。

こちらも、前にReactor Netty 0.5.1の頃に試しているのですが、見直しということで。

Reactor Nettyのhttpパッケージで遊ぶ - CLOVER

簡単なHTTP GET/POSTや、静的ファイルへのアクセスなどをやってみます。

準備

まずは、Maven依存関係から。

dependencyManagementには、以下のように記載。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>io.projectreactor</groupId>
                <artifactId>reactor-bom</artifactId>
                <version>Bismuth-SR4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

依存関係には、Reactor Nettyを追加します。追加されるReactor Nettyは、0.7.2.RELEASEです。

        <dependency>
            <groupId>io.projectreactor.ipc</groupId>
            <artifactId>reactor-netty</artifactId>
        </dependency>

あとは、テスト用のライブラリを追加。

        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.0.3</version>
            <scope>test</scope>
        </dependency>

テストコードの雛形

テストコードの雛形は、こちら。
src/test/java/org/littlewings/reactor/http/HttpClientServerTest.java

package org.littlewings.reactor.http;

import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.Collectors;

import io.netty.handler.codec.http.HttpMethod;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.http.client.HttpClient;
import reactor.ipc.netty.http.server.HttpServer;
import reactor.ipc.netty.tcp.BlockingNettyContext;
import reactor.test.StepVerifier;

public class HttpClientServerTest {
    // ここに、テストを書く!
}

ここに、テストコードとしてサンプルを書いていきます。

HTTP GETしてみる

まずは、単純なHTTP GETからやってみましょう。

書いたコードはこちら。

    @Test
    public void httpGet() {
        HttpServer httpServer = HttpServer.create(8080);

        BlockingNettyContext serverContext =
                httpServer.startRouter(routes -> {
                    routes.get("/index", (request, response) ->
                            response.sendString(Mono.just("Hello Reactor!!"),
                                    StandardCharsets.UTF_8)
                    );

                    routes.route(request -> request.uri().startsWith("/query"), (request, response) -> {
                        request.paramsResolver(uri ->
                                Arrays
                                        .stream(uri.split("\\?")[1].split("&"))
                                        .map(pair -> pair.split("="))
                                        .collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]))
                        );

                        String param = request.param("param");
                        return response.sendString(Mono.just("***" + param + "***"), StandardCharsets.UTF_8);
                    });
                });

        HttpClient httpClient = HttpClient.create("localhost", 8080);

        Mono<String> indexResponse =
                httpClient
                        .get("/index")
                        .flatMap(res -> res.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        Mono<String> queryResponse =
                httpClient
                        .get("/query?param=value")
                        .flatMap(res -> res.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        StepVerifier.create(Flux.merge(indexResponse, queryResponse))
                .expectNext("Hello Reactor!!")
                .expectNext("***value***")
                .verifyComplete();

        serverContext.shutdown();
    }

最初なので、順を追って書いていきます。

HttpServer#createで、HttpServerのインスタンスを作成します。今回はリッスンポートをのみを指定していますが、バインドするアドレスを指定したり、
もっと細かい設定を行ってインスタンスを作成する方法もあったりします。

        HttpServer httpServer = HttpServer.create(8080);

続いて、HttpServer#startRouterで、各メソッドとパスに割り当てるハンドラを登録していきます。HttpServer#startRouterの引数には、HttpServerRoutesを
引数に取るConsumerを渡すことになるので、この中でHttpServerRoutesに対する設定を行います。

今回は、HttpServerRoutes#getメソッドで、HTTP GETに対応するハンドラを2つ登録してみます。

        BlockingNettyContext serverContext =
                httpServer.startRouter(routes -> {
                    routes.get("/index", (request, response) ->
                            response.sendString(Mono.just("Hello Reactor!!"),
                                    StandardCharsets.UTF_8)
                    );

                    routes.route(request -> request.uri().startsWith("/query"), (request, response) -> {
                        request.paramsResolver(uri ->
                                Arrays
                                        .stream(uri.split("\\?")[1].split("&"))
                                        .map(pair -> pair.split("="))
                                        .collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]))
                        );

                        String param = request.param("param");
                        return response.sendString(Mono.just("***" + param + "***"), StandardCharsets.UTF_8);
                    });
                });

「/index」に割り当てた内容。こちらは、単純に「Hello Reactor!!」と返すだけです。

                    routes.get("/index", (request, response) ->
                            response.sendString(Mono.just("Hello Reactor!!"),
                                    StandardCharsets.UTF_8)
                    );

もうひとつは「/query」に割り当てますが、こちらはQueryStringを扱えるようにします。

                    routes.route(request -> request.uri().startsWith("/query"), (request, response) -> {
                        request.paramsResolver(uri ->
                                Arrays
                                        .stream(uri.split("\\?")[1].split("&"))
                                        .map(pair -> pair.split("="))
                                        .collect(Collectors.toMap(kv -> kv[0], kv -> kv[1]))
                        );

                        String param = request.param("param");
                        return response.sendString(Mono.just("***" + param + "***"), StandardCharsets.UTF_8);
                    });

QueryStringを使うような場合は、マッチするリクエストを条件にできるrouteメソッドを使うのが良さそうです。また、QueryStringを名前と値に分解するような
機能はなさそうなので、HttpServerRequest#paramsResolverを使ってURIからパラメーターをMapとして構築するFunctionを設定します。

このFunctionで返したMapを使って、HttpServerRequest#paramやHttpServerRequest#paramsで作成したMapから値を取得できるようになります。今回は、取得した
値をちょっと加工して返しています。

続いて、HttpClient側。まずは、インスタンスの作成。

        HttpClient httpClient = HttpClient.create("localhost", 8080);

こちらは比較的簡単で、HttpClient#getでHTTP GETでアクセスすることができます。

        Mono<String> indexResponse =
                httpClient
                        .get("/index")
                        .flatMap(res -> res.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        Mono<String> queryResponse =
                httpClient
                        .get("/query?param=value")
                        .flatMap(res -> res.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        StepVerifier.create(Flux.merge(indexResponse, queryResponse))
                .expectNext("Hello Reactor!!")
                .expectNext("***value***")
                .verifyComplete();

最後にStepVerifierでverifyしておきました。

テストが終わったら、HttpServerをシャットダウン。

        serverContext.shutdown();

HttpClientの方は、shutdownなどは外部からはできそうな感じがありませんが…?Reactor Netty 0.5の頃は、shutdownメソッドがあったんですけどねぇ。
あんまり気にしなくていいのかな…?

HTTP POST

続いて、HTTP POST。

HttpServer側は、このようになりました。

        HttpServer httpServer = HttpServer.create(8080);

        BlockingNettyContext serverContext =
                httpServer.startRouter(routes ->
                        routes.post("/post", (request, response) ->
                                response
                                        .sendString(
                                                request
                                                        .receiveContent()
                                                        .next()
                                                        .map(c -> "***" + c.content().toString(StandardCharsets.UTF_8) + "***"))
                        )
                );

受け取った内容を、ちょっと加工して返す感じで。

HttpClient側。HttpClient#postでリクエストについての処理を書くのですが、こちらではHttpClientRequestに対して操作を行います。

        HttpClient httpClient = HttpClient.create("localhost", 8080);

        Mono<String> postResponse =
                httpClient
                        .post("/post", request -> request.sendString(Mono.just("Hello Reactor!!"), StandardCharsets.UTF_8))
                        .flatMap(response -> response.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        StepVerifier.create(postResponse)
                .expectNext("***Hello Reactor!!***")
                .verifyComplete();

最後は、シャットダウン。

        serverContext.shutdown();

もうちょっと汎用的にHTTP POST

このような感じで、HTTP GETやHTTP POSTをHttpClientから行うことができます。また、同様にPUTやDELETEなどについても、HttpClient#put、HttpClient#deleteなどが
それぞれ用意されています。

もう少し汎用的な感じでHTTPリクエストを投げるには、HttpClient#requestメソッドを使用します。こちらを使うと、HttpMethodの指定や、HttpClientRequestに対する
FunctionをいずれのHttpMethodに対しても設定することができます。

        HttpClient httpClient = HttpClient.create("localhost", 8080);

        Mono<String> postResponse =
                httpClient
                        .request(HttpMethod.POST, "/post", request -> request.sendString(Mono.just("Hello Reactor!!"), StandardCharsets.UTF_8))
                        .flatMap(response -> response.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        StepVerifier.create(postResponse)
                .expectNext("***Hello Reactor!!***")
                .verifyComplete();

静的ファイルにハンドラを割り当てる

続いて、ちょっと趣向を変えてHttpServerに対して静的ファイルを割り当ててみます。

今回は、このようなファイルを用意。

$ find src/test/resources -type f
src/test/resources/sub/hello-in-subdirectory.txt
src/test/resources/hello.txt

内容は、こんな感じ。
src/test/resources/hello.txt

Hello Reactor!!

src/test/resources/sub/hello-in-subdirectory.txt

Hello Reactor!! (in sub-directory)

これらのファイルに対して、HttpServerを介してアクセスしてみます。

個々のパスに対して、それぞれファイルを割り当てていくには、HttpServerRoutes#fileメソッドを使用します。

        HttpServer httpServer = HttpServer.create(8080);

        BlockingNettyContext serverContext =
                httpServer
                        .startRouter(routes -> {
                            routes.file("/hello.txt", Paths.get("src/test/resources/hello.txt"));
                            routes.file("/sub/hello-in-subdirectory.txt", Paths.get("src/test/resources/sub/hello-in-subdirectory.txt"));
                        });

確認。

        HttpClient httpClient = HttpClient.create("localhost", 8080);

        Mono<String> response1 =
                httpClient
                        .get("/hello.txt")
                        .flatMap(response -> response.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        Mono<String> response2 =
                httpClient
                        .get("/sub/hello-in-subdirectory.txt")
                        .flatMap(response -> response.receiveContent().next())
                        .map(content -> content.content().toString(StandardCharsets.UTF_8));

        StepVerifier.create(Flux.merge(response1, response2))
                .expectNext("Hello Reactor!!")
                .expectNext("Hello Reactor!! (in sub-directory)")
                .verifyComplete();

また、個々のファイルではなく、ディレクトリに対して割り当てる場合はHttpServerRoutes#directoryを使用します。

        HttpServer httpServer = HttpServer.create(8080);

        BlockingNettyContext serverContext =
                httpServer.startRouter(routes -> routes.directory("/", Paths.get("src/test/resources")));

結果は一緒なので、割愛。

もう少し見てみる

HttpServer#startRouterはBlockingNettyContextを返すので、TcpServer#startとかと似てるなぁと思っていたら、内部でstartを呼んでいました。
https://github.com/reactor/reactor-netty/blob/v0.7.2.RELEASE/src/main/java/reactor/ipc/netty/http/server/HttpServer.java#L192

このstartとかnewHandlerって、NettyConnectorに定義されているものなんですよね。https://github.com/reactor/reactor-netty/blob/v0.7.2.RELEASE/src/main/java/reactor/ipc/netty/NettyConnector.java

HttpClientの方は、getやpostなどは最終的にrequestメソッドに集約されていきます。
https://github.com/reactor/reactor-netty/blob/v0.7.2.RELEASE/src/main/java/reactor/ipc/netty/http/client/HttpClient.java#L267-L276

中身を見ていると、HttpServerのようにBlockingNettyContextを取得する方法はなさそうな雰囲気が。

HttpPredicateに定義されている、UriPathTemplateとかも気になるところ。
https://github.com/reactor/reactor-netty/blob/v0.7.2.RELEASE/src/main/java/reactor/ipc/netty/http/server/HttpPredicate.java#L209

他に参考にしたコードは、テストコードです。
https://github.com/reactor/reactor-netty/blob/master/src/test/java/reactor/ipc/netty/http/client/HttpClientTest.java
https://github.com/reactor/reactor-netty/blob/master/src/test/java/reactor/ipc/netty/http/client/HttpClientTest.java
https://github.com/reactor/reactor-netty/blob/master/src/test/java/reactor/ipc/netty/http/HttpTests.java

まとめ

Reactor Netty 0.7を使って、HttpServerとHttpClientを書いてみました。Reactor Netty 0.5の頃からまあまあAPIは変わっているとはいえ、なんとかなる範囲な
感じでした。

もうちょっといろいろ試して、Reactorに慣れていかないとなぁと。