扒開TCP的“神秘外衣”,一文吃透它的本質!
在如今這個信息爆炸的時代,網絡早已成為我們生活中不可或缺的一部分。無論是日常刷社交媒體、觀看視頻,還是進行在線辦公、購物,背后都離不開數據在網絡中的傳輸。而在這復雜的網絡世界里,有一位默默奉獻的 “明星”——TCP(Transmission Control Protocol),它在數據傳輸過程中扮演著舉足輕重的角色 。
想象一下,你在網上購買了一件心儀已久的商品,下單后,商家就像發送數據的源頭,而你則是接收數據的目的地。為了確保商品能準確無誤地送到你手中,就需要一個可靠的運輸系統,這就好比網絡中的 TCP。TCP 就像是一位認真負責的快遞員,會確保數據從發送方準確、完整地抵達接收方,并且保證數據的順序正確,不會出現丟包、亂序的情況 。
從專業角度來講,TCP 是一種面向連接的、可靠的、基于字節流的傳輸層通信協議 。簡單來說,“面向連接” 意味著在數據傳輸之前,發送方和接收方需要先建立起一個連接,就像你要寄快遞,得先和快遞公司確定好收件人和寄件人的信息以及運輸路線;“可靠” 表示 TCP 會采取各種機制來保證數據的準確傳輸,如確認應答、重傳機制等;“基于字節流” 則是將數據看作是一連串的字節進行處理,而不是一個個獨立的數據包。
一、TCP協議概述
TCP(傳輸控制協議)是一種常用的網絡通信協議,它位于OSI模型的傳輸層。TCP提供可靠的、面向連接的數據傳輸服務。它通過使用序列號、確認應答和重傳機制,保證數據在網絡中的可靠傳輸。TCP還具有流量控制和擁塞控制功能,以確保發送方不會壓倒接收方和網絡。TCP協議是基于IP(Internet Protocol)協議之上構建的,它負責將應用程序分割成小塊(稱為報文段),并對這些報文段進行排序、重組和傳輸。
1.1 TCP歷史歷程
TCP 的誕生,就像是一部充滿傳奇色彩的科技史詩,它的出現徹底改變了網絡世界的格局 。故事要從20世紀60年代末講起,當時正處于美蘇冷戰時期,美國國防部為了在核戰爭威脅下確保通信的穩定性,啟動了 ARPANET 項目 。這個項目旨在建立一個分散的、容錯性強的通信網絡,即使部分節點遭到破壞,網絡的其他部分仍能正常通信 。1969 年,ARPANET 的首批 4 個節點建立,分別位于 UCLA、斯坦福研究所、加州大學圣巴巴拉分校和猶他大學,這便是互聯網的雛形 。
隨著 ARPANET 的發展,網絡中的計算機數量不斷增加,不同類型的計算機和網絡之間需要一種統一的通信標準,TCP/IP 協議便應運而生 。1974 年,Vinton Cerf 和 Robert Kahn 發表了關于 TCP/IP 協議的論文,提出了一種新型的網絡互聯協議,奠定了 TCP/IP 協議的基礎 。他們就像是網絡世界的 “建筑師”,精心設計著 TCP/IP 協議這座宏偉的大廈 。TCP/IP 協議最初設計用于保證數據的可靠傳輸,它能夠處理數據的傳輸、路由和網絡連接等功能,其設計的彈性和可擴展性,使其能夠適應網絡的不斷發展和變化 。
在接下來的幾年里,TCP/IP 協議不斷完善和發展。1978 年,TCP/IP 協議首次被實驗性使用;1983 年 1 月 1 日,ARPANET 正式啟用了 TCP/IP 協議,標志著互聯網技術的正式成型 。這一天,就像是網絡世界的 “成人禮”,TCP/IP 協議從此成為互聯網的核心協議,開啟了互聯網發展的新紀元 。
此后,TCP/IP協議搭上了UNIX操作系統這輛 “快車”,很快推出了基于套接字(socket)的實際編程接口,這使得開發人員能夠更方便地利用TCP/IP協議進行網絡編程 。同時,TCP/IP 協議是免費或者少量收費的,這大大擴大了其使用人群 。這些因素共同作用,使得TCP/IP 協議逐漸成為全球范圍內網絡通信的事實標準 。
1.2TCP協議特點
TCP 協議最主要的特點如下:
- 面向連接:應用程序在使用 TCP 協議之前,必須先建立 TCP 連接。
- 可靠性:TCP 提供可靠交付的服務。通過 TCP 連接傳送的數據,無差錯、不丟失、不重復,并且按序到達。如果數據包丟失或出現差錯,則 TCP 負責重發數據。
- 有序性:TCP 能夠把發送的數據劃分成一個個數據塊,編號后發送,接收方根據編號將這些數據塊組裝成完整的數據。因此,在接收端可以確保數據塊按照發送的順序進行組裝。
- 流量控制:TCP 還提供了流量控制的功能,保證發送方的發送速度不會過快,導致接收方處理不及時,從而導致數據丟失。發送方會根據接收方返回的確認信息,調整自己的發送速度。
- 擁塞控制:TCP 能夠根據網絡狀況調整傳輸數據的速率,防止出現擁塞。如果網絡出現擁塞,TCP 會通過降低發送方的數據傳輸速率和進行重傳等措施來保證數據的可靠傳輸。
1.3TCP常見問題解析
(1)什么是面向連接?
面向連接是一種網絡通信的方式,其中在通信前需要先建立一個雙方都認可的連接。在這個連接建立之后,通信的雙方可以進行數據傳輸。
面向連接的通信具有以下幾個特點:
- 連接建立階段:通信的雙方首先要進行握手過程來建立連接,確保彼此能夠相互識別和確認。
- 可靠性:在連接建立后,數據傳輸過程中會使用各種機制來確保數據的可靠性,如序列號、確認應答、重傳等。
- 順序性:數據在傳輸過程中會按照發送順序進行傳遞,接收方能夠按照發送順序正確地接收和處理數據。
- 面向字節流:面向連接的協議將數據視為連續的字節流,在發送端可能會對數據進行分割和組裝,在接收端則負責將字節流重新組裝成完整的數據。
常見的面向連接協議包括TCP(傳輸控制協議)和TLS(安全套接層協議),它們廣泛應用于互聯網通信、文件傳輸、電子郵件等場景。與之相對的是無連接通信,如UDP(用戶數據報協議),它不需要事先建立連接而直接進行數據傳輸。
舉例:就好比,你去醫院看病,如果是專家號,一般要提前預約,對只要預約(三次握手建立了連接)上了,你去了就不會看不上病,這是 TCP ;而如果你沒有預約,就直接跑過去,那不好意思,你只能看普通門診,而普通門診等的人很多,你就不一定能看得上病了。這是 UDP。既然是連接,必然是一對一的,就像繩子的兩端,所以 TCP 是一對一發送消息;而 UDP 協議不需要連接,可以一對一,也可以一對多,也可以多對多發送消息。
(2)什么是可靠的通信協議?
可靠的通信協議是指在數據傳輸過程中能夠確保數據的正確性、完整性和可達性的協議。它通過使用各種機制和算法來處理可能出現的錯誤、丟失、重復或亂序等問題,以保證數據能夠按照預期的方式傳輸到目標地址。
以下是一些常見的用于實現可靠通信的機制和算法:
- 序列號:發送方對每個發送的數據包進行編號,接收方按序接收并確認已接收的序列號,從而保證數據按正確順序傳遞。
- 確認應答:接收方在成功接收到數據后向發送方發送確認消息,告知發送方數據已經到達,若發送方未收到確認則進行重發。
- 超時重傳:如果發送方在規定時間內未收到確認應答,則會將該數據包視為丟失,并重新發送。
- 滑動窗口:通過設置滑動窗口大小來控制發送方可以連續發送多少個數據包,在接收方按順序接收之前不斷向前滑動,以提高效率和吞吐量。
- 錯誤檢測與糾正:利用校驗和、循環冗余檢測(CRC)等技術來檢測并修復傳輸中可能引入的錯誤。
- 流量控制:通過控制發送方的發送速率,以避免接收方無法及時處理大量數據導致緩沖溢出。
- 擁塞控制:根據網絡擁塞程度動態調整發送速率,防止網絡過載而導致數據丟失或延遲增加。
TCP 自身有三次握手和超時重傳等機制,所以無論網絡如何變化,主要不是主機宕機等原因都可以保證一個報文可以到達目標主機。
與之對比, UDP 就比較不負責任了,不管你收不收得到,反正我就無腦發,網絡擁堵我也發,它的職責是發出去。
(3)什么是面向字節流的?
面向字節流是一種通信模式,指的是數據在傳輸過程中被看作是連續的字節流,而不考慮數據之間的邊界。在這種模式下,發送方將數據按照字節序列發送,接收方則按照相同的順序接收和處理字節。
面向字節流的通信并不關心消息或數據包的邊界。它將數據切分為一個個字節,并通過底層的傳輸協議(如TCP)將字節流逐個傳送給接收方。接收方根據自己定義的解析規則來處理這些字節,并將它們組合成有意義的數據。
由于面向字節流不關心消息邊界,因此需要使用特定的機制來標識消息或數據包的開始和結束位置。常見的做法是在數據流中加入特殊字符或者長度信息來標識消息邊界。
面向字節流具有靈活性和可靠性,在實現上相對簡單,但同時也帶來了一些挑戰,例如粘包(多個消息黏在一起)和拆包(一個消息被分割成多個部分)問題需要額外處理。
1.4TCP協議數據傳輸流程
TCP 協議的數據傳輸流程包括三個步驟:
建立連接(三次握手):
a. 客戶端發送一個SYN(同步)包給服務器,并設置初始序列號。
b. 服務器收到SYN包后,回復一個帶有確認碼和新的序列號的SYN-ACK(同步-確認)包。
c. 客戶端再次發送一個帶有確認碼和序列號的ACK(確認)包。
數據傳輸:
a. 連接建立后,客戶端可以開始發送數據。將數據分割成適當大小的段,并添加序列號。
b. 接收方會對每個收到的段進行確認,確保數據按正確順序到達,并且沒有丟失或損壞。
c. 如果發現丟失的段,接收方會請求發送方重新傳輸該段。
連接關閉:
a. 當發送方完成數據傳輸后,它會發送一個FIN(結束)包來關閉連接。
b. 接收方收到FIN包后,回復一個ACK確認。
c. 然后接收方也發送一個FIN包給發送方來關閉雙向連接。
d. 發送方回復一個ACK確認,最終連接關閉。
二、TCP協議的工作原理
TCP協議工作在OSI七層模型的第四層,即傳輸層。TCP協議通過三次握手建立連接,在連接建立后進行數據傳輸,并通過四次揮手關閉連接。
2.1tcp報文格式
①TCP報文是TCP層傳輸的數據單元,也叫報文段
圖片
1、端口號:用來標識同一臺計算機的不同的應用進程。
1)源端口:源端口和IP地址的作用是標識報文的返回地址。
2)目的端口:端口指明接收方計算機上的應用程序接口。
TCP報頭中的源端口號和目的端口號同IP數據報中的源IP與目的IP唯一確定一條TCP連接。
②序號和確認號:是TCP可靠傳輸的關鍵部分。序號是本報文段發送的數據組的第一個字節的序號。在TCP傳送的流中,每一個字節一個序號。e.g.一個報文段的序號為300,此報文段數據部分共有100字節,則下一個報文段的序號為400。所以序號確保了TCP傳輸的有序性。確認號,即ACK,指明下一個期待收到的字節序號,表明該序號之前的所有數據已經正確無誤的收到。確認號只有當ACK標志為1時才有效。比如建立連接時,SYN報文的ACK標志位為0。
③數據偏移/首部長度:4bits。由于首部可能含有可選項內容,因此TCP報頭的長度是不確定的,報頭不包含任何任選字段則長度為20字節,4位首部長度字段所能表示的最大值為1111,轉化為10進制為15,15*32/8 = 60,故報頭最大長度為60字節。首部長度也叫數據偏移,是因為首部長度實際上指示了數據區在報文段中的起始偏移值。
④保留:為將來定義新的用途保留,現在一般置0。
⑤控制位:URG ACK PSH RST SYN FIN,共6個,每一個標志位表示一個控制功能。
1)URG:緊急指針標志,為1時表示緊急指針有效,為0則忽略緊急指針。
2)ACK:確認序號標志,為1時表示確認號有效,為0表示報文中不含確認信息,忽略確認號字段。
3)PSH:push標志,為1表示是帶有push標志的數據,指示接收方在接收到該報文段以后,
應盡快將這個報文段交給應用程序,而不是在緩沖區排隊。
4)RST:重置連接標志,用于重置由于主機崩潰或其他原因而出現錯誤的連接。或者用于拒絕非法的報文段和拒絕連接請求。
5)SYN:同步序號,用于建立連接過程,在連接請求中,SYN=1和ACK=0表示該數據段沒有使用捎帶的確認域,
而連接應答捎帶一個確認,即SYN=1和ACK=1。
6)FIN:finish標志,用于釋放連接,為1時表示發送方已經沒有數據發送了,即關閉本方數據流。
⑥窗口:滑動窗口大小,用來告知發送端接受端的緩存大小,以此控制發送端發送數據的速率,從而達到流量控制。窗口大小時一個16bit字段,因而窗口大小最大為65535。
⑦校驗和:奇偶校驗,此校驗和是對整個的 TCP 報文段,包括 TCP 頭部和 TCP 數據,以 16 位字進行計算所得。由發送端計算和存儲,并由接收端進行驗證。
⑧緊急指針:只有當 URG 標志置 1 時緊急指針才有效。緊急指針是一個正的偏移量,和順序號字段中的值相加表示緊急數據最后一個字節的序號。TCP 的緊急方式是發送端向另一端發送緊急數據的一種方式。
⑨選項和填充:最常見的可選字段是最長報文大小,又稱為MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段(為建立連接而設置SYN標志為1的那個段)中指明這個選項,它表示本端所能接受的最大報文段的長度。選項長度不一定是32位的整數倍,所以要加填充位,即在這個字段中加入額外的零,以保證TCP頭是32的整數倍。
⑩數據部分:TCP 報文段中的數據部分是可選的。在一個連接建立和一個連接終止時,雙方交換的報文段僅有 TCP 首部。如果一方沒有數據要發送,也使用沒有任何數據的首部來確認收到的數據。在處理超時的許多情況中,也會發送不帶任何數據的報文段。
2.2三次握手
(1)具體步驟拆解
在數據傳輸之前,TCP 需要在客戶端和服務器之間建立起可靠的連接,而這個建立連接的過程就像是一場精心編排的 “默契之舞”,被稱為三次握手 。接下來,我們就來詳細拆解一下這場 “舞蹈” 的每一個步驟 。
- 第一次握手:客戶端向服務器發送一個 SYN(同步)報文,這個報文就像是客戶端向服務器發出的一個連接邀請 。報文中會包含客戶端隨機生成的初始序列號(Initial Sequence Number,ISN),假設這個序列號為 x 。此時,客戶端就像一個熱情的舞者,已經做好了跳舞的準備,進入了 SYN_SEND 狀態,滿懷期待地等待著服務器的回應 。這個初始序列號的作用就像是給數據傳輸這條 “項鏈” 上的每顆 “珠子”(數據字節)都編上了號,以便后續接收方能夠正確地將它們串起來,保證數據的順序性 。
- 第二次握手:服務器就像一位優雅的舞者,收到客戶端的 SYN 報文后,會立即回應一個 SYN - ACK(同步確認)報文 。這個報文是服務器對客戶端邀請的回應,它包含兩部分重要信息 。一方面,服務器通過 ACK 標志位來確認收到了客戶端的 SYN 報文,確認號(Acknowledgment Number)設置為 x + 1,表示服務器期望下一次收到的序列號是 x + 1 。這就好比在舞會上,一方收到邀請后回應說:“我收到你的邀請啦,下一個動作就按這個順序來哦 。” 另一方面,服務器也會發送自己的 SYN 報文,帶上自己隨機生成的初始序列號,假設為 y 。這樣,服務器也準備好了跳舞,進入了 SYN_RECV 狀態 。
- 第三次握手:客戶端收到服務器的 SYN - ACK 報文后,就像舞者確認了對方的舞蹈節奏和順序,會發送一個 ACK(確認)報文 。報文中的確認號設置為 y + 1,確認收到了服務器的 SYN 報文 。此時,客戶端和服務器就像兩位默契十足的舞者,已經確定了彼此的節奏和順序,都進入了 ESTABLISHED 狀態,成功建立了 TCP 連接,這場 “默契之舞” 也就圓滿完成了 。之后,它們就可以開始愉快地進行數據傳輸 “舞蹈表演” 了 。
(2)為什么是三次?
在了解了三次握手的具體步驟后,你可能會好奇,為什么建立連接偏偏需要三次握手呢?兩次不行嗎?四次會不會更保險呢?其實,三次握手背后蘊含著深刻的邏輯,是在可靠性和效率之間找到的最佳平衡點 。
我們先來看看兩次握手的情況 。如果只有兩次握手,客戶端發送 SYN 報文,服務器響應 ACK 報文 。表面上看,似乎雙方都表明了自己的態度,客戶端說 “我想和你建立連接”,服務器回答 “好呀” 。但實際上,這里存在很大的問題 。因為服務器發送ACK報文后,它并不知道客戶端是否能收到這個確認 。如果客戶端因為網絡問題沒有收到 ACK 報文,那么客戶端就會認為連接沒有建立成功,可能會再次發送 SYN 報文 。而服務器卻以為連接已經建立,在那里干等著客戶端發送數據,這就導致了服務器資源的浪費,而且還可能出現數據傳輸錯誤 。就好比兩個人約好見面,一個人說 “我在老地方等你”,另一個人回答 “我知道啦”,但如果回答的這個人聲音太小,對方沒聽見,那么第一個人可能會一直等下去,而第二個人卻不知道對方沒收到自己的回應,這就容易產生誤會 。
再說說四次握手 。從理論上來說,四次握手當然可以建立連接 。比如,客戶端發送 SYN 報文,服務器先回復 ACK 報文確認收到,然后再發送 SYN 報文,客戶端再回復 ACK 報文確認 。但這樣做會增加連接建立的時間和網絡開銷 。在如今這個追求高效的網絡時代,每一次額外的握手都會帶來一定的延遲,就像你去餐廳吃飯,服務員如果每做一個小步驟都要跟你確認一次,雖然很保險,但會讓你等得不耐煩 。而且,三次握手已經足以保證雙方都能確認彼此的發送和接收能力,四次握手并沒有帶來實質性的好處,反而有點 “畫蛇添足” 。
而三次握手就巧妙地解決了這些問題 。通過三次握手,客戶端和服務器都能確認對方的接收和發送能力 。第一次握手,客戶端發送 SYN 報文,證明客戶端的發送能力正常;第二次握手,服務器接收并回復 SYN - ACK 報文,證明服務器的接收和發送能力正常;第三次握手,客戶端接收并回復 ACK 報文,證明客戶端的接收能力正常 。這樣一來,雙方都清楚地知道對方已經準備好了,而且避免了資源的浪費和不必要的延遲,就像兩個經驗豐富的舞者,通過簡單而有效的溝通,迅速達成默契,開始精彩的舞蹈表演 。
2.3TCP的數據傳輸
應用層的數據放在write bytes中,通過應用程序將write bytes寫入TCP Send buffer,TCP Send buffer里面是以Segment的形式來存儲的,TCP進行發送的時候首先進行分段,數據發送出去后,接收方首先也會將接收到的這些段存儲到接收緩存區里,應用程序可以調用相關的函數,從接收緩存區中將數據讀出來,讀到Read bytes里。數據在send buffer和receive buffer中都是以段的形式存儲的。
圖片
⑴數據傳輸
發送方將數據發送出去后,會附有下次發送的序列號,而接收方會回應兩個比較重要的參數,一個是回應值,另一個是提示窗口,來告訴發送方下一次可以發送多大的數據。
圖片
現在我們以一個簡單的狀態機來看一下TCP的發送過程。在考慮簡單狀態機的時候,設定以下條件:單向傳輸過程,同時不考慮流量,也不考慮擁塞控制。
那對于TCP的發送方來講,應該有三個事件需要進行處理:
- 當從應用程序中接收到要發送的數據后,進行創建分段。
- 當發現某一個段的回應號超時,需要進行重傳,這是為了確保TCP的數據到達對方的一個方式。
- 當看到對方回應ACK時,要對ACK進行處理,主要是看數據是否到達了對方。
⑵TCP的接收方對ACK的處理方式
圖片
⑶TCP交互式數據傳輸
交互式數據傳輸最典型的一種應用就是Telnet,Telnet和Rlogin比較偏向發送交換式的數據。
圖片
注意:ACK與下一段的數據一起發送。TCP會將ACK和數據封成一個包,也叫作delay ACK。
2.4TCP的四次揮手
當數據傳輸完成后,就像一場精彩的演出結束,客戶端和服務器需要優雅地結束它們之間的 TCP 連接,這個過程被稱為四次揮手 。四次揮手的過程就像是兩個人告別時的禮貌對話,確保雙方都做好了結束交流的準備 。
圖片
(1)揮手之前主動釋放連接的客戶端結束ESTABLISHED階段。
隨后開始“四次揮手”:
①首先客戶端想要釋放連接,向服務器端發送一段TCP報文,其中:標記位為FIN,表示“請求釋放連接“;序號為Seq=U;隨后客戶端進入FIN-WAIT-1階段,即半關閉階段。并且停止在客戶端到服務器端方向上發送數據,但是客戶端仍然能接收從服務器端傳輸過來的數據。注意:這里不發送的是正常連接時傳輸的數據(非確認報文),而不是一切數據,所以客戶端仍然能發送ACK確認報文。
②服務器端接收到從客戶端發出的TCP報文之后,確認了客戶端想要釋放連接,隨后服務器端結束ESTABLISHED階段,進入CLOSE-WAIT階段(半關閉狀態)并返回一段TCP報文,其中:標記位為ACK,表示“接收到客戶端發送的釋放連接的請求”;序號為Seq=V;確認號為Ack=U+1,表示是在收到客戶端報文的基礎上,將其序號Seq值加1作為本段報文確認號Ack的值;隨后服務器端開始準備釋放服務器端到客戶端方向上的連接。客戶端收到從服務器端發出的TCP報文之后,確認了服務器收到了客戶端發出的釋放連接請求,隨后客戶端結束FIN-WAIT-1階段,進入FIN-WAIT-2階段前"兩次揮手"既讓服務器端知道了客戶端想要釋放連接,也讓客戶端知道了服務器端了解了自己想要釋放連接的請求。于是,可以確認關閉客戶端到服務器端方向上的連接了
③服務器端自從發出ACK確認報文之后,經過CLOSED-WAIT階段,做好了釋放服務器端到客戶端方向上的連接準備,再次向客戶端發出一段TCP報文,其中:標記位為FIN,ACK,表示“已經準備好釋放連接了”。注意:這里的ACK并不是確認收到服務器端報文的確認報文。序號為Seq=W;確認號為Ack=U+1;表示是在收到客戶端報文的基礎上,將其序號Seq值加1作為本段報文確認號Ack的值。隨后服務器端結束CLOSE-WAIT階段,進入LAST-ACK階段。并且停止在服務器端到客戶端的方向上發送數據,但是服務器端仍然能夠接收從客戶端傳輸過來的數據。
④客戶端收到從服務器端發出的TCP報文,確認了服務器端已做好釋放連接的準備,結束FIN-WAIT-2階段,進入TIME-WAIT階段,并向服務器端發送一段報文,其中:標記位為ACK,表示“接收到服務器準備好釋放連接的信號”。序號為Seq=U+1;表示是在收到了服務器端報文的基礎上,將其確認號Ack值作為本段報文序號的值。確認號為Ack=W+1;表示是在收到了服務器端報文的基礎上,將其序號Seq值作為本段報文確認號的值。隨后客戶端開始在TIME-WAIT階段等待2MSL
(2)為什么客戶端發起了ack之后,服務端可以立馬關閉,而客戶端則要等待2MSL,才能進入關閉狀態呢。
要理解這個問題,首先我們需要弄清楚什么叫MSL。MSL是Maximum Segment Lifetime的英文縮寫,可譯為“最長報文段壽命”,它是任何報文在網絡上存在的最長的最長時間,超過這個時間報文將被丟棄,RFC793中規定MSL為2分鐘。要記住TCP是可靠的協議,之所以要有2MSL的等待,是因為端口是可以復用的,保證服務器已經接收到來客戶端的ack,然后正常關閉服務器測的連接,要不復用端口的時候會對新的連接造成干擾。
為了更方便說明,我們先假設我們認同是2MSL是最保守可靠的方案,什么現象表明服務器已經接收到了ack了,一個方法是服務器對于ack進行ack,客戶端就ack又ack,這樣子就無限循環了。還有一種就是TCP之所以可靠是因為還有針對唯有ack的數據段會有重發機制。所以如果服務器沒有關閉了沒有再次發送FIN請求,我們就基本可以假定服務器已經收到了ack了,所以2MSL=FIN報文(來)+ack報文(去),當然也可以是2MSL=ack報文(去)+FIN報文(來),在2MSL的時間再也沒有收到服務器的fIN請求,所以我們可以認為服務已經收到ack關閉連接了。
服務器端收到從客戶端發出的TCP報文之后結束LAST-ACK階段,進入CLOSED階段。由此正式確認關閉服務器端到客戶端方向上的連接。客戶端等待完2MSL之后,結束TIME-WAIT階段,進入CLOSED階段,由此完成“四次揮手”。后“兩次揮手”既讓客戶端知道了服務器端準備好釋放連接了,也讓服務器端知道了客戶端了解了自己準備好釋放連接了。于是,可以確認關閉服務器端到客戶端方向上的連接了,由此完成“四次揮手”。與“三次揮手”一樣,在客戶端與服務器端傳輸的TCP報文中,雙方的確認號Ack和序號Seq的值,都是在彼此Ack和Seq值的基礎上進行計算的,這樣做保證了TCP報文傳輸的連貫性,一旦出現某一方發出的TCP報文丟失,便無法繼續"揮手",以此確保了"四次揮手"的順利完成。
“四次揮手”的通俗理解:
舉個栗子:把客戶端比作男孩,服務器比作女孩。通過他們的分手來說明“四次揮手”過程。
圖片
“第一次揮手”:日久見人心,男孩發現女孩變成了自己討厭的樣子,忍無可忍,于是決定分手,隨即寫了一封信告訴女孩。“第二次揮手”:女孩收到信之后,知道了男孩要和自己分手,怒火中燒,心中暗罵:你算什么東西,當初你可不是這個樣子的!于是立馬給男孩寫了一封回信:分手就分手,給我點時間,我要把你的東西整理好,全部還給你!男孩收到女孩的第一封信之后,明白了女孩知道自己要和她分手。隨后等待女孩把自己的東西收拾好。“第三次揮手”:過了幾天,女孩把男孩送的東西都整理好了,于是再次寫信給男孩:你的東西我整理好了,快把它們拿走,從此你我恩斷義絕!“第四次揮手”:男孩收到女孩第二封信之后,知道了女孩收拾好東西了,可以正式分手了,于是再次寫信告訴女孩:我知道了,這就去拿回來!這里雙方都有各自的堅持。女孩自發出第二封信開始,限定一天內收不到男孩回信,就會再發一封信催促男孩來取東西!男孩自發出第二封信開始,限定兩天內沒有再次收到女孩的信就認為,女孩收到了自己的第二封信;若兩天內再次收到女孩的來信,就認為自己的第二封信女孩沒收到,需要再寫一封信,再等兩天……
倘若雙方信都能正常收到,最少只用四封信就能徹底分手!這就是“四次揮手”。
(3)為什么“握手”是三次,“揮手”卻要四次?
TCP建立連接時之所以只需要"三次握手",是因為在第二次"握手"過程中,服務器端發送給客戶端的TCP報文是以SYN與ACK作為標志位的。SYN是請求連接標志,表示服務器端同意建立連接;ACK是確認報文,表示告訴客戶端,服務器端收到了它的請求報文。
即SYN建立連接報文與ACK確認接收報文是在同一次"握手"當中傳輸的,所以"三次握手"不多也不少,正好讓雙方明確彼此信息互通。
TCP釋放連接時之所以需要“四次揮手”,是因為FIN釋放連接報文與ACK確認接收報文是分別由第二次和第三次"握手"傳輸的。為何建立連接時一起傳輸,釋放連接時卻要分開傳輸?
建立連接時,被動方服務器端結束CLOSED階段進入“握手”階段并不需要任何準備,可以直接返回SYN和ACK報文,開始建立連接。釋放連接時,被動方服務器,突然收到主動方客戶端釋放連接的請求時并不能立即釋放連接,因為還有必要的數據需要處理,所以服務器先返回ACK確認收到報文,經過CLOSE-WAIT階段準備好釋放連接之后,才能返回FIN釋放連接報文。
所以是“三次握手”,“四次揮手”。
三、TCP 協議的使用
3.1建立TCP連接
在C++中,可以使用Socket類來建立TCP連接。下面是一個簡單的示例代碼,展示了如何使用Socket類建立TCP連接的流程:
- 實例化 Socket 對象:可以使用帶有主機名和端口號參數的構造器來新建 Socket 對象。
- 建立連接:使用 Socket 類中的 connect() 方法來建立連接。
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
// 創建套接字
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
// 設置服務器地址和端口號
struct sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(8000); // 設置服務器端口號
inet_pton(AF_INET, "127.0.0.1", &(serverAddress.sin_addr)); // 設置服務器地址
// 建立連接
if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) < 0) {
std::cerr << "Failed to connect to the server." << std::endl;
return -1;
}
// 發送數據
const char* message = "Hello, server!"; // 待發送的消息
if (send(clientSocket, message, strlen(message), 0) < 0) {
std::cerr << "Failed to send data to the server." << std::endl;
return -1;
}
// 接收數據
char buffer[1024];
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesRead < 0) {
std::cerr << "Failed to receive data from the server." << std::endl;
return -1;
}
buffer[bytesRead] = '\0';
std::cout << "Received: " << buffer << std::endl;
// 關閉連接
close(clientSocket);
return 0;
}
在這個示例中,首先創建了一個客戶端套接字,然后設置服務器地址和端口號。之后使用connect()
函數建立與服務器的連接;接下來,使用send()函數發送數據到服務器,并使用recv()函數從服務器接收數據。最后調用close()函數關閉連接。
3.2發送數據
在C++中,使用Socket類建立TCP連接后,可以通過send()
函數向服務器發送數據,而無需使用getOutputStream()
方法。下面是一個示例代碼展示如何發送數據給服務器:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
// 創建套接字
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
// 設置服務器地址和端口號
struct sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_port = htons(8000); // 設置服務器端口號
inet_pton(AF_INET, "127.0.0.1", &(serverAddress.sin_addr)); // 設置服務器地址
// 建立連接
if (connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress)) < 0) {
std::cerr << "Failed to connect to the server." << std::endl;
return -1;
}
// 發送數據
const char* message = "Hello, server!"; // 待發送的消息
if (send(clientSocket, message, strlen(message), 0) < 0) {
std::cerr << "Failed to send data to the server." << std::endl;
return -1;
}
// ...
// 關閉連接
close(clientSocket);
return 0;
}
在這個示例中,通過調用send()
函數將消息發送給服務器。你可以將要發送的數據存儲在一個字符數組或字符串中,并指定其長度作為第三個參數傳遞給send()
函數。請確保在發送之前將數據轉換為適當的格式。
需要注意的是,這個示例僅展示了向服務器發送數據的部分,其他相關步驟(如建立連接、接收響應等)可以參考之前提供的代碼。同時,你可能需要根據具體情況進行適當的錯誤處理和異常處理。
3.3接收數據
在C++中,建立連接并接收從服務器返回的數據通常使用套接字(socket)和標準庫函數。不同于Java的Socket類,C++需要通過操作系統提供的底層API來進行網絡編程。
以下是一個簡單的示例代碼,展示如何使用C++中的socket和recv函數來接收服務器返回的數據:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
int main() {
// 創建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 設置服務器地址
struct sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8000); // 指定服務器端口號
inet_pton(AF_INET, "127.0.0.1", &(serverAddr.sin_addr)); // 指定服務器IP地址
// 連接到服務器
connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
// 接收從服務器返回的數據
char buffer[1024];
ssize_t bytesRead = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (bytesRead > 0) {
buffer[bytesRead] = '\0'; // 添加字符串結束符
std::cout << "Received data: " << buffer << std::endl;
}
// 關閉套接字
close(sockfd);
return 0;
}
在這個示例中,我們首先創建了一個套接字(sockfd),然后設置了服務器的地址信息,并調用connect函數與服務器建立連接。接著,我們使用recv函數來接收從服務器返回的數據,并將其存儲在buffer中。最后,我們打印出接收到的數據并關閉套接字,需要注意的是,在實際應用中,你可能還需要進行錯誤處理和異常處理,并確保適當地關閉套接字(socket)等資源。
3.4釋放連接
在C++中釋放連接通常使用socket和close函數來關閉套接字。
以下是一個簡單的示例代碼,展示如何在C++中釋放連接:
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
int main() {
// 創建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 設置服務器地址
struct sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8000); // 指定服務器端口號
inet_pton(AF_INET, "127.0.0.1", &(serverAddr.sin_addr)); // 指定服務器IP地址
// 連接到服務器
connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
// 發送數據或進行其他操作
// 關閉套接字
close(sockfd);
return 0;
}
在這個示例中,在與服務器建立連接并完成相關操作后,我們調用close函數來關閉套接字(sockfd),以釋放連接資源。需要注意的是,釋放連接時應確保已經完成了所有需要傳輸的數據或操作,并根據實際需求進行適當的錯誤處理和異常處理。
四、可靠傳輸:TCP 的 “看家本領”
4.1序列號與確認應答機制
在 TCP 的世界里,數據就像是一列有序的火車,而序列號則是給每節車廂(字節)貼上的獨一無二的編號 。TCP 會為每個發送的字節都編上號,這樣接收方在收到數據后,就可以根據這些序列號來對數據進行排序,確保數據按序到達 。比如,你要發送一段包含 100 個字節的數據,TCP 會從某個初始序列號開始,依次為這 100 個字節編號 。
而確認應答機制就像是接收方給發送方的 “小紙條”,用來告訴發送方哪些數據已經成功接收 。當接收方收到數據后,會檢查數據的序列號,然后發送一個 ACK(確認)報文給發送方 。報文中的確認號(Acknowledgment Number)會設置為下一個期望接收的序列號 。例如,接收方收到了序列號為 1 - 100 的字節,它會回復一個 ACK 報文,確認號設置為 101,表示它已經成功接收了 1 - 100 的字節,期望下一次收到序列號為 101 的字節 。通過這種方式,發送方就可以知道哪些數據已經被接收,哪些還需要重發 。
4.2超時重傳機制
在網絡傳輸過程中,就像現實中的交通一樣,可能會出現各種意外情況,導致數據丟失或延遲 。超時重傳機制就是 TCP 應對這些情況的 “秘密武器” 。當發送方發送數據后,會啟動一個重傳定時器 。如果在規定的時間內(這個時間被稱為超時重傳時間,Retransmission TimeOut,RTO)沒有收到接收方的 ACK 確認報文,發送方就會認為數據可能丟失了,于是重新發送這些數據 。
重傳定時器的作用就像是一個 “鬧鐘”,提醒發送方什么時候該重傳數據 。而這個 “鬧鐘” 的時間設置可不是固定不變的,它需要根據網絡狀況動態調整 。如果 RTO 設置得過大,發送方會等待很長時間才發現數據丟失,這會降低數據傳輸的效率;如果 RTO 設置得過小,發送方可能會頻繁重傳一些并沒有丟失的數據,浪費網絡資源 。為了找到一個合適的 RTO 值,TCP 采用了自適應算法 。它會根據網絡的往返時間(Round - Trip Time,RTT),也就是數據從發送方到接收方再返回發送方所需要的時間,來動態調整 RTO 。比如,如果當前網絡比較擁堵,RTT 變長,TCP 就會相應地增大 RTO;如果網絡狀況良好,RTT 變短,RTO 也會隨之減小 。
4.3流量控制:避免 “交通堵塞”
在網絡傳輸中,發送方和接收方就像兩個不同速度的 “搬運工”,如果發送方發送數據的速度太快,而接收方處理數據的速度跟不上,就會像交通堵塞一樣,導致數據在接收方堆積,甚至丟失 。流量控制就是 TCP 為了避免這種情況而采取的措施,它通過滑動窗口機制來實現 。
滑動窗口機制就像是一個可伸縮的 “窗口”,控制著發送方和接收方之間的數據傳輸量 。接收方會根據自身的接收能力,在 ACK 報文中告訴發送方自己當前能夠接收的數據量,這個數據量就是接收窗口的大小 。發送方會根據接收方通告的接收窗口大小,來調整自己的發送窗口大小 。例如,接收方的接收窗口大小為 1000 字節,發送方的發送窗口大小也會相應地設置為 1000 字節,這意味著發送方最多可以連續發送 1000 字節的數據而不需要等待接收方的確認 。當接收方處理完一部分數據后,接收窗口會向前滑動,發送方也會根據新的接收窗口大小來調整自己的發送窗口,繼續發送數據 。這樣,就可以確保發送方發送數據的速度與接收方的接收能力相匹配,避免數據丟失和網絡擁塞 。
4.4擁塞控制:應對網絡 “大堵車”
擁塞控制是 TCP 為了應對網絡擁塞而采取的策略,就像是交通警察在道路擁堵時采取的疏導措施 。當網絡中出現擁塞時,路由器可能會丟棄數據包,導致發送方收不到 ACK 確認報文,從而觸發超時重傳 。而超時重傳又會進一步加重網絡擁塞,形成惡性循環 。為了打破這個循環,TCP 采用了一系列擁塞控制算法 。
- 慢啟動:在 TCP 連接剛建立時,就像一輛剛啟動的汽車,發送方會以較小的擁塞窗口(通常為 1 個最大報文段長度,MSS)開始傳輸數據 。然后,每收到一個 ACK 確認報文,擁塞窗口就會增加 1 個 MSS 。這樣,擁塞窗口會以指數級的速度增長,逐漸探測網絡的擁塞情況 。就好比汽車剛啟動時,速度慢慢加快,觀察路況 。
- 擁塞避免:當擁塞窗口增長到一定程度(慢啟動門限,ssthresh)時,就進入了擁塞避免階段 。此時,擁塞窗口不再以指數級增長,而是每收到一個 ACK 確認報文,就增加 1/MSS 。這就像是汽車在路況良好時,保持穩定的速度行駛 。通過這種線性增長的方式,避免網絡突然擁塞 。
- 快速重傳:如果接收方收到了失序的報文段,它會立即發送重復確認(對前面有序部分的確認),而不是等待自己發送數據時才捎帶確認 。當發送方連續收到三個重復確認時,就會知道中間有報文段丟失了,于是立即重傳丟失的報文段,而不是等待超時計時器超時后再重傳 。這就好比你發現快遞少了一件,馬上聯系商家補發,而不是等很久才發現 。
- 快速恢復:當發送方收到三個重復確認,執行快速重傳后,會進入快速恢復階段 。此時,慢啟動門限(ssthresh)會設置為當前擁塞窗口的一半,然后擁塞窗口會設置為慢啟動門限加上3個MSS(因為收到了三個重復確認,說明有三個數據報文段已經離開了網絡,到達了接收方) 。之后,擁塞窗口會以線性增長的方式繼續調整,逐漸恢復到最佳的傳輸速率 。這就像是道路擁堵緩解后,汽車逐漸提速,但也不會一下子開得太快 。
通過這些擁塞控制算法,TCP 能夠根據網絡擁塞狀況動態調整發送速率,確保網絡的穩定和高效運行 。
五、TCP 的應用場景
TCP 在我們的日常生活中無處不在,就像一個隱形的助手,默默地保障著各種網絡應用的穩定運行 。下面,我們就來看看 TCP 在一些常見場景中的應用 。
5.1網頁瀏覽:讓世界觸手可及
當你在瀏覽器中輸入一個網址,然后輕松地瀏覽著豐富多彩的網頁時,背后離不開 TCP 的支持 。網頁瀏覽通常使用 HTTP(超文本傳輸協議)或 HTTPS(安全超文本傳輸協議),而這兩種協議都是基于 TCP 實現的 。比如,當你訪問淘寶的網站時,瀏覽器會作為客戶端,與淘寶的服務器建立 TCP 連接 。通過三次握手,雙方確認連接無誤后,瀏覽器向服務器發送 HTTP 請求,請求獲取網頁的內容 。服務器接收到請求后,將網頁數據按照 TCP 協議的規則,分割成多個數據包,依次發送給瀏覽器 。
在這個過程中,TCP 的可靠傳輸機制確保了每個數據包都能準確無誤地到達瀏覽器 。如果某個數據包在傳輸過程中丟失,TCP 會通過重傳機制重新發送,保證網頁數據的完整性 。同時,TCP 的流量控制和擁塞控制機制會根據網絡狀況調整數據傳輸速度,避免網絡擁塞,讓你能夠快速、穩定地加載網頁 。當你點擊網頁上的鏈接、查看圖片、觀看視頻時,每一次的數據請求和傳輸,都有 TCP 在背后保駕護航 。
5.2電子郵件:跨越時空的信息傳遞
電子郵件是我們日常生活和工作中常用的通信方式之一,它的正常運行也依賴于 TCP 協議 。在電子郵件的發送過程中,發送方的郵件客戶端會與發送方的郵件服務器建立 TCP 連接,通過 SMTP(簡單郵件傳輸協議)將郵件發送到發送方的郵件服務器 。發送方的郵件服務器再通過 TCP 連接,將郵件轉發給接收方的郵件服務器 。接收方的郵件客戶端在需要接收郵件時,同樣會與接收方的郵件服務器建立 TCP 連接,使用 POP3(郵局協議版本 3)或 IMAP(互聯網郵件訪問協議)從郵件服務器獲取郵件 。
在這個過程中,TCP 確保了郵件在傳輸過程中的準確性和完整性 。想象一下,你給遠方的朋友發送一封重要的郵件,包含了一些珍貴的照片和文件 。如果沒有 TCP 的可靠傳輸機制,這些照片和文件可能會在傳輸過程中丟失或損壞,導致朋友無法正常接收 。而有了 TCP,你可以放心地發送郵件,不用擔心郵件會 “迷路” 或 “受傷” 。
5.3文件傳輸:大數據的穩定搬運工
在網絡中傳輸文件時,我們希望文件能夠完整、準確地到達目的地,TCP 正好滿足了這一需求 。許多文件傳輸協議,如FTP(文件傳輸協議)、SFTP(安全文件傳輸協議)等,都是基于TCP 實現的 。以 FTP 為例,當你使用 FTP 客戶端上傳文件到 FTP 服務器時,首先會建立TCP連接 。連接建立后,客戶端和服務器之間會進行一系列的命令交互,確定傳輸的文件、傳輸模式等信息 。
然后,文件數據會被分割成多個TCP 數據包,在可靠傳輸機制的保障下,從客戶端傳輸到服務器 。同樣,在下載文件時,服務器也會通過TCP將文件數據準確地發送給客戶端 。比如,你要將一份大型的項目文檔上傳到公司的文件服務器,或者從服務器上下載一些重要的資料 。這些文件可能包含了大量的數據,如果使用不可靠的傳輸方式,很容易出現數據丟失或錯誤的情況 。而 TCP 就像一個專業的搬運工,能夠穩穩地將文件從一端搬運到另一端,確保文件的完整性 。