初識Netty原理 (二)——ByteBuf緩衝區

jie12366發表於2019-12-17

上文已經瞭解到了Netty中的啟動器、反應器、通道、處理器、流水線,下面來了解一下Netty中較為重要的ByteBuf緩衝區。

ByteBuf原理

優勢

  • Pooling池化,減少了記憶體複製和GC,提升了效率。
  • 讀寫分開儲存,索引也分開了,不需要切換讀寫模式。
  • 方法可鏈式呼叫,引入了引用計數法,方便了池化與記憶體回收。

重要屬性

  • readerIndex(讀指標):讀取的起始位置,每讀取一個位元組,就加1,當它等於writerIndex時,說明已經讀完了。
  • writerIndex(寫指標):寫入的起始位置,每寫入一個位元組,就加1,當它等於capacity()時,說明當前容量滿了。此時可擴容,如果不能繼續擴容,則不能寫了。
  • maxCapacity(最大容量):可以擴容的最大容量,當前容量等於這個值時,說明不能再擴容了。

引用計數

Netty採用“計數器”來追蹤ByteBuf的生命週期,主要是用於對池化的支援。(池化就是當ByteBuf的引用為0時,就會放到物件快取池中,當需要用緩衝區時,可以直接從這個池裡面取出來用,而不用重新建立一個了)。通過原始碼可以發現ByteBuf實現了一個類ReferenceCounted。這個類就是用於引用計數的。

當建立完ByteBuf時,引用數為1, 通過refCnt()方法可以獲取當前緩衝區的引用數,呼叫retain()方法可以使引用數加1,呼叫release()方法可以使引用數減1,當引用數為0時,緩衝區就會被完全釋放。如果池化了就放到緩衝池中。如果沒池化就分兩種情況,如果是分配在堆記憶體上的,就通過JVM的垃圾回收機制把它回收,如果分配在堆外直接記憶體上,就通過本地方法來釋放堆外記憶體。在Handler處理器中,Netty會自動給流水線在最後加一個處理器用來呼叫release()去釋放緩衝區,如果要在中間中斷流水線,則需要自己呼叫release()釋放緩衝區。

Allocator分配器

Netty提供了ByteAllocator的兩種實現:PoolByteAllocator(池化)和UnpooledByteAllocator(未池化)。Netty預設使用的是PoolByteAllocator,預設使用的記憶體是堆外直接記憶體(寫入速度比堆記憶體更快,池化分配器配合堆外直接記憶體,可將堆外緩衝區複用(彌補了堆外分配和釋放空間的代價較高的缺點),從來大大提升了效能)。

淺層複製

淺層複製有兩個方法,切片淺層複製和整體淺層複製

  • slice切片淺層複製 :切片只複製了原緩衝區的可讀部分,不會複製底層陣列(引用同一個),也不會增加引用數。
  • duplicate整體淺層複製 :這個是將整體都複製了,可讀可寫,但是引用還是一樣的(同slice)。

ByteBuf使用

來寫個小例子,分析一下。這裡的Logger使用的是自己封裝的靜態日誌類。分析堆疊資訊封裝一個SLF4J的靜態類

public class testBuffer {

	public static void main(String[] args) {
		// 預設使用池化緩衝區,分配的是堆外直接記憶體
		ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(9, 100);
		// 使用堆記憶體來分配緩衝區記憶體
		ByteBuf buf1 = ByteBufAllocator.DEFAULT.heapBuffer(9, 100);
		// 寫入一個位元組陣列
		buf.writeBytes(new byte[] { 1, 2, 3, 4 });
		Logger.info("引用次數--[{}]", buf.refCnt());
		// 依次讀出來
		int i = 0;
		while (buf.isReadable() && i < buf.readableBytes()) {
			// 讀位元組,不改變指標(讀完後,readIndex還是0)
			Logger.info("取一個位元組--[{}]", buf.getByte(i++));
		}
		Logger.info("buf是否使用的堆記憶體--[{}]", buf.hasArray());
		Logger.info("buf1是否使用的堆記憶體--[{}]", buf1.hasArray());
		int len = buf.readableBytes();
		byte[] array = new byte[len];
		// 把資料讀取到堆記憶體中
		buf.getBytes(buf.readerIndex(), array);
		Logger.info("buf讀出的資料--[{}]", array);
		// 與原緩衝區buf的底層引用一樣
		ByteBuf slice = buf.slice();
        // 增加一次淺層複製的引用
		slice.retain();
        // 減少一次原緩衝區的引用
		buf.release();
        // 會發現兩個引用是同一個
		Logger.info("引用次數--[{}]", buf.refCnt());
		Logger.info("切片結果--{}", slice);
		Logger.info("引用次數--[{}]", slice.refCnt());
	}
}
複製程式碼

執行結果:

21:02:59.693 [main] INFO byteBuf.test1 - 引用次數--[1]
21:02:59.695 [main] INFO byteBuf.test1 - 取一個位元組--[1]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個位元組--[2]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個位元組--[3]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個位元組--[4]
21:02:59.696 [main] INFO byteBuf.test1 - buf是否使用的堆記憶體--[false]
21:02:59.696 [main] INFO byteBuf.test1 - buf1是否使用的堆記憶體--[true]
21:02:59.696 [main] INFO byteBuf.test1 - buf讀出的資料--[[1, 2, 3, 4]]
21:02:59.698 [main] INFO byteBuf.test1 - 引用次數--[1]
21:02:59.698 [main] INFO byteBuf.test1 - 切片結果--UnpooledSlicedByteBuf(ridx: 0, widx: 4, cap: 4/4, unwrapped: PooledUnsafeDirectByteBuf(ridx: 0, widx: 4, cap: 9/100))
21:02:59.698 [main] INFO byteBuf.test1 - 引用次數--[1]
複製程式碼

回顯伺服器實戰

回顯伺服器的伺服器端如下。這裡的業務處理器使用了單例模式建立,因為這裡的業務處理是多執行緒安全的,可以在多個通道間共享使用,所以使用單例模式讓多個通道共享同一個例項,從而減少例項的建立,從而減少記憶體空間的浪費。
@ChannelHandler.Sharable這個註解是Netty中的註解,它是用於標註一個Handler例項可以被多個通道安全地共享,如果不加這個註解,直接共享的話將會丟擲異常。

public class NettyEchoServer {

    private final static Logger log = LoggerFactory.getLogger(NettyEchoServer.class);
    private final int port;
    private ServerBootstrap serverBootstrap = new ServerBootstrap();

    private NettyEchoServer(int port){
        this.port = port;
    }

    private void runServer(){
        // 建立父通道反應器執行緒組
        EventLoopGroup boss = new NioEventLoopGroup(1);
        // 建立子通道反應器執行緒組
        EventLoopGroup workers = new NioEventLoopGroup();
        try {
            serverBootstrap.group(boss, workers)
                    .channel(NioServerSocketChannel.class)
                    .localAddress("127.0.0.1", port)
                    .option(ChannelOption.SO_KEEPALIVE, true)
                    .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(NettyEchoServerHandler.getInstance());
                        }
                    });
            ChannelFuture future = serverBootstrap.bind();
            future.addListener((channelFuture) -> {
                if (channelFuture.isSuccess()){
                    log.info("伺服器啟動成功,監聽地址: [{}]", future.channel().localAddress());
                }else {
                    log.info("伺服器啟動失敗");
                }
            });
            // 阻塞直到啟動成功
            future.sync();
            ChannelFuture close = future.channel().closeFuture();
            // 阻塞直到伺服器關閉
            close.sync();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            boss.shutdownGracefully();
            workers.shutdownGracefully();
        }
    }

    /**
     * 使用單例模式實現處理器,使得多個通道可以使用同一個處理器例項
     * ChannelHandler.Sharable這個註解表示處理器可以共享
     */
    @ChannelHandler.Sharable
    static class NettyEchoServerHandler extends ChannelInboundHandlerAdapter{

        // 使用volatile修飾變數,保證instance在多執行緒下的可見性
        volatile static NettyEchoServerHandler instance;

        // 雙重檢查鎖實現單例模式
        static NettyEchoServerHandler getInstance(){
            if (instance == null){
                synchronized (NettyEchoServerHandler.class){
                    if (instance == null){
                        instance = new NettyEchoServerHandler();
                    }
                }
            }
            return instance;
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ByteBuf buf = (ByteBuf)msg;
            log.info("msg type: [{}]" ,(buf.hasArray() ? "堆記憶體" : "直接記憶體"));
            int len = buf.readableBytes();
            // 用取得的位元組的長度來初始化位元組陣列
            byte[] array = new byte[len];
            // 將堆外直接記憶體上的資料讀到堆記憶體上的位元組陣列中
            buf.getBytes(buf.readerIndex(), array);
            log.info("server received:[{}]", new String(array, StandardCharsets.UTF_8));

            log.info("寫回前的引用計數:[{}]", buf.refCnt());
            // 寫回資料,非同步任務(寫完後會釋放引用)
            ChannelFuture cl = ctx.writeAndFlush(msg);
            // I/O操作完成後的操作
            cl.addListener((ChannelFuture future) -> log.info("寫回後的引用計數:[{}]", ((ByteBuf) msg).refCnt()));
        }
    }

    public static void main(String[] args){
        new NettyEchoServer(66).runServer();
    }
}
複製程式碼

回顯伺服器的客戶端:

public class NettyEchoClient {

    private final static Logger log = LoggerFactory.getLogger(NettyEchoClient.class);

    private int serverPort;
    private String serverIp;
    private Bootstrap bootstrap = new Bootstrap();

    private NettyEchoClient(String ip, int port){
        serverIp = ip;
        serverPort = port;
    }

    private void runClient(){
        // 建立反應器執行緒組
        EventLoopGroup worker = new NioEventLoopGroup();
        try{
            bootstrap.group(worker)
                    .channel(NioSocketChannel.class)
                    .remoteAddress(serverIp, serverPort)
                    .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(NettyEchoClientHandler.getInstance());
                        }
                    });
            ChannelFuture future = bootstrap.connect();
            // 客戶端連線的非同步通知
            future.addListener((channelFuture) -> {
               if (channelFuture.isSuccess()){
                   log.info("客戶端連線成功");
               }else {
                   log.info("客戶端連線失敗");
               }
            });
            // 阻塞直到連線成功
            future.sync();
            Channel channel = future.channel();
            Scanner scanner = new Scanner(System.in);
            System.out.println("請輸入要傳送的內容: ");
            while (scanner.hasNext()){
                // 獲取輸入的內容
                String text = scanner.next();
                byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
                // 分配一個直接記憶體的緩衝區
                ByteBuf buf = channel.alloc().buffer();
                // 將位元組陣列寫入緩衝區
                buf.writeBytes(bytes);
                // 將緩衝區的資料寫入到通道中並重新整理通道
                channel.writeAndFlush(buf);
                System.out.println("請輸入要傳送的內容: ");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            worker.shutdownGracefully();
        }
    }

    @ChannelHandler.Sharable
    private static class NettyEchoClientHandler extends ChannelInboundHandlerAdapter{
        static volatile NettyEchoClientHandler instance;
        static NettyEchoClientHandler getInstance(){
            if (instance == null){
                synchronized (NettyEchoClientHandler.class){
                    if (instance == null){
                        instance = new NettyEchoClientHandler();
                    }
                }
            }
            return instance;
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            ByteBuf buf = (ByteBuf)msg;
            int len = buf.readableBytes();
            byte[] array = new byte[len];
            // 將資料讀到位元組陣列中,這中讀不會改變讀指標
            buf.getBytes(buf.readerIndex(), array);
            log.info("客戶端回顯的資料:[{}]", new String(array, StandardCharsets.UTF_8));
            // 手動釋放引用
            buf.release();
        }
    }

    public static void main(String[] args){
        new NettyEchoClient("127.0.0.1", 66).runClient();
    }
}
複製程式碼

執行結果:
客戶端:

15:12:49.677 [nioEventLoopGroup-2-1] INFO echoserver.NettyEchoClient - 客戶端連線成功
請輸入要傳送的內容: 
哈哈哈,伺服器你好
15:12:59.821 [nioEventLoopGroup-2-1] INFO echoserver.NettyEchoClient - 客戶端回顯的資料:[哈哈哈,伺服器你好]
複製程式碼

伺服器端:

15:12:40.747 [nioEventLoopGroup-2-1] INFO echoserver.NettyEchoServer - 伺服器啟動成功,監聽地址: [/127.0.0.1:66]
15:12:59.816 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - msg type: [直接記憶體]
15:12:59.818 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - server received:[哈哈哈,伺服器你好]
15:12:59.818 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - 寫回前的引用計數:[1]
15:12:59.821 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - 寫回後的引用計數:[0]
複製程式碼

相關文章