【netty in action】学习笔记-第五章
【netty in action】学习笔记-第五章
本章涉及的内容:
- ByteBuf
- ByteBufHolder
- ByteBufAllocator
基本介绍
开头几段就是告诉你,netty的ByteBuf相比较JDK的ByteBuffer很牛逼,JDK的缺点它都没有,然后还比JDK的优点多。
netty的ByteBuf内部使用引用计数的机制,内部可以自动处理资源的释放。不过尽管如此,在应用层你也应该今早释放不再使用的资源。总结下优势:
- 可以自定义缓冲类型
- 通过一个内置的复合缓冲类型实现零拷贝
- 扩展性好,比如StringBuffer
- 不需要调用flip()来切换读/写模式
- 读取和写入索引分开
- 方法链
- 引用计数
- Pooling(池)
ByteBuf读写数据效率很高,不同于JDK使用flip来切换读写模式,它内部维护了读写的索引,通过这个索引可以序列化的读取数据并且可以跳到任意位置读。
往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。
如上图,一条消息由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);
}
顺序访问是通过readIndex
和writeIndex
这两个方法,netty内部维护了两个索引,如下图所示,
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的继承关系图,
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));
}
推荐阅读
-
【netty in action】学习笔记-第五章
-
【netty in action】学习笔记-第四章
-
ASP.NET MVC3 学习笔记----HTML.Action()和HTML.RenderAction()
-
Hadoop in Action学习笔记
-
Django学习笔记之为Model添加Action
-
Spark学习笔记之RDD中的Transformation和Action函数
-
《C++ Primer Plus》学习笔记——第五章 循环和关系表达式(四)
-
《C++ Primer Plus》学习笔记——第五章 循环和关系表达式(一)
-
R语言实战学习笔记-第五章 高级数据管理
-
netty5学习笔记-内存池6-可调优参数