TCP/IP協(xié)議中,無論發(fā)送多少數(shù)據(jù),總是要在數(shù)據(jù)前面加上協(xié)議頭,同時(shí),對方接收到數(shù)據(jù),也需要發(fā)送ACK表示確認(rèn)。為了盡可能的利用網(wǎng)絡(luò)帶寬,TCP總是希望盡可能的發(fā)送足夠大的數(shù)據(jù)。(一個(gè)連接會設(shè)置MSS參數(shù),因此,TCP/IP希望每次都能夠以MSS尺寸的數(shù)據(jù)塊來發(fā)送數(shù)據(jù))。Nagle算法就是為了盡可能發(fā)送大塊數(shù)據(jù),避免網(wǎng)絡(luò)中充斥著許多小數(shù)據(jù)塊。
Nagle算法的基本定義是任意時(shí)刻,最多只能有一個(gè)未被確認(rèn)的小段。 所謂“小段”,指的是小于MSS尺寸的數(shù)據(jù)塊,所謂“未被確認(rèn)”,是指一個(gè)數(shù)據(jù)塊發(fā)送出去后,沒有收到對方發(fā)送的ACK確認(rèn)該數(shù)據(jù)已收到。
Nagle算法的規(guī)則(可參考tcp_output.c文件里tcp_nagle_check函數(shù)注釋):
(1)如果包長度達(dá)到MSS,則允許發(fā)送;
(2)如果該包含有FIN,則允許發(fā)送;
(3)設(shè)置了TCP_NODELAY選項(xiàng),則允許發(fā)送;
(4)未設(shè)置TCP_CORK選項(xiàng)時(shí),若所有發(fā)出去的小數(shù)據(jù)包(包長度小于MSS)均被確認(rèn),則允許發(fā)送;
(5)上述條件都未滿足,但發(fā)生了超時(shí)(一般為200ms),則立即發(fā)送。
偽代碼:

Nagle算法只允許一個(gè)未被ACK的包存在于網(wǎng)絡(luò),它并不管包的大小,因此它事實(shí)上就是一個(gè)擴(kuò)展的停-等協(xié)議,只不過它是基于包停-等的,而不是基于字節(jié)停-等的。Nagle算法完全由TCP協(xié)議的ACK機(jī)制決定,這會帶來一些問題,比如如果對端ACK回復(fù)很快的話,Nagle事實(shí)上不會拼接太多的數(shù)據(jù)包,雖然避免了網(wǎng)絡(luò)擁塞,網(wǎng)絡(luò)總體的利用率依然很低。
Nagle算法是silly window syndrome(SWS)預(yù)防算法的一個(gè)半集。SWS算法預(yù)防發(fā)送少量的數(shù)據(jù),Nagle算法是其在發(fā)送方的實(shí)現(xiàn),而接收方要做的是不要通告緩沖空間的很小增長,不通知小窗口,除非緩沖區(qū)空間有顯著的增長。這里顯著的增長定義為完全大小的段(MSS)或增長到大于最大窗口的一半。
注意:BSD的實(shí)現(xiàn)是允許在空閑鏈接上發(fā)送大的寫操作剩下的最后的小段,也就是說,當(dāng)超過1個(gè)MSS數(shù)據(jù)發(fā)送時(shí),內(nèi)核先依次發(fā)送完n個(gè)MSS的數(shù)據(jù)包,然后再發(fā)送尾部的小數(shù)據(jù)包,其間不再延時(shí)等待。(假設(shè)網(wǎng)絡(luò)不阻塞且接收窗口足夠大)
舉個(gè)例子,client端調(diào)用socket的write操作將一個(gè)int型數(shù)據(jù)(稱為A塊)寫入到網(wǎng)絡(luò)中,由于此時(shí)連接是空閑的(也就是說還沒有未被確認(rèn)的小段),因此這個(gè)int型數(shù)據(jù)會被馬上發(fā)送到server端,接著,client端又調(diào)用write操作寫入‘\r\n’(簡稱B塊),這個(gè)時(shí)候,A塊的ACK沒有返回,所以可以認(rèn)為已經(jīng)存在了一個(gè)未被確認(rèn)的小段,所以B塊沒有立即被發(fā)送,一直等待A塊的ACK收到(大概40ms之后),B塊才被發(fā)送。
這里還隱藏了一個(gè)問題,就是A塊數(shù)據(jù)的ACK為什么40ms之后才收到?這是因?yàn)門CP/IP中不僅僅有nagle算法,還有一個(gè)TCP確認(rèn)延遲機(jī)制。當(dāng)Server端收到數(shù)據(jù)之后,它并不會馬上向client端發(fā)送ACK,而是會將ACK的發(fā)送延遲一段時(shí)間(假設(shè)為t),它希望在t時(shí)間內(nèi)server端會向client端發(fā)送應(yīng)答數(shù)據(jù),這樣ACK就能夠和應(yīng)答數(shù)據(jù)一起發(fā)送,就像是應(yīng)答數(shù)據(jù)捎帶著ACK過去。在我之前的時(shí)間中,t大概就是40ms。這就解釋了為什么'\r\n'(B塊)總是在A塊之后40ms才發(fā)出。
當(dāng)然,TCP確認(rèn)延遲40ms并不是一直不變的,TCP連接的延遲確認(rèn)時(shí)間一般初始化為最小值40ms,隨后根據(jù)連接的重傳超時(shí)時(shí)間(RTO)、上次收到數(shù)據(jù)包與本次接收數(shù)據(jù)包的時(shí)間間隔等參數(shù)進(jìn)行不斷調(diào)整。另外可以通過設(shè)置TCP_QUICKACK選項(xiàng)來取消確認(rèn)延遲。
2. TCP_NODELAY 選項(xiàng)
默認(rèn)情況下,發(fā)送數(shù)據(jù)采用Nagle 算法。這樣雖然提高了網(wǎng)絡(luò)吞吐量,但是實(shí)時(shí)性卻降低了,在一些交互性很強(qiáng)的應(yīng)用程序來說是不允許的,使用TCP_NODELAY選項(xiàng)可以禁止Nagle 算法。
此時(shí),應(yīng)用程序向內(nèi)核遞交的每個(gè)數(shù)據(jù)包都會立即發(fā)送出去。需要注意的是,雖然禁止了Nagle 算法,但網(wǎng)絡(luò)的傳輸仍然受到TCP確認(rèn)延遲機(jī)制的影響。
3. TCP_CORK 選項(xiàng)
所謂的CORK就是塞子的意思,形象地理解就是用CORK將連接塞住,使得數(shù)據(jù)先不發(fā)出去,等到拔去塞子后再發(fā)出去。設(shè)置該選項(xiàng)后,內(nèi)核會盡力把小數(shù)據(jù)包拼接成一個(gè)大的數(shù)據(jù)包(一個(gè)MTU)再發(fā)送出去,當(dāng)然若一定時(shí)間后(一般為200ms,該值尚待確認(rèn)),內(nèi)核仍然沒有組合成一個(gè)MTU時(shí)也必須發(fā)送現(xiàn)有的數(shù)據(jù)(不可能讓數(shù)據(jù)一直等待吧)。
然而,TCP_CORK的實(shí)現(xiàn)可能并不像你想象的那么完美,CORK并不會將連接完全塞住。內(nèi)核其實(shí)并不知道應(yīng)用層到底什么時(shí)候會發(fā)送第二批數(shù)據(jù)用于和第一批數(shù)據(jù)拼接以達(dá)到MTU的大小,因此內(nèi)核會給出一個(gè)時(shí)間限制,在該時(shí)間內(nèi)沒有拼接成一個(gè)大包(努力接近MTU)的話,內(nèi)核就會無條件發(fā)送。也就是說若應(yīng)用層程序發(fā)送小包數(shù)據(jù)的間隔不夠短時(shí),TCP_CORK就沒有一點(diǎn)作用,反而失去了數(shù)據(jù)的實(shí)時(shí)性(每個(gè)小包數(shù)據(jù)都會延時(shí)一定時(shí)間再發(fā)送)。
4. Nagle算法與CORK算法區(qū)別
Nagle算法和CORK算法非常類似,但是它們的著眼點(diǎn)不一樣,Nagle算法主要避免網(wǎng)絡(luò)因?yàn)樘嗟男“▍f(xié)議頭的比例非常之大)而擁塞,而CORK算法則是為了提高網(wǎng)絡(luò)的利用率,使得總體上協(xié)議頭占用的比例盡可能的小。如此看來這二者在避免發(fā)送小包上是一致的,在用戶控制的層面上,Nagle算法完全不受用戶socket的控制,你只能簡單的設(shè)置TCP_NODELAY而禁用它,CORK算法同樣也是通過設(shè)置或者清除TCP_CORK使能或者禁用之,然而Nagle算法關(guān)心的是網(wǎng)絡(luò)擁塞問題,只要所有的ACK回來則發(fā)包,而CORK算法卻可以關(guān)心內(nèi)容,在前后數(shù)據(jù)包發(fā)送間隔很短的前提下(很重要,否則內(nèi)核會幫你將分散的包發(fā)出),即使你是分散發(fā)送多個(gè)小數(shù)據(jù)包,你也可以通過使能CORK算法將這些內(nèi)容拼接在一個(gè)包內(nèi),如果此時(shí)用Nagle算法的話,則可能做不到這一點(diǎn)。