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操作時候不要做複雜的操作,比如內存拷貝,會帶來嚴重的性能問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章