TCP連接的建立和斷開、滑動窗口
一、TCP段格式:
TCP的段格式如下圖所示
源端口號與目的端口號
源端口號和目的端口號,加上IP首部的源IP地址和目的IP地址唯一確定一個TCP連接。
序列號
序號表示在這個報文段中的第一個數據字節序號。
確認號
僅當ACK標誌為1時有效。確認號表示期望收到的下一個字節的序號。
頭部長度
4位,TCP頭部最多60個字節,最少20個字節
保留位
6位,必須為0
6個標誌位
URG-緊急指針有效
ACK-確認序號有效
PSH-接收方應盡快將這個報文段交給應用層
RST-連接重置
SYN-同步序號用來發起一個連接
FIN-表示將要終止一個連接
窗口大小
通過窗口大小來達到流量控制。
校驗和
對tcp表頭與數據進行校驗。
緊急指針
是一個正的偏移量,與序號字段中的值相加表示緊急數據最後一個字節的序號。TCP的緊急方式是發送端向另一端發送緊急數據的一種方式。實際上,緊急數據跟帶外數據不是一回事,tcp並沒有另外建立一條邏輯連接傳輸數據,只是socket
api 中把緊急數據叫做帶外數據而已。
The confusion between TCP‘s urgent mode and out-of-band data is also because the predominant programming interface, the sockets API, maps TCP‘s urgent mode into what sockets calls out-of-band data.
C++ Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//開啟fd能夠接收帶外數據的功能 void active_oobinline(int fd) { int ret; int oobinline = 1; ret = setsockopt(fd, SOL_SOCKET, SO_OOBINLINE, &oobinline, sizeof(oobinline)); if (ret == -1) { ERR_EXIT("setsockopt"); } } // 當fd 產生帶外數據時會產生SIGURG信號 // 函數開啟當前進程接收SIGURG信號的功能 // 在信號處理函數中去接收帶外數據 void active_sigurg(int fd) { int ret; ret = fcntl(fd, F_SETOWN, getpid()); if (ret == -1) { ERR_EXIT("fcntl"); } } |
選項與填充(選項為4字節整數倍,否則用0填充)
最常見的可選字段是最長報文大小MSS(Maximum Segment Size),每個連接方通常都在通信的第一個報文段中指明這個選項。它指明本端所能接收的最大長度的報文段(payload)。該選項如果不設置,默認為536(20+20+536=576字節的IP數據報),其中ip首部和tcp首部各20個字節,而internet
上標準的MTU (最小)為576B。
The maximum segment size (MSS) is the largest segment that a TCP is willing to receive from its peer and, consequently, the largest size its peer should ever use when sending. The MSS value
counts only TCP data bytes and does not include the sizes of any associated TCP or IP header
二、通訊時序(3次握手-->傳輸數據-->4次揮手)
下圖是一次TCP通訊的時序圖:
在這個例子中,首先客戶端主動發起連接、發送請求,然後服務器端響應請求,然後客戶端主動關閉連接。兩條豎線表示通訊的兩端,從上到下表示時間的先後順序,註意,數據從一端傳到網絡的另一端也需要時間,所以圖中的箭頭都是斜的。雙方發送的段按時間順序編號為1-10,各段中的主要信息在箭頭上標出,例如段2的箭頭上標著SYN,
8000(0), ACK 1001, <mss
1024>,表示該段中的SYN位置1,32位序號是8000,該段不攜帶有效載荷(數據字節數為0),ACK位置1,32位確認序號是1001,帶有一個mss選項值為1024。
建立連接的過程:
1. 客戶端發出段1,SYN位表示連接請求。序號是1000,這個序號在網絡通訊中用作臨時的地址,每發一個數據字節,這個序號要加1,這樣在接收端可以根據序號排出數據包的正確順序,也可以發現丟包的情況,另外,規定SYN位和FIN位也要占一個序號,這次雖然沒發數據,但是由於發了SYN位,因此下次再發送應該用序號1001。mss表示最大段尺寸,如果一個段太大,封裝成幀後超過了鏈路層的最大幀長度,就必須在IP層分片,為了避免這種情況,客戶端聲明自己的最大段尺寸,建議服務器端發來的段不要超過這個長度。
2. 服務器發出段2,也帶有SYN位,同時置ACK位表示確認,確認序號是1001,表示“我接收到序號1000及其以前所有的段,請你下次發送序號為1001的段”,也就是應答了客戶端的連接請求,同時也給客戶端發出一個連接請求,同時聲明最大尺寸為1024。
3. 客戶端發出段3,對服務器的連接請求進行應答,確認序號是8001。
在這個過程中,客戶端和服務器分別給對方發了連接請求,也應答了對方的連接請求,其中服務器的請求和應答在一個段中發出,因此一共有三個段用於建立連接,稱為‘‘‘三方握手(three-way-handshake)‘‘‘。在建立連接的同時,雙方協商了一些信息,例如雙方發送序號的初始值、最大段尺寸等。
在TCP通訊中,如果一方收到另一方發來的段,讀出其中的目的端口號,發現本機並沒有任何進程使用這個端口,就會應答一個包含RST位的段給另一方。例如,服務器並沒有任何進程使用8080端口,我們卻用telnet客戶端去連接它,服務器收到客戶端發來的SYN段就會應答一個RST段,客戶端的telnet程序收到RST段後報告錯誤Connection
refused.
In general, a reset is sent by TCP whenever a segment arrives that does not appear to be correct for the referenced connection. (We use the term referenced connection to mean the connection specified by the 4-tuple in the TCP and IP headers of the reset.) Resets ordinarily result in a fast teardown of a TCP connection.
數據傳輸的過程:
1. 客戶端發出段4,包含從序號1001開始的20個字節數據。
2. 服務器發出段5,確認序號為1021,對序號為1001-1020的數據表示確認收到,同時請求發送序號1021開始的數據,服務器在應答的同時也向客戶端發送從序號8001開始的10個字節數據,這稱為piggyback。
3. 客戶端發出段6,對服務器發來的序號為8001-8010的數據表示確認收到,請求發送序號8011開始的數據。
在數據傳輸過程中,ACK和確認序號是非常重要的,應用程序交給TCP協議發送的數據會暫存在TCP層的發送緩沖區中,發出TCP 數據段給對方之後,只有收到對方應答的ACK段才知道該數據段確實發到了對方,可以從發送緩沖區中釋放掉了,如果因為網絡故障丟失了數據段或者丟失了對方發回的ACK段,經過等待超時後TCP協議自動將發送緩沖區中的數據段重發。
這個例子只描述了最簡單的一問一答的情景,實際的TCP數據傳輸過程可以收發很多數據段,雖然典型的情景是客戶端主動請求服務器被動應答,但也不是必須如此,事實上TCP協議為應用層提供了全雙工(full-duplex)的服務,雙方都可以主動甚至同時給對方發送數據。
如果通訊過程只能采用一問一答的方式,收和發兩個方向不能同時傳輸,在同一時間只允許一個方向的數據傳輸,則稱為‘‘‘半雙工(half-duplex)‘‘‘,假設某種面向連接的協議是半雙工的,則只需要一套序號就夠了,不需要通訊雙方各自維護一套序號。
關閉連接的過程:
1. 客戶端發出段7,FIN位表示關閉連接的請求。
2. 服務器發出段8,應答客戶端的關閉連接請求。
3. 服務器發出段9,其中也包含FIN位,向客戶端發送關閉連接請求。
4. 客戶端發出段10,應答服務器的關閉連接請求。
建立連接的過程是三方握手,而關閉連接通常需要4個段,服務器的應答和關閉連接請求通常不合並在一個段中,因為有連接半關閉的情況(調用shutdown而不是close),這種情況下客戶端關閉連接之後就不能再發送數據給服務器了,但是服務器還可以發送數據給客戶端,直到服務器也關閉連接為止。
三、滑動窗口和流量控制
如果發送端發送的速度較快,接收端接收到數據後處理的速度較慢,而接收緩沖區的大小是固定的,就會丟失數據。TCP協議通過‘‘‘滑動窗口(SlidingWindow)‘‘‘機制解決這一問題。看下圖的通訊過程。
1. 發送端發起連接,聲明最大段尺寸是1460,初始序號是0,窗口大小是4K,表示“我的接收緩沖區還有4K字節空閑,你發的數據不要超過4K”。接收端應答連接請求,聲明最大段尺寸是1024,初始序號是8000,窗口大小是6K。發送端應答,三方握手結束。
2. 發送端發出段4-9,每個段帶1K的數據,發送端根據窗口大小知道接收端的緩沖區滿了,因此停止發送數據。
3. 接收端的應用程序提走2K數據,接收緩沖區又有了2K空閑,接收端發出段10,在應答已收到6K數據的同時聲明窗口大小為2K。
4. 接收端的應用程序又提走2K數據,接收緩沖區有4K空閑,接收端發出段11,重新聲明窗口大小為4K。
5. 發送端發出段12-13,每個段帶1K數據,段13同時還包含FIN位。
6. 接收端應答接收到的2K數據(6145-8192),再加上FIN位占一個序號8193,因此應答序號是8194,接收端同時聲明窗口大小為2K。
7. 接收端的應用程序提走2K數據,接收端重新聲明窗口大小為4K。
8. 接收端的應用程序提走剩下的2K數據,接收緩沖區全空,接收端重新聲明窗口大小為6K。
9. 接收端的應用程序在提走全部數據後,決定關閉連接,發出段17包含FIN位,發送端應答,連接完全關閉。
上圖在接收端用小方塊表示1K數據,實心的小方塊表示已接收到的數據,虛線框表示接收緩沖區,因此套在虛線框中的空心小方塊表示窗口大小,從圖中可以看出,隨著應用程序提走數據,虛線框是向右滑動的,因此稱為滑動窗口。
從這個例子還可以看出,發送端是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。而UDP是面向消息的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不同的。
四、TCP如何保證可靠性
1、應用數據被分割成TCP認為最適合發送的數據塊,稱為段傳遞給IP層。
2、當TCP發出一個段後,它啟動一個定時器,等待目的端確認收到這個報文段。如果不能及時收到一個確認,將重發這個報文段。
3、當TCP收到發自TCP連接另一端的數據段,它將發送一個確認。這個確認不是立即發送,通常將推遲幾分之一秒。
4、TCP將保持它首部和數據的校驗和。這是一個端到端的校驗和,目的是檢測數據在傳輸過程中的任何變化。如果收到段的校驗和有差錯,TCP將丟棄這個報文段並且不確認(導致對方超時重傳)
5、TCP承載於IP數據報來傳輸,而IP數據報的到達可能會失序,因此TCP報文段的到達也可能會失序。TCP將對收到的數據段進行重新排序後呈現在接收緩沖區給應用層提取。
6、IP數據報會發生重復,TCP的接收端必須丟棄重復的數據。
7、TCP還能提供流量控制。TCP連接的每一方都有一定大小的緩沖空間。
參考:
《Linux C 編程一站式學習》
《TCP/IP詳解 卷一》
TCP連接的建立和斷開、滑動窗口