TCP/IP面試要點淺析
一.TCP基本認識
1.TCP頭部格式
- 序列號
- 在建立連接時由計算機生成的隨機數作爲初始值,通過SYN包傳輸給接收端
- 每發送一次數據,就
累加
一次該數據字節數的大小 - 用來解決
網絡包亂序
的問題
- 確認應答號
- 指下次
期望
收到數據的序列號 - 發送端收到這個確認應答號以後,可以認爲在這個號以前的數據都已經被正常接收
- 用來解決
丟包
問題
- 指下次
- 控制位
- ACK
- 該位爲1時,
確認應答
的字段變爲有效 - TCP 規定除了最初建立連接時的
SYN
包之外該位必須設置爲1
- 該位爲1時,
- RST
- 該位爲1時,表示TCP鏈接中出現異常,必須強制斷開連接
- SYN
- 該位爲1時,表示希望建立連接,並捨得序列號字段初始值
- FIN
- 該位爲1時,表示今後不會再有數據發送,希望斷開連接
- 當通信希望斷開連接時,通信雙方就可以相互交換FIN位爲1的TCP段
- ACK
2.什麼是TCP?什麼是TCP連接?如何確定一個TCP連接?
什麼是TCP?
TCP是面向連接的
,可靠的
,基於字節流
的傳輸層通信協議
- 面向連接:一對一的連接,不能向UDP那樣一對多
- 可靠的:無論網絡怎麼變化,TCP都保證一個報文一定能到達接收端
- 基於字節流:消息是
無邊界的
,無論消息多大都可以傳輸,並且消息是有序的
,當前一個消息沒有收到的時候,即使先收到了後面的字節,那麼也不能扔給應用層去處理,同時對重複的報文會自動丟棄。
什麼是TCP連接?
TCP連接:用於保證可靠性和流量控制維護的某些狀態信息,這些信息的組合叫做連接(包括socket,序列號,窗口大小)
如何確定一個TCP連接?
四元組
- 源地址
- 源端口
- 目標地址
- 目標端口
源地址和目標地址是在IP頭部中,作用是通過IP協議發送報文給對方主機
源端口和目標端口是在TCP頭部中,作用是告訴TCP協議應該把報文發送給哪個進程
3.爲什麼需要TCP?TCP工作在哪一層?
TCP工作在傳輸層
因爲網絡層是不可靠的,如果需要保證網絡數據包的可靠性,那麼就需要有傳輸層的TCP來保證
因爲TCP是一個工作在傳輸層的可靠數據傳輸服務,他能確保接收端接收的網絡包是無損壞,無間隔,非冗餘和按序的
4.TCP最大連接數如何計算?實際受哪些因素影響?
理論上,TCP最大連接數=客戶端IP數(2的32次方)*客戶端端口數(2的16次方)
實際上TCP的最大連接數遠不能達到理論上限,受以下因素影響:
- 文件描述符fd數量限制:每個tcp連接都是一個文件,Linux對文件描述符做了三個方面的限制
- 系統級:當前系統可打開的fd最大數量
- 用戶級:當前用戶可打開的fd最大數量
- 進程級:當前進程可打開的fd最大數量
- 內存大小限制:每個tcp都要佔用一定內存,內存有限,被佔滿後會OOM
5.UDP和TCP有什麼區別?分別對應什麼應用場景?
區別如下:
1.連接
- TCP傳輸數據前要先建立連接
- UDP不需要建立連接,即刻傳輸數據
2.服務對象的數量
- TCP僅支持一對一
- UDP支持一對一,一對多,多對多
3.可靠性
- TCP是可靠的
- UDP是不可靠的,盡最大努力交付
4.控制機制
- TCP有擁塞控制和流量控制
- UDP沒有,即使網絡很堵,UPDDR發送速率也不會變
5.首部大小
- TCP首部較長,大小會變
- UDP首部僅8個字節,固定不變,開銷小
6.傳輸方式
- TCP是流式傳輸,沒有邊界,但保證順序和可靠
- UDP是一個一個包發送,有邊界,會丟包和亂序
應用場景:
TCP
-
HTTP協議:超文本傳輸協議,用於普通瀏覽
-
HTTPS協議:安全超文本傳輸協議,身披SSL外衣的HTTP協議
-
FTP協議:文件傳輸協議,用於文件傳輸
-
POP3協議:郵局協議,收郵件使用
-
SMTP協議:簡單郵件傳輸協議,用來發送電子郵件
-
Telent協議:遠程登陸協議,通過一個終端登陸到網絡
-
SSH協議:安全外殼協議,用於加密安全登陸,替代安全性差的Telent協議
UDP
-
包總量少的通信,比如DNS
-
音頻視頻等多媒體通信
-
廣播通信
-
DHCP協議:動態主機配置協議,動態配置IP地址
-
NTP協議:網絡時間協議,用於網絡時間同步
-
BOOTP協議:引導程序協議,DHCP協議的前身,用於無盤工作站從中心服務器上獲取IP地址
6.TCP協議的11種狀態分別代表什麼意思?
-
CLOSED狀態:初始狀態,表示TCP連接是“關閉的”或者“未打開的”
-
LISTEN狀態:表示服務端的某個端口正處於監聽狀態,正在等待客戶端連接的到來
-
SYN_SENT狀態:當客戶端發送SYN請求建立連接之後,客戶端處於SYN_SENT狀態,等待服務器發送SYN+ACK
-
SYN_RCVD狀態:當服務器收到來自客戶端的連接請求SYN之後,服務器處於SYN_RCVD狀態,在接收到SYN請求之後會向客戶端回覆一個SYN+ACK的確認報文
-
ESTABLISED狀態:當客戶端回覆服務器一個ACK和服務器收到該ACK(TCP最後一次握手)之後,服務器和客戶端都處於該狀態,表示TCP連接已經成功建立
-
FIN_WAIT_1狀態:當數據傳輸期間當客戶端想斷開連接,向服務器發送了一個FIN之後,客戶端處於該狀態
-
FIN_WAIT_2狀態:當客戶端收到服務器發送的連接斷開確認ACK之後,客戶端處於該狀態
-
CLOSE_WAIT狀態:當服務器發送連接斷開確認ACK之後但是還沒有發送自己的FIN之前的這段時間,服務器處於該狀態
-
TIME_WAIT狀態:當客戶端收到了服務器發送的FIN並且發送了自己的ACK之後,客戶端處於該狀態,關於TIME_WAIT狀態在整個TCP體系中的作用,請參考:https://www.cnblogs.com/yinbiao/p/10945836.html
-
LAST_ACK狀態:表示被動關閉的一方(比如服務器)在發送FIN之後,等待對方的ACK報文時,就處於該狀態
-
CLOSING狀態:連接斷開期間,一般是客戶端發送一個FIN,然後服務器回覆一個ACK,然後服務器發送完數據後再回復一個FIN,當客戶端和服務器同時接受到FIN時,客戶端和服務器處於CLOSING狀態,也就是此時雙方都正在關閉同一個連接
二.TCP連接建立
1.TCP三次握手的過程和狀態變遷
服務端監聽端口,客戶端主動發起TCP請求
整個過程:
第一次握手:客戶端發SYN報文
第二次握手:服務端發SYN+ACK報文
第三次握手:客戶端發ACK報文
下面分別闡述一下每次握手的報文是如何構造的
- 第一次:SYN報文
- 客戶端隨機初始化
序列號字段
,SYN標誌位爲1,表示此報文是SYN報文 - 把SYN報文發送給服務端,表示向服務端發起連接,然後客戶端處於
SYN-SENT
狀態 - 此報文不包含應用層數據
- 客戶端隨機初始化
- 第二次:SYN+ACK報文
- 服務端收到客戶端的SYN報文後,也構建一個報文發過去,然後服務端處於
SYN-RCVD
狀態隨機初始化序列號
,確認應答號是客戶端SYN報文的序列號+1
- SYN和ACK標誌位置爲1
- 此報文也不包含應用層數據
- 服務端收到客戶端的SYN報文後,也構建一個報文發過去,然後服務端處於
- 第三次:ACK報文
- 客戶端收到服務端報文後,還要向服務端迴應最後一個應答ACK報文,按如下要求構建報文
- ACK標誌位置爲1
- 確認應答號是收到的服務端報文的序列號+1
- 將構建好的報文發送給服務端,之後客戶端處於
ESTABLISHED
狀態,服務器收到報文後,也處於此狀態 - 此處報文可以攜帶應用層數據
- 客戶端收到服務端報文後,還要向服務端迴應最後一個應答ACK報文,按如下要求構建報文
第三次握手可以攜帶應用數據,前兩次不可以
一旦完成三次握手,雙方都處於ESTABLISHED
狀態,此時連接就已建立完成,客戶端和服務端就可以相互發送數據了
2.爲什麼握手需要三次?2次不行嗎?
2次不行
第一個原因:三次握手可以阻止歷史連接的初始化
客戶端連續多次發送請求建立連接的SYN報文,在網絡堵塞情況下:
- 一箇舊的SYN報文比最新的SYN報文提早到達了服務端
- 那麼此時服務端會回一個SYN+ACK報文
- 客戶端收到服務端的SYN+ACK報文後,可以根據自身的上下文,判斷這是一個歷史連接(序列號過期或超時),那麼客戶端就會發送RST報文給服務端,表示終止這一次連接
而如果只有兩次握手,則無法處理歷史連接的情況
原因2:可靠的同步序列號
序列號在TCP中十分重要:
- 接收方可以去重重複數據
- 接收方可以根據序列號按序接收
- 可以標識發出去的數據包,哪些是已經收到的
如果只有兩次握手,那麼其只保證了一方的序列號已經被另一方接收,沒辦法保證雙方的初始序列號都能被確認接收
原因三:避免資源浪費
如果只有兩次握手,那麼一發SYN報文就建立連接,如果重複發了多次SYN報文,那麼就會建立多個冗餘的無效連接,造成不必要的資源浪費
3.爲什麼每次建立TCP連接時,初始化的序列號都要求不一樣呢?
-
防止歷史報文被被下一個相同的四元組接收
如果序列號每次都一樣:
- 客戶端和服務端建立一個tpc連接,客戶端發送時網絡阻塞了,服務端進程重啓了,服務端會發送RST報文來斷開連接
- 緊接着,客戶端又和服務端建立了與上一個連接相同四元組的連接
- 在新連接建立完成後,上一個連接中被網絡阻塞的數據包正好抵達了服務端,剛好該數據包的序列號正好在服務端的接收窗口內,所以該數據包會被服務端正常接收,就會造成數據混亂
-
爲了安全性,防止黑客僞造相同序列號的TCP報文被對方正常接收
4.第一次握手丟失了,會發生什麼?
- 客戶端觸發超時重傳機制,重傳第一次握手的SYN報文
第一次握手:客戶端給服務端發SYN報文,然後客戶端處於SYN—SENT
狀態。
當此報文丟失後,服務端沒有收到此報文,就不會發SYN-ACK報文,客戶端就會觸發超時重傳機制,重傳SYN報文
Linux中,超時重傳次數由內核控制,默認是五次
通常,第一次超時重傳是在一秒後,第二次超時重傳是在2秒後,第三次超時重傳是在4秒後,第四次超時重傳是在8秒後,第五次超時重傳是在16秒後,當第五次超時重傳後,會繼續等待32秒,如果服務端仍然沒有響應ACK,那麼就斷開連接,不再重傳
所以,總耗時是 1+2+4+8+16+32=63 秒,大約 1 分鐘左右。
5.第二次握手丟失了,會發生什麼?
- 客戶端和服務端都觸發超時重傳機制,客戶端重傳第一次握手SYN的報文,服務端重傳第二次握手的SYN+ACK報文。
第二次握手指的是,服務端發送的SYN+ACK報文
當此報文丟失後
客戶端遲遲沒有收到第二次握手報文,會以爲自己的第一次報文丟失了,會重傳第一次握手報文
服務端沒有收到第三次握手報文,會認爲自己的第二次報文丟失了,會重傳第二次握手報文
6.第三次握手丟失了,會發生什麼?
- 服務端重傳第二次握手的SYN—ACK報文
第三次握手指的是,客戶端收到服務端的SYN-ACK報文後,會響應一個ACK報文
當此報文丟失後,服務端遲遲收不到第三次握手的ACK報文,會超時重傳第二次握手的SYN-ACK報文,直到收到第三次握手報文或者到達最大重傳次數
7.什麼是SYN攻擊?如何避免?
SYN攻擊:攻擊者短時間僞造不同IP地址的SYN報文(第一次握手),服務端每接收到一個SYN報文,就處於SYN_RECV
狀態,但服務端發送出去的SYN+ACK報文,無法得到未知IP的ACK響應,久而久之就會佔滿服務端的半連接隊列
,使得服務器不能服務正常用戶
如何避免SYN攻擊?
-
方法1:修改 Linux 內核參數,調整半連接隊列大小和當隊列滿時應做什麼處理
比如隊列滿時,對新SYN報文直接響應RST,丟棄連接
-
方法2:SYN Cookie
服務器在收到SYN包時並不馬上分配存儲連接的數據區,而是根據這個syn包計算出一個cookie,填入到響應的SYN+ACK報文的序列號中,等對方迴應ack包時,再檢查ack包序列號是否爲預期,如果爲預期則再分配數據區
此方法的缺點就是cookie的計算仍需要消耗cpu資源,因爲cookie的計算較爲複雜,不能被黑客猜到
三.TCP連接斷開
1.TCP四次揮手的過程
整體流程如下:
整體概述:
- 第一次揮手:客戶端發送FIN
- 第二次揮手:服務端發ACK,表示收到了客戶端的FIN
- 第三次揮手:服務端發FIN
- 第四次揮手:客戶端發ACK,表示收到了服務端的FIN
可以看到,每個方向都要一個ACK和一個FIN,總共是四次
具體過程:
- 客戶端打算關閉連接,此時會發送一個 TCP 首部
FIN
標誌位被置爲1
的報文,也即FIN
報文,之後客戶端進入FIN_WAIT_1
狀態。 - 服務端收到該報文後,就向客戶端發送
ACK
應答報文,接着服務端進入CLOSED_WAIT
狀態。 - 客戶端收到服務端的
ACK
應答報文後,之後進入FIN_WAIT_2
狀態。 - 等待服務端處理完數據後,也向客戶端發送
FIN
報文,之後服務端進入LAST_ACK
狀態。 - 客戶端收到服務端的
FIN
報文後,回一個ACK
應答報文,之後進入TIME_WAIT
狀態 - 服務器收到了
ACK
應答報文後,就進入了CLOSED
狀態,至此服務端已經完成連接的關閉。 - 客戶端在經過
2MSL
一段時間後,自動進入CLOSED
狀態,至此客戶端也完成連接的關閉。
2.爲什麼揮手需要四次?
之所以多了一次,是因爲服務端的ACK和FIN分開發送了,爲什麼需要分開發送呢?
是爲了處理服務端數據還沒有全部發送完的情況,服務端先發ACK響應客戶端,等服務端數據全部發送完了再發FIN,表示同意關閉連接
3.第一次揮手丟失了,會發生什麼?
- 會觸發客戶端的超時重傳機制,重傳客戶端的FIN報文
第一次揮手,即客戶端發送的FIN報文
丟失了則服務端不會響應ACK,客戶端則會一直等待,直到觸發超時重傳,當重傳次數超過某個閾值時,不再進行重傳,客戶端直接進入close狀態
4.第二次揮手丟失了,會發生什麼?
第二次揮手:即服務端響應的ACK報文丟失
服務端是不會重傳ACK報文的,客戶端一直沒有收到服務端的ACK,會重傳第一次揮手的FIN,直到收到第二次揮手報文或者重傳達到最大次數
5.第三次揮手丟失了,會發生什麼?
第三次揮手:服務端響應的FIN
正常情況下,服務端響應FIN,客戶端會進行第四次揮手響應ACK,當服務端因爲丟失了第三次揮手報文,而導致收不到第四次揮手的ACK時,會重傳第三次揮手的FIN
6.第四次揮手丟失了,會發生什麼?
第四次揮手:客戶端響應的ACK
當第四次揮手的ACK丟失了,服務端會認爲自己的第三次揮手的FIN可能丟失了,會重傳第三次揮手的FIN
7.什麼是TIME_WAIT狀態?爲什麼需要它?
主動關閉的一方在收到第三次揮手的FIN後,會響應一個ACK,此時主動關閉方進入TIME_WAIT狀態
爲什麼需要TIME_WAIT狀態?
- 因爲tcp無法根據序列號來判斷不同連接的新老數據,爲了防止歷史連接中的數據,被後面相同的四元組連接錯誤的接收,因此TCP設計了TIME_WAIT狀態,狀態會持續2MSL,這個時間足以讓兩個方向上的數據包都被丟棄,使得原來連接的數據包在網絡中自然的消失,再出現的數據包一定都是新建立的連接產生的。
- TIME_WAIT等待足夠的時間後,可以
確保最後一次揮手的ACK能讓被動關閉方接收,從而幫助其正常關閉
- 假如客戶端沒有TIME_WAIT狀態,而是在發完最後一次揮手ACK時就直接進入
CLOSE
狀態,如果最後一次握手的報文丟失了,服務端重傳FIN,此時客戶端若直接進入關閉狀態,會直接響應RST報文,導致服務端不能正常的關閉。 - 所以,爲了防止這種情況的出現,客戶端必須等待足夠長的時間來確保服務端可以收到最後一次揮手ACK,從而幫助服務端進行正常關閉
- 假如客戶端沒有TIME_WAIT狀態,而是在發完最後一次揮手ACK時就直接進入
8.爲什麼TIME_WAIT的等待時間是2 MSL?
MSL: 報文的最大生存時間
TIME_WAIT 等待 2 倍的 MSL,比較合理的解釋是: 網絡中可能存在來自發送方的數據包,當這些發送方的數據包被接收方處理後又會向對方發送響應,所以一來一回需要等待 2 倍的時間。
比如,如果被動關閉方沒有收到斷開連接的最後的 ACK 報文,就會觸發超時重發 FIN
報文,另一方接收到 FIN 後,會重發 ACK 給被動關閉方, 一來一去正好 2 個 MSL。
2MSL
的時間是從客戶端接收到 FIN 後發送 ACK 開始計時的。如果在 TIME-WAIT 時間內,因爲客戶端的 ACK 沒有傳輸到服務端,客戶端又接收到了服務端重發的 FIN 報文,那麼 2MSL 時間將重新計時。
在 Linux 系統裏 2MSL
默認是 60
秒,那麼一個 MSL
也就是 30
秒。Linux 系統停留在 TIME_WAIT 的時間爲固定的 60 秒。
如果要修改 TIME_WAIT 的時間長度,只能修改 Linux 內核代碼裏 TCP_TIMEWAIT_LEN 的值,並重新編譯 Linux 內核。
9.如果已經建立了連接,但是客戶端突然出現故障了怎麼辦?
TCP 有一個機制是保活機制。這個機制的原理是這樣的:
定義一個時間段,在這個時間段內,如果沒有任何連接相關的活動,TCP 保活機制會開始作用,每隔一個時間間隔,發送一個探測報文,該探測報文包含的數據非常少,如果連續幾個探測報文都沒有得到響應,則認爲當前的 TCP 連接已經死亡,系統內核將錯誤信息通知給上層應用程序。
在 Linux 內核可以有對應的參數可以設置保活時間、保活探測的次數、保活探測的時間間隔,以下都爲默認值:
- tcp_keepalive_time=7200:表示保活時間是 7200 秒(2小時),也就 2 小時內如果沒有任何連接相關的活動,則會啓動保活機制
- tcp_keepalive_intvl=75:表示每次檢測間隔 75 秒;
- tcp_keepalive_probes=9:表示檢測 9 次無響應,認爲對方是不可達的,從而中斷本次的連接。
也就是說在 Linux 系統中,最少需要經過 2 小時 11 分 15 秒纔可以發現一個「死亡」連接。
如果開啓了 TCP 保活,需要考慮以下幾種情況:
- 第一種,對端程序是正常工作的。當 TCP 保活的探測報文發送給對端, 對端會正常響應,這樣 TCP 保活時間會被重置,等待下一個 TCP 保活時間的到來。
- 第二種,對端程序崩潰並重啓。當 TCP 保活的探測報文發送給對端後,對端是可以響應的,但由於沒有該連接的有效信息,會產生一個 RST 報文,這樣很快就會發現 TCP 連接已經被重置。
- 第三種,是對端程序崩潰,或對端由於其他原因導致報文不可達。當 TCP 保活的探測報文發送給對端後,石沉大海,沒有響應,連續幾次,達到保活探測次數後,TCP 會報告該 TCP 連接已經死亡。
TCP 保活的這個機制檢測的時間是有點長,我們可以自己在應用層實現一個心跳機制。
比如,web 服務軟件一般都會提供 keepalive_timeout
參數,用來指定 HTTP 長連接的超時時間。如果設置了 HTTP 長連接的超時時間是 60 秒,web 服務軟件就會啓動一個定時器,如果客戶端在完後一個 HTTP 請求後,在 60 秒內都沒有再發起新的請求,定時器的時間一到,就會觸發回調函數來釋放該連接。
四.TCP數據傳輸
TCP是如何保證可靠性傳輸的呢?主要可以概括爲以下四個方面
- 重傳機制
- 滑動窗口
- 流量控制
- 擁塞控制
1.重傳機制
重傳機制,用於解決數據包丟失的情況,主要有以下四種重傳機制:
- 超時重傳
- 快速重傳
- SACK
- D-SACK
1.超時重傳
什麼叫超時重傳?
超時重傳就是在發送數據時,設定一個計時器,當超過指定時間後,沒有收到對方ack的應答報文,就重發該數據
「超時重傳時間 RTO 的值」是一個動態變化的值。
如果超時重發的數據,再次超時的時候,又需要重傳的時候,TCP 的策略是超時間隔加倍。
也就是每當遇到一次超時重傳的時候,都會將下一次超時時間間隔設爲先前值的兩倍。兩次超時,就說明網絡環境差,不宜頻繁反覆發送。
Linux中,超時重傳次數由內核控制,默認是五次
通常,第一次超時重傳是在一秒後,第二次超時重傳是在2秒後,第三次超時重傳是在4秒後,第四次超時重傳是在8秒後,第五次超時重傳是在16秒後,當第五次超時重傳後,會繼續等待32秒,如果服務端仍然沒有響應ACK,那麼就斷開連接,不再重傳
超時觸發重傳存在的問題是,超時週期可能相對較長
。那是不是可以有更快的方式呢?
於是就可以用「快速重傳」機制來解決超時重發的時間等待。
2.快速重傳
快速重傳的工作方式是當收到三個相同的 ACK 報文時,會在定時器過期之前,重傳丟失的報文段。
具體樣例如下:
在上圖,發送方發出了 1,2,3,4,5 份數據:
- 第一份 Seq1 先送到了,於是就 Ack 回 2;
- 結果 Seq2 因爲某些原因沒收到,Seq3 到達了,於是還是 Ack 回 2;
- 後面的 Seq4 和 Seq5 都到了,但還是 Ack 回 2,因爲 Seq2 還是沒有收到;
- 發送端收到了三個 Ack = 2 的確認,知道了 Seq2 還沒有收到,就會在定時器過期之前,重傳丟失的 Seq2。
- 最後,收到了 Seq2,此時因爲 Seq3,Seq4,Seq5 都收到了,於是 Ack 回 6 。
快速重傳機制只解決了一個問題,就是超時時間的問題,但是它依然面臨着另外一個問題。就是重傳的時候,是重傳之前的一個,還是重傳所有的問題。
比如對於上面的例子,是重傳 Seq2 呢?還是重傳 Seq2、Seq3、Seq4、Seq5 呢?因爲發送端並不清楚這連續的三個 Ack 2 是誰傳回來的。
根據 TCP 不同的實現,以上兩種情況都是有可能的。可見,這是一把雙刃劍。
爲了解決不知道該重傳哪些 TCP 報文,於是就有 SACK 選擇性確認
方法
3.SACK 選擇性確認 重傳
這種方式需要在 TCP 頭部「選項」字段里加一個 SACK
的東西,它可以將緩存的地圖發送給發送方,這樣發送方就可以知道哪些數據收到了,哪些數據沒收到,知道了這些信息,就可以只重傳丟失的數據。
樣例如下:
如下圖,發送方收到了三次同樣的 ACK 確認報文,於是就會觸發快速重發機制,通過 SACK
信息發現只有 200~299
這段數據丟失,則重發時,就只選擇了這個 TCP 段進行重複
如果要支持 SACK
,必須雙方都要支持。在 Linux 下,可以通過 net.ipv4.tcp_sack
參數打開這個功能(Linux 2.4 後默認打開)。
4.Duplicate SACK
Duplicate SACK 又稱 D-SACK
,其主要使用了 SACK 來告訴「發送方」有哪些數據被重複接收了。
D-SACK
有這麼幾個好處:
- 可以讓「發送方」知道,是發出去的包丟了,還是接收方迴應的 ACK 包丟了;
- 可以知道是不是「發送方」的數據包被網絡延遲了;
- 可以知道網絡中是不是把「發送方」的數據包給複製了;
在 Linux 下可以通過 net.ipv4.tcp_dsack
參數開啓/關閉這個功能(Linux 2.4 後默認打開)。
樣例1:ACK丟包
- 「接收方」發給「發送方」的兩個 ACK 確認應答都丟失了,所以發送方超時後,重傳第一個數據包(3000 ~ 3499)
- 於是「接收方」發現數據是重複收到的,於是回了一個 SACK = 3000~3500,告訴「發送方」 3000~3500 的數據早已被接收了,因爲 ACK 都到了 4000 了,已經意味着 4000 之前的所有數據都已收到,所以這個 SACK 就代表着
D-SACK
。 - 這樣「發送方」就知道了,數據沒有丟,是「接收方」的 ACK 確認報文丟了。
樣例2:網絡延時
- 數據包(1000~1499) 被網絡延遲了,導致「發送方」沒有收到 Ack 1500 的確認報文。
- 而後面報文到達的三個相同的 ACK 確認報文,就觸發了快速重傳機制,但是在重傳後,被延遲的數據包(1000~1499)又到了「接收方」;
- 所以「接收方」回了一個 SACK=1000~1500,因爲 ACK 已經到了 3000,所以這個 SACK 是 D-SACK,表示收到了重複的包。
- 這樣發送方就知道快速重傳觸發的原因不是發出去的包丟了,也不是因爲迴應的 ACK 包丟了,而是因爲網絡延遲了。
2.滑動窗口機制
可以先看兩個協議
-
停止等待協議:每發送完一個分組,就等待對方確認,收到確認之後纔再發送一個分組(簡單,但是信道的利用率低)
-
連續ARQ協議(滑動窗口協議):接收方不必對收到的分組逐個發送確認,而是在收到幾個分組後,對按序到達的最後一個分組發送確認,這就表示:到這個分組爲止的所有分組都已經正確收到了(不對每個分組返回確認,只對收到的連續幾個分組返回確認,這樣返回確認的次數就變少了)
滑動窗口機制正是連續ARQ協議的實現
樣例如下:
圖中的 ACK 600 確認應答報文丟失,也沒關係,因爲可以通過下一個確認應答進行確認,只要發送方收到了 ACK 700 確認應答,就意味着 700 之前的所有數據「接收方」都收到了。這個模式就叫累計確認或者累計應答。
窗口大小是指無需等待確認應答,而可以繼續發送數據的最大值。
TCP 頭裏有一個字段叫 Window
,也就是窗口大小。
這個字段是接收端告訴發送端自己還有多少緩衝區可以接收數據。於是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。
所以,通常窗口的大小是由接收方的窗口大小來決定的。
發送方發送的數據大小不能超過接收方的窗口大小,否則接收方就無法正常接收到數據。
發送方的滑動窗口
當收到之前發送的數據 32~36
字節的 ACK 確認應答後,如果發送窗口的大小沒有變化,則滑動窗口往右邊移動 5 個字節,因爲有 5 個字節的數據被應答確認,接下來 52~56
字節又變成了可用窗口,那麼後續也就可以發送 52~56
這 5 個字節的數據了。
接收方的滑動窗口
接收窗口和發送窗口的大小是相等的嗎?
並不是完全相等,接收窗口的大小是約等於發送窗口的大小的。
因爲滑動窗口並不是一成不變的。比如,當接收方的應用進程讀取數據的速度非常快的話,這樣的話接收窗口可以很快的就空缺出來。那麼新的接收窗口大小,是通過 TCP 報文中的 Windows 字段來告訴發送方。那麼這個傳輸過程是存在時延的,所以接收窗口和發送窗口是約等於的關係。
3.流量控制
利用滑動窗口實現流量控制:在合適的時候調節滑動窗口的大小
當發送方可用窗口變爲 0 時,發送方實際上會定時發送窗口探測報文,以便知道接收方的窗口是否發生了改變
當服務端系統資源非常緊張的時候,操心繫統可能會直接減少了接收緩衝區大小,這時應用程序又無法及時讀取緩存數據,那麼這時候就有嚴重的事情發生了,會出現數據包丟失的現象。
爲了防止這種情況發生,TCP 規定是不允許同時減少緩存又收縮窗口的,而是採用先收縮窗口,過段時間再減少緩存,這樣就可以避免了丟包情況。
如果窗口大小爲 0 時,就會阻止發送方給接收方傳遞數據,直到窗口變爲非 0 爲止,這就是窗口關閉。
窗口關閉潛在的危險
接收方向發送方通告窗口大小時,是通過 ACK
報文來通告的。
那麼,當發生窗口關閉時,接收方處理完數據後,會向發送方通告一個窗口非 0 的 ACK 報文,如果這個通告窗口的 ACK 報文在網絡中丟失了,那麻煩就大了。
這會導致發送方一直等待接收方的非 0 窗口通知,接收方也一直等待發送方的數據,如不採取措施,這種相互等待的過程,會造成了死鎖的現象。
TCP 是如何解決窗口關閉時,潛在的死鎖現象呢?
爲了解決這個問題,TCP 爲每個連接設有一個持續定時器,只要 TCP 連接一方收到對方的零窗口通知,就啓動持續計時器。
如果持續計時器超時,就會發送窗口探測 ( Window probe ) 報文,而對方在確認這個探測報文時,給出自己現在的接收窗口大小。
什麼叫糊塗窗口綜合徵?
如果接收方太忙了,來不及取走接收窗口裏的數據,那麼就會導致發送方的發送窗口越來越小。
到最後,如果接收方騰出幾個字節並告訴發送方現在有幾個字節的窗口,而發送方會義無反顧地發送這幾個字節,
要知道,我們的 TCP + IP
頭有 40
個字節,爲了傳輸那幾個字節的數據,要達上這麼大的開銷,這太不經濟了。這就是糊塗窗口綜合症。
如何解決糊塗窗口綜合徵?
接收方通常的策略如下:
當「窗口大小」小於 min( MSS,緩存空間/2 ) ,也就是小於 MSS 與 1/2 緩存大小中的最小值時,就會向發送方通告窗口爲 0
,也就阻止了發送方再發數據過來。
等到接收方處理了一些數據後,窗口大小 >= MSS,或者接收方緩存空間有一半可以使用,就可以把窗口打開讓發送方發送數據過來。
發送方通常的策略:
使用 Nagle 算法,該算法的思路是延時處理,它滿足以下兩個條件中的一條纔可以發送數據:
- 要等到窗口大小 >=
MSS
或是 數據大小 >=MSS
- 收到之前發送數據的
ack
回包
只要沒滿足上面條件中的一條,發送方一直在囤積數據,直到滿足上面的發送條件。
另外,Nagle 算法默認是打開的,如果對於一些需要小數據包交互的場景的程序,比如,telnet 或 ssh 這樣的交互性比較強的程序,則需要關閉 Nagle 算法。
可以在 Socket 設置 TCP_NODELAY
選項來關閉這個算法(關閉 Nagle 算法沒有全局參數,需要根據每個應用自己的特點來關閉)
4.擁塞控制
即網絡堵塞時,犧牲自己,少發點數據到網絡上,避免網絡堵塞惡性循環
爲了在「發送方」調節所要發送數據的量,定義了一個叫做「擁塞窗口」的概念。
什麼是擁塞窗口?和發送窗口有什麼關係呢?
擁塞窗口 cwnd是發送方維護的一個的狀態變量,它會根據網絡的擁塞程度動態變化的。
發送窗口的值 = min(擁塞窗口,接收窗口)
擁塞窗口 cwnd
變化的規則:
- 只要網絡中沒有出現擁塞,
cwnd
就會增大; - 但網絡中出現了擁塞,
cwnd
就減少;
那麼怎麼知道當前網絡是否出現了擁塞呢?
其實只要「發送方」沒有在規定時間內接收到 ACK 應答報文,也就是發生了超時重傳,就會認爲網絡出現了用擁塞。
擁塞控制有哪些控制算法?
- 慢啓動
- 擁塞避免
- 擁塞發生
- 快速恢復
1.慢啓動算法
慢啓動的意思就是一點一點的提高發送數據包的數量,如果一上來就發大量的數據,這不是給網絡添堵嗎?
慢啓動的算法記住一個規則就行:當發送方每收到一個 ACK,擁塞窗口 cwnd 的大小就會加 1。
可以看出慢啓動算法,發包的個數是指數性的增長。
有一個叫慢啓動門限 ssthresh
(slow start threshold)狀態變量。
- 當
cwnd
<ssthresh
時,使用慢啓動算法。 - 當
cwnd
>=ssthresh
時,就會使用「擁塞避免算法」。
2.擁塞避免算法
擁塞避免算法就是將原本慢啓動算法的指數增長變成了線性增長,還是增長階段,但是增長速度緩慢了一些。
擁塞窗口 cwnd
「超過」慢啓動門限 ssthresh
就會進入擁塞避免算法。
一般來說 ssthresh
的大小是 65535
字節。
那麼進入擁塞避免算法後,它的規則是:每當收到一個 ACK 時,cwnd 增加 1/cwnd。
就這麼一直增長着後,網絡就會慢慢進入了擁塞的狀況了,於是就會出現丟包現象,這時就需要對丟失的數據包進行重傳。
當觸發了重傳機制,也就進入了「擁塞發生算法」。
3.快重傳算法
發送端只要一連收到三個重複的ACK即可斷定有分組丟失了,應理解重傳丟失的報文段而不必繼續等待爲該報文段設置的重傳計時器的超時(快重傳並非取消重傳計時器,而是在某些情況下更早的重傳丟失的報文段)
4.快恢復算法
根據收到的重複ACK的多少調節慢開始門限ssthresh
五.TCP相關實踐
1.如何在 Linux 系統中查看 TCP 狀態?
TCP 的連接狀態查看,在 Linux 可以通過 netstat -napt
命令查看。