18.1 引言
TCP是一個面向連接的協(xié)議。無論哪一方向另一方發(fā)送數(shù)據(jù)之前,都必須先在雙方之間建立一條連接。本章將詳細(xì)討論一個TCP連接是如何建立的以及通信結(jié)束后是如何終止的。
這種兩端間連接的建立與無連接協(xié)議如UDP不同。我們在第11章看到一端使用UDP向另一端發(fā)送數(shù)據(jù)報時,無需任何預(yù)先的握手。
18.2 連接的建立與終止
為了了解一個TCP連接在建立及終止時發(fā)生了什么,我們在系統(tǒng)svr4上鍵入下列命令:

telnet命令在與丟棄(discard)服務(wù)(參見1.12節(jié))對應(yīng)的端口上與主機bsdi建立一條TCP連接。這服務(wù)類型正是我們需要觀察的一條連接建立與終止的服務(wù)類型,而不需要服務(wù)器發(fā)起任何數(shù)據(jù)交換。
18.2.1tcpdump的輸出
圖18-1顯示了這條命令產(chǎn)生TCP報文段的tcpdump輸出。

這7個TCP報文段僅包含TCP首部。沒有任何數(shù)據(jù)。
對于TCP段,每個輸出行開始按如下格式顯示:
源>目的:標(biāo)志
這里的標(biāo)志代表TCP首部(圖17-2)中6個標(biāo)志比特中的4個。圖18-2顯示了表示標(biāo)志的5個字符的含義。

在這個例子中,我們看到了S、F和句點“.”標(biāo)志符。我們將在以后看到其他的兩個標(biāo)志(R和P)。TCP首部中的其他兩個標(biāo)志比特—ACK和URG—tcpdump將作特殊顯示。
圖18-2所示的4個標(biāo)志比特中的多個可能同時出現(xiàn)在一個報文段中,但通常一次只見到一個。
RFC 1025[Postel 1987],“TCP and IP Bake Off”,將一種報文段稱為Kamikaze分組Θ,在這樣的報文段中有最大數(shù)量的標(biāo)志比特同時被置為1(SYN,URG,PSH,FIN和1字節(jié)的數(shù)據(jù))。這樣的報文段也叫作nastygram,圣誕樹分組,燈測試報文段(lamp test segment)。
在第1行中,字段1415531521:1415531521(0)表示分組的序號是1415531521,而報文段中數(shù)據(jù)字節(jié)數(shù)為0。tcpdump顯示這個字段的格式是開始的序號、一個冒號、隱含的結(jié)尾序號及圓括號內(nèi)的數(shù)據(jù)字節(jié)數(shù)。顯示序號和隱含結(jié)尾序號的優(yōu)點是便于了解數(shù)據(jù)字節(jié)數(shù)大于0時的隱含結(jié)尾序號。這個字段只有在滿足條件(1)報文段中至少包含一個數(shù)據(jù)字節(jié);或者(2)SYN、FIN或RST被設(shè)置為1時才顯示。圖18-1中的第1、2、4和6行是因為標(biāo)志比特被置為1而顯示這個字段的,在這個例子中通信雙方?jīng)]有交換任何數(shù)據(jù)。
在第2行中,字段ack1415531522表示確認(rèn)序號。它只有在首部中的ACK標(biāo)志比特被設(shè)置1時才顯示。
每行顯示的字段win4096表示發(fā)端通告的窗口大小。在這些例子中,我們沒有交換任何數(shù)據(jù),窗口大小就維持默認(rèn)情況下的4096(我們將在20.4節(jié)中討論TCP窗口大?。?。
圖18-1中的最后一個字段<mss1024>表示由發(fā)端指明的最大報文段長度選項。發(fā)端將不接收超過這個長度的TCP報文段。這通常是為了避免分段(見11.5節(jié))。我們將在18.4節(jié)討論最大報文段長度,而在18.10節(jié)介紹不同TCP選項的格式。
18.2.2 時間系列
圖18-3顯示了這些分組序列的時間系列(在圖6-11中已經(jīng)首次介紹了這些時間系列的一些基本特性)。這個圖顯示出哪一端正在發(fā)送分組。我們也將對tcpdump輸出作一些擴展(例如,印出SYN而不是S)。在這個時間系列中也省略窗口大小的值,因為它和我們的討論無關(guān)。
18.2.3 建立連接協(xié)議
現(xiàn)在讓我們回到圖18-3所示的TCP協(xié)議中來。為了建立一條TCP連接:
Θ Kamikaze是神風(fēng)隊隊員或神風(fēng)隊所使用的飛機。在第二次世界大戰(zhàn)末期,日本空軍的神風(fēng)隊隊員駕駛滿載炸彈的飛機去撞擊轟炸目標(biāo),企圖與之同歸于盡。
1:請求端(通常稱為客戶)發(fā)送一個SYN段指明客戶打算連接的服務(wù)器的端口,以及初始序號(ISN,在這個例子中為1415531521)。這個SYN段為報文段1。
2:服務(wù)器發(fā)回包含服務(wù)器的初始序號的SYN報文段(報文段2)作為應(yīng)答。同時,將確認(rèn)序號設(shè)置為客戶的ISN加1以對客戶的SYN報文段進(jìn)行確認(rèn)。一個SYN將占用一個序號。
3:客戶必須將確認(rèn)序號設(shè)置為服務(wù)器的ISN加1以對服務(wù)器的SYN報文段進(jìn)行確認(rèn)(報文段3)。

發(fā)送第一個SYN的一端將執(zhí)行主動打開(active open)。接收這個SYN并發(fā)回下一個SYN的另一端執(zhí)行被動打開(passive open)(在18.8節(jié)我們將介紹雙方如何都執(zhí)行主動打開)。
當(dāng)一端為建立連接而發(fā)送它的SYN時,它為連接選擇一個初始序號。ISN隨時間而變化,因此每個連接都將具有不同的ISN。RFC 793[Postel 1981c]指出ISN可看作是一個32比特的計數(shù)器,每4ms加1。這樣選擇序號的目的在于防止在網(wǎng)絡(luò)中被延遲的分組在以后又被傳送,而導(dǎo)致某個連接的一方對它作錯誤的解釋。
如何進(jìn)行序號選擇?在4.4BSD(和多數(shù)的伯克利的實現(xiàn)版)中,系統(tǒng)初始化時初始的發(fā)送序號被初始化為1。這種方法違背了Host Requirements RFC(在這個代碼中的一個注釋確認(rèn)這是一個錯誤)。這個變量每0.5秒增加64000,并每隔9.5小時又回到0(對應(yīng)這個計數(shù)器每8 ms加1,而不是每4 ms加1)。另外,每次建立一個連接后,這個變量將增加64000。
報文段3與報文段4之間4.1秒的時間間隔是建立TCP連接到向telnet鍵入quit命令來中止該連接的時間。
18.2.4 連接終止協(xié)議
建立一個連接需要三次握手,而終止一個連接要經(jīng)過4次握手。這由TCP的半關(guān)閉(halfclose)造成的。既然一個TCP連接是全雙工(即數(shù)據(jù)在兩個方向上能同時傳遞),因此每個方向必須單獨地進(jìn)行關(guān)閉。這原則就是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個FIN來終止這個方向連接。當(dāng)一端收到一個FIN,它必須通知應(yīng)用層另一端幾經(jīng)終止了那個方向的數(shù)據(jù)傳送。發(fā)送FIN通常是應(yīng)用層進(jìn)行關(guān)閉的結(jié)果。
收到一個FIN只意味著在這一方向上沒有數(shù)據(jù)流動。一個TCP連接在收到一個FIN后仍能發(fā)送數(shù)據(jù)。而這對利用半關(guān)閉的應(yīng)用來說是可能的,盡管在實際應(yīng)用中只有很少的TCP應(yīng)用程序這樣做。正常關(guān)閉過程如圖18-3所示。我們將在18.5節(jié)中詳細(xì)介紹半關(guān)閉。
首先進(jìn)行關(guān)閉的一方(即發(fā)送第一個FIN)將執(zhí)行主動關(guān)閉,而另一方(收到這個FIN)執(zhí)行被動關(guān)閉。通常一方完成主動關(guān)閉而另一方完成被動關(guān)閉,但我們將在18.9節(jié)看到雙方如何都執(zhí)行主動關(guān)閉。
圖18-3中的報文段4發(fā)起終止連接,它由Te lnet客戶端關(guān)閉連接時發(fā)出。這在我們鍵入quit命令后發(fā)生。它將導(dǎo)致TCP客戶端發(fā)送一個FIN,用來關(guān)閉從客戶到服務(wù)器的數(shù)據(jù)傳送。
當(dāng)服務(wù)器收到這個FIN,它發(fā)回一個ACK,確認(rèn)序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。同時TCP服務(wù)器還向應(yīng)用程序(即丟棄服務(wù)器)傳送一個文件結(jié)束符。接著這個服務(wù)器程序就關(guān)閉它的連接,導(dǎo)致它的TCP端發(fā)送一個FIN(報文段6),客戶必須發(fā)回一個確認(rèn),并將確認(rèn)序號設(shè)置為收到序號加1(報文段7)。
圖18-4顯示了終止一個連接的典型握手順序。我們省略了序號。在這個圖中,發(fā)送FIN將導(dǎo)致應(yīng)用程序關(guān)閉它們的連接,這些FIN的ACK是由TCP軟件自動產(chǎn)生的。

連接通常是由客戶端發(fā)起的,這樣第一個SYN從客戶傳到服務(wù)器。每一端都能主動關(guān)閉這個連接(即首先發(fā)送FIN)。然而,一般由客戶端決定何時終止連接,因為客戶進(jìn)程通常由用戶交互控制,用戶會鍵入諸如“quit”一樣的命令來終止進(jìn)程。在圖18-4中,我們能改變上邊的標(biāo)識,將左方定為服務(wù)器,右方定為客戶,一切仍將像顯示的一樣工作(例如在14.4節(jié)中的第一個例子中就是由daytime服務(wù)器關(guān)閉連接的)。
18.2.5 正常的tcpdump輸出
對所有的數(shù)值很大的序號進(jìn)行排序是很麻煩的,因此默認(rèn)情況下tcpdump只在顯示SYN報文段時顯示完整的序號,而對其后的序號則顯示它們與初始序號的相對偏移值(為了得到圖18-1的輸出顯示必須加上-S選項)。對應(yīng)于圖18-1的正常tcpdump顯示如圖18-5所示:
除非我們需要顯示完整的序號,否則將在以下的例子中使用這種形式的輸出顯示。

18.3 連接建立的超時
有很多情況導(dǎo)致無法建立連接。一種情況是服務(wù)器主機沒有處于正常狀態(tài)。為了模擬這種情況,我們斷開服務(wù)器主機的電纜線,然后向它發(fā)出telnet命令。圖18-6顯示了tcpdump的輸出。

在這個輸出中有趣的一點是客戶間隔多長時間發(fā)送一個SYN,試圖建立連接。第2個SYN與第1個的間隔是5.8秒,而第3個與第2個的間隔是24秒。
作為一個附注,這個例子運行38分鐘后客戶重新啟動。這對應(yīng)初始序號為291 008 001(約為38×60×64000×2)。我們曾經(jīng)介紹過使用典型的伯克利實現(xiàn)版的系統(tǒng)將初始序號初始化為1,然后每隔0.5秒就增加64 000。
另外,因為這是系統(tǒng)啟動后的第一個TCP連接,因此客戶的端口號是1024。
圖18-6中沒有顯示客戶端在放棄建立連接嘗試前進(jìn)行SYN重傳的時間。為了了解它我們必須對telnet命令進(jìn)行計時:

時間差值是76秒。大多數(shù)伯克利系統(tǒng)將建立一個新連接的最長時間限制為75秒。我們將在21.4節(jié)看到由客戶發(fā)出的第3個分組大約在16:25:29超時,客戶在它第3個分組發(fā)出后48秒而不是75秒后放棄連接。
18.3.1 第一次超時時間
在圖18-6中一個令人困惑的問題是第一次超時時間為5.8秒,接近6秒,但不準(zhǔn)確,相比之下第二個超時時間幾乎準(zhǔn)確地為24秒。運行十多次測試,發(fā)現(xiàn)第一次超時時間在5.59秒~5.93秒之間變化。然而,第二次超時時間則總是24.00秒(精確到小數(shù)點后面兩位)。
這是因為BSD版的TCP軟件采用一種500 ms的定時器。這種500 ms的定時器用于確定本章中所有的各種各樣的TCP超時。當(dāng)我們鍵入telnet命令,將建立一個6秒的定時器(12個時鐘滴答(tick)),但它可能在之后的5.5秒6秒內(nèi)的任意時刻超時。圖18-7顯示了這一發(fā)生過程。盡管定時器初始化為12個時鐘滴答,但定時計數(shù)器會在設(shè)置后的第一個0500 ms中的任意時秒刻減1。從那以后,定時計數(shù)器大約每隔500 ms減1,但在第1個500 ms內(nèi)是可變的(我們使用限定詞“大約”是因為在TCP每隔500 ms獲得系統(tǒng)控制的瞬間,系統(tǒng)內(nèi)核可能會優(yōu)先處理其他中斷)。

當(dāng)?shù)未鹩嫈?shù)器為0時,6秒的定時器便會超時(見圖18-7),這個定時器會在以后的24秒(48個滴答)重新復(fù)位。之后的下一個定時器將更接近24秒,因為當(dāng)TCP的500 ms定時器被內(nèi)核調(diào)用時,它就會被修改一次。
18.3.2 服務(wù)類型字段
在圖18-6中,出現(xiàn)了符號 [tos 0x10]。這是IP數(shù)據(jù)報內(nèi)的服務(wù)類型(TOS)字段(參見圖3-2)。BSD/386中的Telnet客戶進(jìn)程將這個字段設(shè)置為最小時延。
18.4 最大報文段長度
最大報文段長度(MSS)表示TCP傳往另一端的最大塊數(shù)據(jù)的長度。當(dāng)一個連接建立時,連接的雙方都要通告各自的MSS。我們已經(jīng)見過MSS都是1024。這導(dǎo)致IP數(shù)據(jù)報通常是40字節(jié)長:20字節(jié)的TCP首部和20字節(jié)的IP首部。
在有些書中,將它看作可“協(xié)商”選項。它并不是任何條件下都可協(xié)商。當(dāng)建立一個連接時,每一方都有用于通告它期望接收的MSS選項(MSS選項只能出現(xiàn)在SYN報文段中)。如果一方不接收來自另一方的MSS值,則MSS就定為默認(rèn)值536字節(jié)(這個默認(rèn)值允許20字節(jié)的IP首部和20字節(jié)的TCP首部以適合576字節(jié)IP數(shù)據(jù)報)。
一般說來,如果沒有分段發(fā)生,MSS還是越大越好(這也并不總是正確,參見圖24-3和圖24-4中的例子)。報文段越大允許每個報文段傳送的數(shù)據(jù)就越多,相對IP和TCP首部有更高的網(wǎng)絡(luò)利用率。當(dāng)TCP發(fā)送一個SYN時,或者是因為一個本地應(yīng)用進(jìn)程想發(fā)起一個連接,或者是因為另一端的主機收到了一個連接請求,它能將MSS值設(shè)置為外出接口上的MTU長度減去固定的IP首部和TCP首部長度。對于一個以太網(wǎng),MSS值可達(dá)1460字節(jié)。使用IEEE 802.3的封裝(參見2.2節(jié)),它的MSS可達(dá)1452字節(jié)。
在本章見到的涉及BSD/386和SVR4的MSS為1024,這是因為許多BSD的實現(xiàn)版本需要MSS為512的倍數(shù)。其他的系統(tǒng),如SunOS 4.1.3、Solaris 2.2和AIX 3.2.2,當(dāng)雙方都在一個本地以太網(wǎng)上時都規(guī)定MSS為1460。[Mogul 1993] 的比較顯示了在以太網(wǎng)上1460的MSS在性能上比1024的MSS更好。
如果目的IP地址為“非本地的(nonlocal)”,MSS通常的默認(rèn)值為536。而區(qū)分地址是本地還是非本地是簡單的,如果目的IP地址的網(wǎng)絡(luò)號與子網(wǎng)號都和我們的相同,則是本地的;如果目的IP地址的網(wǎng)絡(luò)號與我們的完全不同,則是非本地的;如果目的IP地址的網(wǎng)絡(luò)號與我們的相同而子網(wǎng)號與我們的不同,則可能是本地的,也可能是非本地的。大多數(shù)TCP實現(xiàn)版都提供了一個配置選項(附錄E和圖E-1),讓系統(tǒng)管理員說明不同的子網(wǎng)是屬于本地還是非本地。這個選項的設(shè)置將確定MSS可以選擇盡可能的大(達(dá)到外出接口的MTU長度)或是默認(rèn)值536。
MSS讓主機限制另一端發(fā)送數(shù)據(jù)報的長度。加上主機也能控制它發(fā)送數(shù)據(jù)報的長度,這將使以較小MTU連接到一個網(wǎng)絡(luò)上的主機避免分段。
考慮我們的主機slip,通過MTU為296的SLIP鏈路連接到路由器bsdi上。圖18-8顯示這些系統(tǒng)和主機sun。

從sun向slip發(fā)起一個TCP連接,并使用tcpdump來觀察報文段。圖18-9顯示這個連接的建立(省略了通告窗口大?。?/p>

在這個例子中,sun發(fā)送的報文段不能超過256字節(jié)的數(shù)據(jù),因為它收到的MSS選項值為256(第2行)。此外,由于slip知道它外出接口的MTU長度為296,即使sun已經(jīng)通告它的MSS為1460,但為避免將數(shù)據(jù)分段,它不會發(fā)送超過256字節(jié)數(shù)據(jù)的報文段。系統(tǒng)允許發(fā)送的數(shù)據(jù)長度小于另一端的MSS值。
只有當(dāng)一端的主機以小于576字節(jié)的MTU直接連接到一個網(wǎng)絡(luò)中,避免這種分段才會有效。如果兩端的主機都連接到以太網(wǎng)上,都采用536的MSS,但中間網(wǎng)絡(luò)采用296的MTU,也將會出現(xiàn)分段。使用路徑上的MTU發(fā)現(xiàn)機制(參見24.2節(jié))是關(guān)于這個問題的唯一方法。
18.5 TCP的半關(guān)閉
TCP提供了連接的一端在結(jié)束它的發(fā)送后還能接收來自另一端數(shù)據(jù)的能力。這就是所謂的半關(guān)閉。正如我們早些時候提到的只有很少的應(yīng)用程序使用它。
為了使用這個特性,編程接口必須為應(yīng)用程序提供一種方式來說明“我已經(jīng)完成了數(shù)據(jù)傳送,因此發(fā)送一個文件結(jié)束(FIN)給另一端,但我還想接收另一端發(fā)來的數(shù)據(jù),直到它給我發(fā)來文件結(jié)束(FIN)”。
如果應(yīng)用程序不調(diào)用close而調(diào)用shutdown,且第2個參數(shù)值為1,則插口的API支持半關(guān)閉。然而,大多數(shù)的應(yīng)用程序通過調(diào)用close終止兩個方向的連接。
圖18-10顯示了一個半關(guān)閉的典型例子。讓左方的客戶端開始半關(guān)閉,當(dāng)然也可以由另一端開始。開始的兩個報文段和圖18-4是相同的:初始端發(fā)出的FIN,接著是另一端對這個FIN的ACK報文段。但后面就和圖18-4不同,因為接收半關(guān)閉的一方仍能發(fā)送數(shù)據(jù)。我們只顯示一個數(shù)據(jù)報文段和一個ACK報文段,但可能發(fā)送了許多數(shù)據(jù)報文段(將在第19章討論數(shù)據(jù)報文段和確認(rèn)報文段的交換)。當(dāng)收到半關(guān)閉的一端在完成它的數(shù)據(jù)傳送后,將發(fā)送一個FIN關(guān)閉這個方向的連接,這將傳送一個文件結(jié)束符給發(fā)起這個半關(guān)閉的應(yīng)用進(jìn)程。當(dāng)對第二個FIN進(jìn)行確認(rèn)后,這個連接便徹底關(guān)閉了。

為什么要有半關(guān)閉?一個例子是Unix中的rsh(1)命令,它將完成在另一個系統(tǒng)上執(zhí)行一個命令。
命令
sun % rsh bsdi sort < datafile
將在主機bsdi上執(zhí)行sort排序命令,rsh命令的標(biāo)準(zhǔn)輸入來自文件datafile。rsh將在它與在另一主機上執(zhí)行的程序間建立一個TCP連接。rsh的操作很簡單:它將標(biāo)準(zhǔn)輸入(datafile)復(fù)制給TCP連接,并將結(jié)果從TCP連接中復(fù)制給標(biāo)準(zhǔn)輸出(我們的終端)。圖18-11顯示了這個建立過程(牢記TCP連接是全雙工的)。

在遠(yuǎn)端主機bsdi上,rshd服務(wù)器將執(zhí)行sort程序,它的標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出都是TCP連接。第14章的[Stevens 1990]詳細(xì)介紹了有關(guān)Unix進(jìn)程的結(jié)構(gòu),但這兒涉及的是使用TCP連接以及需要使用TCP的半關(guān)閉。
sort程序只有讀取到所有輸入數(shù)據(jù)后才能產(chǎn)生輸出。所有的原始數(shù)據(jù)通過TCP連接從rsh客戶端傳送到sort服務(wù)器進(jìn)行排序。當(dāng)輸入(datafile)到達(dá)文件尾時,rsh客戶端執(zhí)行這個TCP連接的半關(guān)閉。接著sort服務(wù)器在它的標(biāo)準(zhǔn)輸入(這個TCP連接)上收到一個文件結(jié)束符,對數(shù)據(jù)進(jìn)行排序,并將結(jié)果寫在它的標(biāo)準(zhǔn)輸出上(TCP連接)。rsh客戶端繼續(xù)接收來自TCP連接另一端的數(shù)據(jù),并將排序的文件復(fù)制到它的標(biāo)準(zhǔn)輸出上。
沒有半關(guān)閉,需要其他的一些技術(shù)讓客戶通知服務(wù)器,客戶端已經(jīng)完成了它的數(shù)據(jù)傳送,但仍要接收來自服務(wù)器的數(shù)據(jù)。使用兩個TCP連接也可作為一個選擇,但使用半關(guān)閉的單連接更好。
18.6 TCP的狀態(tài)變遷圖
我們已經(jīng)介紹了許多有關(guān)發(fā)起和終止TCP連接的規(guī)則。這些規(guī)則都能從圖18-12所示的狀態(tài)變遷圖中得出。

在這個圖中要注意的第一點是一個狀態(tài)變遷的子集是“典型的”。我們用粗的實線箭頭表示正常的客戶端狀態(tài)變遷,用粗的虛線箭頭表示正常的服務(wù)器狀態(tài)變遷。
第二點是兩個導(dǎo)致進(jìn)入ESTABLISH-ED狀態(tài)的變遷對應(yīng)打開一個連接,而兩個導(dǎo)致從ESTABLISHED狀態(tài)離開的變遷主動打開對應(yīng)關(guān)閉一個連接。ESTABLISHED狀態(tài)是連接雙方能夠進(jìn)行雙向數(shù)據(jù)傳遞的狀態(tài)。以后的章節(jié)將介紹這個狀態(tài)。
將圖中左下角4個狀態(tài)放在一個虛線框內(nèi),并標(biāo)為“主動關(guān)閉”。其他兩個狀態(tài)(CLOSE_WAIT和LAST_ACK)也用虛線框住,并標(biāo)為“被動關(guān)閉”。
這個圖中11個狀態(tài)的名稱關(guān)閉) (CLOSED,LISTEN,SYN_SENT等)是有意與netstat命令顯示的狀態(tài)名稱一致。netstat對狀態(tài)的命名幾乎與在RFC793中的最初描述一致。CLOSED狀態(tài)不是一個真正的狀態(tài),而是這個狀態(tài)圖的假想起點和終點。
從LISTEN到SYN_SENT的變遷是正確的,但伯克利版的TCP軟件并不支持它。
只有當(dāng)SYN_RCVD狀態(tài)是從LISTEN狀態(tài)(正常情況)進(jìn)入,而不是從SYN_SENT狀態(tài)(同時打開)進(jìn)入時,從SYN_RCVD回到LISTEN的狀態(tài)變遷才是有效的。這意味著如果我們執(zhí)行被動關(guān)閉(進(jìn)入LISTEN),收到一個SYN,發(fā)送一個帶ACK的SYN(進(jìn)入SYN_RCVD),然后收到一個RST,而不是一個ACK,便又回到LISTEN狀態(tài)并等待另一個連接請求的到來。
圖18-13顯示了在正常的TCP連接的建立與終止過程中,客戶與服務(wù)器所經(jīng)歷的不同狀態(tài)。它是圖18-3的再現(xiàn),不同的是僅顯示了一些狀態(tài)。

假定在圖18-13中左邊的客戶執(zhí)行主動打開,而右邊的服務(wù)器執(zhí)行被動打開。盡管圖中顯示出由客戶端執(zhí)行主動關(guān)閉,但和早前我們提到的一樣,另一端也能執(zhí)行主動關(guān)閉。
可以使用圖18-12的狀態(tài)圖來跟蹤圖18-13的狀態(tài)變化過程,以便明白每個狀態(tài)的變化。
18.6.1 2MSL等待狀態(tài)
TIME_WAIT狀態(tài)也稱為2MSL等待狀態(tài)。每個具體TCP實現(xiàn)必須選擇一個報文段最大生存時間MSL(Maximum Segment Lifetime)。它是任何報文段被丟棄前在網(wǎng)絡(luò)內(nèi)的最長時間。我們知道這個時間是有限的,因為TCP報文段以IP數(shù)據(jù)報在網(wǎng)絡(luò)內(nèi)傳輸,而IP數(shù)據(jù)報則有限制其生存時間的TTL字段。
RFC 793 [Postel 1981c]指出MSL為2分鐘。然而,實現(xiàn)中的常用值是30秒,1分鐘,或2分鐘。
從第8章我們知道在實際應(yīng)用中,對IP數(shù)據(jù)報TTL的限制是基于跳數(shù),而不是定時器。
對一個具體實現(xiàn)所給定的MSL值,處理的原則是:當(dāng)TCP執(zhí)行一個主動關(guān)閉,并發(fā)回最后一個ACK,該連接必須在TIME_WAIT狀態(tài)停留的時間為2倍的MSL。這樣可讓TCP再次發(fā)送最后的ACK以防這個ACK丟失(另一端超時并重發(fā)最后的FIN)。
這種2MSL等待的另一個結(jié)果是這個TCP連接在2MSL等待期間,定義這個連接的插口(客戶的IP地址和端口號,服務(wù)器的IP地址和端口號)不能再被使用。這個連接只能在2MSL結(jié)束后才能再被使用。
遺憾的是,大多數(shù)TCP實現(xiàn)(如伯克利版)強加了更為嚴(yán)格的限制。在2MSL等待期間,插口中使用的本地端口在默認(rèn)情況下不能再被使用。我們將在下面看到這個限制的例子。
某些實現(xiàn)和API提供了一種避開這個限制的方法。使用插口API時,可說明其中的SO_REUSEADDR選項。它將讓調(diào)用者對處于2MSL等待的本地端口進(jìn)行賦值,但我們將看到TCP原則上仍將避免使用仍處于2MSL連接中的端口。
在連接處于2MSL等待時,任何遲到的報文段將被丟棄。因為處于2MSL等待的、由該插口對(socket pair)定義的連接在這段時間內(nèi)不能被再用,因此當(dāng)要建立一個有效的連接時,來自該連接的一個較早替身(incarnation)的遲到報文段作為新連接的一部分不可能不被曲解(一個連接由一個插口對來定義。一個連接的新的實例(instance)稱為該連接的替身)。
我們說圖18-13中客戶執(zhí)行主動關(guān)閉并進(jìn)入TIME_WAIT是正常的。服務(wù)器通常執(zhí)行被動關(guān)閉,不會進(jìn)入TIME_WAIT狀態(tài)。這暗示如果我們終止一個客戶程序,并立即重新啟動這個客戶程序,則這個新客戶程序?qū)⒉荒苤赜孟嗤谋镜囟丝?。這不會帶來什么問題,因為客戶使用本地端口,而并不關(guān)心這個端口號是什么。
然而,對于服務(wù)器,情況就有所不同,因為服務(wù)器使用熟知端口。如果我們終止一個已經(jīng)建立連接的服務(wù)器程序,并試圖立即重新啟動這個服務(wù)器程序,服務(wù)器程序?qū)⒉荒馨阉倪@個熟知端口賦值給它的端點,因為那個端口是處于2MSL連接的一部分。在重新啟動服務(wù)器程序前,它需要在1~4分鐘。
可以通過sock程序看到這一切。我們啟動服務(wù)器程序,從一個客戶程序進(jìn)行連接,然后停止這個服務(wù)器程序。

當(dāng)重新啟動服務(wù)器程序時,程序報告一個差錯信息說明不能綁定它的熟知端口,因為該端口已被使用(即它處于2MSL等待)。
運行netstat程序來查看連接的狀態(tài),以證實它的確處于2MSL等待狀態(tài)。
如果我們一直試圖重新啟動服務(wù)器程序,并測量它直到成功所需的時間,我們就能確定出2MSL值。對于SunOS 4.1.3、SVR4、BSD/386和AIX 3.2.2,它需要1分鐘才能重新啟動服務(wù)器程序,這意味著它們的MSL值為30秒。而對于Solaris 2.2,它需要4分鐘才能重新啟動服務(wù)器程序,這表示它的MSL值為2分鐘。
如果一個客戶程序試圖申請一個處于2MSL等待的端口(客戶程序通常不會這么做),就會出現(xiàn)同樣的差錯。

我們在第1次執(zhí)行客戶程序時采用-v選項來查看它使用的本地端口為(11 62)。第2次執(zhí)行客戶程序時則采用-b選項來選擇端口11 62為它的本地端口。正如我們所預(yù)料的那樣,客戶程序無法那么做,因為那個端口是一個還處于2MSL等待連接的一部分。
需要再次強調(diào)2MSL等待的一個效果,因為我們將在第27章的文件傳輸協(xié)議FTP中遇到它。和以前介紹的一樣,一個插口對(即包含本地IP地址、本地端口、遠(yuǎn)端IP地址和遠(yuǎn)端端口的4元組)在它處于2MSL等待時,將不能再被使用。盡管許多具體的實現(xiàn)中允許一個進(jìn)程重新使用仍處于2MSL等待的端口(通常是設(shè)置選項SO_REUSEADDR),但TCP不能允許一個新的連接建立在相同的插口對上。可通過下面的試驗來看到這一點:

第1次運行sock程序中,我們將它作為服務(wù)器程序,端口號為6666,并從主機bsdi上的一個客戶程序與它連接,這個客戶程序使用的端口為1098。我們終止服務(wù)器程序,因此它將執(zhí)行主動關(guān)閉。這將導(dǎo)致4元組140.252.13.33(本地IP地址)、6666(本地端口號)、140.252.13.35(另一端IP地址)和1098(另一端的端口號)在服務(wù)器主機進(jìn)入2MSL等待。
在第2次運行sock程序時,我們將它作為客戶程序,并試圖將它的本地端口號指明為6666,同時與主機bsdi在端口1098上進(jìn)行連接。但這個程序在試圖將它的本地端口號賦值為6666時產(chǎn)生了一個差錯,因為這個端口是處于2MSL等待4元組的一部分。
為了避免這個差錯,我們再次運行這個程序,并使用選項-A來設(shè)置前面提到的SO_REUSEADDR。這將讓sock程序能將它的本地端口號設(shè)置為6666,但當(dāng)我們試圖進(jìn)行主動打開時,又出現(xiàn)了一個差錯。即使它能將它的本地端口設(shè)置為6666,但它仍不能和主機bsdi在端口1098上進(jìn)行連接,因為定義這個連接的插口對仍處于2MSL等待狀態(tài)。
如果我們試圖從其他主機來建立這個連接會如何?首先我們必須在sun上以-A標(biāo)記來重新啟動服務(wù)器程序,因為它需要的端口(6666)是還處于2MSL等待連接的一部分。
sun % sock -A -s 6666 啟動服務(wù)器程序,在端口6666監(jiān)聽
接著,在2MSL等待結(jié)束前,我們在bsdi上啟動客戶程序:
bsdi % sock -b1098 sun 6666
connected on 140.252.13.35.1098 to 140.252.13.33.6666
不幸的是它成功了!這違反了TCP規(guī)范,但被大多數(shù)的伯克利版實現(xiàn)所支持。這些實現(xiàn)允許一個新的連接請求到達(dá)仍處于TIME_WAIT狀態(tài)的連接,只要新的序號大于該連接前一個替身的最后序號。在這個例子中,新替身的ISN被設(shè)置為前一個替身最后序號與128 000的和。附錄的RFC 1185 [Jacobsan、Braden和Zhang 1990]指出了這項技術(shù)仍可能存在缺陷。
對于同一連接的前一個替身,這個具體實現(xiàn)中的特性讓客戶程序和服務(wù)器程序能連續(xù)地重用每一端的相同端口號,但這只有在服務(wù)器執(zhí)行主動關(guān)閉才有效。我們將在圖27-8中使用FTP時看到這個2MSL等待條件的另一個例子。也見習(xí)題18.5。
18.6.2 平靜時間的概念
對于來自某個連接的較早替身的遲到報文段,2MSL等待可防止將它解釋成使用相同插口對的新連接的一部分。但這只有在處于2MSL等待連接中的主機處于正常工作狀態(tài)時才有效。
如果使用處于2MSL等待端口的主機出現(xiàn)故障,它會在MSL秒內(nèi)重新啟動,并立即使用故障前仍處于2MSL的插口對來建立一個新的連接嗎?如果是這樣,在故障前從這個連接發(fā)出而遲到的報文段會被錯誤地當(dāng)作屬于重啟后新連接的報文段。無論如何選擇重啟后新連接的初始序號,都會發(fā)生這種情況。
為了防止這種情況,RFC 793指出TCP在重啟動后的MSL秒內(nèi)不能建立任何連接。這就稱為平靜時間(quiet time)。
只有極少的實現(xiàn)版遵守這一原則,因為大多數(shù)主機重啟動的時間都比MSL秒要長。
18.6.3 FIN_WAIT_2狀態(tài)
在FIN_WAIT_2狀態(tài)我們已經(jīng)發(fā)出了FIN,并且另一端也已對它進(jìn)行確認(rèn)。除非我們在實行半關(guān)閉,否則將等待另一端的應(yīng)用層意識到它已收到一個文件結(jié)束符說明,并向我們發(fā)一個FIN來關(guān)閉另一方向的連接。只有當(dāng)另一端的進(jìn)程完成這個關(guān)閉,我們這端才會從FIN_WAIT_2狀態(tài)進(jìn)入TIME_WAIT狀態(tài)。
這意味著我們這端可能永遠(yuǎn)保持這個狀態(tài)。另一端也將處于CLOSE_WAIT狀態(tài),并一直保持這個狀態(tài)直到應(yīng)用層決定進(jìn)行關(guān)閉。
許多伯克利實現(xiàn)采用如下方式來防止這種在FIN_WAIT_2狀態(tài)的無限等待。如果執(zhí)行主動關(guān)閉的應(yīng)用層將進(jìn)行全關(guān)閉,而不是半關(guān)閉來說明它還想接收數(shù)據(jù),就設(shè)置一個定時器。如果這個連接空閑10分鐘75秒,TCP將進(jìn)入CLOSED狀態(tài)。在實現(xiàn)代碼的注釋中確認(rèn)這個實現(xiàn)代碼違背協(xié)議的規(guī)范。
18.7 復(fù)位報文段
我們已經(jīng)介紹了TCP首部中的RST比特是用于“復(fù)位”的。一般說來,無論何時一個報文段發(fā)往基準(zhǔn)的連接(referenced connection)出現(xiàn)錯誤,TCP都會發(fā)出一個復(fù)位報文段(這里提到的“基準(zhǔn)的連接”是指由目的IP地址和目的端口號以及源IP地址和源端口號指明的連接。這就是為什么RFC 793稱之為插口)。
18.7.1 到不存在的端口的連接請求
產(chǎn)生復(fù)位的一種常見情況是當(dāng)連接請求到達(dá)時,目的端口沒有進(jìn)程正在聽。對于UDP,我們在6.5節(jié)看到這種情況,當(dāng)一個數(shù)據(jù)報到達(dá)目的端口時,該端口沒在使用,它將產(chǎn)生一個ICMP端口不可達(dá)的信息。而TCP則使用復(fù)位。
產(chǎn)生這個例子也很容易,我們可使用Te lnet客戶程序來指明一個目的端口沒在使用的情況:
bsdi % telnet svr4 20000 端口20000未使用
Trying 140.252.13.34...
telnet: Unable to connect to remote host: Connection refused
Telnet客戶程序會立即顯示這個差錯信息。圖18-14顯示了對應(yīng)這個命令的分組交換過程。

在這個圖中需要注意的值是復(fù)位報文段中的序號字段和確認(rèn)序號字段。因為ACK比特在到達(dá)的報文段中沒有被設(shè)置為1,復(fù)位報文段中的序號被置為0,確認(rèn)序號被置為進(jìn)入的ISN加上數(shù)據(jù)字節(jié)數(shù)。盡管在到達(dá)的報文段中沒有真正的數(shù)據(jù),但SYN比特從邏輯上占用了1字節(jié)的序號空間;因此,在這個例子中復(fù)位報文段中確認(rèn)序號被置為ISN與數(shù)據(jù)長度(0)、SYN比特所占的1的總和。
18.7.2 異常終止一個連接
我們在18.2節(jié)中看到終止一個連接的正常方式是一方發(fā)送FIN。有時這也稱為有序釋放(orderly release),因為在所有排隊數(shù)據(jù)都已發(fā)送之后才發(fā)送FIN,正常情況下沒有任何數(shù)據(jù)丟失。但也有可能發(fā)送一個復(fù)位報文段而不是FIN來中途釋放一個連接。有時稱這為異常釋放(abortive release)。
異常終止一個連接對應(yīng)用程序來說有兩個優(yōu)點:(1)丟棄任何待發(fā)數(shù)據(jù)并立即發(fā)送復(fù)位報文段;(2)RST的接收方會區(qū)分另一端執(zhí)行的是異常關(guān)閉還是正常關(guān)閉。應(yīng)用程序使用的API必須提供產(chǎn)生異常關(guān)閉而不是正常關(guān)閉的手段。
使用sock程序能夠觀察這種異常關(guān)閉的過程。Socket API通過“l(fā)inger on close”選項(SO_LINGER)提供了這種異常關(guān)閉的能力。我們加上-L選項并將停留時間設(shè)為0。這將導(dǎo)致連接關(guān)閉時進(jìn)行復(fù)位而不是正常的FIN。我們連接到處于服務(wù)器上的sock程序,并鍵入一輸入行:

圖18-15是這個例子的tcpdump輸出顯示(在這個圖中我們已經(jīng)刪除了所有窗口大小的說明,因為它們與討論無關(guān))。
第1~3行顯示出建立連接的正常過程。第4行發(fā)送我們鍵入的數(shù)據(jù)行(12個字符和Unix換行符),第5行是對收到數(shù)據(jù)的確認(rèn)。

第6行對應(yīng)為終止客戶程序而鍵入的文件結(jié)束符(Control_D)。由于我們指明使用異常關(guān)閉而不是正常關(guān)閉(命令行中的-L0選項),因此主機bsdi端的TCP發(fā)送一個RST而不是通常的FIN。RST報文段中包含一個序號和確認(rèn)序號。需要注意的是RST報文段不會導(dǎo)致另一端產(chǎn)生任何響應(yīng),另一端根本不進(jìn)行確認(rèn)。收到RST的一方將終止該連接,并通知應(yīng)用層連接復(fù)位。
我們在服務(wù)器上得到下面的差錯信息:

這個服務(wù)器程序從網(wǎng)絡(luò)中接收數(shù)據(jù)并將它接收的數(shù)據(jù)顯示到其標(biāo)準(zhǔn)輸出上。通常,從它的TCP上收到文件結(jié)束符后便將結(jié)束,但這里我們看到當(dāng)收到RST時,它產(chǎn)生了一個差錯。這個差錯正是我們所期待的:連接被對方復(fù)位了。
18.7.3 檢測半打開連接
如果一方已經(jīng)關(guān)閉或異常終止連接而另一方卻還不知道,我們將這樣的TCP連接稱為半打開(Half-Open)的。任何一端的主機異常都可能導(dǎo)致發(fā)生這種情況。只要不打算在半打開連接上傳輸數(shù)據(jù),仍處于連接狀態(tài)的一方就不會檢測另一方已經(jīng)出現(xiàn)異常。
半打開連接的另一個常見原因是當(dāng)客戶主機突然掉電而不是正常的結(jié)束客戶應(yīng)用程序后再關(guān)機。這可能發(fā)生在使用PC機作為Telnet的客戶主機上,例如,用戶在一天工作結(jié)束時關(guān)閉PC機的電源。當(dāng)關(guān)閉PC機電源時,如果已不再有要向服務(wù)器發(fā)送的數(shù)據(jù),服務(wù)器將永遠(yuǎn)不知道客戶程序已經(jīng)消失了。當(dāng)用戶在第二天到來時,打開PC機,并啟動新的Telnet客戶程序,在服務(wù)器主機上會啟動一個新的服務(wù)器程序。這樣會導(dǎo)致服務(wù)器主機中產(chǎn)生許多半打開的TCP連接(在第23章中我們將看到使用TCP的keepalive選項能使TCP的一端發(fā)現(xiàn)另一端已經(jīng)消失)。
能很容易地建立半打開連接。在bsdi上運行Telnet客戶程序,通過它和svr4上的丟棄服務(wù)器建立連接。我們鍵入一行字符,然后通過tcpdump進(jìn)行觀察,接著斷開服務(wù)器主機與以太網(wǎng)的電纜,并重啟服務(wù)器主機。這可以模擬服務(wù)器主機出現(xiàn)異常(在重啟服務(wù)器之前斷開以太網(wǎng)電纜是為了防止它向打開的連接發(fā)送FIN,某些TCP在關(guān)機時會這么做)。服務(wù)器主機重啟后,我們重新接上電纜,并從客戶向服務(wù)器發(fā)送另一行字符。由于服務(wù)器的TCP已經(jīng)重新啟動,它將丟失復(fù)位前連接的所有信息,因此它不知道數(shù)據(jù)報文段中提到的連接。TCP的處理原則是接收方以復(fù)位作為應(yīng)答。圖18-16是這個例子的tcpdump輸出顯示(已從這個輸出中刪除了窗口大小的說明、服務(wù)類型信息和MSS聲明,因為它們與討論無關(guān))。

圖18-16是這個例子的tcpdump輸出顯示(已從這個輸出中刪除了窗口大小的說明、服務(wù)類型信息和MSS聲明,因為它們與討論無關(guān))。

第1~3行是正常的連接建立過程。第4行向丟棄服務(wù)器發(fā)送字符行“hithere”,第5行是確認(rèn)。
然后是斷開svr4的以太網(wǎng)電纜,重新啟動svr4,并重新接上電纜。這個過程幾乎需要190秒。接著從客戶端輸入下一行(即“another line”),當(dāng)我們鍵入回車鍵后,這一行被發(fā)往服務(wù)器(圖18-16的第6行)。這導(dǎo)致服務(wù)器產(chǎn)生一個響應(yīng),但要注意的是由于服務(wù)器主機經(jīng)過重新啟動,它的ARP高速緩存為空,因此需要一個ARP請求和應(yīng)答(第7、8行)。第9行表示RST被發(fā)送出去。客戶收到復(fù)位報文段后顯示連接已被另一端的主機終止(Te lnet客戶程序發(fā)出的最后信息不再有什么價值)。
18.8 同時打開
兩個應(yīng)用程序同時彼此執(zhí)行主動打開的情況是可能的,盡管發(fā)生的可能性極小。每一方必須發(fā)送一個SYN,且這些SYN必須傳遞給對方。這需要每一方使用一個對方熟知的端口作為本地端口。這又稱為同時打開(simultaneous open)。
例如,主機A中的一個應(yīng)用程序使用本地端口7777,并與主機B的端口8888執(zhí)行主動打開。主機B中的應(yīng)用程序則使用本地端口8888,并與主機A的端口7777執(zhí)行主動打開。
這與下面的情況不同:主機A中的Telnet客戶程序和主機B中Telnet的服務(wù)器程序建立連接,與此同時,主機B中的Telnet客戶程序與主機A的Telnet服務(wù)器程序也建立連接。在這個Telnet例子中,兩個Telnet服務(wù)器都執(zhí)行被動打開,而不是主動打開,并且Telnet客戶選擇的本地端口不是另一端Te lnet服務(wù)器進(jìn)程所熟悉的端口。
TCP是特意設(shè)計為了可以處理同時打開,對于同時打開它僅建立一條連接而不是兩條連接(其他的協(xié)議族,最突出的是OSI運輸層,在這種情況下將建立兩條連接而不是一條連接)。
當(dāng)出現(xiàn)同時打開的情況時,狀態(tài)變遷與圖18-13所示的不同。兩端幾乎在同時發(fā)送SYN,并進(jìn)入SYN_SENT狀態(tài)。當(dāng)每一端收到SYN時,狀態(tài)變?yōu)镾YN_RCVD(如圖18-12),同時它們都再發(fā)SYN并對收到的SYN進(jìn)行確認(rèn)。當(dāng)雙方都收到SYN及相應(yīng)的ACK時,狀態(tài)都變遷為ESTABLISHED。圖18-17顯示了這些狀態(tài)變遷過程。

一個同時打開的連接需要交換4個報文段,比正常的三次握手多一個。此外,要注意的是我們沒有將任何一端稱為客戶或服務(wù)器,因為每一端既是客戶又是服務(wù)器。
一個例子
盡管很難,但仍有可能產(chǎn)生一個同時打開的連接。兩端必須幾乎在同時啟動,以便收到彼此的SYN。只要兩端有較長的往返時間就能保證這一點。這樣我們將一端設(shè)置在主機bsdi上,另一端則設(shè)置在主機vangogh.cs.berkeley.edu上。由于兩端之間有一條撥號鏈路SLIP,它的往返時間對保證雙方同步收到SYN是足夠長的(幾百毫秒)。
一端(bsdi)將本地端口設(shè)置為8888(使用命令行選項-b),并對另一端主機端口7777執(zhí)行主動打開。

另一端也幾乎在同一時間將本地端口設(shè)置為7777,并對端口8888執(zhí)行主動打開。

我們指明帶-v標(biāo)志的sock程序來驗證連接兩端的IP地址和端口號。這個選項也顯示每一端的MSS值。為證實兩端確實在相互交談,我們在每一端還輸入一行字符,看它們是否會被送到另一端并顯示出來。
圖18-18顯示了這個連接的段交換過程(我們刪除了出現(xiàn)在來自vangogh第一個SYN中的一些新的TCP選項,因為vangogh使用4.4BSD系統(tǒng)。將在18.10節(jié)介紹這些較新的選項)。注意兩個SYN(第12行)后跟著兩個帶ACK的SYN(第34行)。它們將執(zhí)行同時打開。
第5行顯示了由bsdi發(fā)送給vangogh的輸入行“hello,world”,第6行對此進(jìn)行確認(rèn)。第7~8行對應(yīng)另一方向的輸入行“and hi there”和確認(rèn)。第9~12行顯示正常的連接關(guān)閉。
許多伯克利版的TCP實現(xiàn)都不能正確地支持同時打開。在這些系統(tǒng)中,如果能夠進(jìn)行SYN的同步接收,你將經(jīng)歷極多的報文段交換過程才能關(guān)閉它們。每個報文段交換過程包括每個方向上的一個SYN和一個ACK。圖18-12中從SYN_SENT到狀態(tài)SYN_RCVD的變遷在許多TCP實現(xiàn)中很少測試過。

18.9 同時關(guān)閉
我們在以前討論過一方(通常但不總是客戶方)發(fā)送第一個FIN執(zhí)行主動關(guān)閉。雙方都執(zhí)行主動關(guān)閉也是可能的,TCP協(xié)議也允許這樣的同時關(guān)閉(simultaneous close)。
在圖18-12中,當(dāng)應(yīng)用層發(fā)出關(guān)閉命令時,兩端均從ESTABLISHED變?yōu)镕IN_WAIT_1。這將導(dǎo)致雙方各發(fā)送一個FIN,兩個FIN經(jīng)過網(wǎng)絡(luò)傳送后分別到達(dá)另一端。收到FIN后,狀態(tài)由FIN_WAIT_1變遷到CLOSING,并發(fā)送最后的ACK。當(dāng)收到最后的ACK時,狀態(tài)變化為TIME_WAIT。圖18-19總結(jié)了這些狀態(tài)的變化。

同時關(guān)閉與正常關(guān)閉使用的段交換數(shù)目相同。
18.10 TCP選項
TCP首部可以包含選項部分(圖17-2)。僅在最初的TCP規(guī)范中定義的選項是選項表結(jié)束、無操作和最大報文段長度。在我們的例子中,幾乎每個SYN報文段中我們都遇到過MSS選項。
新的RFC,主要是RFC 1323 [Jacobson,Braden和Borman 1992],定義了新的TCP選項,這些選項的大多數(shù)只在最新的TCP實現(xiàn)中才能見到(我們將在第24章介紹這些新選項)。圖18-20顯示了當(dāng)前TCP選項的格式,這些選項的定義出自于RFC 793和RFC 1323。

每個選項的開始是1字節(jié)kind字段,說明選項的類型。kind字段為0和1的選項僅占1個字節(jié)。其他的選項在kind字節(jié)后還有l(wèi)en字節(jié)。它說明的長度是指總長度,包括kind字節(jié)和len字節(jié)。
設(shè)置無操作選項的原因在于允許發(fā)方填充字段為4字節(jié)的倍數(shù)。如果我們使用4.4BSD系統(tǒng)進(jìn)行初始化TCP連接,tcpdump將在初始的SYN上顯示下面TCP選項:
<mss 512, nop, wscale 0, nop, nop, timestamp 146647 0>
MSS選項設(shè)置為512,后面是NOP,接著是窗口擴大選項。第一個NOP用來將窗口擴大選項填充為4字節(jié)的邊界。同樣,10字節(jié)的時間戳選項放在兩個NOP后,占12字節(jié),同時使兩個4字節(jié)的時間戳滿足4字節(jié)邊界。
其他kind值為4、5、6和7的四個選項稱為選擇ACK及回顯選項。由于回顯選項已被時間戳選項取代,而目前定義的選擇ACK選項仍未定論,并未包括在RFC 1323中,因此圖18-20沒有將它們列出。另外,作為TCP事務(wù)(第24.7節(jié))的T/TCP建議也指明kind為11,12和13的三個選項。
18.11 TCP服務(wù)器的設(shè)計
我們在1.8節(jié)說過大多數(shù)的TCP服務(wù)器進(jìn)程是并發(fā)的。當(dāng)一個新的連接請求到達(dá)服務(wù)器時,服務(wù)器接受這個請求,并調(diào)用一個新進(jìn)程來處理這個新的客戶請求。不同的操作系統(tǒng)使用不同的技術(shù)來調(diào)用新的服務(wù)器進(jìn)程。在Unix系統(tǒng)下,常用的技術(shù)是使用fork函數(shù)來創(chuàng)建新的進(jìn)程。如果系統(tǒng)支持,也可使用輕型進(jìn)程,即線程(thread)。
我們感興趣的是TCP與若干并發(fā)服務(wù)器的交互作用。需要回答下面的問題:當(dāng)一個服務(wù)器進(jìn)程接受一來自客戶進(jìn)程的服務(wù)請求時是如何處理端口的?如果多個連接請求幾乎同時到達(dá)會發(fā)生什么情況?
18.11.1 TCP服務(wù)器端口號
通過觀察任何一個TCP服務(wù)器,我們能了解TCP如何處理端口號。我們使用netstat命令來觀察Telnet服務(wù)器。下面是在沒有Telnet連接時的顯示(只留下顯示Telnet服務(wù)器的行)。

-a標(biāo)志將顯示網(wǎng)絡(luò)中的所有主機端,而不僅僅是處于ESTABLISHED的主機端。-n標(biāo)志將以點分十進(jìn)制的形式顯示IP地址,而不是通過DNS將地址轉(zhuǎn)化為主機名,同時還要求顯示端口號(例如為23)而不是服務(wù)名稱(如Te lnet)。-f inet選項則僅要求顯示使用TCP或UDP的主機。
顯示的本地地址為*.23,星號通常又稱為通配符。這表示傳入的連接請求(即SYN)將被任何一個本地接口所接收。如果該主機是多接口主機,我們將制定其中的一個IP地址為本地IP地址,并且只接收來自這個接口的連接(在本節(jié)后面我們將看到這樣的例子)。本地端口為23,這是Telnet的熟知端口號。
遠(yuǎn)端地址顯示為.,表示還不知道遠(yuǎn)端IP地址和端口號,因為該端還處于LISTEN狀態(tài),正等待連接請求的到達(dá)。
現(xiàn)在我們在主機slip(140.252.13.65)啟動一個Te lnet客戶程序來連接這個Te lnet服務(wù)器。以下是netstat程序的輸出行:

端口為23的第1行表示處于ESTABLISHED狀態(tài)的連接。另外還顯示了這個連接的本地IP地址、本地端口號、遠(yuǎn)端IP地址和遠(yuǎn)端端口號。本地IP地址為該連接請求到達(dá)的接口(以太網(wǎng)接口,140.252.13.33)。
處于LISTEN狀態(tài)的服務(wù)器進(jìn)程仍然存在。這個服務(wù)器進(jìn)程是當(dāng)前Te lnet服務(wù)器用于接收其他的連接請求。當(dāng)傳入的連接請求到達(dá)并被接收時,系統(tǒng)內(nèi)核中的TCP模塊就創(chuàng)建一個處于ESTABLISHED狀態(tài)的進(jìn)程。另外,注意處于ESTABLISHED狀態(tài)的連接的端口不會變化:也是23,與處于LISTEN狀態(tài)的進(jìn)程相同。
現(xiàn)在我們在主機slip上啟動另一個Telnet客戶進(jìn)程,并仍與這個Telnet服務(wù)器進(jìn)行連接。以下是netstat程序的輸出行:

現(xiàn)在我們有兩條從相同主機到相同服務(wù)器的處于ESTABLISHED的連接。它們的本地端口號均為23。由于它們的遠(yuǎn)端端口號不同,這不會造成沖突。因為每個Telnet客戶進(jìn)程要使用一個外設(shè)端口,并且這個外設(shè)端口會選擇為主機(slip)當(dāng)前未曾使用的端口,因此它們的端口號肯定不同。
這個例子再次重申TCP使用由本地地址和遠(yuǎn)端地址組成的4元組:目的IP地址、目的端口號、源IP地址和源端口號來處理傳入的多個連接請求。TCP僅通過目的端口號無法確定那個進(jìn)程接收了一個連接請求。另外,在三個使用端口23的進(jìn)程中,只有處于LISTEN的進(jìn)程能夠接收新的連接請求。處于ESTABLISHED的進(jìn)程將不能接收SYN報文段,而處于LISTEN的進(jìn)程將不能接收數(shù)據(jù)報文段。
下面我們從主機solaris上啟動第3個Telnet客戶進(jìn)程,這個主機通過SLIP鏈路與主機sun相連,而不是以太網(wǎng)接口。

現(xiàn)在第一個ESTABLISHED連接的本地IP地址對應(yīng)多地址主機sun中的SLIP鏈路接口地址(140.252.1.29)。
18.11.2 限定的本地IP地址
我們來看看當(dāng)服務(wù)器不能任選其本地IP地址而必須使用特定的IP地址時的情況。如果我們?yōu)閟ock程序指明一個IP地址(或主機名),并將它作為服務(wù)器,那么該IP地址就成為處于LISTEN服務(wù)器的本地IP地址。例如
sun % sock -s 140.252.1.29 8888
使這個服務(wù)器程序的連接僅局限于來自SLIP接口(140.252.1.29)。netstat的顯示說明了這一點:
第18章 TCP連接的建立與終止_即時通訊網(wǎng)(52im.net)
如果我們從主機solaris通過SLIP鏈路與這個服務(wù)器相連接,它將正常工作。
第18章 TCP連接的建立與終止_即時通訊網(wǎng)(52im.net)
但如果我們試圖從以太網(wǎng)(140.252.13)中的主機與這個服務(wù)器進(jìn)行連接,連接請求將被TCP模塊拒絕。如果使用tcpdump來觀察這一切,對連接請求SYN的響應(yīng)是一個如圖18-21所示的RST。

這個連接請求將不會到達(dá)服務(wù)器的應(yīng)用程序,因為它根據(jù)應(yīng)用程序中指定的本地IP地址被內(nèi)核中的TCP模塊拒絕。
18.11.3 限定的遠(yuǎn)端IP地址
在11.12節(jié),我們知道UDP服務(wù)器通常在指定IP本地地址和本地端口外,還能指定遠(yuǎn)端IP地址和遠(yuǎn)端端口。RFC 793中顯示的接口函數(shù)允許一個服務(wù)器在執(zhí)行被動打開時,可指明遠(yuǎn)端插口(等待一個特定的客戶執(zhí)行主動打開),也可不指明遠(yuǎn)端插口(等待任何客戶)。
遺憾的是,大多數(shù)API都不支持這么做。服務(wù)器必須不指明遠(yuǎn)端插口,而等待連接請求的到來,然后檢查客戶端的IP地址和端口號。
圖18-22總結(jié)了TCP服務(wù)器進(jìn)行連接時三種類型的地址綁定。在三種情況中,lport是服務(wù)器的熟知端口,而localIP必須是一個本地接口的IP地址。表中行的順序正是TCP模塊在收到一個連接請求時確定本地地址的順序。最常使用的綁定(第1行,如果支持的話)將最先嘗試,最不常用的(最后一行兩端的IP地址都沒有制定)將最后嘗試。

18.11.4 呼入連接請求隊列
一個并發(fā)服務(wù)器調(diào)用一個新的進(jìn)程來處理每個客戶請求,因此處于被動連接請求的服務(wù)器應(yīng)該始終準(zhǔn)備處理下一個呼入的連接請求。那正是使用并發(fā)服務(wù)器的根本原因。但仍有可能出現(xiàn)當(dāng)服務(wù)器在創(chuàng)建一個新的進(jìn)程時,或操作系統(tǒng)正忙于處理優(yōu)先級更高的進(jìn)程時,到達(dá)多個連接請求。當(dāng)服務(wù)器正處于忙時,TCP是如何處理這些呼入的連接請求?
在伯克利的TCP實現(xiàn)中采用以下規(guī)則:
1:正等待連接請求的一端有一個固定長度的連接隊列,該隊列中的連接已被TCP接受(即三次握手已經(jīng)完成),但還沒有被應(yīng)用層所接受。
注意區(qū)分TCP接受一個連接是將其放入這個隊列,而應(yīng)用層接受連接是將其從該隊列中移出。
2:應(yīng)用層將指明該隊列的最大長度,這個值通常稱為積壓值(backlog)。它的取值范圍是0~5之間的整數(shù),包括0和5(大多數(shù)的應(yīng)用程序都將這個值說明為5)。
3:當(dāng)一個連接請求(即SYN)到達(dá)時,TCP使用一個算法,根據(jù)當(dāng)前連接隊列中的連接數(shù)來確定是否接收這個連接。我們期望應(yīng)用層說明的積壓值為這一端點所能允許接受連接的最大數(shù)目,但情況不是那么簡單。圖18-23顯示了積壓值與傳統(tǒng)的伯克利系統(tǒng)和Solaris2.2所能允許的最大接受連接數(shù)之間的關(guān)系。注意,積壓值說明的是TCP監(jiān)聽的端點已被TCP接受而等待應(yīng)用層接受的最大連接數(shù)。這個積壓值對系統(tǒng)所允許的最大連接數(shù),或者并發(fā)服務(wù)器所能并發(fā)處理的客戶數(shù),并無影響。在這個圖中,Solaris系統(tǒng)規(guī)定的值正如我們所期望的。而傳統(tǒng)的BSD系統(tǒng),將這個值(由于某些原因)設(shè)置為積壓值乘3除以2,再加1。

4:如果對于新的連接請求,該TCP監(jiān)聽的端點的連接隊列中還有空間(基于圖18-23),TCP模塊將對SYN進(jìn)行確認(rèn)并完成連接的建立。但應(yīng)用層只有在三次握手中的第三個報文段收到后才會知道這個新連接時。另外,當(dāng)客戶進(jìn)程的主動打開成功但服務(wù)器的應(yīng)用層還不知道這個新的連接時,它可能會認(rèn)為服務(wù)器進(jìn)程已經(jīng)準(zhǔn)備好接收數(shù)據(jù)了(如果發(fā)生這種情況,服務(wù)器的TCP僅將接收的數(shù)據(jù)放入緩沖隊列)。
5:如果對于新的連接請求,連接隊列中已沒有空間,TCP將不理會收到的SYN。也不發(fā)回任何報文段(即不發(fā)回RST)。如果應(yīng)用層不能及時接受已被TCP接受的連接,這些連接可能占滿整個連接隊列,客戶的主動打開最終將超時。
通過sock程序能了解這種情況。我們調(diào)用它,并使用新的選項(-O)。讓它在創(chuàng)建一個新的服務(wù)器進(jìn)程后而沒有接受任何連接請求之前暫停下來。如果在它暫停期間又調(diào)用了多個客戶進(jìn)程,它將導(dǎo)致接受連接隊列被填滿,通過tcpdump能夠看到這一切。
bsdi % sock -s -v -q1 -O30 5555
-q1選項將服務(wù)器端的積壓值置1。在這種情況下,傳統(tǒng)的BSD系統(tǒng)中的隊列允許接受兩個連接請求(圖18-23)。-O30選項使程序在接受任何客戶連接之前暫停30秒。在這30秒內(nèi),我們可啟動其他客戶進(jìn)程來填充這個隊列。在主機sun上啟動4個客戶進(jìn)程。
圖18-24顯示了tcpdump的輸出,首先是第1個客戶進(jìn)程的第1個SYN(省略窗口大小和MSS聲明。當(dāng)TCP連接建立時,將客戶進(jìn)程的端口號用粗體標(biāo)出)。
端口為1090的第一個客戶連接請求被TCP接受(報文段13)。端口為1091的第2個客戶連接請求也被TCP接受(報文段46)。而服務(wù)器的應(yīng)用仍處于休眠狀態(tài),還未接受任何連接。目前的一切工作都由內(nèi)核中的TCP模塊完成。另外,兩個客戶進(jìn)程已經(jīng)成功地完成了它們的主動打開,因為它們建立連接的三次握手已經(jīng)完成。

我們接著在報文段7(端口1092)和報文段8(端口1093)啟動第3和第4個客戶進(jìn)程。由于服務(wù)器的連接隊列已滿,TCP將不理會兩個SYN。這兩個客戶進(jìn)程在報文段9,10,11,12,15重發(fā)它們的SYN。第4個客戶進(jìn)程的第3個SYN重傳被接受了,因為服務(wù)器程序的30秒休眠結(jié)束后,它將已接受的兩個連接從隊列中移出,使連接隊列變空(服務(wù)器程序接收連接的時間是28.19,小于30的原因在于啟動服務(wù)器程序后它需要幾秒的時間來啟動第1個客戶進(jìn)程(報文段1,顯示的就是啟動時間))。第3個客戶進(jìn)程的第4個SYN重傳這時將被接受(報文段15~17)。服務(wù)器程序先接受第4個客戶連接(端口1093)的原因是服務(wù)器程序30秒休眠與客戶程序重傳之間的定時交互作用。
我們期望接收連接隊列按先進(jìn)先出順序傳遞給應(yīng)用層。如TCP接受了端口為1090和1091的連接,我們希望應(yīng)用層先接受端口為1090的連接,然后再接受端口為1091的連接。但許多伯克利的TCP實現(xiàn)都出現(xiàn)按后進(jìn)先出的傳遞順序,這個錯誤已存在了多年。產(chǎn)商最近已開始改正這個錯誤,但在如SunOS 4.13等系統(tǒng)中仍存在這個問題。
當(dāng)隊列已滿時,TCP將不理會傳入的SYN,也不發(fā)回RST作為應(yīng)答,因為這是一個軟錯誤,而不是一個硬錯誤。通常隊列已滿是由于應(yīng)用程序或操作系統(tǒng)忙造成的,這樣可防止應(yīng)用程序?qū)魅氲倪B接進(jìn)行服務(wù)。這個條件在一個很短的時間內(nèi)可以改變。但如果服務(wù)器的TCP以系統(tǒng)復(fù)位作為響應(yīng),客戶進(jìn)程的主動打開將被廢棄(如果服務(wù)器程序沒有啟動我們就會遇到)。由于不應(yīng)答SYN,服務(wù)器程序迫使客戶TCP隨后重傳SYN,以等待連接隊列有空間接受新的連接。
這個例子中有一個巧妙之處,這在大多TCP/IP的具體實現(xiàn)中都能見到,就是如果服務(wù)器的連接隊列未滿時,TCP將接受傳入的連接請求(即SYN),但并不讓應(yīng)用層了解該連接源于何處(即不告知源IP地址和源端口)。這不是TCP所要求的,而只是共同的實現(xiàn)技術(shù)(如伯克利源代碼通常都這么做)。如果一個API如TLI(見1.15節(jié))向應(yīng)用程序提供了解連接請求的到來的方法,并允許應(yīng)用程序選擇是否接受連接。當(dāng)應(yīng)用程序假定被告知連接請求已經(jīng)到來時,TCP的三次握手已經(jīng)結(jié)束!其他運輸層的實現(xiàn)可能將連接請求的到達(dá)與接受分開(如OSI的運輸層),但TCP不是這樣。
Solaris 2.2提供了一個選項使TCP只有在應(yīng)用程序說可以接受(tcp_eager_listeners見E.4),才允許接受傳入的連接請求。
這種行為也意味著TCP服務(wù)器無法使客戶進(jìn)程的主動打開失效。當(dāng)一個新的客戶連接傳遞給服務(wù)器的應(yīng)用程序時,TCP的三次握手就結(jié)束了,客戶的主動打開已經(jīng)完全成功。如果服務(wù)器的應(yīng)用程序此時看到客戶的IP地址和端口號,并決定是否為該客戶進(jìn)行服務(wù),服務(wù)器所能做的就是關(guān)閉連接(發(fā)送FIN),或者復(fù)位連接(發(fā)送RST)。無論哪種情況,客戶進(jìn)程都認(rèn)為一切正常,因為它的主動打開已經(jīng)完成,并且已經(jīng)向服務(wù)器程序發(fā)送過請求。
18.12 小結(jié)
兩個進(jìn)程在使用TCP交換數(shù)據(jù)之前,它們之間必須建立一條連接。完成后,要關(guān)閉這個連接。本章已經(jīng)詳細(xì)介紹了如何使用三次握手來建立連接以及使用4個報文段來關(guān)閉連接。
我們用tcpdump程序顯示了TCP首部中的各個字段。也了解了連接建立是如何超時,連接復(fù)位是如何發(fā)送,使用半打開連接發(fā)生的情況以及TCP是如何提供半關(guān)閉、同時打開和同時關(guān)閉。
弄清TCP操作的關(guān)鍵在于它的狀態(tài)變遷圖。我們跟蹤了連接建立與關(guān)閉的步驟以及它們的狀態(tài)變遷過程。還討論了在設(shè)計TCP并發(fā)服務(wù)器時TCP連接建立的具體實現(xiàn)方法。
一個TCP連接由一個4元組唯一確定:本地IP地址、本地端口號、遠(yuǎn)端IP地址和遠(yuǎn)端端口號。無論何時關(guān)閉一個連接,一端必須保持這個連接,我們看到TIME_WAIT狀態(tài)將處理這個問題。處理的原則是執(zhí)行主動打開的一端在進(jìn)入這個狀態(tài)時要保持的時間為TCP實現(xiàn)中規(guī)定的MSL值的兩倍。