彻底搞懂Netty⾼性能之零拷贝
作为Java⽹络编程学习者,不仅要知道NIO,还⼀定要学习Mina和Netty这两个优秀的⽹络框架。
Netty⾼性能的原因
Netty作为异步事件驱动的⽹络框架,⾼性能主要来⾃于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。
Netty⾼性能的原因总结,智者见智,并没有固定答案。
基于I/O多路复⽤模型
零拷贝
基于NIO的Buffer
基于内存池的缓冲区重⽤机制
⽆锁化的串⾏设计理念
I/O操作的异步处理
玉米的营养价值提供对protobuf等⾼性能序列化协议⽀持
可以对TCP进⾏更加灵活地配置
Netty的零拷贝
在操作系统层⾯上的零拷贝是指避免在⽤户态与内核态之间来回拷贝数据的技术。 Netty中的零拷贝与操作系统层⾯上的零拷贝不完全⼀样, Netty的零拷贝完全是在⽤户态(Java层⾯)的,更多是数据操作的优化。
Netty的零拷贝主要体现在五个⽅⾯
Netty的接收和发送ByteBuffer使⽤直接内存进⾏Socket读写,不需要进⾏字节缓冲区的⼆次拷贝。如果使⽤JVM的堆内存进⾏Socket读
写,JVM会将堆内存Buffer拷贝⼀份到直接内存中,然后才写⼊Socket中。相⽐于使⽤直接内存,消息在发送过程中多了⼀次缓冲区的内存拷贝。
Netty的⽂件传输调⽤FileRegion包装的transferTo⽅法,可以直接将⽂件缓冲区的数据发送到⽬标Channel,避免通过循环write⽅式导致的内存拷贝问题。
Netty提供CompositeByteBuf类, 可以将多个ByteBuf合并为⼀个逻辑上的ByteBuf, 避免了各个ByteBuf之间的拷贝。
通过wrap操作, 我们可以将byte[]数组、ByteBuf、ByteBuffer等包装成⼀个Netty ByteBuf对象, 进⽽避免拷贝操作。
ByteBuf⽀持slice操作,可以将ByteBuf分解为多个共享同⼀个存储区域的ByteBuf, 避免内存的拷贝。
通过FileRegion实现零拷贝
基于上⼀篇博客的知识,理解Netty的零拷贝就很容易。
cabinet
FileRegion底层调⽤NIO FileChannel的transferTo函数。 下⾯的代码节选⾃netty源码中example包的FileServerHandler.java。
@Override
public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
RandomAccessFile raf = null;
long length = -1;
try {
// 1. 通过 RandomAccessFile 打开⼀个⽂件.
raf = new RandomAccessFile(msg, "r");
length = raf.length();
} catch (Exception e) {
ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + '\n');
return;
} finally {
if (length < 0 && raf != null) {知客
raf.clo();
}
policeman是什么意思
}
ctx.write("OK: " + raf.length() + '\n');
if (ctx.pipeline().get(SslHandler.class) == null) {
// SSL not enabled - can u zero-copy file transfer.
// 2. 调⽤ Channel() 获取⼀个 FileChannel.
// 3. 将 FileChannel 封装成⼀个 DefaultFileRegion
ctx.write(new Channel(), 0, length));
} el {
// SSL enabled - cannot u zero-copy file transfer.
ctx.write(new ChunkedFile(raf));
}
ctx.writeAndFlush("\n");
challenging}
通过CompositeByteBuf实现零拷贝
CompositeByteBuf可以把需要合并的多个bytebuf组合起来,对外提供统⼀的readIndex和writerIndex。 但在CompositeByteBuf内部, 合并的多个ByteBuf都是单独存在的,CompositeByteBuf 只是逻辑上是⼀个整体。
CompositeByteBuf⾥⾯有个Component数组,聚合的bytebuf都放在Component数组⾥⾯,最⼩容量为16。
传统做法合并ByteBuf
假设有⼀份协议数据,它由头部和消息体组成,⽽头部和消息体是分别存放在两个ByteBuf中的, 为了⽅便后续处理,要将两个ByteBuf进⾏合并。
ByteBuf header = ...
ByteBuf body = ...
// 按照原本的做法 将header和body合并为⼀个ByteBuf
ByteBuf allBuf = Unpooled.adableBytes() + adableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);
上述过程将header和body都拷贝到了新的allBuf中,这增加了两次额外的数据拷贝操作了。
CompositeByteBuf实现合并bytebuf
CompositeByteBuf合并ByteBuf,减少两次额外的数据拷贝操作。
ByteBuf header = ...
ByteBuf body = ...
// 新建CompositeByteBuf对象
CompositeByteBuf compositeByteBuf = positeBuffer();
// 第⼀个参数是true, 表⽰当添加新的ByteBuf时, ⾃动递增 CompositeByteBuf 的 writeIndex。如果不传第⼀个参数或第⼀个参数为fal,则合并后的compositeByteBuf的writeIndex不移动,即不能从compositeByteBuf中读取到新合并的数据。
compositeByteBuf.addComponents(true,header,body);
⼀张图清楚理解readIndex和writeIndex。
除了上⾯直接使⽤CompositeByteBuf类外, 还可以使⽤ Unpooled.wrappedBuffer⽅法。 Unpooled封装了CompositeByteBuf的操作,使⽤起来更加⽅便:
外教英语1对1哪家好
ByteBuf header = ...好听的英文单词
ByteBuf body = ...
ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);
通过wrap操作实现零拷贝
如果将⼀个byte数组转换为⼀个ByteBuf对象,以便于后续的操作,那么传统的做法是将此byte数组拷贝到ByteBuf中。
byte[] bytes = ...
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);
英讯教育显然这样的⽅式也是有⼀个额外的拷贝操作的, 我们可以使⽤Unpooled的相关⽅法, 包装这个byte数组,
⽣成⼀个新的ByteBuf实例, ⽽不需要进⾏拷贝操作. 上⾯的代码可以改为:
byte[] bytes = ...
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
通过Unpooled.wrappedBuffer⽅法将bytes包装为⼀个UnpooledHeapByteBuf对象, ⽽在包装的过程中, 不会有拷贝操作的,即⽣成的ByteBuf对象是和bytes数组共⽤了同⼀个存储空间,对bytes的修改也就是对ByteBuf对象的修改。
Unpooled类还提供了很多重载的wrappedBuffer⽅法,将⼀个或多个buffer包装为⼀个 ByteBuf对象,从⽽实现零拷贝。
public static ByteBuf wrappedBuffer(byte[] array)
public static ByteBuf wrappedBuffer(byte[] array, int offt, int length)
public static ByteBuf wrappedBuffer(ByteBuffer buffer)
public static ByteBuf wrappedBuffer(ByteBuf buffer)
public static ByteBuf wrappedBuffer(byte[]... arrays)
public static ByteBuf buffers)
public static ByteBuf buffers)
public static ByteBuf wrappedBuffer(int maxNumComponents, byte[]... arrays)
red is the man
public static ByteBuf wrappedBuffer(int maxNumComponents, buffers)
public static ByteBuf wrappedBuffer(int maxNumComponents, buffers)
通过slice操作实现零拷贝
slice操作和wrap操作刚好相反, Unpooled.wrappedBuffer可以将多个ByteBuf 合并为⼀个, ⽽slice操作可以将⼀个ByteBuf切⽚为多个共享⼀个存储区域的 ByteBuf对象。
ByteBuf提供了两个slice操作⽅法:
public ByteBuf slice();
public ByteBuf slice(int index, int length);
前者等同于buf.aderIndex(), adableBytes())调⽤,即返回buf中可读部分的切⽚。
后者相对就⽐较灵活,可以设置不同的参数获取buf不同区域的切⽚。
下⾯的例⼦展⽰了ByteBuf.slice⽅法的简单⽤法:
ByteBuf byteBuf = ...
ByteBuf header = byteBuf.slice(0, 5);
ByteBuf body = byteBuf.slice(5, 10);
⽤slice⽅法产⽣header和body的过程是没有拷贝操作的,header和body对象在内部其实是共享了byteBuf存储空间的不同部分⽽已 。
需要java学习路线图的私信笔者“java”领取哦!另外喜欢这篇⽂章的可以给笔者点个赞同,关注⼀下,每天都会分享Java相关⽂章!还有不定时的福利赠送,包括整理的学习资料,⾯试题,源码等~~
>dmx