HttpClient系列(五),Netty HttpClient

目录



正文


Netty是由JBoss公司开源的NIO框架。它是基于异步和事件驱动的。现今Java的框架中,Netty的身影几乎是无处不在。如Vert.x、Dubbo、Firebase等等。Netty把Java NIO技术发展到了高峰。

Netty一般用来编写Socket程序,在实际项目中很少用来写HttpClient。但还是有必要了解下的,万一用到了呢?

同样还是以GET和POST请求为例,先在pom.xml中加入Netty的依赖。

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.50.Final</version>
</dependency>

然后直接写代码,GET请求走起:

public static void main(String[] args) {
    connect("www.baidu.com", 80, "http://www.baidu.com");
}

public static void connect(String host, int port, String url_str) {
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup).channel(NioSocketChannel.class).option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel sc) throws Exception {
                        // HttpClientCodec 相当于同时添加了HttpResponseDecoder和HttpRequestEncoder
                        sc.pipeline().addLast(new HttpClientCodec());
                        sc.pipeline().addLast(new OutputResultHandler());
                    }
                });
        Channel channel = bootstrap.connect(host, port).sync().channel();

        DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, new URI(url_str).toASCIIString());
        request.headers().set(HttpHeaders.Names.HOST, host);
        request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, request.content().readableBytes());

        channel.writeAndFlush(request).sync();
        channel.closeFuture().sync();
    } catch (InterruptedException | URISyntaxException e) {
        e.printStackTrace();
    } finally {
        workerGroup.shutdownGracefully();
    }
}

private static class OutputResultHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof HttpResponse) {
            HttpResponse response = (HttpResponse) msg;
            System.out.println(response.toString());
        }
        if (msg instanceof HttpContent) {
            HttpContent content = (HttpContent) msg;
            ByteBuf buf = content.content();
            System.out.println(buf.toString(CharsetUtil.UTF_8));
            buf.release();
        }
    }
}

看到代码,有没有感觉晕菜了。跟前面三节HttpClient比起来,Netty怎么这么复杂。请看笔者一一解释。
Netty是一个NIO Socket框架,所以在请求一个URL前需要先连接到服务器,所以建立了connect方法:

Channel channel = bootstrap.connect(host, port).sync().channel();

这个表示同步连接成功后返回一个Channel,Channel就是你跟服务器连接上的一个通道,通过这个通道才可以跟服务器交换数据。这一点跟我们第一节的HttpURLConnection打开连接一个道理。

再看DefaultFullHttpRequest类型对象的创建,这个才是真正的发起Http请求的时候。在参数里面指定了http协议版本,请求方法和URI。如果是POST请求,还会有一个类型为ByteBuf的参数。

那么怎么接收请求后的响应数据呢?需要回到前面看这行代码:

sc.pipeline().addLast(new OutputResultHandler());

因为Netty是异步、事件驱动的网络框架,所以无法在请求发送完就马上知道响应,需要通过添加pipeline来处理响应消息。pipeline又叫管道,是一条链式的结构。当请求发出后,服务端返回请求,会按照这个链条,一层层处理。

最终在OutputResultHandler类中,继承ChannelInboundHandlerAdapter来处理响应。不过在这里需要判断消息类型,因为ChannelInboundHandlerAdapter默认的消息体是拆分的,分成HttpResponse和HttpContent。HttpContent就是你想要的内容,响应中的body,而header信息往往在HttpResponse中。 当然如果你分两次解包很麻烦,也可以继承来SimpleChannelInboundHandler来一次性获得数据响应:

 private static class OutputResultHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpResponse fullHttpResponse) throws Exception {
        ByteBuf buf = fullHttpResponse.content();
        System.out.println(buf.toString(CharsetUtil.UTF_8));
    }
}

记得pipeline加上HttpObjectAggregator,最终pipeline链如下:

sc.pipeline().addLast(new HttpClientCodec());
sc.pipeline().addLast(new HttpObjectAggregator(512 * 1024));
sc.pipeline().addLast(new OutputResultHandler());

POST请求又怎么实现呢?

有两个问题需要解决:
1、怎么传递参数;
2、header中怎么模拟表单请求。

先来回答第一个问题:使用BasicNameValuePair来组装表单数据,在用UrlEncodedFormEntity来encode表单数据,再通过EntityUtils.toByteArray将表单数据变成byte数组,最后用Unpooled将表单数据的byte数组转成ByteBuf。
再说第二个问题:header中的Content-Type需要设置为application/x-www-form-urlencoded,并且要配置Content-Length。Content-Length就是ByteBuf的数据长度。

请看一个POST请的完整示例

 public static void main(String[] args) throws URISyntaxException {
    URI uri = new URI("http://192.168.1.111:8181/login");
    connect(uri);
}

public static void connect(URI uri) {
    NioEventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(workerGroup).channel(NioSocketChannel.class).option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel sc) throws Exception {
                        // HttpClientCodec 相当于同时添加了HttpResponseDecoder和HttpRequestEncoder
                        sc.pipeline().addLast(new HttpClientCodec());
                        sc.pipeline().addLast(new HttpObjectAggregator(512 * 1024));
                        sc.pipeline().addLast(new OutputResultHandler());
                    }
                });
        Channel channel = bootstrap.connect(uri.getHost(), uri.getPort()).sync().channel();

        List<BasicNameValuePair> formData = new ArrayList<>();
        formData.add(new BasicNameValuePair("account", "admin"));
        formData.add(new BasicNameValuePair("password", "admin"));
        HttpEntity httpEntity = new UrlEncodedFormEntity(formData);
        ByteBuf byteBuf = Unpooled.wrappedBuffer(EntityUtils.toByteArray(httpEntity));
        DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, uri.getPath(), byteBuf);

        request.headers().set(HttpHeaders.Names.CONTENT_TYPE, HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED);
        request.headers().set(HttpHeaders.Names.HOST, uri.getHost());
        request.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, request.content().readableBytes());

        channel.writeAndFlush(request).sync();
        channel.closeFuture().sync();
    } catch (InterruptedException | IOException e) {
        e.printStackTrace();
    } finally {
        workerGroup.shutdownGracefully();
    }
}

private static class OutputResultHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, FullHttpResponse fullHttpResponse) throws Exception {
        ByteBuf buf = fullHttpResponse.content();
        System.out.println(buf.toString(CharsetUtil.UTF_8));
    }
}

Netty HttpClient实际用的比较少,但前面四种http client还是经常用的,是有必要记住的。

本博客采用 知识共享署名-禁止演绎 4.0 国际许可协议 进行许可

本文标题:HttpClient系列(五),Netty HttpClient

本文地址:https://dev-tang.com/post/2020/06/httpclient-netty.html

推荐阅读