如何使用 Netty 创建一个高性能的 Http Client
Netty 是一个高性能的NIO网络框架。最常见的做法,就是基于 Netty 构建一个高性能的 TCP 服务器或者 HTTP 服务器。
但有了服务器,有怎么少得了客户端。
得益于 Netty 优秀的架构设置,只需要简单几行代码的改动,我们就可以把一段 HTTP 服务器的代码改成了 HTTP 客户端。
本文直接引用了 Netty 官方的例子: https://github.com/netty/netty/blob/4.1/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java。并加上了解释说明。
1. 引入 Netty 依赖
Maven
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.77.Final</version>
</dependency>
Gradle(Groovy)
implementation 'io.netty:netty-all:4.1.77.Final'
Gradle(Kotlin)
implementation("io.netty:netty-all:4.1.77.Final")
2. 启动 Netty
public final class HttpSnoopClient {
static final String URL = System.getProperty("url", "http://127.0.0.1:8080/");
public static void main(String[] args) throws Exception {
URI uri = new URI(URL);
String scheme = uri.getScheme() == null? "http" : uri.getScheme();
String host = uri.getHost() == null? "127.0.0.1" : uri.getHost();
int port = uri.getPort();
if (port == -1) {
if ("http".equalsIgnoreCase(scheme)) {
port = 80;
} else if ("https".equalsIgnoreCase(scheme)) {
port = 443;
}
}
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
System.err.println("Only HTTP(S) is supported.");
return;
}
// Configure SSL context if necessary.
final boolean ssl = "https".equalsIgnoreCase(scheme);
final SslContext sslCtx;
if (ssl) {
sslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE).build();
} else {
sslCtx = null;
}
// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new HttpSnoopClientInitializer(sslCtx));
// Make the connection attempt.
Channel ch = b.connect(host, port).sync().channel();
// Prepare the HTTP request.
HttpRequest request = new DefaultFullHttpRequest(
HttpVersion.HTTP_1_1, HttpMethod.GET, uri.getRawPath(), Unpooled.EMPTY_BUFFER);
request.headers().set(HttpHeaderNames.HOST, host);
request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
// Set some example cookies.
request.headers().set(
HttpHeaderNames.COOKIE,
ClientCookieEncoder.STRICT.encode(
new DefaultCookie("my-cookie", "foo"),
new DefaultCookie("another-cookie", "bar")));
// Send the HTTP request.
ch.writeAndFlush(request);
// Wait for the server to close the connection.
ch.closeFuture().sync();
} finally {
// Shut down executor threads to exit.
group.shutdownGracefully();
}
}
}
这里的关键是 Bootstrap、NioEventLoopGroup 和 SocketChannel 的创建。
对比创建服务端的代码(https://www.imlc.me/netty/netty-http-server/):
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.option(ChannelOption.SO_BACKLOG, 1024);
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new HttpHelloWorldServerInitializer(sslCtx));
- 我们创建了 Bootstrap 类而非 ServerBootstrap,用于启动客户端的 Netty
- 我们只创建了一个 NioEventLoopGroup。而服务端 Netty 需要 bossGroup 和 workerGroup 两个 EventLoopGroup
- 最后,我们创建了 NioSocketChannel 而非 NioServerSocketChannel。
与服务端被动等待请求不同,客户端需要主动发起请求。因此:
我们需要主动调用 connect() 方法创建 TCP 连接。
Channel ch = b.connect(host, port).sync().channel();
然后往 TCP 连接中写入数据
ch.writeAndFlush(request);
2. 初始化 ChannelPipeline
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new HttpSnoopClientInitializer(sslCtx));
还是 Netty 的风格,我们需要给 Channel 创建一堆 ChannelHandler。
上方代码中,我们使用 HttpSnoopClientInitializer
创建 Handler。其代码如下:
public class HttpSnoopClientInitializer extends ChannelInitializer<SocketChannel> {
private final SslContext sslCtx;
public HttpSnoopClientInitializer(SslContext sslCtx) {
this.sslCtx = sslCtx;
}
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
// Enable HTTPS if necessary.
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
p.addLast(new HttpClientCodec());
// Remove the following line if you don't want automatic content decompression.
p.addLast(new HttpContentDecompressor());
// Uncomment the following line if you don't want to handle HttpContents.
//p.addLast(new HttpObjectAggregator(1048576));
p.addLast(new HttpSnoopClientHandler());
}
}
这段代码非常直观。 initChannel() 里依次往 ChannelPipeline 添加 SslHandler、HttpClientCodec、HttpContentDecompressor。 最后是 HttpSnoopClientHandler ,负责实现自定义逻辑。
当我们发送请求时,数据逆序经过 HttpClientCodec 和 SslHandler,把 Java 对象转换成字节数据,然后 SSL 加密,发送到服务端。
当我们收到服务端的响应,数据顺序流过 ChannelPipeline,
SslHandler 负责解密数据。
HttpClientCodec 负责把字节数据解码成 HTTP 数据。
最后 HttpContentDecompressor 负责解压缩数据(例如数据被 gzip 压缩了)。
走完这个流程,数据被解码成我们需要的 HttpObject Java 对象。提供给后续的 HttpSnoopClientHandler 处理。
- 创建自定义ChannelHandler处理业务逻辑
最后,我们需要实现自己的 ChannelHandler。 最简单的,就是使用 Netty 提供的 SimpleChannelInboundHandler。
下方的代码演示了如果从 HttpObject 里读取 HTTP 响应的信息。
public class HttpSnoopClientHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) {
if (msg instanceof HttpResponse) {
HttpResponse response = (HttpResponse) msg;
System.err.println("STATUS: " + response.status());
System.err.println("VERSION: " + response.protocolVersion());
System.err.println();
if (!response.headers().isEmpty()) {
for (CharSequence name: response.headers().names()) {
for (CharSequence value: response.headers().getAll(name)) {
System.err.println("HEADER: " + name + " = " + value);
}
}
System.err.println();
}
if (HttpUtil.isTransferEncodingChunked(response)) {
System.err.println("CHUNKED CONTENT {");
} else {
System.err.println("CONTENT {");
}
}
if (msg instanceof HttpContent) {
HttpContent content = (HttpContent) msg;
System.err.print(content.content().toString(CharsetUtil.UTF_8));
System.err.flush();
if (content instanceof LastHttpContent) {
System.err.println("} END OF CONTENT");
ctx.close();
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
生产级别的实现
上方的例子只是一个粗浅的例子。 如果要在生产环境,还要实现 HTTP/2 的支持,更多的 HTTP 特性,更简装的错误处理以及更完善的连接管理。
如果你在寻找一个NIO的 HTTP Client,我更建议你使用成熟的框架,例如 vertx-web-client 或者 Java 11 引入的 HTTP client API 。
如果你需要实现一个自定义协议的高性能客户端,那么上方 HTTP 客户端的实现,会是一个很好的出发点。