Author : Kevin Lynx
準(zhǔn)備:
在這里本文將遵循上一篇文章的風(fēng)格,只提TCP協(xié)議中的要點(diǎn),這樣我覺(jué)得可以更容易地掌握TCP。或者
根本談不上掌握,對(duì)于這種純理論的東西,即使你現(xiàn)在掌握了再多的細(xì)節(jié),一段時(shí)間后也會(huì)淡忘。
在以后各種細(xì)節(jié)中,因?yàn)槲覀儠?huì)涉及到分析一些TCP中的數(shù)據(jù)報(bào),因此一個(gè)協(xié)議包截獲工具必不可少。在
<TCP/IP詳解>中一直使用tcpdump。這里因?yàn)槲业南到y(tǒng)是windows,所以只好使用windows平臺(tái)的tcpdump,
也就是WinDump。在使用WinDump之前,你需要安裝該程序使用的庫(kù)WinpCap。
關(guān)于WinDump的具體用法你可以從網(wǎng)上其他地方獲取,這里我只稍微提一下。要讓W(xué)inDump開(kāi)始監(jiān)聽(tīng)數(shù)據(jù),
首先需要確定讓其監(jiān)聽(tīng)哪一個(gè)網(wǎng)絡(luò)設(shè)備(或者說(shuō)是網(wǎng)絡(luò)接口)。你可以:
windump -D
獲取當(dāng)前機(jī)器上的網(wǎng)絡(luò)接口。然后使用:
windump -i 2
開(kāi)始對(duì)網(wǎng)絡(luò)接口2的數(shù)據(jù)監(jiān)聽(tīng)。windump如同tcpdump(其實(shí)就是tcpdump)一樣支持過(guò)濾表達(dá)式,windump
將會(huì)根據(jù)你提供的過(guò)濾表達(dá)式過(guò)濾不需要的網(wǎng)絡(luò)數(shù)據(jù)包,例如:
windump -i 2 port 4000
那么windump只會(huì)顯示端口號(hào)為4000的網(wǎng)絡(luò)數(shù)據(jù)。
序號(hào)和確認(rèn)號(hào):
要講解TCP的建立過(guò)程,也就是那個(gè)所謂的三次握手,就會(huì)涉及到序號(hào)和確認(rèn)號(hào)這兩個(gè)東西。翻書(shū)到TCP
的報(bào)文頭,有兩個(gè)很重要的域(都是32位)就是序號(hào)域和確認(rèn)號(hào)域。可能有些同學(xué)會(huì)對(duì)TCP那個(gè)報(bào)文頭有所
疑惑(能看懂我在講什么的會(huì)產(chǎn)生這樣的疑惑么?),這里我可以告訴你,你可以假想TCP的報(bào)文頭就是個(gè)
C語(yǔ)言結(jié)構(gòu)體(假想而已,去翻翻bsd對(duì)TCP的實(shí)現(xiàn),肯定沒(méi)這么簡(jiǎn)單),那么大致上,所謂的TCP報(bào)文頭就是:
typedef struct _tcp_header


{

/**//// 16位源端口號(hào)
unsigned short src_port;

/**//// 16位目的端口號(hào)
unsigned short dst_port;

/**//// 32位序號(hào)
unsigned long seq_num;

/**//// 32位確認(rèn)號(hào)
unsigned long ack_num;

/**//// 16位標(biāo)志位[4位首部長(zhǎng)度,保留6位,ACK、SYN之類(lèi)的標(biāo)志位]
unsigned short flag;

/**//// 16位窗口大小
unsigned short win_size;

/**//// 16位校驗(yàn)和
short crc_sum;

/**//// 16位緊急指針
short ptr;

/**//// 可選選項(xiàng)
/// how to implement this ?
} tcp_header;

那么,這個(gè)序號(hào)和確認(rèn)號(hào)是什么?TCP報(bào)文為每一個(gè)字節(jié)都設(shè)置一個(gè)序號(hào),覺(jué)得很奇怪?這里并不是為每一
字節(jié)附加一個(gè)序號(hào)(那會(huì)是多么可笑的編程手法?),而是為一個(gè)TCP報(bào)文附加一個(gè)序號(hào),這個(gè)序號(hào)表示報(bào)文
中數(shù)據(jù)的第一個(gè)字節(jié)的序號(hào),而其他數(shù)據(jù)則是根據(jù)離第一個(gè)數(shù)據(jù)的偏移來(lái)決定序號(hào)的,例如,現(xiàn)在有數(shù)據(jù):
abcd。如果這段數(shù)據(jù)的序號(hào)為1200,那么a的序號(hào)就是1200,b的序號(hào)就是1201。而TCP發(fā)送的下一個(gè)數(shù)據(jù)包
的序號(hào)就會(huì)是上一個(gè)數(shù)據(jù)包最后一個(gè)字節(jié)的序號(hào)加一。例如efghi是abcd的下一個(gè)數(shù)據(jù)包,那么它的序號(hào)就
是1204。通過(guò)這種看似簡(jiǎn)單的方法,TCP就實(shí)現(xiàn)了為每一個(gè)字節(jié)設(shè)置序號(hào)的功能(終于明白為什么書(shū)上要告訴
我們‘為每一個(gè)字節(jié)設(shè)置一個(gè)序號(hào)’了吧?)。注意,設(shè)置序號(hào)是一種可以讓TCP成為’可靠協(xié)議‘的手段。
TCP中各種亂七八糟的東西都是有目的的,大部分目的還是為了’可靠‘兩個(gè)字。別把TCP看高深了,如果
讓你來(lái)設(shè)計(jì)一個(gè)網(wǎng)絡(luò)協(xié)議,目的需要告訴你是’可靠的‘,你就會(huì)明白為什么會(huì)產(chǎn)生那些亂七八糟的東西了。
接著看,確認(rèn)號(hào)是什么?因?yàn)門(mén)CP會(huì)對(duì)接收到的數(shù)據(jù)包進(jìn)行確認(rèn),發(fā)送確認(rèn)數(shù)據(jù)包時(shí),就會(huì)設(shè)置這個(gè)確認(rèn)號(hào),
確認(rèn)號(hào)通常表示接收方希望接收到的下一段報(bào)文的序號(hào)。例如某一次接收方收到序號(hào)為1200的4字節(jié)數(shù)舉報(bào),
那么它發(fā)送確認(rèn)報(bào)文給發(fā)送方時(shí),就會(huì)設(shè)置確認(rèn)號(hào)為1204。
大部分書(shū)上在講確認(rèn)號(hào)和序號(hào)時(shí),都會(huì)說(shuō)確認(rèn)號(hào)是序號(hào)加一。這其實(shí)有點(diǎn)誤解人,所以我才在這里廢話了
半天(高手寬容下:D)。
開(kāi)始三次握手:
如果你還不會(huì)簡(jiǎn)單的tcp socket編程,我建議你先去學(xué)學(xué),這就好比你不會(huì)C++基本語(yǔ)法,就別去研究vtable
之類(lèi)。
三次握手開(kāi)始于客戶端試圖連接服務(wù)器端。當(dāng)你調(diào)用諸如connect的函數(shù)時(shí),正常情況下就會(huì)開(kāi)始三次握手。
隨便在網(wǎng)上找張三次握手的圖:
如前文所述,三次握手也就是產(chǎn)生了三個(gè)數(shù)據(jù)包。客戶端主動(dòng)連接,發(fā)送SYN被設(shè)置了的報(bào)文(注意序號(hào)和
確認(rèn)號(hào),因?yàn)檫@里不包含用戶數(shù)據(jù),所以序號(hào)和確認(rèn)號(hào)就是加一減一的關(guān)系)。服務(wù)器端收到該報(bào)文時(shí),正
常情況下就發(fā)送SYN和ACK被設(shè)置了的報(bào)文作為確認(rèn),以及告訴客戶端:我想打開(kāi)我這邊的連接(雙工)。客戶
端于是再對(duì)服務(wù)器端的SYN進(jìn)行確認(rèn),于是再發(fā)送ACK報(bào)文。然后連接建立完畢。對(duì)于阻塞式socket而言,你
的connect可能就返回成功給你。
在進(jìn)行了鋪天蓋地的羅利巴索的基礎(chǔ)概念的講解后,看看這個(gè)連接建立的過(guò)程,是不是簡(jiǎn)單得幾近無(wú)聊?
我們來(lái)實(shí)際點(diǎn),寫(xiě)個(gè)最簡(jiǎn)單的客戶端代碼:
sockaddr_in addr;
memset( &addr, 0, sizeof( addr ) );
addr.sin_family = AF_INET;
addr.sin_port = htons( 80 );

/**//// 220.181.37.55
addr.sin_addr.s_addr = inet_addr( "220.181.37.55" );
printf( "%s : connecting to server.\n", _str_time() );
int err = connect( s, (sockaddr*) &addr, sizeof( addr ) );

主要就是connect。運(yùn)行程序前我們運(yùn)行windump:
windump -i 2 host 220.181.37.55
00:38:22.979229 IP noname.domain.4397 > 220.181.37.55.80: S 2523219966:2523219966(0) win 65535 <mss 1460,nop,nop,sackOK>
00:38:23.024254 IP 220.181.37.55.80 > noname.domain.4397: S 1277008647:1277008647(0) ack 2523219967 win 2920 <mss 1440,nop,nop,sackOK>
00:38:23.024338 IP noname.domain.4397 > 220.181.37.55.80: . ack 1 win 65535


如何分析windump的結(jié)果,建議參看<tcp/ip詳解>中對(duì)于tcpdump的描述。
建立連接的附加信息:
雖然SYN、ACK之類(lèi)的報(bào)文沒(méi)有用戶數(shù)據(jù),但是TCP還是附加了其他信息。最為重要的就是附加的MSS值。這個(gè)
可以被協(xié)商的MSS值基本上就只在建立連接時(shí)協(xié)商。如以上數(shù)據(jù)表示,MSS為1460字節(jié)。
連接的意外:
連接的意外我大致分為兩種情況(也許還有更多情況):目的主機(jī)不可達(dá)、目的主機(jī)并沒(méi)有在指定端口監(jiān)聽(tīng)。
當(dāng)目的主機(jī)不可達(dá)時(shí),也就是說(shuō),SYN報(bào)文段根本無(wú)法到達(dá)對(duì)方(如果你的機(jī)器根本沒(méi)插網(wǎng)線,你就不可達(dá)),
那么TCP收不到任何回復(fù)報(bào)文。這個(gè)時(shí)候,你會(huì)看到TCP中的定時(shí)器機(jī)制出現(xiàn)了。TCP對(duì)發(fā)出的SYN報(bào)文進(jìn)行
計(jì)時(shí),當(dāng)在指定時(shí)間內(nèi)沒(méi)有得到回復(fù)報(bào)文時(shí),TCP就會(huì)重傳剛才的SYN報(bào)文。通常,各種不同的TCP實(shí)現(xiàn)對(duì)于
這個(gè)超時(shí)值都不同,但是據(jù)我觀察,重傳次數(shù)基本上都是3次。例如,我連接一個(gè)不可達(dá)的主機(jī):
12:39:50.560690 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
12:39:53.538734 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
12:39:59.663726 IP cd-zhangmin.1573 > 220.181.37.55.1024: S 3117975575:3117975575(0) win 65535 <mss 1460,nop,nop,sackOK>
發(fā)出了三個(gè)序號(hào)一樣的SYN報(bào)文,但是沒(méi)有得到一個(gè)回復(fù)報(bào)文(廢話)。每一個(gè)SYN報(bào)文之間的間隔時(shí)間都是
有規(guī)律的,在windows上是3秒6秒9秒12秒。上面的數(shù)據(jù)你看不到12秒這個(gè)數(shù)據(jù),因?yàn)檫@是第三個(gè)報(bào)文發(fā)出的
時(shí)間和connect返回錯(cuò)誤信息時(shí)的時(shí)間之差。另一方面,如果連接同一個(gè)網(wǎng)絡(luò),這個(gè)間隔時(shí)間又不同。例如
直接連局域網(wǎng),間隔時(shí)間就差不多為500ms。
(我強(qiáng)烈建議你能運(yùn)行windump去試驗(yàn)這里提到的每一個(gè)現(xiàn)象,如果你在ubuntu下使用tcpdump,記住sudo :D)
出現(xiàn)意外的第二種情況是如果主機(jī)數(shù)據(jù)包可達(dá),但是試圖連接的端口根本沒(méi)有監(jiān)聽(tīng),那么發(fā)送SYN報(bào)文的這
方會(huì)收到RST被設(shè)置的報(bào)文(connect也會(huì)返回相應(yīng)的信息給你),例如:
13:37:22.202532 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:22.202627 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 2417354282 win 0
13:37:22.711415 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:22.711498 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 1 win 0
13:37:23.367733 IP cd-zhangmin.1658 > 7AURORA-CCTEST.7100: S 2417354281:2417354281(0) win 65535 <mss 1460,nop,nop,sackOK>
13:37:23.367826 IP 7AURORA-CCTEST.7100 > cd-zhangmin.1658: R 0:0(0) ack 1 win 0
可以看出,7AURORA-CCTEST.7100返回了RST報(bào)文給我,但是我這邊根本不在乎這個(gè)報(bào)文,繼續(xù)發(fā)送SYN報(bào)文。
三次過(guò)后connect就返回了。(數(shù)據(jù)反映的事實(shí)是這樣)
關(guān)于listen:
TCP服務(wù)器端會(huì)維護(hù)一個(gè)新連接的隊(duì)列。當(dāng)新連接上的客戶端三次握手完成時(shí),就會(huì)將其放入這個(gè)隊(duì)列。這個(gè)隊(duì)
列的大小是通過(guò)listen設(shè)置的。當(dāng)這個(gè)隊(duì)列滿時(shí),如果有新的客戶端試圖連接(發(fā)送SYN),服務(wù)器端丟棄報(bào)文,
同時(shí)不做任何回復(fù)。
總結(jié):
TCP連接的建立的相關(guān)要點(diǎn)就是這些(or more?)。正常情況下就是三次握手,非正常情況下就是SYN三次超時(shí),
以及收到RST報(bào)文卻被忽略。