netty如何實現(xiàn)零拷貝

根據(jù) Wiki 對 Zero-copy 的定義:



即所謂的 Zero-copy, 就是在操作數(shù)據(jù)時, 不需要將數(shù)據(jù) buffer 從一個內(nèi)存區(qū)域拷貝到另一個內(nèi)存區(qū)域. 因為少了一次內(nèi)存的拷貝, 因此 CPU 的效率就得到的提升.

在 OS 層面上的 Zero-copy通常指避免在 用戶態(tài)(User-space)與 內(nèi)核態(tài)(Kernel-space)之間來回拷貝數(shù)據(jù). 例如 Linux 提供的 mmap系統(tǒng)調(diào)用, 它可以將一段用戶空間內(nèi)存映射到內(nèi)核空間, 當映射成功后, 用戶對這段內(nèi)存區(qū)域的修改可以直接反映到內(nèi)核空間; 同樣地, 內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間. 正因為有這樣的映射關(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)在如下幾個個方面:

Netty 提供了 CompositeByteBuf類, 它可以將多個 ByteBuf 合并為一個邏輯上的 ByteBuf, 避免了各個 ByteBuf 之間的拷貝.

通過 wrap 操作, 我們可以將 byte[] 數(shù)組酵颁、ByteBuf、ByteBuffer等包裝成一個 Netty ByteBuf 對象, 進而避免了拷貝操作.

ByteBuf 支持 slice 操作, 因此可以將 ByteBuf 分解為多個共享同一個存儲區(qū)域的 ByteBuf, 避免了內(nèi)存的拷貝.

通過 FileRegion包裝的FileChannel.tranferTo實現(xiàn)文件傳輸, 可以直接將文件緩沖區(qū)的數(shù)據(jù)發(fā)送到目標 Channel, 避免了傳統(tǒng)通過循環(huán) write 方式導(dǎo)致的內(nèi)存拷貝問題.

下面我們就來簡單了解一下這幾種常見的零拷貝操作.

通過 CompositeByteBuf 實現(xiàn)零拷貝
假設(shè)我們有一份協(xié)議數(shù)據(jù), 它由頭部和消息體組成, 而頭部和消息體是分別存放在兩個 ByteBuf 中的, 即:

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

我們在代碼處理中, 通常希望將 header 和 body 合并為一個 ByteBuf, 方便處理, 那么通常的做法是:

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

可以看到, 我們將 header 和 body 都拷貝到了新的 allBuf 中了, 這無形中增加了兩次額外的數(shù)據(jù)拷貝操作了.
那么有沒有更加高效優(yōu)雅的方式實現(xiàn)相同的目的呢? 我們來看一下 CompositeByteBuf是如何實現(xiàn)這樣的需求的吧.

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

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

上面代碼中, 我們定義了一個 CompositeByteBuf對象, 然后調(diào)用

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

方法將 headerbody合并為一個邏輯上的 ByteBuf, 即:

image

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

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

compositeByteBuf.addComponents(header, body);

那么其實 compositeByteBufwriteIndex仍然是0, 因此此時我們就不可能從 compositeByteBuf中讀取到數(shù)據(jù), 這一點希望大家要特別注意.

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

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

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

通過 wrap 操作實現(xiàn)零拷貝

例如我們有一個 byte 數(shù)組, 我們希望將它轉(zhuǎn)換為一個 ByteBuf 對象, 以便于后續(xù)的操作, 那么傳統(tǒng)的做法是將此 byte 數(shù)組拷貝到 ByteBuf 中, 即:

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

顯然這樣的方式也是有一個額外的拷貝操作的, 我們可以使用 Unpooled 的相關(guān)方法, 包裝這個 byte 數(shù)組, 生成一個新的 ByteBuf 實例, 而不需要進行拷貝操作. 上面的代碼可以改為:

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

可以看到, 我們通過 Unpooled.wrappedBuffer方法來將 bytes 包裝成為一個 UnpooledHeapByteBuf 對象, 而在包裝的過程中, 是不會有拷貝操作的. 即最后我們生成的生成的 ByteBuf 對象是和 bytes 數(shù)組共用了同一個存儲空間, 對 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 操作實現(xiàn)零拷貝

slice 操作和 wrap 操作剛好相反, Unpooled.wrappedBuffer可以將多個 ByteBuf 合并為一個, 而 slice 操作可以將一個 ByteBuf 切片為多個共享一個存儲區(qū)域的 ByteBuf 對象.
ByteBuf 提供了兩個 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)方法相對就比較靈活了, 我們可以設(shè)置不同的參數(shù)來獲取到 buf 的不同區(qū)域的切片.

下面的例子展示了 ByteBuf.slice方法的簡單用法:

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

slice方法產(chǎn)生 header 和 body 的過程是沒有拷貝操作的, header 和 body 對象在內(nèi)部其實是共享了 byteBuf 存儲空間的不同部分而已. 即:

image

通過 FileRegion 實現(xiàn)零拷貝

Netty 中使用 FileRegion 實現(xiàn)文件傳輸?shù)牧憧截? 不過在底層 FileRegion 是依賴于 Java NIO FileChannel.transfer的零拷貝功能.

首先我們從最基礎(chǔ)的 Java IO 開始吧. 假設(shè)我們希望實現(xiàn)一個文件拷貝的功能, 那么使用傳統(tǒng)的方式, 我們有如下實現(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();
}

上面是一個典型的讀寫二進制文件的代碼實現(xiàn)了. 不用我說, 大家肯定都知道, 上面的代碼中不斷中源文件中讀取定長數(shù)據(jù)到 temp 數(shù)組中, 然后再將 temp 中的內(nèi)容寫入目的文件, 這樣的拷貝操作對于小文件倒是沒有太大的影響, 但是如果我們需要拷貝大文件時, 頻繁的內(nèi)存拷貝操作就消耗大量的系統(tǒng)資源了.
下面我們來看一下使用 Java NIO 的 FileChannel是如何實現(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) 到目的文件中, 而不需要額外借助一個臨時 buffer, 避免了不必要的內(nèi)存操作.

有了上面的一些理論知識, 我們來看一下在 Netty 中是怎么使用 FileRegion來實現(xiàn)零拷貝傳輸一個文件的:

@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. 調(diào)用 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`即:

<pre class="hljs cpp" style="margin: 0px; padding: 0px; white-space: pre-wrap; word-wrap: break-word;">`new DefaultFileRegion(raf.getChannel(), 0, length)` </pre>

當有了 FileRegion 后, 我們就可以直接通過它將文件的內(nèi)容直接寫入 Channel 中, 而不需要像傳統(tǒng)的做法: 拷貝文件內(nèi)容到臨時 buffer, 然后再將 buffer 寫入 Channel. 通過這樣的零拷貝操作, 無疑對傳輸大文件很有幫助.

轉(zhuǎn)自:http://www.reibang.com/p/1d1fa2fe1ed9

* * *

*歡迎加群交流拯啦,QQ群:66728073斑芜,197321069肿仑,398808948

雪奈爾藍牙鍵盤湿硝,點擊這里查看購買

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末犀斋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宋光,更是在濱河造成了極大的恐慌貌矿,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,294評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跃须,死亡現(xiàn)場離奇詭異站叼,居然都是意外死亡娃兽,警方通過查閱死者的電腦和手機菇民,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,493評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來投储,“玉大人第练,你說我怎么就攤上這事÷贶瘢” “怎么了娇掏?”我有些...
    開封第一講書人閱讀 157,790評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長勋眯。 經(jīng)常有香客問我婴梧,道長,這世上最難降的妖魔是什么客蹋? 我笑而不...
    開封第一講書人閱讀 56,595評論 1 284
  • 正文 為了忘掉前任塞蹭,我火速辦了婚禮,結(jié)果婚禮上讶坯,老公的妹妹穿的比我還像新娘番电。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 65,718評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般授段。 火紅的嫁衣襯著肌膚如雪在讶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,906評論 1 290
  • 那天浊服,我揣著相機與錄音,去河邊找鬼。 笑死率碾,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的屋彪。 我是一名探鬼主播所宰,決...
    沈念sama閱讀 39,053評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼畜挥!你這毒婦竟也來了仔粥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,797評論 0 268
  • 序言:老撾萬榮一對情侶失蹤蟹但,失蹤者是張志新(化名)和其女友劉穎躯泰,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體华糖,經(jīng)...
    沈念sama閱讀 44,250評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡麦向,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,570評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了客叉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片诵竭。...
    茶點故事閱讀 38,711評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖兼搏,靈堂內(nèi)的尸體忽然破棺而出卵慰,到底是詐尸還是另有隱情,我是刑警寧澤佛呻,帶...
    沈念sama閱讀 34,388評論 4 332
  • 正文 年R本政府宣布裳朋,位于F島的核電站,受9級特大地震影響吓著,放射性物質(zhì)發(fā)生泄漏鲤嫡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,018評論 3 316
  • 文/蒙蒙 一绑莺、第九天 我趴在偏房一處隱蔽的房頂上張望暖眼。 院中可真熱鬧,春花似錦紊撕、人聲如沸罢荡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,796評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽区赵。三九已至惭缰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間笼才,已是汗流浹背漱受。 一陣腳步聲響...
    開封第一講書人閱讀 32,023評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留骡送,地道東北人昂羡。 一個月前我還...
    沈念sama閱讀 46,461評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像摔踱,于是被迫代替她去往敵國和親虐先。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,595評論 2 350