Netty で HTTP クライアントの書き方を調べてみたら、意外なことにほとんど見つからなかったので、ブログに書くことにしました。
ChannelInitializer
Pipeline に加えるハンドラーと順番は次のとおり
SslHandler
HttpClientCodec
HttpContentDecompressor
HttpObjectAggregator
(でかいファイルをダウンロードするなどの用途の場合は入れないほうが良いかもしれない)
- 自作の
ChannelInboundHandler
それぞれ、
SslHandler
は https
に対応するために使います。
HttpClientCodec
は HttpRequestEncoder
と HttpResponseDecoder
を組み合わせた DuplexHandler
で、泥臭い HTTP まわりの処理を行っています。
HttpContentDecompressor
はメッセージ等を gzip に圧縮します。
HttpObjectAggregator
は HttpClientCodec
で作られた HttpContent
(チャンク) を1つのレスポンスとしてまとめるハンドラー。複数回呼ばれる channelRead
を1回の channelRead
にまとめる
参考コード
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLEngine;
public class HttpHandlerInitializer extends ChannelInitializer<Channel> {
private final SslContext context;
private final boolean startTls;
HttpHandlerInitializer(SslContext context, boolean startTls) {
this.context = context;
this.startTls = startTls;
}
private SslHandler sslEngine(final Channel channel) {
final SSLEngine sslEngine = context.newEngine(channel.alloc());
return new SslHandler(sslEngine, startTls);
}
@Override
protected void initChannel(Channel ch) {
final SslHandler sslHandler = sslEngine(ch);
ch.pipeline()
.addFirst("ssl", sslHandler)
.addLast("http-codec", new HttpClientCodec())
.addLast("decompress", new HttpContentDecompressor())
.addLast("aggregate", new HttpObjectAggregator(1048576))
.addLast("client", new ClientHandler.ForResponse())
.addLast("req", new ClientHandler.ForRequest());
}
}
Bootstrap
ここは特に難しいものはなさそう。 接続先のホストの名前を connect
で指定しているので、HTTP ヘッダーの Host
の項目を省略したら、 403
が返ってきてハマった。
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLException;
import java.net.InetSocketAddress;
public class NettyHttpClient {
private static final Logger logger = LoggerFactory.getLogger(NettyHttpClient.class);
public static void main(String[] args) throws SSLException, InterruptedException {
final SslContext context = SslContextBuilder.forClient().startTls(false).build();
final String hostname = "api.github.com";
final Bootstrap bootstrap = new Bootstrap();
final EventLoopGroup eventLoopGroup = new NioEventLoopGroup(1);
bootstrap
.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new HttpHandlerInitializer(context, false));
final ChannelFuture channelFuture = bootstrap.connect(new InetSocketAddress(hostname, 443));
channelFuture.addListener(
future -> {
final Channel channel = channelFuture.channel();
final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
httpHeaders.add("Host", hostname);
httpHeaders.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
httpHeaders.add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
httpHeaders.add("User-Agent", "Netty");
httpHeaders.add("Accept", "application/json");
final DefaultHttpRequest request =
new DefaultHttpRequest(
HttpVersion.HTTP_1_1,
HttpMethod.GET,
"/search/repositories?q=java&sort=stars&order=desc&per_page=3",
httpHeaders);
channel.writeAndFlush(request).addListener(future1 -> logger.info("request sent."));
});
channelFuture
.channel()
.closeFuture()
.addListener(
f -> {
logger.info("closed");
eventLoopGroup.shutdownGracefully();
}).sync();
}
}
ハンドラー
ChannelInboundHandler
の実装。 HttpObjectAggregator
を通過しているので、 channelRead
には FullHttpResponse
が渡されることを期待している。
class ClientHandler {
private static final Logger logger = LoggerFactory.getLogger(ClientHandler.class);
static class ForResponse extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
logger.info("response returned.");
if (msg instanceof FullHttpResponse) {
final FullHttpResponse httpResponse = (FullHttpResponse) msg;
logger.info("response type: {}", httpResponse.getClass().getCanonicalName());
logger.info("http object: {}", httpResponse);
final String response = httpResponse.content().toString(StandardCharsets.UTF_8);
logger.info("response body: {}", response);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.warn("exception caught", cause);
final Channel channel = ctx.channel();
if (channel.isOpen()) {
channel
.closeFuture()
.addListener(f -> logger.info("closed"));
} else {
logger.info("already closed");
}
}
}
}
このようなハンドラーを作ると、以下のように出力される(一部データを略した)
[nioEventLoopGroup-2-1] INFO com.example.ClientHandler - response returned.
[nioEventLoopGroup-2-1] INFO com.example.ClientHandler - response type: io.netty.handler.codec.http.HttpObjectAggregator.AggregatedFullHttpResponse
[nioEventLoopGroup-2-1] INFO com.example.ClientHandler - http object: HttpObjectAggregator$AggregatedFullHttpResponse(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 15354, cap: 15354, components=3))
HTTP/1.1 200 OK
Date: Sun, 22 Jul 2018 16:41:42 GMT
Content-Type: application/json; charset=utf-8
Connection: close
Server: GitHub.com
Status: 200 OK
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 1532277762
Cache-Control: no-cache
X-GitHub-Media-Type: github.v3
Link: <https://api.github.com/search/repositories?q=java&sort=stars&order=desc&per_page=3&page=2>; rel="next", <https://api.github.com/search/repositories?q=java&sort=stars&order=desc&per_page=3&page=334>; rel="last"
Access-Control-Expose-Headers: ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin: *
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
X-Frame-Options: deny
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'
X-Runtime-rack: 0.158863
Vary: Accept-Encoding
X-GitHub-Request-Id: F3F8:32A5:7B8A63:A432DE:5B54B3C5
content-length: 15354
[nioEventLoopGroup-2-1] INFO com.example.ClientHandler - response body: {"total_count":830457,"incomplete_results":false,"items":[ ... ]}