摘要:根據(jù)對(duì)的定義即所謂的就是在操作數(shù)據(jù)時(shí)不需要將數(shù)據(jù)從一個(gè)內(nèi)存區(qū)域拷貝到另一個(gè)內(nèi)存區(qū)域因?yàn)樯倭艘淮蝺?nèi)存的拷貝因此的效率就得到的提升在層面上的通常指避免在用戶態(tài)與內(nèi)核態(tài)之間來(lái)回拷貝數(shù)據(jù)例如提供的系統(tǒng)調(diào)用它可以將一段用戶空間內(nèi)存映射到內(nèi)
根據(jù) Wiki 對(duì) Zero-copy 的定義:
"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, 就是在操作數(shù)據(jù)時(shí), 不需要將數(shù)據(jù) buffer 從一個(gè)內(nèi)存區(qū)域拷貝到另一個(gè)內(nèi)存區(qū)域. 因?yàn)樯倭艘淮蝺?nèi)存的拷貝, 因此 CPU 的效率就得到的提升.
在 OS 層面上的 Zero-copy 通常指避免在 用戶態(tài)(User-space) 與 內(nèi)核態(tài)(Kernel-space) 之間來(lái)回拷貝數(shù)據(jù). 例如 Linux 提供的 mmap 系統(tǒng)調(diào)用, 它可以將一段用戶空間內(nèi)存映射到內(nèi)核空間, 當(dāng)映射成功后, 用戶對(duì)這段內(nèi)存區(qū)域的修改可以直接反映到內(nèi)核空間; 同樣地, 內(nèi)核空間對(duì)這段區(qū)域的修改也直接反映用戶空間. 正因?yàn)橛羞@樣的映射關(guān)系, 我們就不需要在 用戶態(tài)(User-space) 與 內(nèi)核態(tài)(Kernel-space) 之間拷貝數(shù)據(jù), 提高了數(shù)據(jù)傳輸?shù)男?
而需要注意的是, Netty 中的 Zero-copy 與上面我們所提到到 OS 層面上的 Zero-copy 不太一樣, Netty的 Zero-coyp 完全是在用戶態(tài)(Java 層面)的, 它的 Zero-copy 的更多的是偏向于 優(yōu)化數(shù)據(jù)操作 這樣的概念.
Netty 的 Zero-copy 體現(xiàn)在如下幾個(gè)個(gè)方面:
Netty 提供了 CompositeByteBuf 類(lèi), 它可以將多個(gè) ByteBuf 合并為一個(gè)邏輯上的 ByteBuf, 避免了各個(gè) ByteBuf 之間的拷貝.
通過(guò) wrap 操作, 我們可以將 byte[] 數(shù)組、ByteBuf、ByteBuffer等包裝成一個(gè) Netty ByteBuf 對(duì)象, 進(jìn)而避免了拷貝操作.
ByteBuf 支持 slice 操作, 因此可以將 ByteBuf 分解為多個(gè)共享同一個(gè)存儲(chǔ)區(qū)域的 ByteBuf, 避免了內(nèi)存的拷貝.
通過(guò) FileRegion 包裝的FileChannel.tranferTo 實(shí)現(xiàn)文件傳輸, 可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標(biāo) Channel, 避免了傳統(tǒng)通過(guò)循環(huán) write 方式導(dǎo)致的內(nèi)存拷貝問(wèn)題.
下面我們就來(lái)簡(jiǎn)單了解一下這幾種常見(jiàn)的零拷貝操作.
通過(guò) CompositeByteBuf 實(shí)現(xiàn)零拷貝假設(shè)我們有一份協(xié)議數(shù)據(jù), 它由頭部和消息體組成, 而頭部和消息體是分別存放在兩個(gè) ByteBuf 中的, 即:
ByteBuf header = ... ByteBuf body = ...
我們?cè)诖a處理中, 通常希望將 header 和 body 合并為一個(gè) ByteBuf, 方便處理, 那么通常的做法是:
ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes()); allBuf.writeBytes(header); allBuf.writeBytes(body);
可以看到, 我們將 header 和 body 都拷貝到了新的 allBuf 中了, 這無(wú)形中增加了兩次額外的數(shù)據(jù)拷貝操作了.
那么有沒(méi)有更加高效優(yōu)雅的方式實(shí)現(xiàn)相同的目的呢? 我們來(lái)看一下 CompositeByteBuf 是如何實(shí)現(xiàn)這樣的需求的吧.
ByteBuf header = ... ByteBuf body = ... CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); compositeByteBuf.addComponents(true, header, body);
上面代碼中, 我們定義了一個(gè) CompositeByteBuf 對(duì)象, 然后調(diào)用
public CompositeByteBuf addComponents(boolean increaseWriterIndex, ByteBuf... buffers) { ... }
方法將 header 與 body 合并為一個(gè)邏輯上的 ByteBuf, 即:
不過(guò)需要注意的是, 雖然看起來(lái) CompositeByteBuf 是由兩個(gè) ByteBuf 組合而成的, 不過(guò)在 CompositeByteBuf 內(nèi)部, 這兩個(gè) ByteBuf 都是多帶帶存在的, CompositeByteBuf 只是邏輯上是一個(gè)整體.
上面 CompositeByteBuf 代碼還以一個(gè)地方值得注意的是, 我們調(diào)用 addComponents(boolean increaseWriterIndex, ByteBuf... buffers) 來(lái)添加兩個(gè) ByteBuf, 其中第一個(gè)參數(shù)是 true, 表示當(dāng)添加新的 ByteBuf 時(shí), 自動(dòng)遞增 CompositeByteBuf 的 writeIndex.
如果我們調(diào)用的是
compositeByteBuf.addComponents(header, body);
那么其實(shí) compositeByteBuf 的 writeIndex 仍然是0, 因此此時(shí)我們就不可能從 compositeByteBuf 中讀取到數(shù)據(jù), 這一點(diǎn)希望大家要特別注意.
除了上面直接使用 CompositeByteBuf 類(lèi)外, 我們還可以使用 Unpooled.wrappedBuffer 方法, 它底層封裝了 CompositeByteBuf 操作, 因此使用起來(lái)更加方便:
ByteBuf header = ... ByteBuf body = ... ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);通過(guò) wrap 操作實(shí)現(xiàn)零拷貝
例如我們有一個(gè) byte 數(shù)組, 我們希望將它轉(zhuǎn)換為一個(gè) ByteBuf 對(duì)象, 以便于后續(xù)的操作, 那么傳統(tǒng)的做法是將此 byte 數(shù)組拷貝到 ByteBuf 中, 即:
byte[] bytes = ... ByteBuf byteBuf = Unpooled.buffer(); byteBuf.writeBytes(bytes);
顯然這樣的方式也是有一個(gè)額外的拷貝操作的, 我們可以使用 Unpooled 的相關(guān)方法, 包裝這個(gè) byte 數(shù)組, 生成一個(gè)新的 ByteBuf 實(shí)例, 而不需要進(jìn)行拷貝操作. 上面的代碼可以改為:
byte[] bytes = ... ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
可以看到, 我們通過(guò) Unpooled.wrappedBuffer 方法來(lái)將 bytes 包裝成為一個(gè) UnpooledHeapByteBuf 對(duì)象, 而在包裝的過(guò)程中, 是不會(huì)有拷貝操作的. 即最后我們生成的生成的 ByteBuf 對(duì)象是和 bytes 數(shù)組共用了同一個(gè)存儲(chǔ)空間, 對(duì) bytes 的修改也會(huì)反映到 ByteBuf 對(duì)象中.
Unpooled 工具類(lèi)還提供了很多重載的 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)
這些方法可以將一個(gè)或多個(gè) buffer 包裝為一個(gè) ByteBuf 對(duì)象, 從而避免了拷貝操作.
通過(guò) slice 操作實(shí)現(xiàn)零拷貝slice 操作和 wrap 操作剛好相反, Unpooled.wrappedBuffer 可以將多個(gè) ByteBuf 合并為一個(gè), 而 slice 操作可以將一個(gè) ByteBuf 切片 為多個(gè)共享一個(gè)存儲(chǔ)區(qū)域的 ByteBuf 對(duì)象.
ByteBuf 提供了兩個(gè) slice 操作方法:
public ByteBuf slice(); public ByteBuf slice(int index, int length);
不帶參數(shù)的 slice 方法等同于 buf.slice(buf.readerIndex(), buf.readableBytes()) 調(diào)用, 即返回 buf 中可讀部分的切片. 而 slice(int index, int length) 方法相對(duì)就比較靈活了, 我們可以設(shè)置不同的參數(shù)來(lái)獲取到 buf 的不同區(qū)域的切片.
下面的例子展示了 ByteBuf.slice 方法的簡(jiǎn)單用法:
ByteBuf byteBuf = ... ByteBuf header = byteBuf.slice(0, 5); ByteBuf body = byteBuf.slice(5, 10);
用 slice 方法產(chǎn)生 header 和 body 的過(guò)程是沒(méi)有拷貝操作的, header 和 body 對(duì)象在內(nèi)部其實(shí)是共享了 byteBuf 存儲(chǔ)空間的不同部分而已. 即:
通過(guò) FileRegion 實(shí)現(xiàn)零拷貝Netty 中使用 FileRegion 實(shí)現(xiàn)文件傳輸?shù)牧憧截? 不過(guò)在底層 FileRegion 是依賴(lài)于 Java NIO FileChannel.transfer 的零拷貝功能.
首先我們從最基礎(chǔ)的 Java IO 開(kāi)始吧. 假設(shè)我們希望實(shí)現(xiàn)一個(gè)文件拷貝的功能, 那么使用傳統(tǒng)的方式, 我們有如下實(shí)現(xiàn):
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(); }
上面是一個(gè)典型的讀寫(xiě)二進(jìn)制文件的代碼實(shí)現(xiàn)了. 不用我說(shuō), 大家肯定都知道, 上面的代碼中不斷中源文件中讀取定長(zhǎng)數(shù)據(jù)到 temp 數(shù)組中, 然后再將 temp 中的內(nèi)容寫(xiě)入目的文件, 這樣的拷貝操作對(duì)于小文件倒是沒(méi)有太大的影響, 但是如果我們需要拷貝大文件時(shí), 頻繁的內(nèi)存拷貝操作就消耗大量的系統(tǒng)資源了.
下面我們來(lái)看一下使用 Java NIO 的 FileChannel 是如何實(shí)現(xiàn)零拷貝的:
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 后, 我們就可以直接將源文件的內(nèi)容直接拷貝(transferTo) 到目的文件中, 而不需要額外借助一個(gè)臨時(shí) buffer, 避免了不必要的內(nèi)存操作.
有了上面的一些理論知識(shí), 我們來(lái)看一下在 Netty 中是怎么使用 FileRegion 來(lái)實(shí)現(xiàn)零拷貝傳輸一個(gè)文件的:
@Override public void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception { RandomAccessFile raf = null; long length = -1; try { // 1. 通過(guò) RandomAccessFile 打開(kāi)一個(gè)文件. raf = new RandomAccessFile(msg, "r"); length = raf.length(); } catch (Exception e) { ctx.writeAndFlush("ERR: " + e.getClass().getSimpleName() + ": " + e.getMessage() + " "); return; } finally { if (length < 0 && raf != null) { raf.close(); } } ctx.write("OK: " + raf.length() + " "); if (ctx.pipeline().get(SslHandler.class) == null) { // SSL not enabled - can use zero-copy file transfer. // 2. 調(diào)用 raf.getChannel() 獲取一個(gè) FileChannel. // 3. 將 FileChannel 封裝成一個(gè) 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(" "); }
上面的代碼是 Netty 的一個(gè)例子, 其源碼在 netty/example/src/main/java/io/netty/example/file/FileServerHandler.java
可以看到, 第一步是通過(guò) RandomAccessFile 打開(kāi)一個(gè)文件, 然后 Netty 使用了 DefaultFileRegion 來(lái)封裝一個(gè) FileChannel 即:
new DefaultFileRegion(raf.getChannel(), 0, length)
當(dāng)有了 FileRegion 后, 我們就可以直接通過(guò)它將文件的內(nèi)容直接寫(xiě)入 Channel 中, 而不需要像傳統(tǒng)的做法: 拷貝文件內(nèi)容到臨時(shí) buffer, 然后再將 buffer 寫(xiě)入 Channel. 通過(guò)這樣的零拷貝操作, 無(wú)疑對(duì)傳輸大文件很有幫助.
本文由 yongshun 發(fā)表于個(gè)人博客, 采用 署名-相同方式共享 3.0 中國(guó)大陸許可協(xié)議.
Email: yongshun1228@gmail .com
本文標(biāo)題為: 對(duì)于 Netty ByteBuf 的零拷貝(Zero Copy) 的理解
本文鏈接為: https://segmentfault.com/a/1190000007560884
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/69818.html
摘要:維基百科中對(duì)的解釋是零拷貝技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域。維基百科里提到的零拷貝是在硬件和操作系統(tǒng)層面的,而本文主要介紹的是在應(yīng)用層面的優(yōu)化。 維基百科中對(duì) Zero-copy 的解釋是 零拷貝技術(shù)是指計(jì)算機(jī)執(zhí)行操作時(shí),CPU不需要先將數(shù)據(jù)從某處內(nèi)存復(fù)制到另一個(gè)特定區(qū)域。這種技術(shù)通常用于通過(guò)網(wǎng)絡(luò)傳輸文件時(shí)節(jié)省CPU周期和內(nèi)存帶寬。 維基百科里提到...
摘要:系統(tǒng)調(diào)用返回,產(chǎn)生了第四次上下文切換。現(xiàn)在這個(gè)方法不僅減少了上下文切換,而且消除了參與的數(shù)據(jù)拷貝。 上一篇說(shuō)到了 CompositeByteBuf ,這一篇接著上篇的講下去。 FileRegion 讓我們先看一個(gè)Netty官方的example // netty-netty-4.1.16.Finalexamplesrcmainjavaio ettyexamplefileFileServe...
摘要:如果什么事都沒(méi)得做,它也不會(huì)死循環(huán),它會(huì)將線程休眠起來(lái),直到下一個(gè)事件來(lái)了再繼續(xù)干活,這樣的一個(gè)線程稱(chēng)之為線程。而請(qǐng)求處理邏輯既可以使用單獨(dú)的線程池進(jìn)行處理,也可以跟放在讀寫(xiě)線程一塊處理。 Netty到底是什么 從HTTP說(shuō)起 有了Netty,你可以實(shí)現(xiàn)自己的HTTP服務(wù)器,F(xiàn)TP服務(wù)器,UDP服務(wù)器,RPC服務(wù)器,WebSocket服務(wù)器,Redis的Proxy服務(wù)器,MySQL的P...
摘要:使用來(lái)優(yōu)化套接字操作,盡可能消除由的緩沖區(qū)實(shí)現(xiàn)所導(dǎo)致的性能以及內(nèi)存使用率的懲罰,這種優(yōu)化發(fā)生在的核心代碼中,不會(huì)被暴露出來(lái)。當(dāng)前將會(huì)被增加所寫(xiě)入的字節(jié)數(shù)。 ByteBuf是Java NIO ByteBuffer的替代品,是網(wǎng)絡(luò)數(shù)據(jù)基本單位字節(jié)的容器。 ByteBuf的API Netty的數(shù)據(jù)處理API通過(guò)兩個(gè)組件暴漏:抽象類(lèi)ByteBuf和接口ByteBufHolder ByteBuf...
摘要:提供了作為它的字節(jié)容器但是這個(gè)類(lèi)使用起來(lái)過(guò)于復(fù)雜而且也有些繁瑣的的代替品是的的數(shù)據(jù)處理通過(guò)兩個(gè)組件暴露下面是的優(yōu)點(diǎn)它可以被用戶自定義的緩沖區(qū)類(lèi)擴(kuò)展通過(guò)內(nèi)置的復(fù)合緩沖區(qū)類(lèi)型實(shí)現(xiàn)了透明的零拷貝容量可以按需增長(zhǎng)在讀和寫(xiě)這兩種模式之間雀環(huán)不需要調(diào)用 Java NIO 提供了 ByteBuffer 作為它的字節(jié)容器, 但是這個(gè)類(lèi)使用起來(lái)過(guò)于復(fù)雜, 而且也有些繁瑣. Netty 的 ByteBuf...
閱讀 3732·2023-04-25 17:45
閱讀 3439·2021-09-04 16:40
閱讀 1008·2019-08-30 13:54
閱讀 2140·2019-08-29 12:59
閱讀 1409·2019-08-26 12:11
閱讀 3285·2019-08-23 15:17
閱讀 1528·2019-08-23 12:07
閱讀 3890·2019-08-22 18:00