一、概述
零拷贝(Zero-copy), CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝零拷贝机制
- Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝
- 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作
- ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝
- 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.
ByteBuf三个重要属性:capacity容量、readerIndex读取位置、writerlndex写入位置。
提供了两个指针变量来支持顺序读和写操作,分别是readerlndex和写操作writerIndex
二、常用方法定义:
随机访问索引 getByte
顺序读 read*
顺序写 write*
清除已读内容 discardReadBytes
清除缓冲区 clear
搜索操作
标记和重置
引用计数和释放
三、ByteBuf 动态扩容
capacity默认值: 256 字节、最大值:Integer.MAX_VALUE(2GB)
write* 方法调用时,通过AbstractByteBuf.ensureWriteable0 进行检查。
容量计算方法:AbstractByteBufAllocator.calculateNewCapacity (新capacity的最小要求,capacity最大值)
根据新capacity的最小值要求,对应有两套计算方法:
没超过4兆:从64字节开始,每次增加一倍,直至计算出来的newCapacity满足新容量最小要求。
示例:当前大小256,已写250,继续写10字节数据,需要的容量最小要求是261,则新容量是6422*2=512
超过4兆:新容量=新容量最小要求/4兆*4兆+4兆
示例:当前大小3兆,已写3兆,继续写2兆数据,需要的容量最小要求是5兆,则新容量是9兆〈不能超过最大值)。
4兆的来源:一个固定的阈值AbstractByteBufAllocator.CALCULATE_THRESHOLD
四、零拷贝实现
1. 通过 CompositeByteBuf 实现零拷贝
import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import java.nio.charset.Charset; /** * 零拷贝示例 */ public class ZeroCopyTest { @org.junit.Test public void wrapTest() { byte[] arr = {1, 2, 3, 4, 5}; ByteBuf byteBuf = Unpooled.wrappedBuffer(arr); System.out.println(byteBuf.getByte(4)); arr[4] = 6; System.out.println(byteBuf.getByte(4)); } @org.junit.Test public void sliceTest() { ByteBuf buffer1 = Unpooled.wrappedBuffer("hello".getBytes()); ByteBuf newBuffer = buffer1.slice(1, 2); newBuffer.unwrap(); System.out.println(newBuffer.toString()); } @org.junit.Test public void compositeTest() { ByteBuf buffer1 = Unpooled.buffer(3); buffer1.writeByte(1); ByteBuf buffer2 = Unpooled.buffer(3); buffer2.writeByte(4); CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); CompositeByteBuf newBuffer = compositeByteBuf.addComponents(true, buffer1, buffer2); System.out.println(newBuffer); } }
2. 通过 wrap 操作实现零拷贝
例如我们有一个 byte 数组, 我们希望将它转换为一个 ByteBuf 对象, 以便于后续的操作, 那么传统的做法是将此 byte 数组拷贝到 ByteBuf 中, 我们可以使用 Unpooled 的相关方法, 包装这个 byte 数组, 生成一个新的 ByteBuf 实例, 而不需要进行拷贝操作:
byte[] bytes = ... ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
3. 通过 slice 操作实现零拷贝
slice 操作和 wrap 操作刚好相反, Unpooled.wrappedBuffer 可以将多个 ByteBuf 合并为一个, 而 slice 操作可以将一个 ByteBuf 切片 为多个共享一个存储区域的 ByteBuf 对象.
ByteBuf 提供了两个 slice 操作方法:
public ByteBuf slice(); public ByteBuf slice(int index, int length);
不带参数的 slice 方法等同于 buf.slice(buf.readerIndex(), buf.readableBytes()) 调用, 即返回 buf 中可读部分的切片. 而 slice(int index, int length) 方法相对就比较灵活了, 我们可以设置不同的参数来获取到 buf 的不同区域的切片.
ByteBuf byteBuf = ... ByteBuf header = byteBuf.slice(0, 5); ByteBuf body = byteBuf.slice(5, 10);
4. 通过 FileRegion 实现零拷贝
public static void copyFile(String srcFile, String destFile) throws Exception { byte[] temp = new byte[1024]; FileInputStream in = new FileInputStream(srcFile); FileOutputStream out = new FileOutputStream(destFile); int length; while ((length = in.read(temp)) != -1) { out.write(temp, 0, length); } in.close(); out.close(); }
五、Netty 中使用 FileRegion 来实现零拷贝传输一个文件的:
@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.close(); } } ctx.write("OK: " + raf.length() + '\n'); if (ctx.pipeline().get(SslHandler.class) == null) { // SSL not enabled - can use zero-copy file transfer. // 2. 调用 raf.getChannel() 获取一个 FileChannel. // 3. 将 FileChannel 封装成一个 DefaultFileRegion ctx.write(new DefaultFileRegion(raf.getChannel(), 0, length)); } else { // SSL enabled - cannot use zero-copy file transfer. ctx.write(new ChunkedFile(raf)); } ctx.writeAndFlush("\n"); }
六、Java NIO 零拷贝示例
NIO中的FileChannel拥有transferTo和transferFrom两个方法,可直接把FileChannel中的数据拷贝到另外一个Channel,或直接把另外一个Channel中的数据拷贝到FileChannel。该接口常被用于高效的网络/文件的数据传输和大文件拷贝。在操作系统支持的情况下,通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”,所以其性能一般高于Java IO中提供的方法。通过网络把一个文件从client传到server:
/** * disk-nic零拷贝 */ class ZerocopyServer { ServerSocketChannel listener = null; protected void mySetup() { InetSocketAddress listenAddr = new InetSocketAddress(9026); try { listener = ServerSocketChannel.open(); ServerSocket ss = listener.socket(); ss.setReuseAddress(true); ss.bind(listenAddr); System.out.println("监听的端口:" + listenAddr.toString()); } catch (IOException e) { System.out.println("端口绑定失败 : " + listenAddr.toString() + " 端口可能已经被使用,出错原因: " + e.getMessage()); e.printStackTrace(); } } public static void main(String[] args) { ZerocopyServer dns = new ZerocopyServer(); dns.mySetup(); dns.readData(); } private void readData() { ByteBuffer dst = ByteBuffer.allocate(4096); try { while (true) { SocketChannel conn = listener.accept(); System.out.println("创建的连接: " + conn); conn.configureBlocking(true); int nread = 0; while (nread != -1) { try { nread = conn.read(dst); } catch (IOException e) { e.printStackTrace(); nread = -1; } dst.rewind(); } } } catch (IOException e) { e.printStackTrace(); } } } class ZerocopyClient { public static void main(String[] args) throws IOException { ZerocopyClient sfc = new ZerocopyClient(); sfc.testSendfile(); } public void testSendfile() throws IOException { String host = "localhost"; int port = 9026; SocketAddress sad = new InetSocketAddress(host, port); SocketChannel sc = SocketChannel.open(); sc.connect(sad); sc.configureBlocking(true); String fname = "src/main/java/zerocopy/test.data"; FileChannel fc = new FileInputStream(fname).getChannel(); long start = System.nanoTime(); long nsent = 0, curnset = 0; curnset = fc.transferTo(0, fc.size(), sc); System.out.println("发送的总字节数:" + curnset + " 耗时(ns):" + (System.nanoTime() - start)); try { sc.close(); fc.close(); } catch (IOException e) { System.out.println(e); } } }
文件到文件的零拷贝:
/** * disk-disk零拷贝 */ class ZerocopyFile { @SuppressWarnings("resource") public static void transferToDemo(String from, String to) throws IOException { FileChannel fromChannel = new RandomAccessFile(from, "rw").getChannel(); FileChannel toChannel = new RandomAccessFile(to, "rw").getChannel(); long position = 0; long count = fromChannel.size(); fromChannel.transferTo(position, count, toChannel); fromChannel.close(); toChannel.close(); } @SuppressWarnings("resource") public static void transferFromDemo(String from, String to) throws IOException { FileChannel fromChannel = new FileInputStream(from).getChannel(); FileChannel toChannel = new FileOutputStream(to).getChannel(); long position = 0; long count = fromChannel.size(); toChannel.transferFrom(fromChannel, position, count); fromChannel.close(); toChannel.close(); } public static void main(String[] args) throws IOException { String from = "src/main/java/zerocopy/1.data"; String to = "src/main/java/zerocopy/2.data"; // transferToDemo(from,to); transferFromDemo(from, to); } }
参考:Netty之ByteBuf零拷贝 - 简书
文章下方有交流学习区!一起学习进步!也可以前往官网,加入官方微信交流群 你的支持和鼓励是我创作的动力❗❗❗
Doker的成长,欢迎大家一起陪伴!!!
我发好文,兄弟们有空请把我的官方旗舰店流量撑起来!!!
官网:Doker 多克; 官方旗舰店:首页-Doker 多克 多克创新科技企业店-淘宝网 全品优惠