第一章Netty,NIO零拷贝详细实现代码
基于前文对 NIO 零拷贝原理(sendfile 与 mmap)及高并发场景的讨论,以下是两种核心零拷贝技术的详细 Java 实现代码。
先看一段传统IO模型的文件拷贝代码:
packagecom.example.demo;importjava.io.File;importjava.io.IOException;importjava.io.RandomAccessFile;importjava.net.Socket;publicclassFileNioTest{publicstaticvoidmain(String[]args)throwsIOException{Filef=newFile("netty.txt");RandomAccessFilefile=newRandomAccessFile(f,"r");byte[]buf=newbyte[(int)f.length()];intread=file.read(buf);Socketsocket=newSocket();socket.getOutputStream().write(buf);}}传统IO工作流程:
一、 基于 transferTo 的文件网络传输(推荐)
这是最纯粹的零拷贝,适用于静态资源服务器或大文件上传/下载。数据直接在内核态从 Page Cache 传输到 Socket Buffer,不经过用户态。
1. 服务端代码(发送文件)
importjava.io.FileInputStream;importjava.net.InetSocketAddress;importjava.nio.channels.FileChannel;importjava.nio.channels.ServerSocketChannel;importjava.nio.channels.SocketChannel;publicclassZeroCopyServer{publicstaticvoidmain(String[]args)throwsException{// 1. 创建 ServerSocketChannelServerSocketChannelserverSocketChannel=ServerSocketChannel.open();serverSocketChannel.bind(newInetSocketAddress(8080));System.out.println("Server started on port 8080...");while(true){// 2. 接受连接SocketChannelsocketChannel=serverSocketChannel.accept();// 3. 获取文件通道FileChannelfileChannel=newFileInputStream("large_file.dat").getChannel();// 4. 【核心】零拷贝传输// transferTo 底层调用 sendfile,数据不经过用户空间longtransferred=fileChannel.transferTo(0,fileChannel.size(),socketChannel);System.out.println("Total bytes transferred: "+transferred);// 5. 关闭资源fileChannel.close();socketChannel.close();}}}2. 客户端代码(接收文件)
importjava.io.FileOutputStream;importjava.net.InetSocketAddress;importjava.nio.channels.FileChannel;importjava.nio.channels.SocketChannel;publicclassZeroCopyClient{publicstaticvoidmain(String[]args)throwsException{// 1. 连接服务器SocketChannelsocketChannel=SocketChannel.open();socketChannel.connect(newInetSocketAddress("localhost",8080));// 2. 获取文件输出通道FileChannelfileChannel=newFileOutputStream("received_file.dat").getChannel();// 3. 读取数据并写入文件// 注意:客户端接收通常仍涉及一次从内核到用户空间的拷贝(除非使用 DirectBuffer + write)// 但服务端发送已实现零拷贝,整体性能显著提升longbytesRead=fileChannel.transferFrom(socketChannel,0,Long.MAX_VALUE);System.out.println("Total bytes received: "+bytesRead);// 4. 关闭资源fileChannel.close();socketChannel.close();}}二、 基于 MappedByteBuffer 的大文件随机读写
适用于数据库索引、日志分析等需要频繁随机访问大文件的场景。通过内存映射,避免传统 read/write 的系统调用开销。
1. 写入大文件示例
importjava.io.RandomAccessFile;importjava.nio.MappedByteBuffer;importjava.nio.channels.FileChannel;publicclassMappedFileWriter{publicstaticvoidmain(String[]args)throwsException{StringfilePath="mapped_large_file.dat";longfileSize=1024*1024*100;// 100MB// 1. 创建 RandomAccessFile 和 FileChannelRandomAccessFileraf=newRandomAccessFile(filePath,"rw");FileChannelchannel=raf.getChannel();// 2. 【核心】内存映射// MapMode.READ_WRITE: 读写模式// position: 0, size: fileSizeMappedByteBuffermappedBuffer=channel.map(FileChannel.MapMode.READ_WRITE,0,fileSize);// 3. 直接操作内存,如同操作数组for(inti=0;i<1024;i++){mappedBuffer.putInt(i*4,i);// 在偏移量 i*4 处写入整数 i}// 4. 强制刷新到磁盘(可选,确保数据持久化)mappedBuffer.force();System.out.println("Data written to mapped file.");// 5. 关闭资源channel.close();raf.close();}}2. 读取大文件示例
importjava.io.RandomAccessFile;importjava.nio.MappedByteBuffer;importjava.nio.channels.FileChannel;publicclassMappedFileReader{publicstaticvoidmain(String[]args)throwsException{StringfilePath="mapped_large_file.dat";// 1. 创建 RandomAccessFile 和 FileChannelRandomAccessFileraf=newRandomAccessFile(filePath,"r");FileChannelchannel=raf.getChannel();// 2. 内存映射(只读模式)MappedByteBuffermappedBuffer=channel.map(FileChannel.MapMode.READ_ONLY,0,channel.size());// 3. 随机读取数据intvalue=mappedBuffer.getInt(0);// 读取偏移量 0 处的整数System.out.println("Value at offset 0: "+value);intvalueAt1024=mappedBuffer.getInt(1024*4);System.out.println("Value at offset 4096: "+valueAt1024);// 4. 关闭资源channel.close();raf.close();}}三、 关键注意事项
资源关闭:FileChannel 和 SocketChannel 必须显式关闭,否则会导致文件描述符泄漏。
内存映射释放:MappedByteBuffer 占用的堆外内存不会立即被 GC 回收。若需手动释放,可借助 sun.misc.Cleaner(非官方 API,需谨慎使用)或通过反射调用 clean() 方法。
小文件性能:对于极小文件(< 1KB),零拷贝的系统调用开销可能高于传统 I/O,此时传统 read/write 可能更优。
平台差异:transferTo 在 Linux 下性能最佳(完整零拷贝),在 Windows 下可能退化为部分拷贝实现。
