一、TCP簡單介紹
我們經常聽人說TCP是一個面向連接的(connection-oriented)、可靠的(reliable)、字節流式(byte stream)傳輸協議, TCP的這三個特性該怎么理解呢?
-
面向連接:在應用TCP協議進行通信之前雙方通常需要通過三次握手來建立TCP連接,連接建立后才能進行正常的數據傳輸,因此廣播和多播不會承載在TCP協議上。(谷歌提交了一個RFC文檔,建議在TCP三次握手的過程允許SYN數據包中帶數據,即 TFO(TCP Fast Open),目前ubuntu14.04已經支持該TFO功能)。但是同時面向連接的特性給TCP帶來了復雜的連接管理以及用於檢測連接狀態的存活檢測機制。
-
可靠性:由於TCP處於多跳通信的IP層之上,而IP層並不提供可靠的傳輸,因此在TCP層看來就有四種常見傳輸錯誤問題,分別是比特錯誤(packet bit errors)、包亂序(packet reordering)、包重復(packet duplication)、丟包(packet erasure或稱為packet drops),TCP要提供可靠的傳輸,就需要有額外的機制處理這幾種錯誤。因此個人理解可靠性體現在三個方面,首先TCP通過超時重傳和快速重傳兩個常見手段來保證數據包的正確傳輸,也就是說接收端在沒有收到數據包或者收到錯誤的數據包的時候會觸發發送端的數據包重傳(處理比特錯誤和丟包)。其次TCP接收端會緩存接收到的亂序到達數據,重排序后在向應用層提供有序的數據(處理包亂序)。最后TCP發送端會維持一個發送"窗口"動態的調整發送速率以適用接收端緩存限制和網絡擁塞情況,避免了網絡擁塞或者接收端緩存滿而大量丟包的問題(降低丟包率)。因此可靠性需要TCP協議具有超時與重傳管理、窗口管理、流量控制、擁塞控制等功能。另外TFO下TCP有可能向應用層提供重復的數據,也就是不可靠傳輸,但是只會發生在連接建立階段,我們后續會進行介紹。
-
字節流式:應用層發送的數據會在TCP的發送端緩存起來,統一分片(例如一個應用層的數據包分成兩個TCP包)或者打包(例如兩個或者多個應用層的數據包打包成一個TCP數據包)發送,到接收端的時候接收端也是直接按照字節流將數據傳遞給應用層。作為對比,同樣是傳輸層的協議,UDP並不會對應用層的數據包進行打包和分片的操作,一般一個應用層的數據包就對應一個UDP包。這個也是伴隨TCP窗口管理、擁塞控制等。
在接下來的TCP系列中,我們將會依次介紹TCP協議的連接管理、超時與重傳、流量控制、窗口管理、擁塞控制、存活檢測等機制。在深入介紹這些內容之前我們先來看一下TCP的封裝和協議頭的格式(TCP/IP的網絡分層等基礎網絡概念本處不在介紹)
二、TCP的封裝和協議頭的格式
TCP封裝在IP報文中的時候,如下圖所示,TCP頭緊接着IP頭(IPV6有擴展頭的時候,則TCP頭在擴展頭后面),不攜帶選項(option)的TCP頭長為20bytes,攜帶選項的TCP頭最長可到60bytes。
其中不攜帶選項的TCP頭如下圖所示(其中陰影部分的四個字段表示了相反方向的數據流信息),其中header length字段由4比特構成,最大為15,單位是32比特(32-bit word),即頭長的最大值為15*32 bits = 60bytes,因此上面說攜帶選項的TCP頭長最長為60bytes。
TCP頭中的相關字段順序解釋如下:
TCP源端口(Source Port):16位的源端口其中包含發送方應用程序對應的端口。源端口和源IP地址標示報文發送端的地址。
TCP目的端口(Destination port):16位的目的端口域定義傳輸的目的。這個端口指明報文接收計算機上的應用程序地址接口。
TCP的源端口、目的端口、以及IP層的源IP地址、目的IP地址四元組唯一的標識了一個TCP連接,一個IP地址和一個端口號的組合叫做一個endpoint或者socket。也即一對endpoint或者一對socket唯一的標識了一個TCP連接。接收端的TCP層就是根據不同的端口號來將數據包傳送給應用層的不同程序,這個過程叫做解復用(demultiplex)。相應的發送端會把應用層不同程序的數據映射到不同的端口號,這個過程叫做復用(multiplex)。
TCP序列號(序列碼SN,Sequence Number):32位的序列號標識了TCP報文中第一個byte在對應方向的傳輸中對應的字節序號。當SYN出現,序列碼實際上是初始序列碼(ISN),而第一個數據字節是ISN+1,單位是byte。比如發送端發送的一個TCP包凈荷(不包含TCP頭)為12byte,SN為5,則發送端接着發送的下一個數據包的時候,SN應該設置為5+12=17。通過系列號,TCP接收端可以識別出重復接收到的TCP包,從而丟棄重復包,同時對於亂序數據包也可以依靠系列號進行重排序,進而對高層提供有序的數據流。另外SYN標志和FIN標志在邏輯上也占用一個byte,當SYN標志位有效的時候,該字段也稱為ISN(initial sequence number),詳細請參考后續的TCP連接管理。
TCP應答號(Acknowledgment Number簡稱ACK Number或簡稱為ACK Field):32位的ACK Number標識了報文發送端期望接收的字節序列。如果設置了ACK控制位,這個值表示一個准備接收的包的序列碼,注意是准備接收的包,比如當前接收端接收到一個凈荷為12byte的數據包,SN為5,則發送端可能會回復一個確認收到的數據包,如果這個數據包之前的數據也都已經收到了,這個數據包中的ACK Number則設置為12+5=17,表示17byte之前的數據都已經收到了。在舉一個例子,如果在這個數據包之前有個SN為3,凈荷為2byte的數據包丟失,則在接受端接收到這個SN為5的亂序數據包的時候,協議要求接收端必須要回復一個ACK確認包,這個確認包中的Ack Number只能設置為3。
頭長(Header Length):4位包括TCP頭大小,指示TCP頭的長度,即數據從何處開始。最大為15,單位是32比特(32-bit word)。
保留(Reserved):4位值域,這些位必須是0。為了將來定義新的用途所保留,其中RFC3540將Reserved字段中的最后一位定義為Nonce標志。后續擁塞控制部分的講解我們會簡單介紹Nonce標志位。
標志(Code Bits):8位標志位,下面介紹。
窗口大小(Window Size):16位,該值指示了從Ack Number開始還願意接收多少byte的數據量,也即用來表示當前接收端的接收窗還有多少剩余空間。用於TCP的流量控制。
校驗位(Checksum):16位TCP頭。發送端基於數據內容計算一個數值,接收端要與發送端數值結果完全一樣,才能證明數據的有效性。接收端checksum校驗失敗的時候會直接丟掉這個數據包。CheckSum是根據偽頭+TCP頭+TCP數據三部分進行計算的。另外對於大的數據包,checksum並不能可靠的反應比特錯誤,應用層應該再添加自己的校驗方式。
優先指針(緊急,Urgent Pointer):16位,指向后面是優先數據的字節,在URG標志設置了時才有效。如果URG標志沒有被設置,緊急域作為填充。加快處理標示為緊急的數據段。
選項(Option):長度不定,但長度必須以是32bits的整數倍。常見的選項包括MSS、SACK、Timestamp等等,后續的內容會分別介紹相關選項。
在標志(Code Bits)中的八位標志位分別介紹如下
CWR(Congestion Window Reduce):擁塞窗口減少標志被發送主機設置,用來表明它接收到了設置ECE標志的TCP包,發送端通過降低發送窗口的大小來降低發送速率
ECE(ECN Echo):ECN響應標志被用來在TCP3次握手時表明一個TCP端是具備ECN功能的,並且表明接收到的TCP包的IP頭部的ECN被設置為11。更多信息請參考RFC793。
URG(Urgent):該標志位置位表示緊急(The urgent pointer) 標志有效。該標志位目前已經很少使用參考后面流量控制和窗口管理部分的介紹。
ACK(Acknowledgment):取值1代表Acknowledgment Number字段有效,這是一個確認的TCP包,取值0則不是確認包。后續文章介紹中當ACK標志位有效的時候我們稱呼這個包為ACK包,使用大寫的ACK稱呼。
PSH(Push):該標志置位時,一般是表示發送端緩存中已經沒有待發送的數據,接收端不將該數據進行隊列處理,而是盡可能快將數據轉由應用處理。在處理 telnet 或 rlogin 等交互模式的連接時,該標志總是置位的。
RST(Reset):用於復位相應的TCP連接。通常在發生異常或者錯誤的時候會觸發復位TCP連接。
SYN(Synchronize):同步序列編號(Synchronize Sequence Numbers)有效。該標志僅在三次握手建立TCP連接時有效。它提示TCP連接的服務端檢查序列編號,該序列編號為TCP連接初始端(一般是客戶端)的初始序列編號。在這里,可以把TCP序列編號看作是一個范圍從0到4,294,967,295的32位計數器。通過TCP連接交換的數據中每一個字節都經過序列編號。在TCP報頭中的序列編號欄包括了TCP分段中第一個字節的序列編號。類似的后續文章介紹中當這個SYN標志位有效的時候我們稱呼這個包為SYN包。
FIN(Finish):帶有該標志置位的數據包用來結束一個TCP會話,但對應端口仍處於開放狀態,准備接收后續數據。當FIN標志有效的時候我們稱呼這個包為FIN包。
另外我們一般稱呼鏈路層的發出去的數據包為幀(frame),稱呼網絡層發給鏈路層的數據包為包(packet),稱呼傳輸層發給網絡層的數據包為段(segment)。但是正如我們描述所用,段、包、幀也經常統稱為數據包或者數據報文。
對應用層來說TCP是一個雙向對稱的全雙工(full-duplex)協議,也就是說應用層可以同時發送數據和接收數據。這就意味着數據流在一個方向上的傳輸是獨立於另一個方向的傳輸的,每個方向上都有獨立的SN。
三、TCP中的數據包窗口和滑窗
在TCP的發送端和接收端都會維持一個窗口,因為一個TCP連接是雙向的,因此實際上一個TCP連接一共有四個窗口。此處我們先簡單介紹一個發送端的窗口如下。圖中的數字表示byte也就是和上面介紹的TCP協議頭中的SN是對應的,3號byte以及3號之前的數據表示已經發送並且收到了接收端的ACK確認包的數據;4、5、6三個byte表示當前可以發送的數據包,也有可能已經已經發送了但是還沒有收到ACK確認包;7號byte及之后的數據表示為了控制發送速率暫時不能發送的數據。其中4-6這三個byte就稱呼為窗口大小(window size)。當TCP連接建立的時候,雙方會通過TCP頭中的窗口大小字段向對方通告自己接收端的窗口大小,發送端依據接收端通告的窗口大小來設置發送端的發送窗口大小,另外在擁塞控制的時候也是通過調整發送端的發送窗口來調整發送速率的。窗口這個詞的來源就是當我們從這一個數據序列中單獨看4、5、6這幾個byte的時候,我們仿佛是從一個"窗口"中觀察的一樣。此處先簡單有個滑窗的概念后續我們講到TCP的窗口管理的時候會繼續進一步介紹TCP的滑窗。
相關補充:
1、TCPIP最初傳輸層和IP層是同一個層的,關於網絡分層的小故事可以參考http://news.cnblogs.com/n/187131/ , IETF上還專門有一個愚人節系列的RFC,參考https://en.wikipedia.org/wiki/April_Fools%27_Day_Request_for_Comments
2、TCP頭中CheckSum的計算可以參考資料
http://www.roman10.net/2011/11/27/how-to-calculate-iptcpudp-checksumpart-1-theory/這個連接里面一共三個系列分別是理論、實現、用例和驗證。
http://www.tcpipguide.com/free/t_TCPChecksumCalculationandtheTCPPseudoHeader-2.htm
或者參考TCPIP詳解卷一第二版(暫時只有英文版,名字TCP/IP Illustrated Volume 1 Second Edition The Protocols) P475頁
3、后面涉及的相關RFC文檔可以去IETF官網 http://www.ietf.org/查詢 直接搜索RFC號碼就行了。整個TCP相關的協議體系,IETF梳理后在2015年以RFC7414發布,想了解或者查詢TCP相關RFC協議的可以先看看RFC7414協議,以對整個TCP協議有個概括了解
4、目前linux4.4實現上還不支持Nonce標志,可以參考內核代碼中struct tcphdr結構對TCP頭的定義。關於Llinux不同版本內核TCP實現相關的更新可以查看linux的changelog,網址https://kernelnewbies.org/LinuxVersions 其中有每個版本的changelog 還有對應的修改代碼
5、TCP傳輸中的包重復,不見得是TCP重傳導致的,也可能是因為IP層提供的不可靠傳輸導致的 http://stackoverflow.com/questions/12871760/packet-loss-and-packet-duplication
6、在網絡傳輸過程中NAT可能會修改checksum甚至系列號seq,后面我們討論窗口管理等內容的時候為了簡化不考慮nat修改seq的場景,即認為發送端發包的seq與接收端接收這個包的時候seq相同。