根据Wiki对Zero-copy的定义:“Zero-copy”描述了CPU不执行将数据从一个内存区域复制到另一个内存区域的任务的计算机操作。这经常用于在通过网络传输文件时节省CPU周期和内存带宽。所谓Zero-copy,就是在操作数据时,不需要将数据缓冲区从一个内存区域复制到另一个内存区域。因为少了一份内存拷贝,提高了CPU的效率。OS层面的零拷贝通常是指避免在用户空间(User-space)和内核空间(Kernel-space)之间来回拷贝数据。例如,Linux提供的mmap系统调用可以将一段用户空间内存映射到内核空间,当映射成功时,用户对这块内存区域的修改可以直接反映到内核空间;同样,内核空间对这个区域的修改也直接体现在用户空间。因为这种映射关系,我们不需要在User-space和Kernel-space之间进行数据拷贝,提高了数据传输的效率。需要注意的是Netty中的Zero-copy和我们上面提到的OS层面的Zero-copy是不一样的。Netty的Zero-coyp完全是在用户态(Java层面),它的Zero-copy更偏向于优化数据操作的理念。Netty的Zerocopy体现在以下几个方面:Netty提供了CompositeByteBuf类,可以将多个ByteBuf合并为一个逻辑ByteBuf,避免了ByteBuf之间的复制。通过wrap操作,我们可以将byte[]数组、ByteBuf、ByteBuffer等都打包成一个NettyByteBuf对象,从而避免了复制操作。ByteBuf支持分片操作,所以ByteBuf可以分解成多个ByteBuf共享同一个存储区,避免了内存拷贝。通过FileRegion包裹的FileChannel.tranferTo实现文件传输,可以将文件缓冲区中的数据直接发送到目标Channel,避免了传统的循环写方式带来的内存拷贝问题。让我们简单了解一下这些常见的零拷贝操作。通过CompositeByteBuf实现零拷贝假设我们有一个协议数据,它由一个header和一个messagebody组成,header和messagebody分别存储在两个ByteBuf中,即:ByteBufheader=...ByteBufbody=...在代码中处理,我们通常希望将header和body合并成一个ByteBuf,方便处理,那么通常的做法是:ByteBufallBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());allBuf.writeBytes(header);allBuf.writeBytes(正文);可以看到,我们将header和body都复制到了新的allBuf中,这无形中增加了两次额外的数据复制操作。那么有没有更高效优雅的方式来达到同样的目的呢?我们来看看CompositeByteBuf是如何实现这个需求的。ByteBufheader=...ByteBufbody=...CompositeByteBufcompositeByteBuf=Unpooled.compositeBuffer();compositeByteBuf.addComponents(true,header,body);在上面的代码中,我们定义了一个CompositeByteBuf对象,然后调用publicCompositeByteBufaddComponents(booleanincreaseWriterIndex,Byte)Buf...buffers{...}方法将header和body合并成一个逻辑上的ByteBuf,即:但是需要注意的是,虽然看起来CompositeByteBuf由两个ByteBuf组成,但是在CompositeByteBuf内部,这两个ByteBuf是独立存在的,CompositeByteBuf只是逻辑上的一个整体。上面的CompositeByteBuf代码值得注意的是我们调用addComponents(booleanincreaseWriterIndex,ByteBuf...buffers)添加两个ByteBuf,其中第一个参数为true,表示当添加一个新的ByteBuf时,CompositeByteBuf的writeIndex会是自动递增。如果我们调用compositeByteBuf.addComponents(header,body);那么实际上compositeByteBuf的writeIndex还是0,所以此时我们不能从compositeByteBuf中读取数据,这一点希望大家特别注意。除了直接使用上面的CompositeByteBuf类,我们还可以使用Unpooled.wrappedBuffer方法,它在底层封装了CompositeByteBuf的操作,使用起来更方便:ByteBufheader=...ByteBufbody=...ByteBufallByteBuf=Unpooled.wrappedBuffer(标题,正文);通过wrap操作实现零拷贝比如我们有一个byte数组,我们希望将其转换成ByteBuf对象进行后续操作,传统的方法是将这个byte数组拷贝到ByteBuf中,即:byte[]bytes=...ByteBufbyteBuf=Unpooled.buffer();byteBuf.writeBytes(字节);显然,这个方法还有一个额外的复制操作,我们可以使用Unpooled相关的方法将这个字节数组包装起来,生成一个新的ByteBuf实例,而不用复制。上面的代码可以改成:byte[]bytes=...ByteBufbyteBuf=Unpooled.wrappedBuffer(bytes);可以看出,我们使用Unpooled.wrappedBuffer方法将字节包装到一个UnpooledHeapByteBuf对象中,并且在包装的过程中,不会有复制操作。也就是说,我们生成的生成的ByteBuf对象与bytes数组共享同一个存储空间,对bytes的修改也会反映到ByteBuf对象中。Unpooled工具类还提供了很多重载的wrappedBuffer方法:...buffers)publicstaticByteBufwrappedBuffer(ByteBuffer...buffers)publicstaticByteBufwrappedBuffer(intmaxNumComponents,byte[]...arrays)publicstaticByteBufwrappedBuffer(intmaxNumComponents,ByteBuf...buffers)publicstaticByteBufwrappedBuffer(intmaxNumComponents,ByteBuffer...buffers)这些方法可以包装一个或多个缓冲区到一个ByteBuf对象中,从而避免了复制操作。通过切片操作实现零拷贝切片操作和wrap操作正好相反,Unpooled.wrappedBuffer可以将多个ByteBuf合并为一个,切片操作可以将一个ByteBuf切片到多个共享存储区字节缓冲区对象。ByteBuf提供了两种切片操作方法:publicByteBufslice();publicByteBufslice(intindex,intlength);不带参数的slice方法相当于调用buf.slice(buf.readerIndex(),buf.readableBytes()),即返回buf可读部分的slice。slice(intindex,intlength)方法相对更加灵活。我们可以通过设置不同的参数来获取buf不同区域的分片。下面的例子展示了ByteBuf.slice方法的简单用法:ByteBufbyteBuf=...ByteBufheader=byteBuf.slice(0,5);ByteBufbody=byteBuf.slice(5,10);在用slice方法生成header和body的过程中没有复制操作,header和bodyobject实际上在内部共享不同部分的byteBuf存储空间。即:通过FileRegion实现零拷贝,在Netty中使用FileRegion实现文件传输的零拷贝,但是FileRegion在底层依赖于JavaNIOFileChannel.transfer的零拷贝功能。首先,让我们从最基本的JavaIO开始。假设我们要实现一个文件复制功能,那么使用传统的方法,我们有如下实现:publicstaticvoidcopyFile(StringsrcFile,StringdestFile)throwsException{byte[]temp=newbyte[1024];FileInputStreamin=newFileInputStream(srcFile);FileOutputStreamout=newFileOutputStream(destFile);intlength;while((length=in.read(temp))!=-1){out.write(temp,0,length);}in.close();out.close();}以上就是典型的读写二进制文件的代码实现。不用多说,想必大家都知道,上面代码中,不断从源文件中读取定长数据到temp数组中,然后将temp中的内容写入到目标文件中。这种拷贝操作对小文件影响不大,但是如果我们需要拷贝大文件,频繁的内存拷贝操作会消耗大量的系统资源。下面看看如何利用JavaNIO的FileChannel实现零拷贝:"rw");FileChannelsrcFileChannel=destFile.getChannel();longposition=0;longcount=srcFileChannel.size();srcFileChannel.transferTo(position,count,destFileChannel);}可以看出,使用FileChannel后,我们可以直接复制(transferTo)将源文件的内容传输到目标文件,而不使用额外的临时缓冲区,避免了不必要的内存操作。有了上面的一些理论知识,我们再来看看Netty中如何使用FileRegion实现文件的零拷贝传输:@OverridepublicvoidchannelRead0(ChannelHandlerContextctx,Stringmsg)throwsException{RandomAccessFileraf=null;length=-1;try{//1.通过RandomAccessFile.raf=newRandomAccessFile(msg,"r");lengt打开一个文件h=raf.length();}catch(Exceptione){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){//SSLnotenabled-canusezero-copyfiletransfer.//2.调用raf.getChannel()获取FileChannel.//3.将FileChannel封装成一个DefaultFileRegionctx.write(newDefaultFileRegion(raf.getChannel(),0,length));}else{//SSEnabled-cannotusezero-copyfiletransfer.ctx.write(newChunkedFile(raf));}ctx.writeAndFlush("\n");}以上代码是Netty的一个例子,其源码可见netty/example/src/main/java/io/netty/example/file/FileServerHandler.java,第一步是打开一个通过RandomAccessFile文件,然后Netty使用DefaultFileRegion封装一个FileChannel是:newDefaultFileRegion(raf.getChannel(),0,length)当我们有了FileRegion后,我们可以直接通过它把文件的内容写入到Channel中,而不是复制文件的内容到一个临时缓冲区,然后将缓冲区写入Channel。通过这样的零拷贝操作,对于传输大文件无疑是有帮助的。
