19.1 引言
前一章我們介紹了TCP連接的建立與釋放,現(xiàn)在來介紹使用TCP進行數(shù)據(jù)傳輸?shù)挠嘘P(guān)問題。
一些有關(guān)TCP通信量的研究如[Caceres et al. 1991]發(fā)現(xiàn),如果按照分組數(shù)量計算,約有一半的TCP報文段包含成塊數(shù)據(jù)(如FTP、電子郵件和Usenet新聞),另一半則包含交互數(shù)據(jù)(如Telnet和Rlogin)。如果按字節(jié)計算,則成塊數(shù)據(jù)與交互數(shù)據(jù)的比例約為90%和10%。這是因為成塊數(shù)據(jù)的報文段基本上都是滿長度(full-sized)的(通常為512字節(jié)的用戶數(shù)據(jù)),而交互數(shù)據(jù)則小得多(上述研究表明Telnet和Rlogin分組中通常約90%左右的用戶數(shù)據(jù)小于10個字節(jié))。
很明顯,TCP需要同時處理這兩類數(shù)據(jù),但使用的處理算法則有所不同。本章將以Rlogin應(yīng)用為例來觀察交互數(shù)據(jù)的傳輸過程。將揭示經(jīng)受時延的確認(rèn)是如何工作的以及Nagle算法怎樣減少了通過廣域網(wǎng)絡(luò)傳輸?shù)男》纸M的數(shù)目,這些算法也同樣適用于Telnet應(yīng)用。下一章我們將介紹成塊數(shù)據(jù)的傳輸問題。
19.2 交互式輸入
首先來觀察在一個Rlogin連接上鍵入一個交互命令時所產(chǎn)生的數(shù)據(jù)流。許多TCP/IP的初學(xué)者很吃驚地發(fā)現(xiàn)通常每一個交互按鍵都會產(chǎn)生一個數(shù)據(jù)分組,也就是說,每次從客戶傳到服務(wù)器的是一個字節(jié)的按鍵(而不是每次一行)。而且,Rlogin需要遠(yuǎn)程系統(tǒng)(服務(wù)器)回顯我們(客戶)鍵入的字符。這樣就會產(chǎn)生4個報文段:(1)來自客戶的交互按鍵;(2)來自服務(wù)器的按鍵確認(rèn);(3)來自服務(wù)器的按鍵回顯;(4)來自客戶的按鍵回顯確認(rèn)。圖19-1表示了這個數(shù)據(jù)流。

然而,我們一般可以將報文段2和3進行合并—按鍵確認(rèn)與按鍵回顯一起發(fā)送。下一節(jié)將描述這種合并的技術(shù)(稱為經(jīng)受時延的確認(rèn))。
本章我們特意使用Rlogin作為例子,因為它每次總是從客戶發(fā)送一個字節(jié)到服務(wù)器。在第26章講到Telnet的時候,將會發(fā)現(xiàn)它有一個選項允許客戶發(fā)送一行到服務(wù)器,通過使用這個選項可以減少網(wǎng)絡(luò)的負(fù)載。
圖19-2顯示的是當(dāng)我們鍵入5個字符date\n時的數(shù)據(jù)流(我們沒有顯示連接建立的過程,并且去掉了所有的服務(wù)類型輸出。BSD/386通過設(shè)置一個Rlogin連接的TO S來獲得最小時延)。
第1行客戶發(fā)送字符d到服務(wù)器。第2行是該字符的確認(rèn)及回顯(也就是圖19-1的中間兩部分?jǐn)?shù)據(jù)的合并)。第3行是回顯字符的確認(rèn)。與字符a有關(guān)的是第46行,與字符t有關(guān)的是第79行,第1012行與字符e有關(guān)。第34、67、910和12~13行之間半秒左右的時間差是鍵入兩個字符之間的時延。
注意到13~15行稍有不同。從客戶發(fā)送到服務(wù)器的是一個字符(按下RETURN鍵后產(chǎn)生的UNIX系統(tǒng)中的換行符),而回顯的則是兩個字符。這兩個字符分別是回車和換行字符(CR/LF),它們的作用是將光標(biāo)回移到左邊并移動到下一行。
第16行是來自服務(wù)器的date命令的輸出。這30個字節(jié)由28個字符與最后的CR/LF組成。緊接著從服務(wù)器發(fā)往客戶的7個字符(第18行)是在服務(wù)器主機上的客戶提示符:svr4 %。第19行確認(rèn)了這7個字符。

注意TCP是怎樣進行確認(rèn)的。第1行以序號0發(fā)送數(shù)據(jù)字節(jié),第2行通過將確認(rèn)序號設(shè)為1,也就是最后成功收到的字節(jié)的序號加1,來對其進行確認(rèn)(也就是所謂的下一個期望數(shù)據(jù)的序號)。在第2行中服務(wù)器還向客戶發(fā)送了一序號為1的數(shù)據(jù),客戶在第3行中通過設(shè)置確認(rèn)序號為2來對該數(shù)據(jù)進行確認(rèn)。
19.3 經(jīng)受時延的確認(rèn)
在圖19-2中有一些與本節(jié)將要論及的時間有關(guān)的細(xì)微之處。圖19-3表示了圖19-2中數(shù)據(jù)交換的時間系列(在該時間系列中,去掉了所有的窗口通告,并增加了一個記號來表明正在傳輸何種數(shù)據(jù))。
把從bsdi發(fā)送到srv4的7個ACK標(biāo)記為經(jīng)受時延的ACK。通常TCP在接收到數(shù)據(jù)時并不立即發(fā)送ACK;相反,它推遲發(fā)送,以便將ACK與需要沿該方向發(fā)送的數(shù)據(jù)一起發(fā)送(有時稱這種現(xiàn)象為數(shù)據(jù)捎帶ACK)。絕大多數(shù)實現(xiàn)采用的時延為200 ms,也就是說,TCP將以最大200 ms的時延等待是否有數(shù)據(jù)一起發(fā)送。
如果觀察bsdi接收到數(shù)據(jù)和發(fā)送ACK之間的時間差,就會發(fā)現(xiàn)它們似乎是隨機的:123.5、65.6、109.0、132.2、42.0、140.3和195.8ms。相反,觀察到發(fā)送ACK的實際時間(從0開始)為:139.9、539.3、940.1、1339.9、1739.9、1940.1和2140.1ms(在圖19-3中用星號標(biāo)出)。這些時間之間的差則是200 ms的整數(shù)倍,這里所發(fā)生的情況是因為TCP使用了一個200 ms的定時器,該定時器以相對于內(nèi)核引導(dǎo)的200 ms固定時間溢出。由于將要確認(rèn)的數(shù)據(jù)是隨機到達的(在時刻16.4,474.3,831.1等),TCP在內(nèi)核的200 ms定時器的下一次溢出時得到通知。這有可能是將來1~200 ms中的任何一刻。

如果觀察svr4為產(chǎn)生所收到的每個字符的回顯所使用的時間,則這些時間分別為16.5、16.3、16.5、16.4和17.3ms。由于這個時間小于200 ms,因此我們在另一端從來沒有觀察到一個經(jīng)受時延的ACK。在經(jīng)受時延的定時器溢出前總是有數(shù)據(jù)需要發(fā)送(如果有一個約為16 ms等待時間越過了內(nèi)核的200 ms時鐘滴答的邊界,則仍可以看到一個經(jīng)受時延的ACK。在本例中我們一個也沒有看到)。
在圖18-7中,當(dāng)為檢測超時而使用500 ms的TCP定時器時,我們會看到同樣的情況。這兩個200 ms和500 ms的定時器都在相對于內(nèi)核引導(dǎo)的時間處溢出。不論TCP何時設(shè)置一個定時器,該定時器都可能在將來1~200 ms和1~500 ms的任一處溢出。
Host Requirements RFC聲明TCP需要實現(xiàn)一個經(jīng)受時延的ACK,但時延必須小于500 ms。
19.4 Nagle算法
在前一節(jié)我們看到,在一個Rlogin連接上客戶一般每次發(fā)送一個字節(jié)到服務(wù)器,這就產(chǎn)生了一些41字節(jié)長的分組:20字節(jié)的IP首部、20字節(jié)的TCP首部和1個字節(jié)的數(shù)據(jù)。在局域網(wǎng)上,這些小分組(被稱為微小分組(tinygram))通常不會引起麻煩,因為局域網(wǎng)一般不會出現(xiàn)擁塞。但在廣域網(wǎng)上,這些小分組則會增加擁塞出現(xiàn)的可能。一種簡單和好的方法就是采用RFC 896 [Nagle 1984]中所建議的Nagle算法。
該算法要求一個TCP連接上最多只能有一個未被確認(rèn)的未完成的小分組,在該分組的確認(rèn)到達之前不能發(fā)送其他的小分組。相反,TCP收集這些少量的分組,并在確認(rèn)到來時以一個分組的方式發(fā)出去。該算法的優(yōu)越之處在于它是自適應(yīng)的:確認(rèn)到達得越快,數(shù)據(jù)也就發(fā)送得越快。而在希望減少微小分組數(shù)目的低速廣域網(wǎng)上,則會發(fā)送更少的分組(我們將在22.3節(jié)看到“小”的含義是小于報文段的大?。?。
在圖19-3中可以看到,在以太網(wǎng)上一個字節(jié)被發(fā)送、確認(rèn)和回顯的平均往返時間約為16ms。為了產(chǎn)生比這個速度更快的數(shù)據(jù),我們每秒鍵入的字符必須多于60個。這表明在局域網(wǎng)環(huán)境下兩個主機之間發(fā)送數(shù)據(jù)時很少使用這個算法。
但是,當(dāng)往返時間(RT T)增加時,如通過一個廣域網(wǎng),情況就會發(fā)生變化。看一下在主機slip和主機vangogh.cs.berkeley.edu之間的Rlogin連接工作的情況。為了從我們的網(wǎng)絡(luò)中出去(參看原書封面內(nèi)側(cè)),需要使用兩個SLIP鏈路和Internet。我們希望獲得更長的往返時間。圖19-4顯示了當(dāng)在客戶端快速鍵入字符(像一個快速打字員一樣)時一些數(shù)據(jù)流的時間系列(去掉了服務(wù)類型信息,但保留了窗口通告)。
比較圖19-4與圖19-3,我們首先注意到從slip到vangogh不存在經(jīng)受時延的ACK。這是因為在時延定時器溢出之前總是有數(shù)據(jù)等待發(fā)送。
其次,注意到從左到右待發(fā)數(shù)據(jù)的長度是不同的,分別為:1、1、2、1、2、2、3、1和3個字節(jié)。這是因為客戶只有收到前一個數(shù)據(jù)的確認(rèn)后才發(fā)送已經(jīng)收集的數(shù)據(jù)。通過使用Nagle算法,為發(fā)送16個字節(jié)的數(shù)據(jù)客戶只需要使用9個報文段,而不再是16個。
報文段14和15看起來似乎是與Nagle算法相違背的,但我們需要通過檢查序號來觀察其中的真相。因為確認(rèn)序號是54,因此報文段14是報文段12中確認(rèn)的應(yīng)答。但客戶在發(fā)送該報文段之前,接收到了來自服務(wù)器的報文段13,報文段15中包含了對序號為56的報文段13的確認(rèn)。因此即使我們看到從客戶到服務(wù)器有兩個連續(xù)返回的報文段,客戶也是遵守了Nagle算法的。
在圖19-4中可以看到存在一個經(jīng)受時延的ACK,但該ACK是從服務(wù)器到客戶的(報文段12),因為它不包含任何數(shù)據(jù),因此我們可以假定這是經(jīng)受時延的ACK。服務(wù)器當(dāng)時一定非常忙,因此無法在服務(wù)器的定時器溢出前及時處理所收到的字符。
最后看一下最后兩個報文段中數(shù)據(jù)的數(shù)量以及相應(yīng)的序號。客戶發(fā)送3個字節(jié)的數(shù)據(jù)(18,19和20),然后服務(wù)器確認(rèn)這3個字節(jié)(最后的報文段中的ACK 21),但是只返回了一個字節(jié)(標(biāo)號為59)。這是因為當(dāng)服務(wù)器的TCP一旦正確收到這3個字節(jié)的數(shù)據(jù),就會返回對該數(shù)據(jù)的確認(rèn),但只有當(dāng)Rlogin服務(wù)器發(fā)送回顯數(shù)據(jù)時,它才能夠發(fā)送這些數(shù)據(jù)的回顯。這表明TCP可以在應(yīng)用讀取并處理數(shù)據(jù)前發(fā)送所接收數(shù)據(jù)的確認(rèn)。TCP確認(rèn)僅僅表明TCP已經(jīng)正確接收了數(shù)據(jù)。最后一個報文段的窗口大小為8189而非8192,表明服務(wù)器進程尚未讀取這三個收到的數(shù)據(jù)。

19.4.1 關(guān)閉Nagle算法
有時我們也需要關(guān)閉Nagle算法。一個典型的例子是X窗口系統(tǒng)服務(wù)器(見30.5節(jié)):小消息(鼠標(biāo)移動)必須無時延地發(fā)送,以便為進行某種操作的交互用戶提供實時的反饋。
這里將舉另外一個更容易說明的例子—在一個交互注冊過程中鍵入終端的一個特殊功能鍵。這個功能鍵通常可以產(chǎn)生多個字符序列,經(jīng)常從ASCII碼的轉(zhuǎn)義(escape)字符開始。如果TCP每次得到一個字符,它很可能會發(fā)送序列中的第一個字符(ASCII碼的ESC),然后緩存其他字符并等待對該字符的確認(rèn)。但當(dāng)服務(wù)器接收到該字符后,它并不發(fā)送確認(rèn),而是繼續(xù)等待接收序列中的其他字符。這就會經(jīng)常觸發(fā)服務(wù)器的經(jīng)受時延的確認(rèn)算法,表示剩下的字符沒有在200 ms內(nèi)發(fā)送。對交互用戶而言,這將產(chǎn)生明顯的時延。
插口API用戶可以使用TCP_NODELAY選項來關(guān)閉Nagle算法。
Host Requirements RFC聲明TCP必須實現(xiàn)Nagle算法,但必須為應(yīng)用提供一種方法來關(guān)閉該算法在某個連接上執(zhí)行。
19.4.2 一個例子
可以在Nagle算法和產(chǎn)生多個字符的按鍵之間看到這種交互的情況。在主機slip和主機vangogh.cs.berkeley.edu之間建立一個Rlogin連接,然后按下F1功能鍵,這將產(chǎn)生3個字節(jié):一個escape、一個左括號和一個M。然后再按下F2功能鍵,這將產(chǎn)生另外3個字節(jié)。圖19-5表示的是tcpdump的輸出結(jié)果(我們?nèi)サ袅似渲械姆?wù)類型和窗口通告)。

圖19-6表示了這個交互過程的時間系列。在該圖的下面部分我們給出了從客戶發(fā)送到服務(wù)器的6個字節(jié)和它們的序號以及將要返回的8個字節(jié)的回顯。

當(dāng)rlogin客戶讀取到輸入的第1個字節(jié)并向TCP寫入時,該字節(jié)作為報文段1被發(fā)送。這是F1鍵所產(chǎn)生的3個字節(jié)中的第1個。它的回顯在報文段2中被返回,此時剩余的2個字節(jié)才被發(fā)送(報文段3)。這兩個字節(jié)的回顯在報文段4被接收,而報文段5則是對它們的確認(rèn)。
第1個字節(jié)的回顯為2個字節(jié)(報文段2)的原因是因為在ASCII碼中轉(zhuǎn)義符的回顯是2個字節(jié):插入記號和一個左括號。剩下的兩個輸入字節(jié):一個左括號和一個M,分別以自身作為回顯內(nèi)容。
當(dāng)按下下一個特殊功能鍵(報文段6~10)時,也會發(fā)生同樣的過程。正如我們希望的那樣,在報文段5和10(slip發(fā)送回顯的確認(rèn))之間的時間差是200 ms的整數(shù)倍,因為這兩個ACK被進行時延。
現(xiàn)在我們使用一個修改后關(guān)閉了Nagle算法的rlogin版本重復(fù)同樣的實驗。圖19-7顯示了tcpdump的輸出結(jié)果(同樣去掉了其中的服務(wù)類型和窗口通告)。

在已知某些報文段在網(wǎng)絡(luò)上形成交叉的情況下,以該結(jié)果構(gòu)造時間系列則更具有啟發(fā)性和指導(dǎo)意義。這個例子同樣也需要隨著數(shù)據(jù)流對序號進行仔細(xì)的檢查。在圖19-8中顯示這個結(jié)果。用圖19-7中tcpdump輸出的號碼對報文段進行了相應(yīng)的編號。
我們注意到的第1個變化是當(dāng)3個字節(jié)準(zhǔn)備好時它們?nèi)勘话l(fā)送(報文段1、2和3)。沒有時延發(fā)生—Nagle算法被禁止。
在tcpdump輸出中的下一個分組(報文段4)中帶有來自服務(wù)器的第5個字節(jié)及一個確認(rèn)序號為4的ACK。這是不正確的,因為客戶并不希望接收到第5個字節(jié),因此它立即發(fā)送一個確認(rèn)序號為2而不是6的響應(yīng)(沒有被延遲)。看起來一個報文段丟失了,在圖19-8中我們用虛線表示。
如何知道這個丟失的報文段中包含第2、3和4個字節(jié),且其確認(rèn)序號為3呢?這是因為正如在報文段5中聲明的那樣,我們希望的下一個字節(jié)是第2個字節(jié)(每當(dāng)TCP接收到一個超出期望序號的失序數(shù)據(jù)時,它總是發(fā)送一個確認(rèn)序號為其期望序號的確認(rèn))。也正是因為丟失的分組中包含第2、3和4個字節(jié),表明服務(wù)器必定已經(jīng)接收到報文段2,因此丟失的報文段中的確認(rèn)序號一定為3(服務(wù)器期望接收的下一個字節(jié)號)。最后,注意到重傳的報文段6中包含有丟失的報文段中的數(shù)據(jù)和報文段4,這被稱為重新分組化。我們將在22.11節(jié)對其進行更多的介紹。
現(xiàn)在回到禁止Nagle算法的討論中來??梢杂^察到鍵入的下一個特殊功能鍵所產(chǎn)生的3個字節(jié)分別作為單獨的報文段(報文段8、9和10)被發(fā)送。這一次服務(wù)器首先回顯了報文段8中的字節(jié)(報文段11),然后回顯了報文段9和10中的字節(jié)(報文段12)。
在這個例子中,我們能夠觀察到的是在跨廣域網(wǎng)運行一個交互應(yīng)用的環(huán)境下,當(dāng)進行多字節(jié)的按鍵輸入時,默認(rèn)使用Nagle算法會引起額外的時延。
在第21章我們將進行有關(guān)時延和重傳方面的討論。

19.5 窗口大小通告
在圖19-4中,我們可以觀察到slip通告窗口大小為4096字節(jié),而vangogh通告其窗口大小為8192個字節(jié)。該圖中的大多數(shù)報文段都包含這兩個值中的一個。
然而,報文段5通告的窗口大小為4095個字節(jié),這意味著在TCP的緩沖區(qū)中仍然有一個字節(jié)等待應(yīng)用程序(Rlogin客戶)讀取。同樣,來自客戶的下一個報文段聲明其窗口大小為4094個字節(jié),這說明仍有兩個字節(jié)等待讀取。
服務(wù)器通常通告窗口大小為8192個字節(jié),這是因為服務(wù)器在讀取并回顯接收到的數(shù)據(jù)之前,其TCP沒有數(shù)據(jù)發(fā)送。當(dāng)服務(wù)器已經(jīng)讀取了來自客戶的輸入后,來自服務(wù)器的數(shù)據(jù)將被發(fā)送。
然而,在ACK到來時,客戶的TCP總是有數(shù)據(jù)需要發(fā)送。這是因為它在等待ACK的過程中緩存接收到的字符。當(dāng)客戶TCP發(fā)送緩存的數(shù)據(jù)時,Rlogin客戶沒有機會讀取來自服務(wù)器的數(shù)據(jù),因此,客戶通告的窗口大小總是小于4096。
19.6 小結(jié)
交互數(shù)據(jù)總是以小于最大報文段長度的分組發(fā)送。在Rlogin中通常只有一個字節(jié)從客戶發(fā)送到服務(wù)器。Te lnet允許一次發(fā)送一行輸入數(shù)據(jù),但是目前大多數(shù)實現(xiàn)仍然發(fā)送一個字節(jié)。
對于這些小的報文段,接收方使用經(jīng)受時延的確認(rèn)方法來判斷確認(rèn)是否可被推遲發(fā)送,以便與回送數(shù)據(jù)一起發(fā)送。這樣通常會減少報文段的數(shù)目,尤其是對于需要回顯用戶輸入字符的Rlogin會話。
在較慢的廣域網(wǎng)環(huán)境中,通常使用Nagle算法來減少這些小報文段的數(shù)目。這個算法限制發(fā)送者任何時候只能有一個發(fā)送的小報文段未被確認(rèn)。但我們給出的一個例子也表明有時需要禁止Nagle算法的功能。