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

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

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

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

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

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

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

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

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

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

從sun向slip發(fā)起一個(gè)TCP連接,并使用tcpdump來(lái)觀(guān)察報(bào)文段。圖18-9顯示這個(gè)連接的建立(省略了通告窗口大小)。

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

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

在遠(yuǎn)端主機(jī)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ù)通過(guò)TCP連接從rsh客戶(hù)端傳送到sort服務(wù)器進(jìn)行排序。當(dāng)輸入(datafile)到達(dá)文件尾時(shí),rsh客戶(hù)端執(zhí)行這個(gè)TCP連接的半關(guān)閉。接著sort服務(wù)器在它的標(biāo)準(zhǔn)輸入(這個(gè)TCP連接)上收到一個(gè)文件結(jié)束符,對(duì)數(shù)據(jù)進(jìn)行排序,并將結(jié)果寫(xiě)在它的標(biāo)準(zhǔn)輸出上(TCP連接)。rsh客戶(hù)端繼續(xù)接收來(lái)自TCP連接另一端的數(shù)據(jù),并將排序的文件復(fù)制到它的標(biāo)準(zhǔn)輸出上。
沒(méi)有半關(guān)閉,需要其他的一些技術(shù)讓客戶(hù)通知服務(wù)器,客戶(hù)端已經(jīng)完成了它的數(shù)據(jù)傳送,但仍要接收來(lái)自服務(wù)器的數(shù)據(jù)。使用兩個(gè)TCP連接也可作為一個(gè)選擇,但使用半關(guān)閉的單連接更好。
18.6 TCP的狀態(tài)變遷圖
我們已經(jīng)介紹了許多有關(guān)發(fā)起和終止TCP連接的規(guī)則。這些規(guī)則都能從圖18-12所示的狀態(tài)變遷圖中得出。

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

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

當(dāng)重新啟動(dòng)服務(wù)器程序時(shí),程序報(bào)告一個(gè)差錯(cuò)信息說(shuō)明不能綁定它的熟知端口,因?yàn)樵摱丝谝驯皇褂茫此幱?MSL等待)。
運(yùn)行netstat程序來(lái)查看連接的狀態(tài),以證實(shí)它的確處于2MSL等待狀態(tài)。
如果我們一直試圖重新啟動(dòng)服務(wù)器程序,并測(cè)量它直到成功所需的時(shí)間,我們就能確定出2MSL值。對(duì)于SunOS 4.1.3、SVR4、BSD/386和AIX 3.2.2,它需要1分鐘才能重新啟動(dòng)服務(wù)器程序,這意味著它們的MSL值為30秒。而對(duì)于Solaris 2.2,它需要4分鐘才能重新啟動(dòng)服務(wù)器程序,這表示它的MSL值為2分鐘。
如果一個(gè)客戶(hù)程序試圖申請(qǐng)一個(gè)處于2MSL等待的端口(客戶(hù)程序通常不會(huì)這么做),就會(huì)出現(xiàn)同樣的差錯(cuò)。

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

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

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

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

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

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

圖18-16是這個(gè)例子的tcpdump輸出顯示(已從這個(gè)輸出中刪除了窗口大小的說(shuō)明、服務(wù)類(lèi)型信息和MSS聲明,因?yàn)樗鼈兣c討論無(wú)關(guān))。

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

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

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

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

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

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

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

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

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

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

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

這個(gè)連接請(qǐng)求將不會(huì)到達(dá)服務(wù)器的應(yīng)用程序,因?yàn)樗鶕?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ù)允許一個(gè)服務(wù)器在執(zhí)行被動(dòng)打開(kāi)時(shí),可指明遠(yuǎn)端插口(等待一個(gè)特定的客戶(hù)執(zhí)行主動(dòng)打開(kāi)),也可不指明遠(yuǎn)端插口(等待任何客戶(hù))。
遺憾的是,大多數(shù)API都不支持這么做。服務(wù)器必須不指明遠(yuǎn)端插口,而等待連接請(qǐng)求的到來(lái),然后檢查客戶(hù)端的IP地址和端口號(hào)。
圖18-22總結(jié)了TCP服務(wù)器進(jìn)行連接時(shí)三種類(lèi)型的地址綁定。在三種情況中,lport是服務(wù)器的熟知端口,而localIP必須是一個(gè)本地接口的IP地址。表中行的順序正是TCP模塊在收到一個(gè)連接請(qǐng)求時(shí)確定本地地址的順序。最常使用的綁定(第1行,如果支持的話(huà))將最先嘗試,最不常用的(最后一行兩端的IP地址都沒(méi)有制定)將最后嘗試。

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

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

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