tcp協定中所有定時器。逾時一詞在軟體領域用途非常廣泛,是解決的很多問題利器。TCP設計精髓在于他自我管理的狀态機,而要想狀态機正常運作,逾時必不可少,如connect flood攻擊。
建立連接配接定時器
開始
TCP屬于可靠連接配接,需要經曆三次握手。

如上圖所示,主要分為以下過程:
- 第一步:伺服器準備好接受外來的連接配接。通常通過調用socket、bind和listen三個函數來完成,稱之為被動打開(passive open)
- 第二步:用戶端通過connect發起主動連接配接(active open)。導緻用戶端TCP發送一個SYN同步分節。一般SYN分節不攜帶資料,其所在的IP資料報隻包含一個IP首部、一個TCP首部以及TCP選項(最為常見的是MSS分節,在這裡并沒有畫出)
- 第三步:伺服器确認客戶的SYN,同時自己也發送一個SYN分節。伺服器在單個分節中發送SYN和對用戶端SYN的ACK确認。此時用戶端已經處于ESTABLISHED狀态
- 第四步:用戶端必須确認服務端的SYN分節。确認成功,此時服務端也進入ESTABLISHED狀态。
下圖為正常的tcp三次握手抓包圖
其中No對應清單的序号.
環境檢視
結果分析
在這裡有可能會出現兩種情況逾時
- 第一種:當TCP用戶端送出請求沒有收到SYN分節響應時,則會傳回ETIMEOUT。即,調用connect函數時,核心會發送一個SYN分節,若無響應則等待6s再發送一個,若仍無響應則等待24s再發送一個。若總共等待75s(4.4BSD規定75s)後仍未收到響應則傳回本錯誤。注意,不同系統對時間值的設定不相同。
- 第二種:當客戶發出的SYN在中間的某個路由器上引發“destination unreachable”(目的地不可達)的ICMP錯誤時,則認為是一種軟錯誤,不會終止。客戶主機核心會儲存該消息,并按照上述第一種情況來間接性繼續發送SYN。在某個規定的時間(4.4BSD規定75s)後仍未收到響應,則把儲存的消息(即ICMP錯誤)作為EHOSTUNREACH或ENETUNREACH錯誤傳回給程序。
實驗驗證
- 系統:centos6.5
- 指令
1
2
3
- 實驗結果
總時間為63s,這個時間還是難以忍受的,想想如果一個食堂因為你一個人等了1分多鐘,估計你會被打shi
- 抓包: tcpdump -i lo ‘tcp[tcpflags] = tcp-syn’
在這裡我們也看到了總共6次,重試了5次。而且也可以看到:1s-à2s-à4s-à8s-à16s方式來進行重試的。
解決方法
-
修改核心參數(強烈不推薦)
vim /etc/sysctl.conf
将net.ipv4.tcp_syn_retries = 5修改為 1,則可以将 connect 逾時時間改為 3 秒,例如:# sysctl net.ipv4.tcp_syn_retries=1,之後使用指令sysctl -p /etc/sysctl.conf儲存
. 應用層進行設定
- 使用alarm,指定逾時時間,逾時滿後産生SIGALRM信号
- Select中阻塞等待I/O,此時套接字必須為非阻塞
- 使用較新的SO_RECVTIMEO和SO_SNDTIMEO
重傳定時器(RTO)
開始
首先介紹一下滑動視窗。來自百度百科:滑動視窗協定的基本原理就是在任意時刻,發送方都維持了一個連續的允許發送的幀的序号,稱為發送視窗;同時,接收方也維持了一個連續的允許接收的幀的序号,稱為接收視窗。發送視窗和接收視窗的序号的上下界不一定要一樣,甚至大小也可以不同。不同的滑動視窗協定視窗大小一般不同。發送方視窗内的序列号代表了那些已經被發送,但是還沒有被确認的幀,或者是那些可以被發送的幀。下面舉一個例子(假設發送視窗尺寸為2,接收視窗尺寸為1)。
在這裡我們重點分析發送視窗,其原理是一個發送視窗,其中包括兩部分内容:已發送但未确認和可以發送但還未發送的。
逾時重傳
-
逾時重傳原理
逾時重傳是TCP保持可靠性中不可缺少的一項。其原理是發送一個資料後就開啟一個計時器,在一定時間内如果沒有得到ACK分節(如上圖Window Alreadly Sent 14bytes),則發送端重新發送資料封包,直到資料全部發送成功為止,此時會重置RTO。
當然需要注意的是,此份資料因為儲存在核心,而且儲存完成路由的資訊,是以在重傳時不會涉及到資料拷貝(應用程序到核心的雙向複制),也不需要上文切換,是以隻需要将資料從核心搬到網卡,重新發送即可。
- Linux下有兩個重要的核心參數和TCP逾時有關:
tcp_retries1:表示指定在底層IP接管之前(也就是不需要網絡層參與)TCP最少執行的重傳次數,預設值為3;
tcp_retries1:表示連接配接放棄前TCP最多執行的重傳次數,預設值為15(
一般對應13-30分鐘)。
堅持定時器(persist timer)
開始
TCP提供流量控制機制,其原理:TCP總是告知對端在任何時刻他一次能夠從對端接受多少位元組的資料,這也被為通告視窗(Advertised Window)。在任何時刻,該視窗指出接受緩沖區目前可用的空間量(可以使用
netstat -lpn檢視),進而確定發送端發送的資料不會使接收端緩沖區溢出。
- 目前接受和發送緩沖區大小
- 系統預設和最大接受緩沖區大小
實驗與分析
- 該視窗時刻動态變化的,當接受到來自發送端的資料時,視窗大小就減小;而接受端讀取資料後,視窗會變大。通過視窗大小可能是0,此時會通知發送端不要在發送資料,那麼問題來了,用戶端什麼時候再繼續發送資料呢?一直傻等着嗎?答案顯然是:No。
- 當然, TCP規範中約定,即使對端為0視窗模式,但依舊接受以下幾種封包 :零視窗探測封包、确認封包和攜帶緊急資料的封包段。是以,為解決這種死鎖問題,此時需要一個堅持定時器。TCP為每個連接配接設定一個堅持定時器,隻要TCP連接配接一方收到對方的0視窗通知,就啟用該定時器。若定時器到期,就發送一個零視窗探測封包(僅攜帶一個位元組的資料),對方在ACK這個探測封包時設定目前的視窗大小。
- 實驗
- 服務端代碼(centos6.7)
-
- 用戶端代碼
-
- 結果
解決方法
可以通過
SO_RCVBUF套接字來修改該TCP選項。(不建議)
延遲定時器(Delayed ACK)
開始
延遲應答定時器和ACK延滞(Delayed ACK Algorithm)算法息息相關。
ACK延滞算法思想是:TCP在接收到資料後并不立即發送ACK,而是等待一小段時間(典型值為50-200ms,即延遲定時器),然後才發送ACK。為提高網絡傳輸效率,避免某一個時刻網絡充斥着大量的ACK小封包,TCP期待在這一小段時間内自身有資料發送回對端,捎帶ACK,進而節省一個TCP分節。延遲應答定時器就是為防止某一時刻沒有資料,導緻ACK一直等待不發送,造成死鎖。
保活定時器(Keep Alive)
開始
- 首先需要明确一點的是TCP中的Keep Alive選項目的旨在檢測對端主機是否崩潰或不可達(譬如電源故障等),不同于HTTP的Keep Alive,HTTP1.1引入的目的是複用連接配接,也就是所謂的長連接配接,降低頻繁建立連接配接(即短連接配接)帶來的性能問題。
參數分析
- 在Linux系統中,SO_KEEPALIVE選項涉及到三個相關參數,可以通過man tcp指令來檢視。預設是7200s,即2小時
TCP_KEEPDILE:設定連接配接上如果沒有資料發送的話,多久後發送keepalive探測分組,機關是秒
TCP_KEEPINTVL :前後兩次探測之間的時間間隔,機關是秒
TCP_KEEPCNT :關閉一個非活躍連接配接之前的最大重試次數
實驗與分析
用戶端代碼
服務端代碼見上
- ==存在缺陷:==
- ==2小時時間太長==
- ==可能網絡抖動,導緻之前的分節丢失,實際上該連接配接依舊有效,導緻對端誤以為終止,進而結束一個有效連接配接。==
- ==針對這個問題,目前解決方案兩種:==
- ==第一種:修改核心參數,但是必須要注意大多數核心是基于整個核心維護的這些時間參數的,而不是基于每個套接字,是以如果把無活動周期從2小時修改為自定義的值,将會影響到該主機上所有開啟本選項的套接字(預設不開啟該選項)。==
- 第二種:避免使用該選項,應用層維護:
- 心跳包(hearbeat)
- 乒乓包(pingpong):和心跳包類似,除了攜帶标志位外,還可以攜帶少量資料==
- 使用TCP的緊急資料位(URG)攜帶心跳包
FIN_WAIT_2定時器
開始
FIN_WAIT_2定時器發生在TCP四次揮手過程,首先來看一下TCP四次揮手。
:首先終止一端發送完FIN分節點,等待對端回複ACK時的狀态。該狀态不應該過多,而且停留的時間很短。
FIN-WAIT-2狀态:收到對端的ACK确認後,在FIN-WAIT-1狀态直接轉至FIN-WAIT-2狀态,此時等待對端的FIN包。有時ACK和FIN一起,此時會跳過該狀态
參數分析
- 對于狀态的檢視可以使用指令:==netstat -nat|awk ‘{print awk $NF}’|sort|uniq -c|sort -n==來進行排序檢視
- 如果對端一直不發送FIN分節,FIN-WAIT-2狀态将可能會一直存在。但實際上核心參數 tcp_fin_timeout 會控制逾時時間。本機(cento6.5)預設值為==60s==
- 而且如果不是為了在半關閉狀态下繼續接受資料,連接配接長時間處于FIN-WAIT-2狀态并無益處。其中若用戶端執行半關閉後,未等伺服器關閉連接配接就強行退出。此時該連接配接将由核心接管,被稱之為 孤兒連接配接 。Linux為了防止孤兒連接配接長時間停留在核心中,将由兩個核心參數來控制。
- tcp_max_orphan:表示最大孤兒連接配接數
- tcp_fin_timeout:表示存活時間
- tcp_orphan_retries:表示重試次數
TIME_WAIT定時器
開始
在tcp有關的網絡程式設計中,最難了解且最容易碰到的就是TIME_WAIT狀态了。由于TIME_WAIT時長為2MSL(最長分節生命周期),是以也被稱為2MSL逾時。
任何TCP實作都必須為MSL選擇一個值。RFC1122建議為2m,但源自伯克利實作的系統,傳統上改用30s。綜合起來,TIME_WAIT狀态将持續在1m到4m。MSL是任何IP資料報能夠在網際網路存活的最長時間,盡管IP封包含有一個8位、稱為跳限的字段,最大值為255,但仍舊假設具有最大跳限的分組在網絡上存在的時間不可能超過MSL秒。
分析
- 可靠的實作全雙工連接配接的終止
結合上圖,假設用戶端(用戶端為主動斷開一方)丢失了服務端發來的FIN,伺服器必然會重新發送該分節,用戶端必須要維護狀态資訊,以允許他重新發送最終的那個ACK。要是用戶端不維護狀态資訊,他将會響應一個RST,該分節被伺服器解釋成一個錯誤。然而,此時可能還有資料流在傳輸。是以如果TCP打算執行所有必要的工作以徹底終止某個連接配接上兩個方向的資料流。那麼他必須正确處理連接配接終止序列4個分節中任何一個分節丢失情況。
- 允許老的重複分節在網絡中消失
因為每個資料包在網絡上最長生命周期為MSL,2MSL可以确任何一個資料包都将消失。這樣可以防止來自某個連接配接的老的重複分組在該連接配接已終止後再現。因為端口、IP資源都會回收,下次在複用。想想,如果你結束了一次支付寶通路,緊接着我又用了你剛才的IP和端口号去請求同一個網站,原先來不及發送的消息結果被我接受了,我有可能就得到你的密碼。
解決方法
- 使用SO_REUSEADDR和SO_REUSRPORT
- SO_REUSRADDR選項而言:
- 在所有TCP伺服器程式中,調用bind之前設定SO_REUSRADDR套接字選項
- 當編寫一個可同一時刻在同一主機運作多次的多點傳播應用程式時,設定SO_REUSRADDR套接字選項,并将鎖參加的多點傳播組的位址作為本地IP位址捆綁。( 這個可以參照nginx的設計 )
- SO_REUSRPORT選項而言:
- 主要是針對SO_REUSRADDR綁定通配位址和端口後,不能再綁定更為詳細的IP和端口
-
使用SO_LINGER選項
本選項指定close函數對面向連接配接的協定如何操作。預設操作是close立即傳回,但是如果有資料殘留在套接字發送緩沖區,系統将試着把這些資料發送給對端,然後在繼續進行正常的FIN序列。
然而SO_LINGER選項可以改變這個預設設定。其資料結構如下:
struct
引用
《TCP/IP詳解》
《Unix網絡程式設計:卷2》
《計算機網絡自頂向下方法》