題外話:剛剛過去的半個月實在是忙得我喘不過來氣,雖然手里還壓着幾個項目得在期末考試之前做完,但是想想還是更新一下隨筆,稍微換個心情。另外小吐槽一下那些在博客園里原封不動抄書當隨筆的人,唉真是....算了我不吐槽了哈哈,進入正題!
TCP/IP協議有關的書籍我在圖書館里翻看了很多,雖說每本側重都不大一樣,但是有一點是一樣的:TCP協議講義的篇幅都是其他協議的三到五倍!
下面總結一下TCP協議里最核心的知識和一些細節,首先從Overview開始:
一. 一張圖--TCP FSM
這張圖是TCP協議基礎之基礎之基礎,凝練了TCP連接宏觀的狀態的遷移,沒記住這個?嗯,那么你就肯定不懂TCP咯~盯着tcpdump和netstat里的一條條數據或許可以幫你背下來這個圖,要注意的是這張圖描述的是TCP連接的狀態,從編程的角度來看就是一對套接字(socket)組建起來的連接的角度來看的,實線是TCP客戶端連接狀態經過的狀態變遷,虛線是服務器端經過的狀態變遷,其中“主動打開”和“被動打開”大部分是從應用程序發起的操作,如果你是用的C/C++的套接字,那么主動打開就是connect()的動作,而被動打開是listen()的操作,圖中箭頭上的描述是連接過程中的數據的流動,關於細節下文再細說。
二. 另一張圖--Connect Process
對於一次正常的TCP連接我們要從兩個角度分別來看連接的過程,這和TCP協議的“雙全工”完全是兩碼事,每條單向的“數據通路”連接的兩端都會同時進行以下的狀態變遷:
客戶端:CLOSED->SYN_SENT->SYN_RCVD->ESTABLISHED->( FIN_WAIT1->FIN_WAIT2 | FIN_WAIT1 )->TIME_WAIT->CLOSED
服務端:CLOSED->LISTEN->SYN_RCVD->ESTABLISHED->CLOSE_WAIT->LACK_ACK->CLOSED
但是只討論正常情況下的TCP協議機制是個沒用意義的話題,真正值得思考的是在特殊情況下的TCP協議狀態的轉換過程,里面有一些細節是不得不提的,篇幅限制我就羅列一些關鍵字及其信息摘要供大家溫習回憶:
鏈接建立三次握手:
這個真的沒有太多好說的,有點不是理解為什么有人非常強調,這個過程非常的清晰明了,和社會中人溝通的方式是相似的:
“阿軍你真帥!” -> “謝謝呀,你也很帥!” -> “謝謝!”
SYN -> SYN,ACK -> ACK
但是有一點需要拋開來單獨討論的是一種叫做同時打開的狀況,盡管這種狀況非常的罕見,但是邊界情況是堅決不能忽略的:
雙方幾乎同時發出SYN包,雙方判斷同時打開的條件就是:
在等待SYN+ACK包的時候卻收到了一個SYN包,於是它就會進入SYN_RCVD狀態並且發出一個ACK包,雙方只進行了兩組包的交換就完成了進入ESTABLISHED狀態的過程。
鏈接關閉三次握手:
什么?這個也是三次握手?一定和上一個一樣簡單沒內含? 大錯特錯,鏈接關閉的三次握手過程比鏈接建立復雜了一個數量級!
和剛才一樣做個人之間溝通的情況模擬:
“阿軍我走了阿” -> "那我也走了阿,再見" -> "再見"
FIN -> FIN,ACK -> ACK
是這個樣子嘛?也許沒錯!但是大多數的情況是這樣的:
“阿軍我走了阿” -> "好的,但你先再聽我說兩句" ->(過了一會兒)-> “那我也走了阿” -> "再見"
FIN -> ACK -> .... -> FIN -> ACK
這是因為TCP協議是一種“雙全工"的協議,TCP鏈接可以想象成有兩條方向相反的數據通路之間相互溝通,當一方主動要求關閉鏈接的時候,另一方要回復答應關閉一個走向的數據通路但是維持另一個數據通路繼續進行數據傳輸,直到想要傳輸的數據傳輸完畢再發出一個關閉數據通路的請求並且等待回應,這個單向數據通路關閉的狀態叫做半關閉狀態。
有半關閉狀態就有半打開狀態,我個人認為半打開狀態應該放在關閉過程之中進行介紹,當一方已經關閉或者異常終止鏈接而另一方不知道,我們將這樣的鏈接狀態叫做半打開。
同樣地有同時打開就有同時關閉,相似地當,一方發出FIN包進入FIN_WAIT1狀態的時候如果接受到了另一方發送的FIN包那么就算作同時關閉了,雙方直接再次交換ACK報文終止。
下面進入TCP協議的主要細節:
首先要看以下TCP的報文格式,
幾個最關鍵的東西:
(1)窗口大小:
用“滑動”來描述窗口的變化過程是最恰當不過的,無論是發送窗口還是接受窗口都有左壁和右壁的概念,窗口是在報文緩沖內容上進行所謂滑動的。窗口從客戶端(發送端)的角度來講,窗口應該分為兩部分,一部分是已經發送但是並未收到確認的報文內容,另一部分是待發送的報文,在未受到確認的報文內容在計時器(RTO計數器)計數完畢之后仍未受到確認則會啟用報文重發機制重新發送一份報文內容,所以TCP協議是一個可靠的協議,在接受端的接受窗口也會隨着不斷地接受報文的同時發送確認報文,這里有一個特殊情況就是當收到的序列號之間出現了斷點或者內容校驗錯誤的時候,會再次發送一個ACK報文使序列號等於下一個希望接受的報文段的序列號(重復的ACK)。
可能產生的死鎖:
有一種可能發生的特殊情況,確認的丟失可能會引起系統的死鎖。當接受方發送了確認,同時把窗口大小調整為0,即請求關閉發送窗口時就會發生這樣的情況。過了一段時間之后,接受方打算取消這一限制,但是如果它沒有數據要發送,就會發送確認包,並且利用一個窗口大小非零的數值來取消這個限制。如果這個確認丟失了,那么就會產生問題,發送方一直在等待確認一個非零的窗口大小,而接收方則認為發送方已經收到了這個確認,因而正在等待數據,這種情況就是典型的死鎖。雙方都是在等待一個純粹的ACK確認,不涉及窗口內報文的確認,所以並沒有啟用RTO機制來重新發送ACK。要避免死鎖,就要設計一種持續計時器來處理這個問題。
(2)控制字段:六種不同的標志分別用於流量控制,鏈接建立和終止,鏈接異常終止及數據傳輸的確認
| URG | ACK | PSH | RST |SYN | FIN |
URG:緊急指針有效
ACK:確認是有效的
PSH:請求確認
RST:連接復位
SYN:同步序號
FIN:終止連接
(3)序號:本報文段第一個字節的編號,保證連接傳送數據的正確性。
(4)確認號:報文段的接受方期望從對方接受的字節編號。
三. FSM模擬偽代碼
TCP-FSM有限狀態機模擬偽代碼:
TCP_Main_Module(segment){ 查找 TCB(TransmitControlBlock) if(相應的TCB未找到) 創建TCB,其狀態為CLOSED 找到TCB表中相應表項的狀態 swith(狀態){ /// case CLOSED 狀態: if(收到 被動打開 報文)進入LISTEN狀態 if(收到 主動打開 報文){ 發送SYN報文段 進入SYN_SENT狀態 } if(收到任何報文段)發送RST報文段 if(收到其他任何報文)發出差錯報文 break /// case LISTEN 狀態: if(收到 發送數據 報文){ 發送SYN報文段 進入SYN_SENT狀態 } if(收到 任何SYN報文段 ){ 發送SYN+ACK報文段 進入SYN_RCVD狀態 } if(收到任何其他報文端或者報文){ 發出發錯報文 } break /// case SYN_SENT 狀態: if(超時)進入CLOSED狀態 if(收到SYN報文段){ 發送SYN+ACK報文段 進入SYN+RCVD狀態 } if(收到SYN+ACK報文段){ 發送ACK報文段 進入ESTABLISHED狀態 } if(收到任何其他報文段或者報文){ 發出差錯報文 } break case SYN_RCVD 狀態: if(收到ACK報文)進入ESTABLISH狀態 if(超時){ 發送RTS報文 進入CLOSED狀態 } if(收到 關閉 報文){ 進入FIN報文段 進入FIN_WAIT1狀態 } if(收到RTS報文段){ 進入LISTEN狀態 } if(收到任何其他報文段或者報文){ 發出差錯報文 } break // case ESTABLISHED 狀態: if(收到FIN報文段){ 發送FIN報文段 進入CLOSED-WAIT狀態 } if(收到 關閉 報文){ 發送FIN報文段 進入FIN-WAIT1狀態 } if(收到RTS或者SYN報文段)發出差錯報文 if(收到數據或者ACK報文段)調用輸入模塊 if(收到 發送 報文)調用輸出模塊 break //// case FIN-WAIT1 狀態: if(收到FIN報文段){ 發送ACK報文段 進入CLOSING狀態 } if(收到FIN+ACK報文段){ 發送ACK報文段 進入TIME-WAIT狀態 } if(收到ACK報文段){ 進入FIN-WAIT2狀態 } if(收到任何其他報文段或者報文){ 發出差錯報文 } break //// case FIN-WAIT2 狀態: if(收到FIN報文段){ 發送ACK報文段 進入TIME-WAIT狀態 } break case CLOSING 狀態: if(收到ACK報文段){ 進入TIME-WAIT狀態 } if(收到任何其他報文段或者報文){ 發出差錯報文 } break case TIME-WAIT 狀態: if(超時){ 進入CLOSED狀態 } if(收到任何其他報文段或者報文){ 發出差錯報文 } break case CLOSED-WAIT 狀態: if(收到 關閉 報文){ 發送FIN報文段 進入LAST-ACK狀態 } if(收到任何其他報文段或者報文){ 發出差錯報文 } break case LAST-ACK 狀態: if(收到ACK報文段){ 進入CLOSED狀態 } if(收到任何其他報文段或者報文){ 發出差錯報文 } break ///Ending } }