傳輸層:TCP協(xié)議
TCP 和 UDP 處在同一層——傳輸層,但是它們有很多的不同。TCP 是 TCP/IP 系列協(xié)議中最復(fù)雜的部分,它具有以下特點(diǎn):
TCP 提供 可靠的 數(shù)據(jù)傳輸服務(wù),TCP 是 面向連接的 。應(yīng)用程序在使用 TCP 通信之前,先要建立連接,這是一個(gè)類(lèi)似“打電話”的過(guò)程,通信結(jié)束后還要“掛電話”。
TCP 連接是 點(diǎn)對(duì)點(diǎn) 的,一條 TCP 連接只能連接兩個(gè)端點(diǎn)。
TCP 提供可靠傳輸,無(wú)差錯(cuò)、不丟失、不重復(fù)、按順序。
TCP 提供 全雙工 通信,允許通信雙方任何時(shí)候都能發(fā)送數(shù)據(jù),因?yàn)?TCP 連接的兩端都設(shè)有發(fā)送緩存和接收緩存。
TCP 面向 字節(jié)流 。TCP 并不知道所傳輸?shù)臄?shù)據(jù)的含義,僅把數(shù)據(jù)看作一連串的字節(jié)序列,它也不保證接收方收到的數(shù)據(jù)塊和發(fā)送方發(fā)出的數(shù)據(jù)塊具有大小對(duì)應(yīng)關(guān)系。
使用 netstat -s 查看數(shù)據(jù)包統(tǒng)計(jì)信息:
以下截圖截取 tcp 部分

截圖中每行所表示含義依次是:主動(dòng)開(kāi)放的連接數(shù),被動(dòng)開(kāi)放的連接數(shù),失敗的連接嘗試,重置連接數(shù),當(dāng)前連接數(shù),接收的 分段數(shù),發(fā)送的分段數(shù),重新傳輸?shù)姆侄螖?shù)。
TCP 報(bào)文段結(jié)構(gòu)
TCP 是面向字節(jié)流的,而 TCP 傳輸數(shù)據(jù)的單元是 報(bào)文段 。一個(gè) TCP 報(bào)文段可分為兩部分:報(bào)頭和數(shù)據(jù)部分。數(shù)據(jù)部分是上層應(yīng)用交付的數(shù)據(jù),而報(bào)頭則是 TCP 功能的關(guān)鍵。
TCP 報(bào)文段的報(bào)頭有前 20 字節(jié)的固定部分,后面 4n 字節(jié)是根據(jù)需要而添加的字段。如圖則是 TCP 報(bào)文段結(jié)構(gòu):

20 字節(jié)的固定部分,各字段功能說(shuō)明:
源端口和目的端口:各占 2 個(gè)字節(jié),分別寫(xiě)入源端口號(hào)和目的端口號(hào)。這和 UDP 報(bào)頭有類(lèi)似之處,因?yàn)槎际莻鬏攲訁f(xié)議。
序號(hào):占 4 字節(jié)序,序號(hào)范圍[0,2^32-1],序號(hào)增加到 2^32-1 后,下個(gè)序號(hào)又回到 0。TCP 是面向字節(jié)流的,通過(guò) TCP 傳送的字節(jié)流中的每個(gè)字節(jié)都按順序編號(hào),而報(bào)頭中的序號(hào)字段值則指的是本報(bào)文段數(shù)據(jù)的第一個(gè)字節(jié)的序號(hào)。
確認(rèn)序號(hào):占 4 字節(jié),期望收到對(duì)方下個(gè)報(bào)文段的第一個(gè)數(shù)據(jù)字節(jié)的序號(hào)。
數(shù)據(jù)偏移:占 4 位,指 TCP 報(bào)文段的報(bào)頭長(zhǎng)度,包括固定的 20 字節(jié)和選項(xiàng)字段。
保留:占 6 位,保留為今后使用,目前為 0。
控制位:共有 6 個(gè)控制位,說(shuō)明本報(bào)文的性質(zhì),意義如下:
URG 緊急:當(dāng) URG=1 時(shí),它告訴系統(tǒng)此報(bào)文中有緊急數(shù)據(jù),應(yīng)優(yōu)先傳送(比如緊急關(guān)閉),這要與緊急指針字段配合使用。
ACK 確認(rèn):僅當(dāng) ACK=1 時(shí)確認(rèn)號(hào)字段才有效。建立 TCP 連接后,所有報(bào)文段都必須把 ACK 字段置為 1。
PSH 推送:若 TCP 連接的一端希望另一端立即響應(yīng),PSH 字段便可以“催促”對(duì)方,不再等到緩存區(qū)填滿才發(fā)送。
RST復(fù)位:若 TCP 連接出現(xiàn)嚴(yán)重差錯(cuò),RST 置為 1,斷開(kāi) TCP 連接,再重新建立連接。
SYN 同步:用于建立和釋放連接,稍后會(huì)詳細(xì)介紹。
FIN 終止:用于釋放連接,當(dāng) FIN=1,表明發(fā)送方已經(jīng)發(fā)送完畢,要求釋放 TCP 連接。
窗口:占 2 個(gè)字節(jié)。窗口值是指發(fā)送者自己的接收窗口大小,因?yàn)榻邮站彺娴目臻g有限。
檢驗(yàn)和:2 個(gè)字節(jié)。和 UDP 報(bào)文一樣,有一個(gè)檢驗(yàn)和,用于檢查報(bào)文是否在傳輸過(guò)程中出差錯(cuò)。
緊急指針:2 字節(jié)。當(dāng) URG=1 時(shí)才有效,指出本報(bào)文段緊急數(shù)據(jù)的字節(jié)數(shù)。
選項(xiàng):長(zhǎng)度可變,最長(zhǎng)可達(dá) 40 字節(jié)。具體的選項(xiàng)字段,需要時(shí)再做介紹。
還記得我們?cè)?IP 網(wǎng)際協(xié)議抓取的報(bào)文嗎?我們下面再用 tcpdump 命令試著抓取一下。
sudo tcpdump -ntx -C 1

其實(shí)輸出結(jié)果中還包含著 TCP 協(xié)議的報(bào)文,試著回顧一下,相信你能很快找到哪部分是 IP 協(xié)議的首部。IP 報(bào)文頭緊接著的一部分就是 TCP 報(bào)文頭,從 170d 開(kāi)始。
源端口:0x170d,轉(zhuǎn)換為十進(jìn)制為 5901。
目的端口:0x9d86,即為 40326。
序號(hào):0xba42638b,即為 3124913035,這和圖中開(kāi)頭的 seq 是一致的。
確認(rèn)序號(hào):0x4c1ad749,即為 1276827465,這和圖中開(kāi)頭的 ack 是一致的。
數(shù)據(jù)偏移:0x8,8*4=32B。
其他可依次類(lèi)推。
連接的建立與釋放
剛才說(shuō)過(guò),TCP 是面向連接的,在傳輸 TCP 報(bào)文段之前先要?jiǎng)?chuàng)建連接,發(fā)起連接的一方被稱(chēng)為客戶端,而響應(yīng)連接請(qǐng)求的一方被稱(chēng)為服務(wù)端,而這個(gè)創(chuàng)建連接的過(guò)程被稱(chēng)為三次握手:

客戶端發(fā)出請(qǐng)求連接報(bào)文段,其中報(bào)頭控制位 SYN=1,初始序號(hào) seq=x??蛻舳诉M(jìn)入 SYN-SENT(同步已發(fā)送)狀態(tài)。
服務(wù)端收到請(qǐng)求報(bào)文段后,向客戶端發(fā)送確認(rèn)報(bào)文段。確認(rèn)報(bào)文段的首部中 SYN=1,ACK=1,確認(rèn)號(hào)是 ack=x+1,同時(shí)為自己選擇一個(gè)初始序號(hào) seq=y。服務(wù)端進(jìn)入 SYN-RCVD(同步收到)狀態(tài)。
客戶端收到服務(wù)端的確認(rèn)報(bào)文段后,還要給服務(wù)端發(fā)送一個(gè)確認(rèn)報(bào)文段。這個(gè)報(bào)文段中 ACK=1,確認(rèn)號(hào) ack=y+1,而自己的序號(hào) seq=x+1。這個(gè)報(bào)文段已經(jīng)可以攜帶數(shù)據(jù),如果不攜帶數(shù)據(jù)則不消耗序號(hào),則下一個(gè)報(bào)文段序號(hào)仍為 seq=x+1。
至此 TCP 連接已經(jīng)建立,客戶端進(jìn)入 ESTABLISHED(已建立連接)狀態(tài),當(dāng)服務(wù)端收到確認(rèn)后,也進(jìn)入 ESTABLISHED 狀態(tài),它們之間便可以正式傳輸數(shù)據(jù)了。
當(dāng)傳輸數(shù)據(jù)結(jié)束后,通信雙方都可以釋放連接,這個(gè)釋放連接過(guò)程被稱(chēng)為釋放連接:

此時(shí) TCP 連接兩端都還處于 ESTABLISHED 狀態(tài),客戶端停止發(fā)送數(shù)據(jù),并發(fā)出一個(gè) FIN 報(bào)文段。首部 FIN=1,序號(hào) seq=u(u 等于客戶端傳輸數(shù)據(jù)最后一字節(jié)的序號(hào)加 1)??蛻舳诉M(jìn)入 FIN-WAIT-1(終止等待 1)狀態(tài)。
服務(wù)端回復(fù)確認(rèn)報(bào)文段,確認(rèn)號(hào) ack=u+1,序號(hào) seq=v(v 等于服務(wù)端傳輸數(shù)據(jù)最后一字節(jié)的序號(hào)加 1),服務(wù)端進(jìn)入 CLOSE-WAIT(關(guān)閉等待)狀態(tài)?,F(xiàn)在 TCP 連接處于半開(kāi)半閉狀態(tài),服務(wù)端如果繼續(xù)發(fā)送數(shù)據(jù),客戶端依然接收。
客戶端收到確認(rèn)報(bào)文,進(jìn)入 FIN-WAIT-2 狀態(tài),服務(wù)端發(fā)送完數(shù)據(jù)后,發(fā)出 FIN 報(bào)文段,F(xiàn)IN=1,確認(rèn)號(hào) ack=u+1,然后進(jìn)入 LAST-ACK(最后確認(rèn))狀態(tài)。
客戶端回復(fù)確認(rèn)報(bào)文段,ACK=1,確認(rèn)號(hào) ack=w+1(w 為半開(kāi)半閉狀態(tài)時(shí),收到的最后一個(gè)字節(jié)數(shù)據(jù)的編號(hào)) ,序號(hào) seq=u+1,然后進(jìn)入 TIME-WAIT(時(shí)間等待)狀態(tài)。
注意此時(shí)連接還沒(méi)有釋放,需要時(shí)間等待狀態(tài)結(jié)束后(4 分鐘)連接兩端才會(huì) CLOSED。設(shè)置時(shí)間等待是因?yàn)?,有可能最后一個(gè)確認(rèn)報(bào)文丟失而需要重傳。
我們使用 tcpdump 命令抓包來(lái)理解握手過(guò)程。首先在終端輸入如下命令:
sudo tcpdump -S host 192.168.42.3 and 115.29.233.149
注意:此處的 192.168.42.3 要根據(jù)你自己的環(huán)境修改,為內(nèi)網(wǎng) ip,可以通過(guò) ifconfig eth0 查看。115.29.233.149 是實(shí)驗(yàn)樓的網(wǎng)址,需要通過(guò) nslookup www.shiyanlou 命令查詢(xún)最新的 IP 地址。
命令目的為抓取本機(jī)到 www.lanqiao.cn 的數(shù)據(jù)包;
-S 參數(shù)的目的是獲得 ack 的絕對(duì)值。
然后使用瀏覽器訪問(wèn) www.shiyanlou.com。再回到終端,可以看到如下輸出:

輸出中展示了三次握手的過(guò)程。紅色為第一次,黃框是第二次,綠框是第三次,試著根據(jù)上面介紹的握手過(guò)程來(lái)對(duì)照 seq 和 ack 值的變化。
TCP 可靠傳輸?shù)膶?shí)現(xiàn)
TCP 報(bào)文段的長(zhǎng)度可變,根據(jù)收發(fā)雙方的緩存狀態(tài)、網(wǎng)絡(luò)狀態(tài)而調(diào)整。
當(dāng) TCP 收到發(fā)自 TCP 連接另一端的數(shù)據(jù),它將發(fā)送一個(gè)確認(rèn)。
當(dāng) TCP 發(fā)出一個(gè)報(bào)文段后,它啟動(dòng)一個(gè)定時(shí)器,等待目的端確認(rèn)收到這個(gè)報(bào)文段,如果不能及時(shí)收到一個(gè)確認(rèn),將重發(fā)這個(gè)報(bào)文段。這就是稍后介紹的超時(shí)重傳。
TCP 將保持它首部和數(shù)據(jù)的檢驗(yàn)和。如果通過(guò)檢驗(yàn)和發(fā)現(xiàn)報(bào)文段有差錯(cuò),這個(gè)報(bào)文段將被丟棄,等待超時(shí)重傳。
TCP 將數(shù)據(jù)按字節(jié)排序,報(bào)文段中有序號(hào),以確保順序的正確性。
TCP 還能提供流量控制。TCP 連接的每一方都有收發(fā)緩存。TCP 的接收端只允許另一端發(fā)送接收端緩沖區(qū)所能接納的數(shù)據(jù)。這將防止較快主機(jī)致使較慢主機(jī)的緩沖區(qū)溢出。
可見(jiàn)超時(shí)重發(fā)機(jī)制是 TCP 可靠性的關(guān)鍵,只要沒(méi)有得到確認(rèn)報(bào)文段,就重新發(fā)送數(shù)據(jù)報(bào),直到收到對(duì)方的確認(rèn)為止。
超時(shí)重傳
TCP 規(guī)定,接收者收到數(shù)據(jù)報(bào)文段后,需回復(fù)一個(gè)確認(rèn)報(bào)文段,以告知發(fā)送者數(shù)據(jù)已經(jīng)收到。而發(fā)送者如果一段時(shí)間內(nèi)(超時(shí)計(jì)時(shí)器)沒(méi)有收到確認(rèn)報(bào)文段,便重復(fù)發(fā)送。
為了實(shí)現(xiàn)超時(shí)間重傳,需要注意:
發(fā)送者發(fā)送一個(gè)報(bào)文段后,暫時(shí)保存該報(bào)文段的副本,為發(fā)生超時(shí)重傳時(shí)使用,收到確認(rèn)報(bào)文后刪除該報(bào)文段。
確認(rèn)報(bào)文段也需要序號(hào),才能明確是發(fā)出去的哪個(gè)數(shù)據(jù)報(bào)得到了確認(rèn)。
超時(shí)計(jì)時(shí)器比傳輸往返時(shí)間略長(zhǎng),但具體值是不確定的,根據(jù)網(wǎng)絡(luò)情況而變。
連續(xù) ARQ 協(xié)議
也許你也發(fā)現(xiàn)了,按上面的介紹,超時(shí)重傳機(jī)制很費(fèi)時(shí)間,每發(fā)送一個(gè)數(shù)據(jù)報(bào)都要等待確認(rèn)。
在實(shí)際應(yīng)用中的確不是這樣的,真實(shí)情況是,采用了流水線傳輸:發(fā)送方可以連續(xù)發(fā)送多個(gè)報(bào)文段(連續(xù)發(fā)送的數(shù)據(jù)長(zhǎng)度叫做窗口),而不必每發(fā)完一段就停下來(lái)等待確認(rèn)。
實(shí)際應(yīng)用中,接收方也不必對(duì)收到的每個(gè)報(bào)文都做回復(fù),而是采用累積確認(rèn)方式:接收者收到多個(gè)連續(xù)的報(bào)文段后,只回復(fù)確認(rèn)最后一個(gè)報(bào)文段,表示在這之前的數(shù)據(jù)都已收到。
這樣,傳輸效率得到了很大的提升。

流量控制和擁塞控制
由于接收方緩存的限制,發(fā)送窗口不能大于接收方接收窗口。在報(bào)文段首部有一個(gè)字段就叫做窗口(rwnd),這便是用于告訴對(duì)方自己的接收窗口,可見(jiàn)窗口的大小是可以變化的。
那么窗口的大小是如何變化的呢?TCP 對(duì)于擁塞的控制總結(jié)為“慢啟動(dòng)、加性增、乘性減”,如圖所示:

慢啟動(dòng) :初始的窗口值很小,但是按指數(shù)規(guī)律漸漸增長(zhǎng),直到達(dá)到慢開(kāi)始門(mén)限(ssthresh)。
加性增 :窗口值達(dá)到慢開(kāi)始門(mén)限后,每發(fā)送一個(gè)報(bào)文段,窗口值增加一個(gè)單位量。
乘性減 :無(wú)論什么階段,只要出現(xiàn)超時(shí),則把窗口值減小一半。
tcpdump 抓取 TCP 報(bào)文段
上一節(jié)實(shí)驗(yàn),我們用 tcpdump 抓取并閱讀了 UDP 報(bào)文,那么這次我們嘗試抓取 TCP 報(bào)文段。
針對(duì)這次實(shí)驗(yàn),需要下載對(duì)應(yīng)的代碼文件,是基于 TCP 的聊天小程序,分為 server(服務(wù)端—)和 client(客戶端):
wget https://labfile.oss.aliyuncs.com/courses/98/client.c
wget https://labfile.oss.aliyuncs.com/courses/98/server.c
gcc -o server server.c
gcc -o client client.c
編譯完成后先不要運(yùn)行,先打開(kāi) tcpdump,使用命令安裝并運(yùn)行 tcpdump:
sudo apt-get update
sudo apt-get install tcpdump
sudo tcpdump -vvv -X -i lo tcp port 7777
新開(kāi)一個(gè)終端,運(yùn)行 server 程序:
./server 127.0.0.1
然后再新開(kāi)第三個(gè)終端,運(yùn)行 client 程序:
./client 127.0.0.1
現(xiàn)在,使用 client 和 server 聊天,輪流互發(fā)幾條簡(jiǎn)短的消息(比如 hello、hi、wei 之類(lèi)的)便可以關(guān)閉 client 和 server,回到運(yùn)行 tcpdump 的終端查看抓取的報(bào)文段內(nèi)容:

通過(guò)抓取的報(bào)文,還可以清晰的看到建立連接三次握手和斷開(kāi)連接四次握手的過(guò)程。
作業(yè)
按實(shí)驗(yàn)步驟,使用 tcpdump 抓取 TCP 報(bào)文段,觀察三次握手建立連接和四次握手釋放連接的過(guò)程。
使用實(shí)驗(yàn)給出的聊天程序收發(fā)消息,用 tcpdump 抓取包含聊天內(nèi)容的 TCP 報(bào)文段,解讀聊天內(nèi)容。