上一篇【第30篇】Netty写数据源码解析——write/flush背后的双队列设计
下一篇【第32篇】Netty背压机制——不让发送方"撑死"接收方
开篇故事
某文件传输系统,传输1GB文件CPU飙到90%。排查:每次发送都要memcpy从堆内拷贝到堆外!
优化后:使用FileRegion.TransferTo,直接DMA传输,CPU降到5%!
零拷贝 = 不进行CPU参与的memcpy
一、传统vs零拷贝对比
传统方式(4次拷贝+2次上下文切换): 磁盘→内核缓冲区→用户缓冲区→Socket缓冲区→网卡 ↑ DMA ↑ CPU memcpy ↑ CPU memcpy ↑ DMA 零拷贝(sendfile)(2次拷贝+0次上下文切换): 磁盘→内核缓冲区→Socket缓冲区→网卡 ↑ DMA ↑ DMA(仅描述符传递)二、Netty四种零拷贝实现
2.1 CompositeByteBuf——组合多个ByteBuf
ByteBufheader=Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n".getBytes());ByteBufbody=Unpooled.copiedBuffer("Hello Netty".getBytes());CompositeByteBufcomposite=Unpooled.compositeBuffer();composite.addComponents(true,header,body);// 零拷贝!header和body的数据没有复制,只保存了引用2.2 slice()——切片共享同一块内存
ByteBufbuf=Unpooled.buffer(1024);buf.writeBytes("Hello World".getBytes());ByteBufslice=buf.slice(0,5);// "Hello" 零拷贝!slice.setByte(0,'h');// 修改slice也影响原buf2.3 wrap()——包裹字节数组
byte[]data="Hello".getBytes();ByteBufbuf=Unpooled.wrappedBuffer(data);// 零拷贝包裹2.4 FileRegion——文件传输零拷贝
// 直接将文件数据DMA传输到Socket,不经过用户空间RandomAccessFilefile=newRandomAccessFile("largefile.dat","r");FileRegionregion=newDefaultFileRegion(file.getChannel(),0,file.length());ctx.writeAndFlush(region);三、FileRegion源码
publicclassDefaultFileRegionextendsAbstractReferenceCountedimplementsFileRegion{privatefinalFileChannelfile;privatefinallongposition;privatefinallongcount;publiclongtransferTo(WritableByteChanneltarget,longpos)throwsIOException{returnfile.transferTo(this.position+pos,count-pos,target);}}// NioSocketChannel中使用protectedbooleandoWriteFileRegion(FileRegionregion){longwritten=region.transferTo(javaChannel(),region.transferred());// 底层调用:FileChannel.transferTo() → sendfile()系统调用}四、实战:高性能文件服务器
importio.netty.bootstrap.ServerBootstrap;importio.netty.channel.*;importio.netty.channel.nio.NioEventLoopGroup;importio.netty.channel.socket.nio.NioServerSocketChannel;importio.netty.handler.codec.LineBasedFrameDecoder;importio.netty.handler.codec.string.StringDecoder;importjava.io.RandomAccessFile;publicclassZeroCopyFileServer{publicstaticvoidmain(String[]args)throwsException{EventLoopGroupboss=newNioEventLoopGroup(1);EventLoopGroupworker=newNioEventLoopGroup();try{newServerBootstrap().group(boss,worker).channel(NioServerSocketChannel.class).childHandler(newChannelInitializer<Channel>(){protectedvoidinitChannel(Channelch){ch.pipeline().addLast(newLineBasedFrameDecoder(1024));ch.pipeline().addLast(newStringDecoder());ch.pipeline().addLast(newFileSendHandler());}}).bind(8080).sync().channel().closeFuture().sync();}finally{boss.shutdownGracefully();worker.shutdownGracefully();}}staticclassFileSendHandlerextendsChannelInboundHandlerAdapter{publicvoidchannelRead(ChannelHandlerContextctx,Objectmsg){Stringpath=((String)msg).trim();try{RandomAccessFilefile=newRandomAccessFile(path,"r");// 零拷贝发送文件!ctx.write(newDefaultFileRegion(file.getChannel(),0,file.length()));ctx.writeAndFlush("\r\n");}catch(Exceptione){ctx.close();}}}}五、性能对比
| 方式 | CPU | 内存拷贝次数 | 适用场景 |
|---|---|---|---|
| 传统read/write | 90% | 4次 | 小文件 |
| HeapBuf + write | 60% | 3次 | 中等文件 |
| DirectBuf + write | 30% | 2次 | 大文件 |
| FileRegion | 5% | 0次(DMA) | 超大文件 |
六、总结
| 方式 | 原理 |
|---|---|
CompositeByteBuf | 多个ByteBuf零拷贝拼接 |
slice() | 同一块内存的不同视图 |
wrap() | byte[]零拷贝包裹 |
FileRegion | sendfile()系统调用,DMA传输 |
上一篇【第30篇】Netty写数据源码解析——write/flush背后的双队列设计
下一篇【第32篇】Netty背压机制——不让发送方"撑死"接收方