前言
不管面試 Java 、C/C++、Python 等開發崗位, TCP 的知識點可以說是必問的了,
任 TCP 虐我千百遍,我仍待 TCP 如初戀,
遙想小林當年校招時常因 TCP 面試題被刷,真是又愛又恨….
過去不會沒關系,今天就讓我們來消除這份恐懼,微笑著勇敢的面對它吧!
所以小林整理了關于 TCP 三次握手和四次揮手的面試題型,跟大家一起探討探討,
- TCP 基本認識

- TCP 連接建立

- TCP 連接斷開

- Socket 編程
PS:本次文章不涉及 TCP 流量控制、擁塞控制、可靠性傳輸等方面知識,這些留在下篇哈!
正文
01 TCP 基本認識
瞧瞧 TCP 頭格式
我們先來看看 TCP 頭的格式,標注顏色的表示與本文關聯比較大的欄位,其他欄位不做詳細闡述,
TCP 頭格式
序列號:在建立連接時由計算機生成的亂數作為其初始值,通過 SYN 包傳給接收端主機,每發送一次資料,就「累加」一次該「資料位元組數」的大小,用來解決網路包亂序問題,
確認應答號:指下一次「期望」收到的資料的序列號,發送端收到這個確認應答以后可以認為在這個序號以前的資料都已經被正常接收,用來解決不丟包的問題,
控制位:
- ACK:該位為
1時,「確認應答」的欄位變為有效,TCP 規定除了最初建立連接時的SYN包之外該位必須設定為1, - RST:該位為
1時,表示 TCP 連接中出現例外必須強制斷開連接, - SYN:該位為
1時,表示希望建立連接,并在其「序列號」的欄位進行序列號初始值的設定, - FIN:該位為
1時,表示今后不會再有資料發送,希望斷開連接,當通信結束希望斷開連接時,通信雙方的主機之間就可以相互交換FIN位為 1 的 TCP 段,
為什么需要 TCP 協議? TCP 作業在哪一層?
IP 層是「不可靠」的,它不保證網路包的交付、不保證網路包的按序交付、也不保證網路包中的資料的完整性,
OSI 參考模型與 TCP/IP 的關系
如果需要保障網路資料包的可靠性,那么就需要由上層(傳輸層)的 TCP 協議來負責,
因為 TCP 是一個作業在傳輸層的可靠資料傳輸的服務,它能確保接收端接收的網路包是無損壞、無間隔、非冗余和按序的,
什么是 TCP ?
TCP 是面向連接的、可靠的、基于位元組流的傳輸層通信協議,
-
面向連接:一定是「一對一」才能連接,不能像 UDP 協議可以一個主機同時向多個主機發送訊息,也就是一對多是無法做到的;
-
可靠的:無論的網路鏈路中出現了怎樣的鏈路變化,TCP 都可以保證一個報文一定能夠到達接收端;
-
位元組流:訊息是「沒有邊界」的,所以無論我們訊息有多大都可以進行傳輸,并且訊息是「有序的」,當「前一個」訊息沒有收到的時候,即使它先收到了后面的位元組,那么也不能扔給應用層去處理,同時對「重復」的報文會自動丟棄,
什么是 TCP 連接?
我們來看看 RFC 793 是如何定義「連接」的:
Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.
簡單來說就是,用于保證可靠性和流量控制維護的某些狀態資訊,這些資訊的組合,包括Socket、序列號和視窗大小稱為連接,
所以我們可以知道,建立一個 TCP 連接是需要客戶端與服務器端達成上述三個資訊的共識,
- Socket:由 IP 地址和埠號組成
- 序列號:用來解決亂序問題等
- 視窗大小:用來做流量控制
如何唯一確定一個 TCP 連接呢?
TCP 四元組可以唯一的確定一個連接,四元組包括如下:
- 源地址
- 源埠
- 目的地址
- 目的埠
TCP 四元組
源地址和目的地址的欄位(32位)是在 IP 頭部中,作用是通過 IP 協議發送報文給對方主機,
源埠和目的埠的欄位(16位)是在 TCP 頭部中,作用是告訴 TCP 協議應該把報文發給哪個行程,
有一個 IP 的服務器監聽了一個埠,它的 TCP 的最大連接數是多少?
服務器通常固定在某個本地埠上監聽,等待客戶端的連接請求,
因此,客戶端 IP 和 埠是可變的,其理論值計算公式如下:
對 IPv4,客戶端的 IP 數最多為 2 的 32 次方,客戶端的埠數最多為 2 的 16 次方,也就是服務端單機最大 TCP 連接數,約為 2 的 48 次方,
當然,服務端最大并發 TCP 連接數遠不能達到理論上限,
- 首先主要是檔案描述符限制,Socket 都是檔案,所以首先要通過
ulimit組態檔描述符的數目; - 另一個是記憶體限制,每個 TCP 連接都要占用一定記憶體,作業系統的記憶體是有限的,
UDP 和 TCP 有什么區別呢?分別的應用場景是?
UDP 不提供復雜的控制機制,利用 IP 提供面向「無連接」的通信服務,
UDP 協議真的非常簡,頭部只有 8 個位元組( 64 位),UDP 的頭部格式如下:
UDP 頭部格式
- 目標和源埠:主要是告訴 UDP 協議應該把報文發給哪個行程,
- 包長度:該欄位保存了 UDP 首部的長度跟資料的長度之和,
- 校驗和:校驗和是為了提供可靠的 UDP 首部和資料而設計,
TCP 和 UDP 區別:
1. 連接
- TCP 是面向連接的傳輸層協議,傳輸資料前先要建立連接,
- UDP 是不需要連接,即刻傳輸資料,
2. 服務物件
- TCP 是一對一的兩點服務,即一條連接只有兩個端點,
- UDP 支持一對一、一對多、多對多的互動通信
3. 可靠性
- TCP 是可靠交付資料的,資料可以無差錯、不丟失、不重復、按需到達,
- UDP 是盡最大努力交付,不保證可靠交付資料,
4. 擁塞控制、流量控制
- TCP 有擁塞控制和流量控制機制,保證資料傳輸的安全性,
- UDP 則沒有,即使網路非常擁堵了,也不會影響 UDP 的發送速率,
5. 首部開銷
- TCP 首部長度較長,會有一定的開銷,首部在沒有使用「選項」欄位時是
20個位元組,如果使用了「選項」欄位則會變長的, - UDP 首部只有 8 個位元組,并且是固定不變的,開銷較小,
6. 傳輸方式
- TCP 是流式傳輸,沒有邊界,但保證順序和可靠,
- UDP 是一個包一個包的發送,是有邊界的,但可能會丟包和亂序,
7. 分片不同
- TCP 的資料大小如果大于 MSS 大小,則會在傳輸層進行分片,目標主機收到后,也同樣在傳輸層組裝 TCP 資料包,如果中途丟失了一個分片,只需要傳輸丟失的這個分片,
- UDP 的資料大小如果大于 MTU 大小,則會在 IP 層進行分片,目標主機收到后,在 IP 層組裝完資料,接著再傳給傳輸層,但是如果中途丟了一個分片,則就需要重傳所有的資料包,這樣傳輸效率非常差,所以通常 UDP 的報文應該小于 MTU,
TCP 和 UDP 應用場景:
由于 TCP 是面向連接,能保證資料的可靠性交付,因此經常用于:
FTP檔案傳輸HTTP/HTTPS
由于 UDP 面向無連接,它可以隨時發送資料,再加上UDP本身的處理既簡單又高效,因此經常用于:
- 包總量較少的通信,如
DNS、SNMP等 - 視頻、音頻等多媒體通信
- 廣播通信
為什么 UDP 頭部沒有「首部長度」欄位,而 TCP 頭部有「首部長度」欄位呢?
原因是 TCP 有可變長的「選項」欄位,而 UDP 頭部長度則是不會變化的,無需多一個欄位去記錄 UDP 的首部長度,
為什么 UDP 頭部有「包長度」欄位,而 TCP 頭部則沒有「包長度」欄位呢?
先說說 TCP 是如何計算負載資料長度:
其中 IP 總長度 和 IP 首部長度,在 IP 首部格式是已知的,TCP 首部長度,則是在 TCP 首部格式已知的,所以就可以求得 TCP 資料的長度,
大家這時就奇怪了問:“ UDP 也是基于 IP 層的呀,那 UDP 的資料長度也可以通過這個公式計算呀? 為何還要有「包長度」呢?”
這么一問,確實感覺 UDP 「包長度」是冗余的,
因為為了網路設備硬體設計和處理方便,首部長度需要是 4位元組的整數倍,
如果去掉 UDP 「包長度」欄位,那 UDP 首部長度就不是 4 位元組的整數倍了,所以小林覺得這可能是為了補全 UDP 首部長度是 4 位元組的整數倍,才補充了「包長度」欄位,
02 TCP 連接建立
TCP 三次握手程序和狀態變遷
TCP 是面向連接的協議,所以使用 TCP 前必須先建立連接,而建立連接是通過三次握手來進行的,
TCP 三次握手
- 一開始,客戶端和服務端都處于
CLOSED狀態,先是服務端主動監聽某個埠,處于LISTEN狀態
第一個報文—— SYN 報文
- 客戶端會隨機初始化序號(
client_isn),將此序號置于 TCP 首部的「序號」欄位中,同時把SYN標志位置為1,表示SYN報文,接著把第一個 SYN 報文發送給服務端,表示向服務端發起連接,該報文不包含應用層資料,之后客戶端處于SYN-SENT狀態,
第二個報文 —— SYN + ACK 報文
- 服務端收到客戶端的
SYN報文后,首先服務端也隨機初始化自己的序號(server_isn),將此序號填入 TCP 首部的「序號」欄位中,其次把 TCP 首部的「確認應答號」欄位填入client_isn + 1, 接著把SYN和ACK標志位置為1,最后把該報文發給客戶端,該報文也不包含應用層資料,之后服務端處于SYN-RCVD狀態,
第三個報文 —— ACK 報文
-
客戶端收到服務端報文后,還要向服務端回應最后一個應答報文,首先該應答報文 TCP 首部
ACK標志位置為1,其次「確認應答號」欄位填入server_isn + 1,最后把報文發送給服務端,這次報文可以攜帶客戶到服務器的資料,之后客戶端處于ESTABLISHED狀態, -
服務器收到客戶端的應答報文后,也進入
ESTABLISHED狀態,
從上面的程序可以發現第三次握手是可以攜帶資料的,前兩次握手是不可以攜帶資料的,這也是面試常問的題,
一旦完成三次握手,雙方都處于 ESTABLISHED 狀態,此時連接就已建立完成,客戶端和服務端就可以相互發送資料了,
如何在 Linux 系統中查看 TCP 狀態?
TCP 的連接狀態查看,在 Linux 可以通過 netstat -napt 命令查看,
TCP 連接狀態查看
為什么是三次握手?不是兩次、四次?
相信大家比較常回答的是:“因為三次握手才能保證雙方具有接收和發送的能力,”
這回答是沒問題,但這回答是片面的,并沒有說出主要的原因,
在前面我們知道了什么是 TCP 連接:
- 用于保證可靠性和流量控制維護的某些狀態資訊,這些資訊的組合,包括Socket、序列號和視窗大小稱為連接,
所以,重要的是為什么三次握手才可以初始化Socket、序列號和視窗大小并建立 TCP 連接,
接下來以三個方面分析三次握手的原因:
- 三次握手才可以阻止重復歷史連接的初始化(主要原因)
- 三次握手才可以同步雙方的初始序列號
- 三次握手才可以避免資源浪費
原因一:避免歷史連接
我們來看看 RFC 793 指出的 TCP 連接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
簡單來說,三次握手的首要原因是為了防止舊的重復連接初始化造成混亂,
網路環境是錯綜復雜的,往往并不是如我們期望的一樣,先發送的資料包,就先到達目標主機,反而它很騷,可能會由于網路擁堵等亂七八糟的原因,會使得舊的資料包,先到達目標主機,那么這種情況下 TCP 三次握手是如何避免的呢?
三次握手避免歷史連接
客戶端連續發送多次 SYN 建立連接的報文,在網路擁堵情況下:
- 一個「舊 SYN 報文」比「最新的 SYN 」 報文早到達了服務端;
- 那么此時服務端就會回一個
SYN + ACK報文給客戶端; - 客戶端收到后可以根據自身的背景關系,判斷這是一個歷史連接(序列號過期或超時),那么客戶端就會發送
RST報文給服務端,表示中止這一次連接,
如果是兩次握手連接,就不能判斷當前連接是否是歷史連接,三次握手則可以在客戶端(發送方)準備發送第三次報文時,客戶端因有足夠的背景關系來判斷當前連接是否是歷史連接:
- 如果是歷史連接(序列號過期或超時),則第三次握手發送的報文是
RST報文,以此中止歷史連接; - 如果不是歷史連接,則第三次發送的報文是
ACK報文,通信雙方就會成功建立連接;
所以,TCP 使用三次握手建立連接的最主要原因是防止歷史連接初始化了連接,
原因二:同步雙方初始序列號
TCP 協議的通信雙方, 都必須維護一個「序列號」, 序列號是可靠傳輸的一個關鍵因素,它的作用:
- 接收方可以去除重復的資料;
- 接收方可以根據資料包的序列號按序接收;
- 可以標識發送出去的資料包中, 哪些是已經被對方收到的;
可見,序列號在 TCP 連接中占據著非常重要的作用,所以當客戶端發送攜帶「初始序列號」的 SYN 報文的時候,需要服務端回一個 ACK 應答報文,表示客戶端的 SYN 報文已被服務端成功接收,那當服務端發送「初始序列號」給客戶端的時候,依然也要得到客戶端的應答回應,這樣一來一回,才能確保雙方的初始序列號能被可靠的同步,
四次握手與三次握手
四次握手其實也能夠可靠的同步雙方的初始化序號,但由于第二步和第三步可以優化成一步,所以就成了「三次握手」,
而兩次握手只保證了一方的初始序列號能被對方成功接收,沒辦法保證雙方的初始序列號都能被確認接收,
原因三:避免資源浪費
如果只有「兩次握手」,當客戶端的 SYN 請求連接在網路中阻塞,客戶端沒有接收到 ACK 報文,就會重新發送 SYN ,由于沒有第三次握手,服務器不清楚客戶端是否收到了自己發送的建立連接的 ACK 確認信號,所以每收到一個 SYN 就只能先主動建立一個連接,這會造成什么情況呢?
如果客戶端的 SYN 阻塞了,重復發送多次 SYN 報文,那么服務器在收到請求后就會建立多個冗余的無效鏈接,造成不必要的資源浪費,
兩次握手會造成資源浪費
即兩次握手會造成訊息滯留情況下,服務器重復接受無用的連接請求 SYN 報文,而造成重復分配資源,
小結
TCP 建立連接時,通過三次握手能防止歷史連接的建立,能減少雙方不必要的資源開銷,能幫助雙方同步初始化序列號,序列號能夠保證資料包不重復、不丟棄和按序傳輸,
不使用「兩次握手」和「四次握手」的原因:
- 「兩次握手」:無法防止歷史連接的建立,會造成雙方資源的浪費,也無法可靠的同步雙方序列號;
- 「四次握手」:三次握手就已經理論上最少可靠連接建立,所以不需要使用更多的通信次數,
為什么客戶端和服務端的初始序列號 ISN 是不相同的?
如果一個已經失效的連接被重用了,但是該舊連接的歷史報文還殘留在網路中,如果序列號相同,那么就無法分辨出該報文是不是歷史報文,如果歷史報文被新的連接接收了,則會產生資料錯亂,
所以,每次建立連接前重新初始化一個序列號主要是為了通信雙方能夠根據序號將不屬于本連接的報文段丟棄,
另一方面是為了安全性,防止黑客偽造的相同序列號的 TCP 報文被對方接收,
初始序列號 ISN 是如何隨機產生的?
起始 ISN 是基于時鐘的,每 4 毫秒 + 1,轉一圈要 4.55 個小時,
RFC1948 中提出了一個較好的初始化序列號 ISN 隨機生成演算法,
ISN = M + F (localhost, localport, remotehost, remoteport)
M是一個計時器,這個計時器每隔 4 毫秒加 1,F是一個 Hash 演算法,根據源 IP、目的 IP、源埠、目的埠生成一個亂數值,要保證 Hash 演算法不能被外部輕易推算得出,用 MD5 演算法是一個比較好的選擇,
既然 IP 層會分片,為什么 TCP 層還需要 MSS 呢?
我們先來認識下 MTU 和 MSS
MTU 與 MSS
MTU:一個網路包的最大長度,以太網中一般為1500位元組;MSS:除去 IP 和 TCP 頭部之后,一個網路包所能容納的 TCP 資料的最大長度;
如果在 TCP 的整個報文(頭部 + 資料)交給 IP 層進行分片,會有什么例外呢?
當 IP 層有一個超過 MTU 大小的資料(TCP 頭部 + TCP 資料)要發送,那么 IP 層就要進行分片,把資料分片成若干片,保證每一個分片都小于 MTU,把一份 IP 資料報進行分片以后,由目標主機的 IP 層來進行重新組裝后,再交給上一層 TCP 傳輸層,
這看起來井然有序,但這存在隱患的,那么當如果一個 IP 分片丟失,整個 IP 報文的所有分片都得重傳,
因為 IP 層本身沒有超時重傳機制,它由傳輸層的 TCP 來負責超時和重傳,
當接收方發現 TCP 報文(頭部 + 資料)的某一片丟失后,則不會回應 ACK 給對方,那么發送方的 TCP 在超時后,就會重發「整個 TCP 報文(頭部 + 資料)」,
因此,可以得知由 IP 層進行分片傳輸,是非常沒有效率的,
所以,為了達到最佳的傳輸效能 TCP 協議在建立連接的時候通常要協商雙方的 MSS 值,當 TCP 層發現資料超過 MSS 時,則就先會進行分片,當然由它形成的 IP 包的長度也就不會大于 MTU ,自然也就不用 IP 分片了,
握手階段協商 MSS
經過 TCP 層分片后,如果一個 TCP 分片丟失后,進行重發時也是以 MSS 為單位,而不用重傳所有的分片,大大增加了重傳的效率,
什么是 SYN 攻擊?如何避免 SYN 攻擊?
SYN 攻擊
我們都知道 TCP 連接建立是需要三次握手,假設攻擊者短時間偽造不同 IP 地址的 SYN 報文,服務端每接收到一個 SYN 報文,就進入SYN_RCVD 狀態,但服務端發送出去的 ACK + SYN 報文,無法得到未知 IP 主機的 ACK 應答,久而久之就會占滿服務端的 SYN 接收佇列(未連接佇列),使得服務器不能為正常用戶服務,
SYN 攻擊
避免 SYN 攻擊方式一
其中一種解決方式是通過修改 Linux 內核引數,控制佇列大小和當佇列滿時應做什么處理,
- 當網卡接收資料包的速度大于內核處理的速度時,會有一個佇列保存這些資料包,控制該佇列的最大值如下引數:
net.core.netdev_max_backlog
- SYN_RCVD 狀態連接的最大個數:
net.ipv4.tcp_max_syn_backlog
- 超出處理能時,對新的 SYN 直接回報 RST,丟棄連接:
net.ipv4.tcp_abort_on_overflow
避免 SYN 攻擊方式二
我們先來看下 Linux 內核的 SYN (未完成連接建立)佇列與 Accpet (已完成連接建立)佇列是如何作業的?
正常流程
正常流程:
- 當服務端接收到客戶端的 SYN 報文時,會將其加入到內核的「 SYN 佇列」;
- 接著發送 SYN + ACK 給客戶端,等待客戶端回應 ACK 報文;
- 服務端接收到 ACK 報文后,從「 SYN 佇列」移除放入到「 Accept 佇列」;
- 應用通過呼叫
accpet()socket 介面,從「 Accept 佇列」取出連接,
應用程式過慢
應用程式過慢:
- 如果應用程式過慢時,就會導致「 Accept 佇列」被占滿,
受到 SYN 攻擊
受到 SYN 攻擊:
- 如果不斷受到 SYN 攻擊,就會導致「 SYN 佇列」被占滿,
tcp_syncookies 的方式可以應對 SYN 攻擊的方法:
net.ipv4.tcp_syncookies = 1
tcp_syncookies 應對 SYN 攻擊
- 當 「 SYN 佇列」滿之后,后續服務器收到 SYN 包,不進入「 SYN 佇列」;
- 計算出一個
cookie值,再以 SYN + ACK 中的「序列號」回傳客戶端, - 服務端接收到客戶端的應答報文時,服務器會檢查這個 ACK 包的合法性,如果合法,直接放入到「 Accept 佇列」,
- 最后應用通過呼叫
accpet()socket 介面,從「 Accept 佇列」取出的連接,
03 TCP 連接斷開
TCP 四次揮手程序和狀態變遷
天下沒有不散的宴席,對于 TCP 連接也是這樣, TCP 斷開連接是通過四次揮手方式,
雙方都可以主動斷開連接,斷開連接后主機中的「資源」將被釋放,
客戶端主動關閉連接 —— TCP 四次揮手
- 客戶端打算關閉連接,此時會發送一個 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狀態,至此客戶端也完成連接的關閉,
你可以看到,每個方向都需要一個 FIN 和一個 ACK,因此通常被稱為四次揮手,
這里一點需要注意是:主動關閉連接的,才有 TIME_WAIT 狀態,
為什么揮手需要四次?
再來回顧下四次揮手雙方發 FIN 包的程序,就能理解為什么需要四次了,
- 關閉連接時,客戶端向服務端發送
FIN時,僅僅表示客戶端不再發送資料了但是還能接收資料, - 服務器收到客戶端的
FIN報文時,先回一個ACK應答報文,而服務端可能還有資料需要處理和發送,等服務端不再發送資料時,才發送FIN報文給客戶端來表示同意現在關閉連接,
從上面程序可知,服務端通常需要等待完成資料的發送和處理,所以服務端的 ACK 和 FIN 一般都會分開發送,從而比三次握手導致多了一次,
為什么 TIME_WAIT 等待的時間是 2MSL?
MSL 是 Maximum Segment Lifetime,報文最大生存時間,它是任何報文在網路上存在的最長時間,超過這個時間報文將被丟棄,因為 TCP 報文基于是 IP 協議的,而 IP 頭中有一個 TTL 欄位,是 IP 資料報可以經過的最大路由數,每經過一個處理他的路由器此值就減 1,當此值為 0 則資料報將被丟棄,同時發送 ICMP 報文通知源主機,
MSL 與 TTL 的區別: MSL 的單位是時間,而 TTL 是經過路由跳數,所以 MSL 應該要大于等于 TTL 消耗為 0 的時間,以確保報文已被自然消亡,
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 秒,
其定義在 Linux 內核代碼里的名稱為 TCP_TIMEWAIT_LEN:
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
state, about 60 seconds */
如果要修改 TIME_WAIT 的時間長度,只能修改 Linux 內核代碼里 TCP_TIMEWAIT_LEN 的值,并重新編譯 Linux 內核,
為什么需要 TIME_WAIT 狀態?
主動發起關閉連接的一方,才會有 TIME-WAIT 狀態,
需要 TIME-WAIT 狀態,主要是兩個原因:
- 防止具有相同「四元組」的「舊」資料包被收到;
- 保證「被動關閉連接」的一方能被正確的關閉,即保證最后的 ACK 能讓被動關閉方接收,從而幫助其正常關閉;
原因一:防止舊連接的資料包
假設 TIME-WAIT 沒有等待時間或時間過短,被延遲的資料包抵達后會發生什么呢?
接收到歷史資料的例外
- 如上圖黃色框框服務端在關閉連接之前發送的
SEQ = 301報文,被網路延遲了, - 這時有相同埠的 TCP 連接被復用后,被延遲的
SEQ = 301抵達了客戶端,那么客戶端是有可能正常接收這個過期的報文,這就會產生資料錯亂等嚴重的問題,
所以,TCP 就設計出了這么一個機制,經過 2MSL 這個時間,足以讓兩個方向上的資料包都被丟棄,使得原來連接的資料包在網路中都自然消失,再出現的資料包一定都是新建立連接所產生的,
原因二:保證連接正確關閉
在 RFC 793 指出 TIME-WAIT 另一個重要的作用是:
TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.
也就是說,TIME-WAIT 作用是等待足夠的時間以確保最后的 ACK 能讓被動關閉方接收,從而幫助其正常關閉,
假設 TIME-WAIT 沒有等待時間或時間過短,斷開連接會造成什么問題呢?
沒有確保正常斷開的例外
- 如上圖紅色框框客戶端四次揮手的最后一個
ACK報文如果在網路中被丟失了,此時如果客戶端TIME-WAIT過短或沒有,則就直接進入了CLOSED狀態了,那么服務端則會一直處在LASE_ACK狀態, - 當客戶端發起建立連接的
SYN請求報文后,服務端會發送RST報文給客戶端,連接建立的程序就會被終止,
如果 TIME-WAIT 等待足夠長的情況就會遇到兩種情況:
- 服務端正常收到四次揮手的最后一個
ACK報文,則服務端正常關閉連接, - 服務端沒有收到四次揮手的最后一個
ACK報文時,則會重發FIN關閉連接報文并等待新的ACK報文,
所以客戶端在 TIME-WAIT 狀態等待 2MSL 時間后,就可以保證雙方的連接都可以正常的關閉,
TIME_WAIT 過多有什么危害?
如果服務器有處于 TIME-WAIT 狀態的 TCP,則說明是由服務器方主動發起的斷開請求,
過多的 TIME-WAIT 狀態主要的危害有兩種:
- 第一是記憶體資源占用;
- 第二是對埠資源的占用,一個 TCP 連接至少消耗一個本地埠;
第二個危害是會造成嚴重的后果的,要知道,埠資源也是有限的,一般可以開啟的埠為 32768~61000,也可以通過如下引數設定指定
net.ipv4.ip_local_port_range
如果發起連接一方的 TIME_WAIT 狀態過多,占滿了所有埠資源,則會導致無法創建新連接,
客戶端受埠資源限制:
- 客戶端TIME_WAIT過多,就會導致埠資源被占用,因為埠就65536個,被占滿就會導致無法創建新的連接,
服務端受系統資源限制:
- 由于一個四元組表示 TCP 連接,理論上服務端可以建立很多連接,服務端確實只監聽一個埠 但是會把連接扔給處理執行緒,所以理論上監聽的埠可以繼續監聽,但是執行緒池處理不了那么多一直不斷的連接了,所以當服務端出現大量 TIME_WAIT 時,系統資源被占滿時,會導致處理不過來新的連接,
如何優化 TIME_WAIT?
這里給出優化 TIME-WAIT 的幾個方式,都是有利有弊:
- 打開 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 選項;
- net.ipv4.tcp_max_tw_buckets
- 程式中使用 SO_LINGER ,應用強制使用 RST 關閉,
方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps
如下的 Linux 內核引數開啟后,則可以復用處于 TIME_WAIT 的 socket 為新的連接所用,
有一點需要注意的是,tcp_tw_reuse 功能只能用客戶端(連接發起方),因為開啟了該功能,在呼叫 connect() 函式時,內核會隨機找一個 time_wait 狀態超過 1 秒的連接給新的連接復用,
net.ipv4.tcp_tw_reuse = 1
使用這個選項,還有一個前提,需要打開對 TCP 時間戳的支持,即
net.ipv4.tcp_timestamps=1(默認即為 1)
這個時間戳的欄位是在 TCP 頭部的「選項」里,用于記錄 TCP 發送方的當前時間戳和從對端接收到的最新時間戳,
由于引入了時間戳,我們在前面提到的 2MSL 問題就不復存在了,因為重復的資料包會因為時間戳過期被自然丟棄,
方式二:net.ipv4.tcp_max_tw_buckets
這個值默認為 18000,當系統中處于 TIME_WAIT 的連接一旦超過這個值時,系統就會將后面的 TIME_WAIT 連接狀態重置,
這個方法過于暴力,而且治標不治本,帶來的問題遠比解決的問題多,不推薦使用,
方式三:程式中使用 SO_LINGER
我們可以通過設定 socket 選項,來設定呼叫 close 關閉連接行為,
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
如果l_onoff為非 0, 且l_linger值為 0,那么呼叫close后,會立該發送一個RST標志給對端,該 TCP 連接將跳過四次揮手,也就跳過了TIME_WAIT狀態,直接關閉,
但這為跨越TIME_WAIT狀態提供了一個可能,不過是一個非常危險的行為,不值得提倡,
如果已經建立了連接,但是客戶端突然出現故障了怎么辦?
TCP 有一個機制是保活機制,這個機制的原理是這樣的:
定義一個時間段,在這個時間段內,如果沒有任何連接相關的活動,TCP 保活機制會開始作用,每隔一個時間間隔,發送一個探測報文,該探測報文包含的資料非常少,如果連續幾個探測報文都沒有得到回應,則認為當前的 TCP 連接已經死亡,系統內核將錯誤資訊通知給上層應用程式,
在 Linux 內核可以有對應的引數可以設定保活時間、保活探測的次數、保活探測的時間間隔,以下都為默認值:
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
- 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 連接已經死亡,
04 Socket 編程
針對 TCP 應該如何 Socket 編程?
基于 TCP 協議的客戶端和服務器作業
- 服務端和客戶端初始化
socket,得到檔案描述符; - 服務端呼叫
bind,將系結在 IP 地址和埠; - 服務端呼叫
listen,進行監聽; - 服務端呼叫
accept,等待客戶端連接; - 客戶端呼叫
connect,向服務器端的地址和埠發起連接請求; - 服務端
accept回傳用于傳輸的socket的檔案描述符; - 客戶端呼叫
write寫入資料;服務端呼叫read讀取資料; - 客戶端斷開連接時,會呼叫
close,那么服務端read讀取資料的時候,就會讀取到了EOF,待處理完資料后,服務端呼叫close,表示連接關閉,
這里需要注意的是,服務端呼叫 accept 時,連接成功了會回傳一個已完成連接的 socket,后續用來傳輸資料,
所以,監聽的 socket 和真正用來傳送資料的 socket,是「兩個」 socket,一個叫作監聽 socket,一個叫作已完成連接 socket,
成功連接建立之后,雙方開始通過 read 和 write 函式來讀寫資料,就像往一個檔案流里面寫東西一樣,
listen 時候引數 backlog 的意義?
Linux內核中會維護兩個佇列:
- 未完成連接佇列(SYN 佇列):接收到一個 SYN 建立連接請求,處于 SYN_RCVD 狀態;
- 已完成連接佇列(Accpet 佇列):已完成 TCP 三次握手程序,處于 ESTABLISHED 狀態;
SYN 佇列 與 Accpet 佇列
int listen (int socketfd, int backlog)
- 引數一 socketfd 為 socketfd 檔案描述符
- 引數二 backlog,這引數在歷史版本有一定的變化
在早期 Linux 內核 backlog 是 SYN 佇列大小,也就是未完成的佇列大小,
在 Linux 內核 2.2 之后,backlog 變成 accept 佇列,也就是已完成連接建立的佇列長度,所以現在通常認為 backlog 是 accept 佇列,
但是上限值是內核引數 somaxconn 的大小,也就說 accpet 佇列長度 = min(backlog, somaxconn),
accept 發生在三次握手的哪一步?
我們先看看客戶端連接服務端時,發送了什么?
客戶端連接服務端
- 客戶端的協議堆疊向服務器端發送了 SYN 包,并告訴服務器端當前發送序列號 client_isn,客戶端進入 SYN_SENT 狀態;
- 服務器端的協議堆疊收到這個包之后,和客戶端進行 ACK 應答,應答的值為 client_isn+1,表示對 SYN 包 client_isn 的確認,同時服務器也發送一個 SYN 包,告訴客戶端當前我的發送序列號為 server_isn,服務器端進入 SYN_RCVD 狀態;
- 客戶端協議堆疊收到 ACK 之后,使得應用程式從
connect呼叫回傳,表示客戶端到服務器端的單向連接建立成功,客戶端的狀態為 ESTABLISHED,同時客戶端協議堆疊也會對服務器端的 SYN 包進行應答,應答資料為 server_isn+1; - 應答包到達服務器端后,服務器端協議堆疊使得
accept阻塞呼叫回傳,這個時候服務器端到客戶端的單向連接也建立成功,服務器端也進入 ESTABLISHED 狀態,
從上面的描述程序,我們可以得知客戶端 connect 成功回傳是在第二次握手,服務端 accept 成功回傳是在三次握手成功之后,
客戶端呼叫 close 了,連接是斷開的流程是什么?
我們看看客戶端主動呼叫了 close,會發生什么?
客戶端呼叫 close 程序
- 客戶端呼叫
close,表明客戶端沒有資料需要發送了,則此時會向服務端發送 FIN 報文,進入 FIN_WAIT_1 狀態; - 服務端接收到了 FIN 報文,TCP 協議堆疊會為 FIN 包插入一個檔案結束符
EOF到接識訓沖區中,應用程式可以通過read呼叫來感知這個 FIN 包,這個EOF會被放在已排隊等候的其他已接收的資料之后,這就意味著服務端需要處理這種例外情況,因為 EOF 表示在該連接上再無額外資料到達,此時,服務端進入 CLOSE_WAIT 狀態; - 接著,當處理完資料后,自然就會讀到
EOF,于是也呼叫close關閉它的套接字,這會使得客戶端會發出一個 FIN 包,之后處于 LAST_ACK 狀態; - 客戶端接收到服務端的 FIN 包,并發送 ACK 確認包給服務端,此時客戶端將進入 TIME_WAIT 狀態;
- 服務端收到 ACK 確認包后,就進入了最后的 CLOSE 狀態;
- 客戶端經過
2MSL時間之后,也進入 CLOSE 狀態;
巨人的肩膀
[1] 趣談網路協議專欄.劉超.極客時間.
[2] 網路編程實戰專欄.盛延敏.極客時間.
[3] 計算機網路-自頂向下方法.陳鳴 譯.機械工業出版社
[4] TCP/IP詳解 卷1:協議.范建華 譯.機械工業出版社
[5] 圖解TCP/IP.竹下隆史.人民郵電出版社
[6] https://www.rfc-editor.org/rfc/rfc793.html
[7] https://draveness.me/whys-the-design-tcp-three-way-handshake
[8] https://draveness.me/whys-the-design-tcp-time-wait/
嘮叨嘮叨
小林為寫此文重學了一篇 TCP,深感 TCP 真的是一個非常復雜的協議,要想輕易拿下,也不是一天兩天的事,所以小林花費了一個星期多才寫完此文章,
正所謂知道的越多,不知道的也越多,
下篇給大家帶來 TCP 滑動視窗、流量控制、擁塞控制的圖解文章!
本文只是拋磚引玉,若你有更好的想法或文章有誤的地方,歡迎留言討論!
小林是專為大家圖解的工具人,Goodbye,我們下次見!
讀者問答
讀者問:“關于文中三次握手最主要的原因,有一個疑問:為什么新包和舊包的 seq 會不一樣?查閱了tcp/ip詳解 卷1,以及一些其他網路書籍和 RFC793 部分,都沒有明確說明關于第一個 SYN 如果發生重傳會改變 seq,”
文章的例子不是超時重發的 SYN 報文,而是新產生的一個 SYN 報文,所以 seq 是不一樣的,
我文章的例子是 RFC 793 :https://www.rfc-editor.org/rfc/rfc793.html , 33 頁的 Figure 9,
讀者問:“請教個問題,為了方便除錯服務器程式,一般會在服務端設定 SO_REUSEADDR 選項,這樣服務器程式在重啟后,可以立刻使用,這里設定SO_REUSEADDR 是不是就等價于對這個 socket 設定了內核中的 net.ipv4.tcp_tw_reuse=1 這個選項?”
這兩個東西沒有關系的哦,
- tcp_tw_reuse 是內核選項,主要用在連接的發起方(客戶端),TIME_WAIT 狀態的連接創建時間超過 1 秒后,新的連接才可以被復用,注意,這里是「連接的發起方」;
- SO_REUSEADDR 是用戶態的選項,用于「連接的服務方」,用來告訴作業系統內核,如果埠已被占用,但是 TCP 連接狀態位于 TIME_WAIT ,可以重用埠,如果埠忙,而 TCP 處于其他狀態,重用會有 “Address already in use” 的錯誤資訊,
tcp_tw_reuse 是為了縮短 time_wait 的時間,避免出現大量的 time_wait 連接而占用系統資源,解決的是 accept 后的問題,
SO_REUSEADDR 是為了解決 time_wait 狀態帶來的埠占用問題,以及支持同一個 port 對應多個 ip,解決的是 bind 時的問題,
讀者問:“請教一下,如果客戶端第四次揮手ack丟失,服務端超時重發的fin報文也丟失,客戶端timewait時間超過了2msl,這個時候會發生什么?認為連接已經關閉嗎?”
當客戶端 timewait 時間超過了 2MSL,則客戶端就直接進入關閉狀態,
服務端超時重發 fin 報文的次數如果超過 tcp_orphan_retries 大小后,服務端也會關閉 TCP 連接,
讀者問:“求教兩個小問題:文章在解釋IP分片和TCP MSS分片時說,如果用IP分片會有兩個問題:(1)IP按MTU分片,如果某一片丟失則需要所有分片都重傳;(2)IP沒有重傳機制,所以需要等TCP發送方超時才能重傳;問題一:MSS跟IP的MTU分片相比,只是多了一步協商MSS值的程序,而IP的MTU可以看作是默認協商好就是1500位元組,所以為什么協商后的MSS可以做到丟失后只發丟失的這一片來提高效率,而默認協商好1500位元組的IP分片就需要所有片都重傳呢?問題二:TCP MSS分片如果丟失了一片,是不是也需要發送方等待超時再重傳?如果不是,MSS的協商如何能在超時前就直到丟了分片從而提高效率的呢?謝謝老師,”
問題一:
- 如果一個大的 TCP 報文是被 MTU 分片,那么只有「第一個分片」才具有 TCP 頭部,后面的分片則沒有 TCP 頭部,接收方 IP 層只有重組了這些分片,才會認為是一個 TCP 報文,那么丟失了其中一個分片,接收方 IP 層就不會把 TCP 報文丟給 TCP 層,那么就會等待對方超時重傳這一整個 TCP 報文,
- 如果一個大的 TCP 報文被 MSS 分片,那么所有「分片都具有 TCP 頭部」,因為每個 MSS 分片的是具有 TCP 頭部的TCP報文,那么其中一個 MSS 分片丟失,就只需要重傳這一個分片就可以,
問題二:
- TCP MSS分片如果丟失了一片,發送方沒收到對方ACK應答,也是會觸發超時重傳的,因為TCP層是會保證資料的可靠交付,
讀者問:“大佬,請教個問題,如果是服務提供方發起的 close ,然后引起過多的 time_wait 狀態的 tcp 鏈接,time_wait 會影響服務端的埠嗎?謝謝,”
不會,
如果發起連接一方(客戶端)的 TIME_WAIT 狀態過多,占滿了所有埠資源,則會導致無法創建新連接,
客戶端受埠資源限制:
- 客戶端TIME_WAIT過多,就會導致埠資源被占用,因為埠就65536個,被占滿就會導致無法創建新連接,
服務端受系統資源限制:
- 由于一個 TCP 四元組表示 TCP 連接,理論上服務端可以建立很多連接,服務端只監聽一個埠,但是會把連接扔給處理執行緒,所以理論上監聽的埠可以繼續監聽,但是執行緒池處理不了那么多一直不斷的連接了,所以當服務端出現大量 TIMEWAIT 時,系統資源容易被耗盡,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/114352.html
標籤:Linux
上一篇:測驗會用到的linux命令

