Netty使用案例 -HTTP协议栈中使用ByteBuf

使用Netty开发Restfull应用

Http服务端代码

public class HttpServer {


    public static void main(String[] args) {
        HttpServer httpServer = new HttpServer();
        int port = Integer.valueOf(args[0]);

        httpServer.bind(port);
    }

    private void bind(int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(1);
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //HTTP解码器可能会将一个HTTP请求解析成多个消息对象。
                            ch.pipeline().addLast(new HttpServerCodec());
                            //HttpObjectAggregator 将多个消息转换为单一的一个FullHttpRequest
                            ch.pipeline().addLast(new HttpObjectAggregator(Short.MAX_VALUE));
                            //
                            ch.pipeline().addLast(new HttpServerHandler());
                        }
                    });

            ChannelFuture f = b.bind("127.0.0.1", port).sync();
            f.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

/*
* 处理http服务的处理
*/
public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
        if (!request.decoderResult().isSuccess()) {
            sendError(ctx, BAD_REQUEST);
            return;
        }
        System.out.println("Http服务器接收请求:" + request);
        ByteBuf body = request.content().copy();
        //构建响应参数
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.OK, body);
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, body.readableBytes());
        ctx.writeAndFlush(response).sync();
        System.out.println("Http服务器响应请求:" + response);
    }

    private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(
                HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", CharsetUtil.UTF_8));
        response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
        System.out.println(response);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }

}

http执行调用的客户端

/**
 * args: 127.0.0.1 8080
 * Created by lijianzhen on 2019/1/16.
 */
public class HttpClient {
    public static void main(String[] args) throws InterruptedException, UnsupportedEncodingException, ExecutionException {
        HttpClient httpClient = new HttpClient();
        httpClient.connect(String.valueOf(args[0]), Integer.valueOf(args[1]));
        ByteBuf body = Unpooled.wrappedBuffer("HttpClient请求消息".getBytes("UTF-8"));
        DefaultFullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, HttpMethod.GET, "http://127.0.0.1/user?id=10&addr=山西", body);
        HttpResponse response = httpClient.blockSend(request);
    }

    /**
     * 阻塞发送
     *
     * @param request
     * @return
     */
    private HttpResponse blockSend(DefaultFullHttpRequest request) throws ExecutionException, InterruptedException {
        request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes());
        //获取线程执行的结果信息
        DefaultPromise<HttpResponse> respPromise = new DefaultPromise<>(channel.eventLoop());
        //设置Promise
        handler.setRespPromise(respPromise);
        channel.writeAndFlush(request);
        HttpResponse response = respPromise.get();
        if (response != null) {
            System.out.println("客户端请求http响应结果:" + new String(response.body()));
        }
        return response;
    }
    private Channel channel;
    HttpClientHandler handler = new HttpClientHandler();

    private void connect(String host, int port) throws InterruptedException {
        EventLoopGroup workerGroup = new NioEventLoopGroup(1);
        Bootstrap b = new Bootstrap();
        b.group(workerGroup);
        b.channel(NioSocketChannel.class);
        b.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new HttpClientCodec());
                ch.pipeline().addLast(new HttpObjectAggregator(Short.MAX_VALUE));
                ch.pipeline().addLast(handler);
            }
        });
        ChannelFuture f = b.connect(host, port).sync();
        channel = f.channel();
    }
}

/**
 * 处理http响应参数的的处理Handler
 * Created by lijianzhen on 2019/1/16.
 */
public class HttpClientHandler extends SimpleChannelInboundHandler<FullHttpResponse> {

    DefaultPromise<HttpResponse> respPromise;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
        if (msg.decoderResult().isFailure())
            throw new Exception("decoder HttpResponse error:" + msg.decoderResult().cause());
        HttpResponse response = new HttpResponse(msg);
        respPromise.setSuccess(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }

    public DefaultPromise<HttpResponse> getRespPromise() {
        return respPromise;
    }

    public void setRespPromise(DefaultPromise<HttpResponse> respPromise) {
        this.respPromise = respPromise;
    }
}

包装响应对象

/**
 * 构建响应参数
 * Created by lijianzhen1 on 2019/1/16.
 */
public class HttpResponse {
    private HttpHeaders header;
    private FullHttpResponse response;
    private byte [] body;
	public HttpResponse(FullHttpResponse response)
	{
		this.header = response.headers();
		this.response = response;
	}
    public HttpHeaders header()
    {
        return header;
    }
	public byte [] body()
	{
        //对直接内存操作不支持array方法,底层调用的是PooledUnsafeDirectByteBuf的array方法
		return body = response.content() != null ?
				response.content().array() : null;
	}
}

启动服务并启动客户端请求后出现以下异常信息。

//服务端日志正常
Http服务器接收请求:HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 22, cap: 22, components=1))
GET http://127.0.0.1/user?id=10&addr=山西 HTTP/1.1
content-length: 22
Http服务器响应请求:DefaultFullHttpResponse(decodeResult: success, version: HTTP/1.1, content: PooledUnsafeDirectByteBuf(freed))
HTTP/1.1 200 OK
content-length: 22

//客户端日志出现以下异常信息
Exception in thread "main" java.lang.UnsupportedOperationException: direct buffer
	at io.netty.buffer.PooledUnsafeDirectByteBuf.array(PooledUnsafeDirectByteBuf.java:343)
	at io.netty.buffer.AbstractUnpooledSlicedByteBuf.array(AbstractUnpooledSlicedByteBuf.java:99)
	at io.netty.buffer.CompositeByteBuf.array(CompositeByteBuf.java:596)
	at com.janle.std.cases.http.HttpResponse.body(HttpResponse.java:46)
	at com.janle.std.cases.http.HttpClient.blockSend(HttpClient.java:50)
	at com.janle.std.cases.http.HttpClient.main(HttpClient.java:32)

对HttpResponse代码分析,发现消息体的获取来源是FullHttpResponse的content字段。response.content().array()底层调用的是PooledUnsafeDirectByteBuf的array方法,为了提升性能,Netty默认的I/O buffer使用直接内存DirectByteBuf,可以减少JVM用户态到内核态Socket读写的内存拷贝即“零拷贝”,由于是直接内存,无法直接转换成堆内存,因此并不支持array方法,用户需要自己做内存拷贝操作。

对body方法进行调整

调整HttpResponse类中的body方法,采用字节拷贝的方式将Http body拷贝到byte[]数组中,修改之后的代码

/**
 * 构建响应参数
 * Created by lijianzhen1 on 2019/1/16.
 */
public class HttpResponse {
    private HttpHeaders header;
    private FullHttpResponse response;
    private byte[] body;

	public HttpResponse(FullHttpResponse response)
    {
		this.header = response.headers();
		this.response = response;
	}
    public byte [] body()
    {
        //Http body拷贝到byte[]数组中
        body=new byte[response.content().readableBytes()];
        //将body的数据返回到response中
        response.content().getBytes(0,body);
        return body;
    }
    public HttpHeaders header() {
        return header;
    }
}

启动测试后还是有异常

Exception in thread "main" io.netty.util.IllegalReferenceCountException: refCnt: 0
	at io.netty.buffer.AbstractByteBuf.ensureAccessible(AbstractByteBuf.java:1417)
	at io.netty.buffer.AbstractByteBuf.checkIndex(AbstractByteBuf.java:1356)
	at io.netty.buffer.AbstractByteBuf.checkDstIndex(AbstractByteBuf.java:1376)
	at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:854)
	at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:44)
	at io.netty.buffer.AbstractByteBuf.getBytes(AbstractByteBuf.java:474)
	at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:1740)
	at io.netty.buffer.CompositeByteBuf.getBytes(CompositeByteBuf.java:44)
	at com.janle.std.cases.http.HttpResponse.body(HttpResponse.java:46)
	at com.janle.std.cases.http.HttpClient.blockSend(HttpClient.java:50)
	at com.janle.std.cases.http.HttpClient.main(HttpClient.java:32)

io.netty.util.IllegalReferenceCountException: refCnt: 0 说明是操作了已经被释放的对象。具体代码如下。ByteBuf实现ReferenceCounted接口,所以每次操作Bytebuf之前,都需要对ByteBuf的生命周期状态进行判断。

 /**
     * Should be called by every method that tries to access the buffers content to check
     * if the buffer was released before.
     */
    protected final void ensureAccessible() {
        if (checkAccessible && refCnt() == 0) {
            throw new IllegalReferenceCountException(0);
        }
    }

对业务代码进行分析,在收到一个完整的HTTP响应消息之后,调用respPromise的setSuccess方法,唤醒业务线程继续执行,相关代码如下:
在setSuccess前看见response.body()是能拿到数据的。

  @Override
    protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
        if (msg.decoderResult().isFailure())
            throw new Exception("decoder HttpResponse error:" + msg.decoderResult().cause());
        HttpResponse response = new HttpResponse(msg);
        //这里打印出返回的参数
        System.out.println("response unchannlRead "+ new String(response.body()));
        respPromise.setSuccess(response);
    }

但是在执行完 respPromise.setSuccess(response);后数据就没有了,这就回到了之前我们知道继承SimpleChannelInboundHandler是会自己释放内存的。channelRead0方法调用完成后,Netty会自动释放FullHttpResponse。源代码在SimpleChannelInboundHandler#channelRead之前已经讨论过了。
由于执行完channelRead0方法之后,线程就会调用ReferenceCountUtil.release(msg);释放内存,所有后续业务调用方的线程再访问FullHttpResponse就会出现非法引用问题。

再调整body的代码

public class HttpResponse {
    private HttpHeaders header;
    private FullHttpResponse response;
    private byte[] body;

    public HttpResponse(FullHttpResponse response) {
        this.header = response.headers();
        this.response = response;
        //调整为在构造时候就直接放入body中
        if (response.content() != null) {
            body = new byte[response.content().readableBytes()];
            response.content().getBytes(0, body);
        }
    }
    public byte[] body() {
        return body;
    }

    public HttpHeaders header() {
        return header;
    }
}

再次测试验证Http客户端运行正常

总结:

  • 跨线程操作ByteBuf操作,要防止Netty NioEventLoop线程与应用线程并发操作Bytebuf
  • ByteBuf的申请和释放,避免忘记释放,重复释放,以及释放之后继续访问。需要注意ByteBuf隐式释放的问题。
  • 在get操作时候不要做复杂的操作,比如内存拷贝,会带来严重的性能问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青0721松

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值