Netty05——NIO与零拷贝

本文介绍了Java中的零拷贝技术,包括mmap内存映射和sendFile方法,以及它们如何减少数据拷贝和上下文切换,提高性能。通过案例展示了零拷贝在服务端和客户端的实现,特别提到了`transferTo()`方法在实现零拷贝中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、零拷贝

 零拷贝是网络编程的关键,很多性能优化都离不开零拷贝。在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。
 Java 中传统的 IO 和网络编程:

File file = new File("test.txt"); 
RandomAccessFile raf = new RandomAccessFile(file, "rw"); 
byte[] arr = new byte[(int) file.length()]; 
raf.read(arr); 
Socket socket = new ServerSocket(8080).accept(); 
socket.getOutputStream().write(arr);

在这里插入图片描述
  注:DMA —— direct memory access 直接内存拷贝(不适用CPU)
 mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图:
在这里插入图片描述
 Linux 2.1 版本提供了 sendFile 函数,其基本原理是:数据根本不经过用户态,直接从内核缓冲区进入到 Socket Buffer,由于和用户态完全无关,就减少了一次上下文切换。
在这里插入图片描述
 Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,而是直接拷贝到协议栈,从而再一次减少了数据拷贝。
在这里插入图片描述
  注:
   ①这里其实有一次 cpu 拷贝 kernel buffer -> socket buffer, 但是拷贝的信息很少,比如 lenght、offset,消耗很低,可以忽略
   ②所谓零拷贝,就是拷贝由操作系统完成,不需要 CPU 参与。这样可以避免 CPU 的上下文切换,也可以避免资源从内核空间到用户空间的拷贝,节约了时间。这也保证了内核缓冲区之间数据不会重复,在内核缓冲区中只有一份数据。零拷贝不仅仅带来更少的数据复制,还有更少的 CPU 缓存伪共享以及无 CPU 校验和计算等
 mmap 和 sendFile 的区别:
  ①mmap 适合小数据量读写,sendFile 适合大文件传输;
  ②mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝;
  ③sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区);

二、零拷贝案例

 服务端:

public class ZeroCopyServer {

    public static void main(String[] args) throws Exception {
        InetSocketAddress address = new InetSocketAddress(7001);
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket serverSocket = serverSocketChannel.socket();
        serverSocket.bind(address);
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
        File file = new File("E:\\b.pdf");
        SocketChannel socketChannel = serverSocketChannel.accept();
        // 追加文件内容
        FileChannel fileChannel = new FileOutputStream(file, true).getChannel();
        while (true) {
            int readCount = 0;
            while (-1 != readCount) {
                try {
                    readCount = socketChannel.read(byteBuffer);
                    // 注意读写转换
                    byteBuffer.flip();
                    fileChannel.write(byteBuffer);
                    byteBuffer.clear();
                } catch (Exception ex) {
                    ex.printStackTrace();
                    break;
                }
            }
            break;
        }
        fileChannel.close();
    }
}

 客户端:

public class ZeroCopyClient {

    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 7001));
        String filename = "E:\\a.pdf";
        FileChannel fileChannel = new FileInputStream(filename).getChannel();
        long startTime = System.currentTimeMillis();
        // 在 linux 下一个 transferTo 方法就可以完成传输,但 windows 下调用一次 transferTo 只能发送 8m,就需要分段传输而且要注意传输的位置
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println("发送的总字节数:" + transferCount + ",耗时:" + (System.currentTimeMillis() - startTime));
        fileChannel.close();
        // 保证客户端不会在文件传输后立即死掉,这会使其与服务端的连接立即断开,而此时服务端可能尚未读取完客户端发送的文件数据,断开连接后剩余的数据将不可读,从而导致传输的文件打不开
        Thread.sleep(1000);
    }
}

 注:transferTo()方法的底层用到了零拷贝。

### Netty 实现零拷贝技术的代码示例 #### 使用 `CompositeByteBuf` 进行组合缓冲区操作 通过将多个 `ByteBuf` 对象组合成一个逻辑上的 `ByteBuf`,可以在不复制数据的情况下处理这些字节缓冲区。 ```java import io.netty.buffer.ByteBuf; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; public class CompositeByteBufExample { public static void main(String[] args) { // 创建两个独立的 ByteBuf ByteBuf buffer1 = Unpooled.copiedBuffer("Hello, ".getBytes()); ByteBuf buffer2 = Unpooled.copiedBuffer("World!".getBytes()); // 将这两个 Buffer 合并为一个 CompositeByteBuf try (CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer()) { compositeBuffer.addComponents(true, buffer1, buffer2); // 输出合并后的结果 System.out.println(compositeBuffer.toString(io.netty.util.CharsetUtil.UTF_8)); } finally { buffer1.release(); buffer2.release(); } } } ``` 此段代码展示了如何利用 `CompositeByteBuf` 来避免不必要的内存复制[^3]。 #### 利用 `Unpooled.wrappedBuffer()` 方法封装现有缓冲区 当需要包装已有的 `ByteBuffer` 或者其他类型的数组而不希望发生实际的数据迁移时,可以采用这种方式来构建新的 `ByteBuf` 实例。 ```java import java.nio.ByteBuffer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; public class WrappedBufferExample { public static void main(String[] args) { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024); byteBuffer.put("Some data".getBytes()); // 包装现有的 NIO Direct Buffer 成 Netty 的 ByteBuf ByteBuf wrappedBuffer = Unpooled.wrappedBuffer(byteBuffer); // 处理 wrappedBuffer... // 释放资源 wrappedBuffer.release(); } } ``` 这段程序说明了怎样使用 `Unpooled.wrappedBuffer()` 函数把 Java NIO 的直接缓存转换成 Netty 支持的形式,并保持原始数据不变[^2]。 #### 文件传输中的零拷贝应用 (`FileRegion`) 对于大文件上传下载等场景下,Netty 提供了专门设计用来优化这类操作效率的支持类 —— `FileRegion`。它允许应用程序高效地发送文件内容给客户端而无需额外占用大量 JVM 堆内空间。 ```java import io.netty.channel.ChannelHandlerContext; import io.netty.channel.FileRegion; import java.io.RandomAccessFile; public class FileTransferHandler { private final RandomAccessFile file; public FileTransferHandler(RandomAccessFile file) { this.file = file; } public void sendFile(ChannelHandlerContext ctx) throws Exception { long position = 0; // 开始位置 long count = file.length(); // 要传送的数量 // 构建 FileRegion 并发起写入请求 FileRegion region = new DefaultFileRegion(file.getChannel(), position, count); ctx.writeAndFlush(region).addListener(future -> { if (!future.isSuccess()) { future.cause().printStackTrace(); } try { file.close(); } catch (IOException e) { e.printStackTrace(); } }); } } ``` 上述例子中定义了一个处理器方法用于执行基于 `FileRegion` 接口的大规模二进制流传输任务[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值