欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【netty in action】学习笔记-第五章

程序员文章站 2024-02-16 09:35:22
...

【netty in action】学习笔记-第五章

本章涉及的内容:

  • ByteBuf
  • ByteBufHolder
  • ByteBufAllocator

基本介绍

开头几段就是告诉你,netty的ByteBuf相比较JDK的ByteBuffer很牛逼,JDK的缺点它都没有,然后还比JDK的优点多。

netty的ByteBuf内部使用引用计数的机制,内部可以自动处理资源的释放。不过尽管如此,在应用层你也应该今早释放不再使用的资源。总结下优势:

  • 可以自定义缓冲类型
  • 通过一个内置的复合缓冲类型实现零拷贝
  • 扩展性好,比如StringBuffer
  • 不需要调用flip()来切换读/写模式
  • 读取和写入索引分开
  • 方法链
  • 引用计数
  • Pooling(池)

ByteBuf读写数据效率很高,不同于JDK使用flip来切换读写模式,它内部维护了读写的索引,通过这个索引可以序列化的读取数据并且可以跳到任意位置读。

【netty in action】学习笔记-第五章

往ByteBuf写入数据时,writeIndex会增加,同理读的时候readIndex会增加,当二者相等时说明没有数据可读了。Bytebuf有些方法带有write或者read前缀,这种调用的时候会自动更新索引的位置。还有些get或者set开头的方法则不会。下面会有些代码示例,会看到这些方法。

一般在netty里会遇到三种类型的ByteBuf。下面分别介绍。

heap buffers

最常用的一种buffer类型,直接在JVM的堆上存储数据,很高效。同时也提供了一种直接访问数组的方法。通过ByteBuf#array方法直接访问。参考如下的示例:

ByteBuf heapBuf = ...;
if (heapBuf.hasArray()) { 
    byte[] array = heapBuf.array(); 
    int offset = heapBuf.arrayOffset() + heapBuf.position();
    int length = heapBuf.readableBytes();
    YourImpl.method(array, offset, length);
}

direct buffers

直接缓冲区使用的是对外内存,它有优点也有缺点。优点是socket访问比较高效,避免了从堆内到堆外的拷贝。缺点是内存的管理比堆内更复杂。netty使用内存池来管理直接缓冲区。直接缓冲区不支持数组访问数据,但是我们可以间接的访问数据数组,比如下面的例子:

ByteBuf directBuf = ...;
if (!directBuf.hasArray()) { 
    int length = directBuf.readableBytes(); 
    byte[] array = new byte[length]; 
    directBuf.getBytes(array); 
    YourImpl.method(array, 0, array.length);

composite buffers

复合缓冲去提供可以组合多个缓冲去,并提供一个统一的视图。可以动态的增加或者删除ByteBuf。复合缓冲区的hasArayy方法总是返回false,因为它可能包含不同的类型的ByteBuf。

【netty in action】学习笔记-第五章

如上图,一条消息由header和body两部分组成,将header和body组装成一条消息发送出去,有可能body相同,header不同,使用CompositeByteBuf就不用每次都重新分配一个新的缓冲区。

下面是用JDK的方案实现上面的案例,你会发现比较麻烦。需要创建一个新的ByteBuf,然后分把header和body拷贝到新的里面。

// Use an array to composite them
ByteBuffer[] message = new ByteBuffer[] { header, body };

// Use copy to merge both
ByteBuffer message2 = ByteBuffer.allocate(
header.remaining()+ body.remaining();
message2.put(header);
message2.put(body);
message2.flip();

下面是netty的方案。明显灵活很多,可以动态的删除任意的ButeBuf。

CompositeByteBuf compBuf = ...;
ByteBuf heapBuf = ...;
ByteBuf directBuf = ...;
compBuf.addComponent(heapBuf, directBuf); 
.....
compBuf.removeComponent(0); 
for (ByteBuf buf: compBuf) { 
System.out.println(buf.toString());
}

ByteBuf相关操作

随机访问和顺序访问

随机访问的示例代码,

ByteBuf buffer = ...;
for (int i = 0; i < buffer.capacity(); i ++) {
byte b = buffer.getByte(i);
System.out.println((char) b);
}

顺序访问是通过readIndexwriteIndex这两个方法,netty内部维护了两个索引,如下图所示,

【netty in action】学习笔记-第五章

ByteBuf一定符合:

0 <= readerIndex <= writerIndex <= capacity

废弃字节

调用ByteBuf.discardReadBytes()来回收已经读取过的字节,discardReadBytes()将丢弃从索引0到readerIndex之间的字
节。

可读字节和可写字节

任何读操作会增加readerIndex,如果读取操作的参数也是一个ByteBuf而没有指定目的索引,指定的目的缓冲区的writerIndex会一
起增加,没有足够的内容时会抛出IndexOutOfBoundException。

// Iterates the readable bytes of a buffer.
ByteBuf buffer = ...;
while (buffer.isReadable()) {
System.out.println(buffer.readByte());
}

这里的代码示例和书中有些区别,跟netty的版本有关。

方法的源码,

@Override
public boolean isReadable() {
    return writerIndex > readerIndex;
}

任何写的操作会增加writerIndex。若写操作的参数也是一个ByteBuf并且没有指定数据源索引,那么指定缓冲区的readerIndex也会
一起增加。

下面代码显示了随机int数字来填充缓冲区,直到缓冲区空间耗尽

// Fills the writable bytes of a buffer with random integers.
ByteBuf buffer = ...;
while (buffer.writableBytes() >= 4) {
buffer.writeInt(random.nextInt());

方法的源码,

@Override
    public int writableBytes() {
        return capacity() - writerIndex;
    }

清除缓冲区索引,

调用ByteBuf.clear()可以设置readerIndex和writerIndex为0,clear()不会清除缓冲区的内容,只是将两个索引值设置为0。

调用clear()比调用discardReadBytes()轻量的多。仅仅重置readerIndex和writerIndex的值,不会拷贝任何内存。

clear的默认实现位于AbstractByteBuf抽象类中,源码很简单,

@Override
    public ByteBuf clear() {
        readerIndex = writerIndex = 0;
        return this;
    }

ByteBufHolder等相关工具类

ByteBufHolder

我们有时会遇到这样的情况,需要另外存储除有效的实际数据各种属性值。比如HTTP响应,与内容一起的字节的还有状态码,cookies等。

Netty 提供的ByteBufHolder 可以对这种常见情况进行处理。我们可以把 ByteBufHolder 理解成ByteBuf的容器,它提供了对于 Netty 的高级功能,如缓冲池,其中保存实际数据的 ByteBuf 可以从池中借用,如果需要还可以自动释放。

ByteBufHolder是个接口,来看下接口的定义(没有全部贴出源码):

public interface ByteBufHolder extends ReferenceCounted {

    /**
     * Return the data which is held by this {@link ByteBufHolder}.
     */
    ByteBuf content();

    /**
     * Creates a deep copy of this {@link ByteBufHolder}.
     */
    ByteBufHolder copy();

    /**
     * Duplicates this {@link ByteBufHolder}. Be aware that this will not automatically call {@link #retain()}.
     */
    ByteBufHolder duplicate();

   ...
}

它有个默认实现类DefaultByteBufHolder,当然还有其它一些内置实现类。我们还可以通过继承来自定义ByteBufHolder。

下面是一些示例:

//除了数据外,还有一些其他的属性,如http的状态码,cookie等
        ByteBufHolder byteBufHolder = new DefaultLastHttpContent();
        ByteBuf httpContent = byteBufHolder.content();//返回一个http格式的ByteBuf
        ByteBufHolder copyBufHolder = byteBufHolder.copy();//深拷贝,不共享
        ByteBufHolder duplicateBufHolder = byteBufHolder.duplicate();//浅拷贝,共享

ByteBufAllocator

ByteBufAllocator负责分配ByteBuf实
例,ByteBufAllocator提供了各种分配不同ByteBuf的方法,如需要一个堆缓冲区可以使用ByteBufAllocator.heapBuffer(),需要一个直接
缓冲区可以使用ByteBufAllocator.directBuffer(),需要一个复合缓冲区可以使用ByteBufAllocator.compositeBuffer()。

ByteBufAllocator提供的这些方法,是池化的操作(默认实现,也有非池化的实现),为了减少分配和释放内存的开销。接口里面还有很多其它的方法,这里不列举了。

得到ByteBufAllocator的引用可以得到从 Channel,或通过绑定到的 ChannelHandler 的 ChannelHandlerContext 得到它,比如:

Channel channel = ...;
ByteBufAllocator allocator = channel.alloc(); //1
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc(); //2
...

Netty有两种不同的ByteBufAllocator实现,一个实现ByteBuf实例池将分配和回收成本以及内存使用降到最低;另一种实现是每次使
用都创建一个新的ByteBuf实例。Netty默认使用前者,实现类是PooledByteBufAllocator。我们可以通过ByteBufUtil的源码窥探netty对于池化和非池化的选择策略:

String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
        allocType = allocType.toLowerCase(Locale.US).trim();

        ByteBufAllocator alloc;
        if ("unpooled".equals(allocType)) {
            alloc = UnpooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else if ("pooled".equals(allocType)) {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
        }

下面是PooledByteBufAllocator的继承关系图,

【netty in action】学习笔记-第五章

Unpooled

首先,这里的Unpooled和ByteBufAllocator的非池化没有关系。Unpooled也是用来创建缓冲区的工具类,它不继承任何借口或者类。它提供了很多方法,下面是示例:

//创建复合缓冲区
CompositeByteBuf compBuf = Unpooled.compositeBuffer();

//创建堆缓冲区
ByteBuf heapBuf = Unpooled.buffer(8);

//创建直接缓冲区
ByteBuf directBuf = Unpooled.directBuffer(16);

从名字能看出来这个工具类都是非池化的操作,性能比较池化是稍差一些的。大部分时候我们应该使用ByteBufAllocator而不是这个。需要使用Unpooled的场景比如对数组的封装,下面这个示例来自ByteArrayEncoder的源码,

@Override
protected void encode(ChannelHandlerContext ctx, byte[] msg, List<Object> out) throws Exception {
    out.add(Unpooled.wrappedBuffer(msg));
}