對於 Netty ByteBuf 的零拷貝(Zero Copy) 的理解

做者:@xys1228
本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-pededtnv-bu.html
Email:yongshun1228@gmail.comhtml


目錄

經過 CompositeByteBuf 實現零拷貝
經過 wrap 操做實現零拷貝
經過 slice 操做實現零拷貝
經過 FileRegion 實現零拷貝

此文章已同步發佈在個人 segmentfault 專欄.java

根據 Wiki 對 Zero-copy 的定義:segmentfault

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.數組

即所謂的 Zero-copy, 就是在操做數據時, 不須要將數據 buffer 從一個內存區域拷貝到另外一個內存區域. 由於少了一次內存的拷貝, 所以 CPU 的效率就獲得的提高.app

在 OS 層面上的 Zero-copy 一般指避免在 用戶態(User-space) 與 內核態(Kernel-space) 之間來回拷貝數據. 例如 Linux 提供的 mmap 系統調用, 它能夠將一段用戶空間內存映射到內核空間, 當映射成功後, 用戶對這段內存區域的修改能夠直接反映到內核空間; 一樣地, 內核空間對這段區域的修改也直接反映用戶空間. 正由於有這樣的映射關係, 咱們就不須要在 用戶態(User-space) 與 內核態(Kernel-space) 之間拷貝數據, 提升了數據傳輸的效率.dom

而須要注意的是, Netty 中的 Zero-copy 與上面咱們所提到到 OS 層面上的 Zero-copy 不太同樣, Netty的 Zero-coyp徹底是在用戶態(Java 層面)的, 它的 Zero-copy 的更多的是偏向於 優化數據操做 這樣的概念.ide

Netty 的 Zero-copy 體如今以下幾個個方面:工具

  • Netty 提供了 CompositeByteBuf 類, 它能夠將多個 ByteBuf 合併爲一個邏輯上的 ByteBuf, 避免了各個 ByteBuf 之間的拷貝.
  • 經過 wrap 操做, 咱們能夠將 byte[] 數組、ByteBuf、ByteBuffer等包裝成一個 Netty ByteBuf 對象, 進而避免了拷貝操做.
  • ByteBuf 支持 slice 操做, 所以能夠將 ByteBuf 分解爲多個共享同一個存儲區域的 ByteBuf, 避免了內存的拷貝.
  • 經過 FileRegion 包裝的FileChannel.tranferTo 實現文件傳輸, 能夠直接將文件緩衝區的數據發送到目標 Channel, 避免了傳統經過循環 write 方式致使的內存拷貝問題.

下面咱們就來簡單瞭解一下這幾種常見的零拷貝操做.優化

經過 CompositeByteBuf 實現零拷貝

假設咱們有一份協議數據, 它由頭部和消息體組成, 而頭部和消息體是分別存放在兩個 ByteBuf 中的, 即:spa

ByteBuf header = ...
ByteBuf body = ...

咱們在代碼處理中, 一般但願將 header 和 body 合併爲一個 ByteBuf, 方便處理, 那麼一般的作法是:

ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes()); allBuf.writeBytes(header); allBuf.writeBytes(body);

能夠看到, 咱們將 header 和 body 都拷貝到了新的 allBuf 中了, 這無形中增長了兩次額外的數據拷貝操做了.

那麼有沒有更加高效優雅的方式實現相同的目的呢? 咱們來看一下 CompositeByteBuf 是如何實現這樣的需求的吧.

ByteBuf header = ...
ByteBuf body = ...

CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents(true, header, body);

上面代碼中, 咱們定義了一個 CompositeByteBuf 對象, 而後調用

public CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers) { ... }

方法將 header 與 body 合併爲一個邏輯上的 ByteBuf, 即:

不過須要注意的是, 雖然看起來 CompositeByteBuf 是由兩個 ByteBuf 組合而成的, 不過在 CompositeByteBuf 內部, 這兩個 ByteBuf 都是單獨存在的, CompositeByteBuf 只是邏輯上是一個總體.

上面 CompositeByteBuf 代碼還以一個地方值得注意的是, 咱們調用 addComponents(boolean increaseWriterIndex, ByteBuf... buffers) 來添加兩個 ByteBuf, 其中第一個參數是 true, 表示當添加新的 ByteBuf 時, 自動遞增 CompositeByteBuf 的 writeIndex.
若是咱們調用的是

compositeByteBuf.addComponents(header, body);

那麼其實 compositeByteBuf 的 writeIndex 仍然是0, 所以此時咱們就不可能從 compositeByteBuf 中讀取到數據, 這一點但願你們要特別注意.

除了上面直接使用 CompositeByteBuf 類外, 咱們還可使用 Unpooled.wrappedBuffer 方法, 它底層封裝了 CompositeByteBuf 操做, 所以使用起來更加方便:

ByteBuf header = ...
ByteBuf body = ...

ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);

經過 wrap 操做實現零拷貝

例如咱們有一個 byte 數組, 咱們但願將它轉換爲一個 ByteBuf 對象, 以便於後續的操做, 那麼傳統的作法是將此 byte 數組拷貝到 ByteBuf 中, 即:

byte[] bytes = ... ByteBuf byteBuf = Unpooled.buffer(); byteBuf.writeBytes(bytes);

顯然這樣的方式也是有一個額外的拷貝操做的, 咱們可使用 Unpooled 的相關方法, 包裝這個 byte 數組, 生成一個新的 ByteBuf 實例, 而不須要進行拷貝操做. 上面的代碼能夠改成:

byte[] bytes = ... ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

能夠看到, 咱們經過 Unpooled.wrappedBuffer 方法來將 bytes 包裝成爲一個 UnpooledHeapByteBuf 對象, 而在包裝的過程當中, 是不會有拷貝操做的. 即最後咱們生成的生成的 ByteBuf 對象是和 bytes 數組共用了同一個存儲空間, 對 bytes 的修改也會反映到 ByteBuf 對象中.

Unpooled 工具類還提供了不少重載的 wrappedBuffer 方法:

public static ByteBuf wrappedBuffer(byte[] array) public static ByteBuf wrappedBuffer(byte[] array, int offset, int length) public static ByteBuf wrappedBuffer(ByteBuffer buffer) public static ByteBuf wrappedBuffer(ByteBuf buffer) public static ByteBuf wrappedBuffer(byte[]... arrays) public static ByteBuf wrappedBuffer(ByteBuf... buffers) public static ByteBuf wrappedBuffer(ByteBuffer... buffers) public static ByteBuf wrappedBuffer(int maxNumComponents, byte[]... arrays) public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuf... buffers) public static ByteBuf wrappedBuffer(int maxNumComponents, ByteBuffer... buffers)

這些方法能夠將一個或多個 buffer 包裝爲一個 ByteBuf 對象, 從而避免了拷貝操做.

經過 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.slice 方法的簡單用法:

ByteBuf byteBuf = ...
ByteBuf header = byteBuf.slice(0, 5); ByteBuf body = byteBuf.slice(5, 10);

用 slice 方法產生 header 和 body 的過程是沒有拷貝操做的, header 和 body 對象在內部實際上是共享了 byteBuf 存儲空間的不一樣部分而已. 即:

經過 FileRegion 實現零拷貝

Netty 中使用 FileRegion 實現文件傳輸的零拷貝, 不過在底層 FileRegion 是依賴於 Java NIO FileChannel.transfer 的零拷貝功能.

首先咱們從最基礎的 Java IO 開始吧. 假設咱們但願實現一個文件拷貝的功能, 那麼使用傳統的方式, 咱們有以下實現:

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(); }

上面是一個典型的讀寫二進制文件的代碼實現了. 不用我說, 你們確定都知道, 上面的代碼中不斷中源文件中讀取定長數據到 temp 數組中, 而後再將 temp 中的內容寫入目的文件, 這樣的拷貝操做對於小文件卻是沒有太大的影響, 可是若是咱們須要拷貝大文件時, 頻繁的內存拷貝操做就消耗大量的系統資源了.
下面咱們來看一下使用 Java NIO 的 FileChannel 是如何實現零拷貝的:

public static void copyFileWithFileChannel(String srcFileName, String destFileName) throws Exception { RandomAccessFile srcFile = new RandomAccessFile(srcFileName, "r"); FileChannel srcFileChannel = srcFile.getChannel(); RandomAccessFile destFile = new RandomAccessFile(destFileName, "rw"); FileChannel destFileChannel = destFile.getChannel(); long position = 0; long count = srcFileChannel.size(); srcFileChannel.transferTo(position, count, destFileChannel); }

能夠看到, 使用了 FileChannel 後, 咱們就能夠直接將源文件的內容直接拷貝(transferTo) 到目的文件中, 而不須要額外借助一個臨時 buffer, 避免了沒必要要的內存操做.

有了上面的一些理論知識, 咱們來看一下在 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"); }

上面的代碼是 Netty 的一個例子, 其源碼在 netty/example/src/main/java/io/netty/example/file/FileServerHandler.java
能夠看到, 第一步是經過 RandomAccessFile 打開一個文件, 而後 Netty 使用了 DefaultFileRegion 來封裝一個 FileChannel 即:

new DefaultFileRegion(raf.getChannel(), 0, length)

當有了 FileRegion 後, 咱們就能夠直接經過它將文件的內容直接寫入 Channel 中, 而不須要像傳統的作法: 拷貝文件內容到臨時 buffer, 而後再將 buffer 寫入 Channel. 經過這樣的零拷貝操做, 無疑對傳輸大文件頗有幫助.

相關文章
相關標籤/搜索