你知道Netty的零拷貝機(jī)制原理嗎译断?這次帶你徹底搞懂!

前言

理解零拷貝或悲,零拷貝是Netty的重要特性之一孙咪,而究竟什么是零拷貝呢堪唐?WIKI中對(duì)其有如下定義:

“Zero-copy” describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.

從WIKI的定義中,我們看到“零拷貝”是指計(jì)算機(jī)操作的過程中翎蹈,CPU不需要為數(shù)據(jù)在內(nèi)存之間的拷貝消耗資源淮菠。而它通常是指計(jì)算機(jī)在網(wǎng)絡(luò)上發(fā)送文件時(shí),不需要將文件內(nèi)容拷貝到用戶空間(User Space)而直接在內(nèi)核空間(Kernel Space)中傳輸?shù)骄W(wǎng)絡(luò)的方式荤堪。

Non-Zero Copy方式:

image.png

Zero Copy方式:

image

從上圖中可以清楚的看到合陵,Zero Copy的模式中,避免了數(shù)據(jù)在用戶空間和內(nèi)存空間之間的拷貝澄阳,從而提高了系統(tǒng)的整體性能拥知。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都實(shí)現(xiàn)了零拷貝的功能,而在Netty中也通過在FileRegion中包裝了NIO的FileChannel.transferTo()方法實(shí)現(xiàn)了零拷貝碎赢。

而在Netty中還有另一種形式的零拷貝低剔,即Netty允許我們將多段數(shù)據(jù)合并為一整段虛擬數(shù)據(jù)供用戶使用,而過程中不需要對(duì)數(shù)據(jù)進(jìn)行拷貝操作肮塞,這也是我們今天要講的重點(diǎn)户侥。我們都知道在stream-based transport(如TCP/IP)的傳輸過程中,數(shù)據(jù)包有可能會(huì)被重新封裝在不同的數(shù)據(jù)包中峦嗤,例如當(dāng)你發(fā)送如下數(shù)據(jù)時(shí):

image.png

有可能實(shí)際收到的數(shù)據(jù)如下:

image.png

因此在實(shí)際應(yīng)用中蕊唐,很有可能一條完整的消息被分割為多個(gè)數(shù)據(jù)包進(jìn)行網(wǎng)絡(luò)傳輸,而單個(gè)的數(shù)據(jù)包對(duì)你而言是沒有意義的烁设,只有當(dāng)這些數(shù)據(jù)包組成一條完整的消息時(shí)你才能做出正確的處理替梨,而Netty可以通過零拷貝的方式將這些數(shù)據(jù)包組合成一條完整的消息供你來使用。而此時(shí)装黑,零拷貝的作用范圍僅在用戶空間中副瀑。

image.png

以Netty 3.8.0.Final的源代碼來進(jìn)行說明 ###ChannelBuffer接口 Netty為需要傳輸?shù)臄?shù)據(jù)制定了統(tǒng)一的ChannelBuffer接口。該接口的主要設(shè)計(jì)思路如下:

1.使用getByte(int index)方法來實(shí)現(xiàn)隨機(jī)訪問

2.使用雙指針的方式實(shí)現(xiàn)順序訪問

每個(gè)Buffer都有一個(gè)讀指針(readIndex)和寫指針(writeIndex)

在讀取數(shù)據(jù)時(shí)讀指針后移恋谭,在寫入數(shù)據(jù)時(shí)寫指針后移

image.png

定義了統(tǒng)一的接口之后糠睡,就是來做各種實(shí)現(xiàn)了。Netty主要實(shí)現(xiàn)了HeapChannelBuffer,ByteBufferBackedChannelBuffer等等疚颊,下面我們就來講講與Zero Copy直接相關(guān)的CompositeChannelBuffer類狈孔。###CompositeChannelBuffer類 CompositeChannelBuffer類的作用是將多個(gè)ChannelBuffer組成一個(gè)虛擬的ChannelBuffer來進(jìn)行操作材义。

為什么說是虛擬的呢均抽,因?yàn)镃ompositeChannelBuffer并沒有將多個(gè)ChannelBuffer真正的組合起來,而只是保存了他們的引用其掂,這樣就避免了數(shù)據(jù)的拷貝堤魁,實(shí)現(xiàn)了Zero Copy刽沾。下面我們來看看具體的代碼實(shí)現(xiàn)排拷,首先是成員變量

private int readerIndex; private int writerIndex; private ChannelBuffer[] components; private int[] indices; private int lastAccessedComponentId;

以上這里列出了幾個(gè)比較重要的成員變量侧漓。其中readerIndex既讀指針和writerIndex既寫指針是從AbstractChannelBuffer繼承而來的;然后components是一個(gè)ChannelBuffer的數(shù)組监氢,他保存了組成這個(gè)虛擬Buffer的所有子Buffer布蔗,indices是一個(gè)int類型的數(shù)組,它保存的是各個(gè)Buffer的索引值浪腐;最后的lastAccessedComponentId是一個(gè)int值纵揍,它記錄了最后一次訪問時(shí)的子Buffer ID。

從這個(gè)數(shù)據(jù)結(jié)構(gòu)议街,我們不難發(fā)現(xiàn)所謂的CompositeChannelBuffer實(shí)際上就是將一系列的Buffer通過數(shù)組保存起來泽谨,然后實(shí)現(xiàn)了ChannelBuffer 的接口,使得在上層看來特漩,操作這些Buffer就像是操作一個(gè)單獨(dú)的Buffer一樣吧雹。

創(chuàng)建 接下來,我們?cè)倏匆幌翪ompositeChannelBuffer.setComponents方法拾稳,它會(huì)在初始化

CompositeChannelBuffer時(shí)被調(diào)用吮炕。
/**
 * Setup this ChannelBuffer from the list
 */
private void setComponents(List<ChannelBuffer> newComponents) {
    assert !newComponents.isEmpty();

    // Clear the cache.
    lastAccessedComponentId = 0;

    // Build the component array.
    components = new ChannelBuffer[newComponents.size()];
    for (int i = 0; i < components.length; i ++) {
        ChannelBuffer c = newComponents.get(i);
        if (c.order() != order()) {
            throw new IllegalArgumentException(
                    "All buffers must have the same endianness.");
        }

        assert c.readerIndex() == 0;
        assert c.writerIndex() == c.capacity();

        components[i] = c;
    }

    // Build the component lookup table.
    indices = new int[components.length + 1];
    indices[0] = 0;
    for (int i = 1; i <= components.length; i ++) {
        indices[i] = indices[i - 1] + components[i - 1].capacity();
    }

    // Reset the indexes.
    setIndex(0, capacity());
}

通過代碼可以看到該方法的功能就是將一個(gè)ChannelBuffer的List給組合起來。它首先將List中得元素放入到components數(shù)組中访得,然后創(chuàng)建indices用于數(shù)據(jù)的查找,最后使用setIndex來重置指針。這里需要注意的是setIndex(0, capacity())會(huì)將讀指針設(shè)置為0悍抑,寫指針設(shè)置為當(dāng)前Buffer的長度鳄炉,這也就是前面需要做assert c.readerIndex() == 0和assert c.writerIndex() == c.capacity()這兩個(gè)判斷的原因,否則很容易會(huì)造成數(shù)據(jù)重復(fù)讀寫的問題搜骡。

所以Netty推薦我們使用ChannelBuffers.wrappedBuffer方法來進(jìn)行Buffer的合并拂盯,因?yàn)樵谠摲椒ㄖ蠳etty會(huì)通過slice()方法來確保構(gòu)建CompositeChannelBuffer是傳入的所有子Buffer都是符合要求的。

數(shù)據(jù)訪問 CompositeChannelBuffer.getByte(int index)的實(shí)現(xiàn)如下:

public byte getByte(int index) {
    int componentId = componentId(index);
    return components[componentId].getByte(index - indices[componentId]);
}
`

從代碼我們可以看到记靡,在隨機(jī)查找時(shí)會(huì)首先通過index獲取這個(gè)字節(jié)所在的componentId既字節(jié)所在的子Buffer序列谈竿,然后通過index - indices[componentId]計(jì)算出它在這個(gè)子Buffer中的第幾個(gè)字節(jié),然后返回結(jié)果摸吠。

下面再來看一下componentId(int index) 的實(shí)現(xiàn):

private int componentId(int index) {
    int lastComponentId = lastAccessedComponentId;
    if (index >= indices[lastComponentId]) {
        if (index < indices[lastComponentId + 1]) {
            return lastComponentId;
        }

        // Search right
        for (int i = lastComponentId + 1; i < components.length; i ++) {
            if (index < indices[i + 1]) {
                lastAccessedComponentId = i;
                return i;
            }
        }
    } else {
        // Search left
        for (int i = lastComponentId - 1; i >= 0; i --) {
            if (index >= indices[i]) {
                lastAccessedComponentId = i;
                return i;
            }
        }
    }

    throw new IndexOutOfBoundsException("Invalid index: " + index + ", maximum: " + indices.length);
}

從代碼中我們發(fā)現(xiàn)空凸,Netty以lastComponentId既上次訪問的子Buffer序號(hào)為中心,向左右兩邊進(jìn)行搜索寸痢,這樣做的目的是呀洲,當(dāng)我們兩次隨機(jī)查找的字符序列相近時(shí)(大部分情況下都是這樣),可以最快的搜索到目標(biāo)索引的componentId啼止。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末道逗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子献烦,更是在濱河造成了極大的恐慌滓窍,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件巩那,死亡現(xiàn)場離奇詭異吏夯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)拢操,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門锦亦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人令境,你說我怎么就攤上這事杠园。” “怎么了舔庶?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵抛蚁,是天一觀的道長。 經(jīng)常有香客問我惕橙,道長瞧甩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任弥鹦,我火速辦了婚禮肚逸,結(jié)果婚禮上爷辙,老公的妹妹穿的比我還像新娘。我一直安慰自己朦促,他們只是感情好膝晾,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著务冕,像睡著了一般血当。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上禀忆,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天臊旭,我揣著相機(jī)與錄音,去河邊找鬼箩退。 笑死离熏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的乏德。 我是一名探鬼主播撤奸,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼喊括!你這毒婦竟也來了胧瓜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤郑什,失蹤者是張志新(化名)和其女友劉穎府喳,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蘑拯,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钝满,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了申窘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弯蚜。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖剃法,靈堂內(nèi)的尸體忽然破棺而出碎捺,到底是詐尸還是另有隱情,我是刑警寧澤贷洲,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布收厨,位于F島的核電站,受9級(jí)特大地震影響优构,放射性物質(zhì)發(fā)生泄漏诵叁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一钦椭、第九天 我趴在偏房一處隱蔽的房頂上張望拧额。 院中可真熱鬧碑诉,春花似錦、人聲如沸势腮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捎拯。三九已至,卻和暖如春盲厌,著一層夾襖步出監(jiān)牢的瞬間署照,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國打工吗浩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留建芙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓懂扼,卻偏偏與公主長得像禁荸,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子阀湿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359