lwIP TCP/IP 協議棧筆記之十五: TCP協議

目錄

1. TCP 服務簡介

2. TCP 的特性

2.1 連接機制

2.2 確認與重傳

2.3 緩衝機制

2.4 全雙工通信

2.5 流量控制

2.6 差錯控制

2.7 擁塞控制

3. 端口號的概念

4. TCP 報文段結構

4.1 TCP 報文段的封裝

4.2 TCP 報文段格式

 5. TCP 連接

5.1 “三次握手”建立連接

 5.2 “四次揮手”終止連接

6. TCP 狀態

6.1 LwIP 中定義的TCP 狀態

6.2 TCP 狀態轉移

7. TCP 中的數據結構

8. 窗口的概念

8.1 接收窗口

8.2 發送窗口

9. TCP 報文段處理

9.1 報文段緩衝隊列

9.2 TCP 報文段發送

9.3 TCP 報文段接收


TCP 協議(TransmissionControl Protocol,傳輸控制協議)在LwIP 協議棧中佔據了大半的代碼,它是最常用傳輸層協議,也是最穩定傳輸層協議,很多上層應用都是依賴於TCP 協議進程傳輸數據,如SMTP、FTP 等等。

1. TCP 服務簡介

TCP 與UDP 一樣,都是傳輸層的協議,但是提供的服務卻大不相同,UDP 爲上層應用提供的是一種不可靠的,無連接的服務,而TCP 則提供一種面向連接、可靠的字節流傳輸服務,TCP 讓兩個主機建立連接的關係,應用數據以數據流的形式進行傳輸,這與UDP協議是不一樣:

UDP 運載的數據是以報文的形式,各個報文在網絡中互不相干傳輸,UDP 每收到一個報文就遞交給上層應用,因此如果對於大量數據來說,應用層的重裝是非常麻煩的,因爲UDP 報文在網絡中到達目標主機的順序是不一樣的;

TCP 採用數據流的形式傳輸,先後發出的數據在網絡中雖然也是互不相干的傳輸,但是這些數據本身攜帶的信息卻是緊密聯繫的,TCP 協議會給每個傳輸的字節進行編號,當然啦,兩個主機方向上的數據編號是彼此獨立的,在傳輸的過程中,發送方把數據的起始編號與長度放在TCP 報文中,在接收方將所有數據按照編號組裝起來,然後返回一個確認,當所有數據接收完成後纔將數據遞交到應用層中。

2. TCP 的特性

2.1 連接機制

TCP 是一個面向連接的協議,無論哪一方向另一方發送數據之前,都必須先在雙方之間建立一個連接,否則將無法發送數據,一個TCP 連接必須有雙方IP 地址與端口號。

2.2 確認與重傳

一個完整的TCP 傳輸必須有數據的交互,接收方在接收到數據之後必須正面進行確認,向發送方報告接收的結果,而發送方在發送數據之後必須等待接收方的確認,同時發送的時候會啓動一個定時器,在指定超時時間內沒收到確認,發送方就會認爲發送失敗,然後進行重發操作,這就是重傳報文。

TCP 提供可靠的運輸層,但它依賴的是IP 層的服務,IP 數據報的傳輸是無連接、不可靠的,因此它要通過確認來知道接收方確實已經收到數據了。但數據和確認都有可能會丟失,因此TCP 通過在發送時設置一個超時機制(定時器)來解決這種問題,如果當超時時間到達的時候還沒有收到對方的確認,它就重傳該數據。

2.3 緩衝機制

在發送方想要發送數據的時候,由於應用程序的數據大小、類型都是不可預估的,而TCP 協議提供了緩衝機制來處理這些數據,如在數據量很小的時候,TCP 會將數據存儲在一個緩衝空間中,等到數據量足夠大的時候在進行發送數據,這樣子能提供傳輸的效率並且減少網絡中的通信量,而且在數據發送出去的時候並不會立即刪除數據,還是讓數據保存在緩衝區中,因爲發送出去的數據不一定能被接收方正確接收,它需要等待到接收方的確認再將數據刪除。同樣的,在接收方也需要有同樣的緩衝機制,因爲在網絡中傳輸的數據報到達的時間是不一樣的,而且TCP 協議還需要把這些數據報組裝成完整的數據,然後再遞交到應用層中。

2.4 全雙工通信

在TCP 連接建立後,那麼兩個主機就是對等的,任何一個主機都可以向另一個主機發送數據,數據是雙向流通的,所以TCP 協議是一個全雙工的協議,這種機制爲TCP 協議傳輸數據帶來很大的方便,一般來說,TCP 協議的確認是通過捎帶的方式來實現,即接收方把確認信息放到反向傳來的是數據報文中,不必單獨爲確認信息申請一個報文,捎帶機制減少了網絡中的通信流量。由於雙方主機是對等的存在,那麼任意一方都可以斷開連接,此時這個方向上的數據流就斷開了,但是另一個 方向上的數據仍是連通的狀態,這種情況就稱之爲半雙工。

2.5 流量控制

一條TCP 連接每一側的主機都設置了緩衝區域。當該接收方收到數據後,它就將數據放入接收緩衝區,當確認這段數據是正常的時候,就會向發送方返回一個確認。並且向相關的應用層遞交該數據,但不一定是數據剛一到達就立即遞交。事實上,接收方應用也許正忙於其他任務,甚至要過很長時間後纔會去處理這些數據。這樣子如果接收方處理這些數據時相對緩慢,而發送方發送得太多、太快,就會很容易地使接收方的接收緩衝區發生溢出。

因此TCP 提供了流量控制服務(flow-control service)以消除發送方使接收方緩衝區溢出的可能性。流量控制是一個速度匹配服務,即發送方的發送速率與接收方應用程序的讀取速率相匹配,TCP 通過讓發送方維護一個稱爲接收窗口(receive window)的變量來提供流量控制,是的,你沒看錯,是接收窗口(rwnd),它用於給發送方一個指示:接收方還能接收多少數據,接收方會將此窗口值放在 TCP 報文的首部中的窗口字段,然後傳遞給發送方,這個窗口的大小是在發送數據的時候動態調整的。

這個窗口既然是動態調整的,那有沒有可能是0,這樣子發送方不就是沒法繼續發送數據到接收方了?爲了解決這個問題,TCP 協議的規範中有些要求,當接收方主機的接收窗口爲0 時,發送方繼續發送只有一個字節的報文段,這些報文段將被接
收方接收,直到緩存清空,並在確認報文中包含一個非0 的接收窗口值。

流量控制是雙方通信之間的控制信息,這是很有必要的,比如兩個新能不對等的主機,建立了TCP 協議連接,但是其中一個主機一直髮送數據,但是接收的主機來不及處理,這樣子的處理就不是最佳的,因此,TCP 協議中使用滑動窗口Sliding window的流量控制方法,它允許接收方根據自身的處理能力來確定能接收數據的多少,因此會告訴發送方可以發送多少數據過來,即窗口的大小,而發送方儘可能將數據都多發到對方那裏,所以發送方會根據這個窗口的大小發送對應的數據 ,通俗地來說就是接收方告訴發送方“我還有能力處理那麼多的數據,你就發那麼多數據給我就行了,不要發多了,否則我處理不了”。

2.6 差錯控制

除了確認與重傳之外,TCP 協議也會採用校驗和的方式來檢驗數據的有效性,主機在接收數據的時候,會將重複的報文丟棄,將亂序的報文重組,發現某段報文丟失了會請求發送方進行重發,因此在TCP 往上層協議遞交的數據是順序的、無差錯的完整數據。

2.7 擁塞控制

什麼是擁塞?當數據從一個大的管道(如一個快速局域網)向一個較小的管道(如一個較慢的廣域網)發送時便會發生擁塞。當多個輸入流到達一個路由器,而路由器的輸出流小於這些輸入流的總和時也會發生擁塞,這種是網絡狀況的原因。如果一個主機還是以很大的流量給另一個主機發送數據,但是其中間的路由器通道很小,無法承受這樣大的數據流量的時候,就會導致擁塞的發生,這樣子就導致了接收方無法在超時時間內完成接收(接收方此時完全有能力處理大量數據),而發送方又進行重傳,這樣子就導致了鏈路上的更加擁塞,延遲發送方必須實現一直自適應的機制,在網絡中擁塞的情況下調整自身的發送速度,這種形式對發送方的控制被稱爲擁塞控制(congestioncontrol),與前面我們說的流量控制是非常相似的,而且TCP 協議採取的措施也非常相似,均是限制發送方的發送速度。

3. 端口號的概念

TCP 協議的連接是包括上層應用間的連接,簡單來說,TCP 連接是兩個不同主機的應用連接,而傳輸層與上層協議是通過端口號進行識別的,如IP 協議中以IP 地址作爲識別一樣,端口號的取值範圍是0~65535,這些端口標識着上層應用的不同線程,一個主機內可能只有一個IP 地址,但是可能有多個端口號,每個端口號表示不同的應用線程。一臺擁有IP 地址的主機可以提供許多服務,比如Web 服務、FTP 服務、SMTP 服務等,這些服務完全可以通過1 個IP 地址來實現,主機是怎樣區分不同的網絡服務呢?顯然不能只靠IP地址,因爲IP 地址只能識別一臺主機而非主機提供的服務,這些服務就是主機上的應用線程,因此是通過“IP 地址+端口號”來區分主機不同的線程。

4. TCP 報文段結構

按照協議棧實現的方式,這TCP 協議也肯定像ARP 協議、IP 協議一樣,都是使用報文進行描述,爲了使用更加官方的描述,我們將TCP 報文(數據包)稱爲報文段。

4.1 TCP 報文段的封裝

TCP 報文段依賴IP 協議進行發送,因此TCP 報文段與ICMP 報文一樣,都是封裝在IP 數據報中,IP 數據報封裝在以太網幀中,因此TCP 報文段也是經過了兩次的封裝,然後發送出去。

4.2 TCP 報文段格式

TCP 報文段如APR 報文、IP 數據報一樣,也是由首部+數據區域組成,TCP 報文段的首部我們稱之爲TCP 首部,其首部內推很豐富,各個字段都有不一樣的含義,如果不計算選項字段,一般來說TCP 首部只有20 個字節

在LwIP 中,報文段首部採用一個名字叫tcp_hdr 的結構體進行描述

PACK_STRUCT_BEGIN
struct tcp_hdr {
  PACK_STRUCT_FIELD(u16_t src);
  PACK_STRUCT_FIELD(u16_t dest);
  PACK_STRUCT_FIELD(u32_t seqno);
  PACK_STRUCT_FIELD(u32_t ackno);
  PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags);
  PACK_STRUCT_FIELD(u16_t wnd);
  PACK_STRUCT_FIELD(u16_t chksum);
  PACK_STRUCT_FIELD(u16_t urgp);
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

每個TCP 報文段都包含源主機和目標主機的端口號,用於尋找發送端和接收端應用線程,這兩個值加上I P 首部中的源I P 地址和目標I P 地址就能確定唯一一個TCP 連接。 

序號字段用來標識從TCP 發送端向TCP 接收端發送的數據字節流,它的值表示在這報文段中的第一個數據字節所處位置。根據接收到的數據區域長度,就能計算出報文最後一個數據所處的序號,因爲TCP 協議會對發送或者接收的數據進行編號(按字節的形式),那麼使用序號對每個字節進行計數,就能很輕易管理這些數據。序號是32 bit 的無符號整數。

當建立一個新的連接時,TCP 報文段首部的 SYN 標誌變1,序號字段包含由這個主機隨機選擇的初始序號ISN(Initial Sequence Number)。該主機要發送數據的第一個字節序號爲 ISN+1,因爲SYN 標誌會佔用一個序號,在這裏我們只需要瞭解一下即可,後面會講解的。

既然TCP 協議給每個傳輸的字節都了編號,那麼確認序號就包含接收端所期望收到的下一個序號,因此,確認序號應當是上次已成功收到數據的最後一個字節序號加 1。當然,只有ACK 標誌爲 1 時確認序號字段纔有效,TCP 爲應用層提供全雙工服務,這意味數據能在兩個方向上獨立地進行傳輸,因此確認序號通常會與反向數據(即接收端傳輸給發送端的數據)封裝在同一個報文中(即捎帶),所以連接的每一端都必須保持每個方向上的傳輸數據序號準確性。

首部長度字段佔據4bit 空間,它指出了TCP 報文段首部長度,以字節爲單位,最大能記錄15*4=60 字節的首部長度,因此,TCP 報文段首部最大長度爲60 字節。在字段後接下來有6bit 空間是保留未用的。

此外還有6bit 空間,是TCP 報文段首部的標誌字段,用於標誌一些信息:

  • URG(urgent):緊急位,首部中的緊急指針字段標誌,如果是1 表示緊急指針字段有效。
  •  ACK(acknowledgement):確認位,首部中的確認序號字段標誌,如果是1 表示確認序號字段有效。
  • PSH(push):急迫位,該字段置一表示接收方應該儘快將這個報文段交給應用層。
  • RST(reset):重置位,重新建立TCP 連接。
  • SYN:同步位,用同步序號發起連接。
  • FIN:終止位,中止連接。

TCP 的流量控制由連接的每一端通過聲明的窗口大小來提供,窗口大小爲字節數,起始於確認序號字段指明的值,這個值是接收端正期望接收的數據序號,發送方根據窗口大小調整發送數據,以實現流量控制。窗口大小是一個佔據16 bit 空間的字段,因而窗口最大爲 65535 字節,當接收方告訴發送方一個大小爲0 的窗口時,將完全阻止發送方的數據發送。

檢驗和覆蓋了整個的 TCP 報文段:TCP 首部和TCP 數據區域,由發送端計算和填寫,並由接收端進行驗證。

只有當URG 標誌置1 時緊急指針纔有效,緊急指針是一個正的偏移量,和序號字段中的值相加表示緊急數據最後一個字節的序號。簡單來說,本TCP 報文段的緊急數據在報文段數據區域中,從序號字段開始,偏移緊急指針的值結束

 5. TCP 連接

TCP 是一個面向連接的協議,無論哪一方向另一方發送數據之前,都必須先在雙方之間建立一條連接,俗稱“握手”.

5.1 “三次握手”建立連接

建立連接的過程是由客戶端發起,而服務器無時無刻都在等待着客戶端的連接.

TCP建立連接,也就是我們常說的三次握手,它需要三步完成。在TCP的三次握手中,發送第一個SYN的一端執行的是主動打開。而接收這個SYN併發回下一個SYN的另一端執行的是被動打開.

  • 第1步:客戶端向服務器發送一個同步數據包請求建立連接,該數據包中,初始序列號(ISN)是客戶端隨機產生的一個值,確認號是0;

  • 第2步:服務器收到這個同步請求數據包後,會對客戶端進行一個同步確認。這個數據包中,序列號(ISN)是服務器隨機產生的一個值,確認號是客戶端的初始序列號+1;

  • 第3步:客戶端收到這個同步確認數據包後,再對服務器進行一個確認。該數據包中,序列號是上一個同步請求數據包中的確認號值,確認號是服務器的初始序列號+1。

注意:因爲一個SYN將佔用一個序號,所以要加1。

初始序列號(ISN)隨時間而變化的,而且不同的操作系統也會有不同的實現方式,所以每個連接的初始序列號是不同的。TCP連接兩端會在建立連接時,交互一些信息,如窗口大小、MSS等,以便爲接着的數據傳輸做準備。

RFC793指出ISN可以看作是一個32bit的計數器,每4ms加1,這樣選擇序號的目的在於防止在網絡中被延遲的分組在以後被重複傳輸,而導致某個連接的一端對它作錯誤的判斷。

科萊網絡分析工具(www.csna.cn

 5.2 “四次揮手”終止連接

前面我們提到,建立一個連接需要3個步驟,但是關閉一個連接需要經過4個步驟。因爲TCP連接是全雙工的工作模式,所以每個方向上需要單獨關閉。在TCP關閉連接時,首先關閉的一方(即發送第一個終止數據包的)將執行主動關閉,而另一方(收到這個終止數據包的)再執行被動關閉。

關閉連接的4個步驟如下:

  • 第1步:服務器完成它的數據發送任務後,會主動向客戶端發送一個終止數據包,以關閉在這個方向上的TCP連接。該數據包中,序列號爲客戶端發送的上一個數據包中的確認號值,而確認號爲服務器發送的上一個數據包中的序列號+該數據包所帶的數據的大小;

  • 第2步:客戶端收到服務器發送的終止數據包後,將對服務器發送確認信息,以關閉該方向上的TCP連接。這時的數據包中,序列號爲第1步中的確認號值,而確認號爲第1步的數據包中的序列號+1;

  • 第3步:同理,客戶端完成它的數據發送任務後,就也會向服務器發送一個終止數據包,以關閉在這個方向上的TCP連接,該數據包中,序列號爲服務器發送的上一個數據包中的確認號值,而確認號爲客戶端發送的上一個數據包中的序列號+該數據包所帶數據的大小;

  • 第4步:服務器收到客戶端發送的終止數據包後,將對客戶端發送確認信息,以關閉該方向上的TCP連接。這時在數據包中,序列號爲第3步中的確認號值,而確認號爲第3步數據包中的序列號+1;

注意:因爲FIN和SYN一樣,也要佔一個序號。理論上服務器在TCP連接關閉時發送的終止數據包中,只有終止位是置1,然後客戶端進行確認。但是在實際的TCP實現中,在終止數據包中,確認位和終止位是同時置爲1的,確認位置爲1表示對最後一次傳輸的數據進行確認,終止位置爲1表示關閉該方向的TCP連接。

6. TCP 狀態

6.1 LwIP 中定義的TCP 狀態

TCP 協議根據連接時接收到報文的不同類型,採取相應動作也不同,還要處理各個狀態的關係,如當收到握手報文時候、超時的時候、用戶主動關閉的時候等都需要不一樣的狀態去採取不一樣的處理。在LwIP 中,爲了實現TCP 協議的穩定連接,採用數組的形式定義了11 種連接時候的狀態.

  • ESTABLISHED 狀態:這個狀態是處於穩定連接狀態,建立連接的TCP 協議兩端的主機都是處於這個狀態,它們相互知道彼此的窗口大小、序列號、最大報文段等信息 
  • FIN_WAIT_1 與FIN_WAIT_2 狀態:處於這個狀態一般都是單向請求終止連接,然後主機等待對方的迴應,而如果對方產生應答,則主機狀態轉移爲FIN_WAIT_2,此時{主機->對方}方向上的TCP 連接就斷開,但是{對方->主機}方向上的連接還是存在的。此處有一個注意的地方:如果主機處於FIN_WAIT_2狀態,說明主機已經發出了FIN 報文段,並且對方也已對它進行確認,除非主機是在實行半關閉狀態,否則將等待對方主機的應用層處理關閉連接,因爲對方已經意識到它已收到FIN 報文段,它需要主機發一個 FIN 來關閉{對方->主機}方向上的連接。只有當另一端的進程完成這個關閉,主機這端纔會從FIN_WAIT_2 狀態進入TIME_WAIT 狀態。否則這意味着主機這端可能永遠保持這個FIN_WAIT_2 狀態,另一端的主機也將處於 CLOSE_WAIT 狀態,並一直保持這個狀態直到應用層決定進行關閉。
  • TIME_WAIT 狀態:TIME_WAIT 狀態也稱爲 2MSL 等待狀態。每個具體TCP 連接的實現必須選擇一個TCP 報文段最大生存時間MSL(Maximum SegmentLifetime),如IP 數據報中的TTL 字段,表示報文在網絡中生存的時間,它是任何報文段被丟棄前在網絡內的最長時間,這個時間是有限的,爲什麼需要等待呢?我們知道IP 數據報是不可靠的,而TCP 報文段是封裝在IP 數據報中,TCP 協議必須保證發出的ACK 報文段是正確被對方接收, 因此處於該狀態的主機必須在這個狀態停留最長時間爲2 倍的MSL,以防最後這個ACK 丟失,因爲TCP 協議必須保證數據能準確送達目的地。

6.2 TCP 狀態轉移

TCP 協議狀態轉移圖

  • 虛線:表示服務器的狀態轉移。
  • 實線:表示客戶端的狀態轉移。
  • 圖中所有“關閉”、“打開”都是應用程序主動處理。
  • 圖中所有的“超時”都是內核超時處理。

三次握手過程

  1.  (7) 服務器的應用程序主動使服務器進入監聽狀態,等待客戶端的連接請求
  2.  (1) 首先客戶端的應用程序會主動發起連接,發送SYN 報文段給服務器,在發送之後就進入SYN_SENT 狀態等待服務器的SYN ACK 報文段進行確認,如果在指定超時時間內服務器不進行應答確認,那麼客戶端將關閉連接。
  3.  (8)處於監聽狀態的服務器收到客戶端的連接請求(SYN 報文段),那麼服務器就返回一個SYN ACK 報文段應答客戶端的響應,並且服務器進入SYN_RCVD 狀態。
  4.  (1) 如果客戶端收到了服務器的SYN ACK 報文段,那麼就進入ESTABLISHED 穩定連接狀態,並向服務器發送一個ACK 報文段。
  5.  (9) 同時,服務器收到來自客戶端的ACK 報文段,表示連接成功,進入ESTABLISHED 穩定連接狀態,這正是我們建立連接的三次握手過程

四次揮手過程

  1. 3) 一般來說,都是客戶端主動發送一個FIN 報文段來終止連接,此時客戶端從ESTABLISHED 穩定連接狀態轉移爲FIN_WAIT_1 狀態,並且等待來自服務器的應答確認。
  2. 10)服務器收到FIN 報文段,知道客戶端請求終止連接,那麼將返回一個ACK 報文段到客戶端確認終止連接,並且服務器狀態由穩定狀態轉移爲CLOSE_WAIT 等待終止連接狀態。
  3. 4)客戶端收到確認報文段後,進入FIN_WAIT_2 狀態,等待來自服務器的主動請求終止連接,此時{客戶端->服務器}方向上的連接已經斷開。
  4. 11)一般來說,當客戶端終止了連接之後,服務器也會終止{服務器->客戶端}方向上的連接,因此服務器的原因程序會主動關閉該方向上的連接,發送一個FIN 報文段給客戶端。
  5. (5)處於FIN_WAIT_2 的客戶端收到FIN 報文段後,發送一個ACK 報文段給服務器。
  6. (12)服務器收到ACK 報文段,就直接關閉,此時{服務器->客戶端}方向上的連接已經終止,進入CLOSED 狀態。
  7. (6)客戶端還會等待2MSL,以防ACK 報文段沒被服務器收到,這就是四次揮手的全部過程。

7. TCP 中的數據結構

爲了描述TCP 協議,LwIP 定義了一個名字叫tcp_pcb 的結構體,我們稱之爲TCP 控制塊,其內定義了大量的成員變量,基本定義了整個TCP 協議運作過程的所有需要的東西,如發送窗口、接收窗口、數據緩衝區。超時處理、擁塞控制、滑動窗口等等。

/** This is the common part of all PCB types. It needs to be at the
   beginning of a PCB type definition. It is located here so that
   changes to this common part are made in one location instead of
   having to change all PCB structs. */
#define IP_PCB                             \
  /* ip addresses in network byte order */ \
  ip_addr_t local_ip;                      \
  ip_addr_t remote_ip;                     \
  /* Bound netif index */                  \
  u8_t netif_idx;                          \
  /* Socket options */                     \
  u8_t so_options;                         \
  /* Type Of Service */                    \
  u8_t tos;                                \
  /* Time To Live */                       \
  u8_t ttl                                 \
  /* link layer address resolution hint */ \
  IP_PCB_NETIFHINT
/**
 * members common to struct tcp_pcb and struct tcp_listen_pcb
 */
#define TCP_PCB_COMMON(type) \
  type *next; /* for the linked list */ \
  void *callback_arg; \
  TCP_PCB_EXTARGS \
  enum tcp_state state; /* TCP state */ \
  u8_t prio; \
  /* ports are in host byte order */ \
  u16_t local_port
/** the TCP 協議控制塊*/
struct tcp_pcb {
/** common PCB members */
  IP_PCB;
/** 協議特定的PCB 成員 */
  TCP_PCB_COMMON(struct tcp_pcb);

  /* 遠端端口號 */
  u16_t remote_port;

  tcpflags_t flags;
#define TF_ACK_DELAY   0x01U   /* 延遲發送ACK */
#define TF_ACK_NOW     0x02U   /* 立即發送ACK. */
#define TF_INFR        0x04U   /* 在快速恢復 */
#define TF_CLOSEPEND   0x08U   /* 關閉掛起 */
#define TF_RXCLOSED    0x10U   /* rx 由tcp_shutdown 關閉 */
#define TF_FIN         0x20U   /* 連接在本地關閉 (FIN segment enqueued). */
#define TF_NODELAY     0x40U   /* 禁用Nagle 算法 */
#define TF_NAGLEMEMERR 0x80U   /* nagle enabled,本地緩衝區溢出*/
#if LWIP_WND_SCALE
#define TF_WND_SCALE   0x0100U /* Window Scale option enabled */
#endif
#if TCP_LISTEN_BACKLOG
#define TF_BACKLOGPEND 0x0200U /* If this is set, a connection pcb has increased the backlog on its listener */
#endif
#if LWIP_TCP_TIMESTAMPS
#define TF_TIMESTAMP   0x0400U   /* Timestamp option enabled */
#endif
#define TF_RTO         0x0800U /* RTO timer has fired, in-flight data moved to unsent and being retransmitted */
#if LWIP_TCP_SACK_OUT
#define TF_SACK        0x1000U /* Selective ACKs enabled */
#endif

  /* the rest of the fields are in host byte order
     as we have to do some math with them */

  /* Timers */
  u8_t polltmr, pollinterval;
  u8_t last_timer;       // 控制塊被最後一次處理的時間
  u32_t tmr;

  /* 接收窗口相關的字段 */
  u32_t rcv_nxt;   /* 下一個期望收到的序號 */
  tcpwnd_size_t rcv_wnd;   /* 接收窗口大小 */
  tcpwnd_size_t rcv_ann_wnd; /* 告訴對方窗口的大小 */
  u32_t rcv_ann_right_edge; /* 告訴窗口的右邊緣 */

#if LWIP_TCP_SACK_OUT
  /* SACK ranges to include in ACK packets (entry is invalid if left==right) */
  struct tcp_sack_range rcv_sacks[LWIP_TCP_MAX_SACK_NUM];
#define LWIP_TCP_SACK_VALID(pcb, idx) ((pcb)->rcv_sacks[idx].left != (pcb)->rcv_sacks[idx].right)
#endif /* LWIP_TCP_SACK_OUT */

  /* 重傳計時器. */
  s16_t rtime;

  u16_t mss;   /* 最大報文段大小 */

  /* RTT(往返時間)估計變量 */
  u32_t rttest; /* RTT estimate in 500ms ticks */
  u32_t rtseq;  /* sequence number being timed */
  s16_t sa, sv; /* RTT 估計得到的平均值與時間差 */

  s16_t rto;    /* 重傳超時 (in ticks of TCP_SLOW_INTERVAL) */
  u8_t nrtx;    /* 重傳次數 */

  /* 快速重傳/恢復 */
  u8_t dupacks;
  u32_t lastack; /* 接收到的最大確認序號. */

  /* congestion avoidance/control variables 擁塞避免/控制變量 */
  tcpwnd_size_t cwnd;                /* 連接當前的窗口大小 */
  tcpwnd_size_t ssthresh;            /* 擁塞避免算法啓動的閾值 */

  /* first byte following last rto byte */
  u32_t rto_end;

  /* sender variables */
  u32_t snd_nxt;   /* 下一個要發送的序號 */
  u32_t snd_wl1, snd_wl2; /* 上一次收到的序號和確認號. */
  u32_t snd_lbb;       /* 要緩衝的下一個字節的序列號. */
  tcpwnd_size_t snd_wnd;   /* 發送窗口大小*/
  tcpwnd_size_t snd_wnd_max; /* 對方的最大發送方窗口 */

  tcpwnd_size_t snd_buf;   /* 可用的緩衝區空間(以字節爲單位. */
#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3)
  u16_t snd_queuelen; /* Number of pbufs currently in the send buffer. */

#if TCP_OVERSIZE
  /* Extra bytes available at the end of the last pbuf in unsent. */
  u16_t unsent_oversize;
#endif /* TCP_OVERSIZE */

  tcpwnd_size_t bytes_acked;

  /* These are ordered by sequence number: */
  struct tcp_seg *unsent;   /* 未發送的報文段. */
  struct tcp_seg *unacked;  /* 已發送但未收到確認的報文段. */
#if TCP_QUEUE_OOSEQ
  struct tcp_seg *ooseq;    /* 已收到的無序報文. */
#endif /* TCP_QUEUE_OOSEQ */

  struct pbuf *refused_data; /* 以前收到但未被上層處理的數據*/

#if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG
  struct tcp_pcb_listen* listener;
#endif /* LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG */

#if LWIP_CALLBACK_API  //TCP 協議相關的回調函數
  /* Function to be called when more send buffer space is available. */
  tcp_sent_fn sent;
  /* Function to be called when (in-sequence) data has arrived. */
  tcp_recv_fn recv;
  /* Function to be called when a connection has been set up. */
  tcp_connected_fn connected;
  /* Function which is called periodically.該函數被內核週期調用 */
  tcp_poll_fn poll;
  /* Function to be called whenever a fatal error occurs. */
  tcp_err_fn errf;
#endif /* LWIP_CALLBACK_API */

#if LWIP_TCP_TIMESTAMPS
  u32_t ts_lastacksent;
  u32_t ts_recent;
#endif /* LWIP_TCP_TIMESTAMPS */

  /* 保持活性 */
  u32_t keep_idle;
#if LWIP_TCP_KEEPALIVE
  u32_t keep_intvl;
  u32_t keep_cnt;
#endif /* LWIP_TCP_KEEPALIVE */

  /* Persist timer counter */
  u8_t persist_cnt;
  /* Persist timer back-off */
  u8_t persist_backoff;
  /* Number of persist probes */
  u8_t persist_probe;

  /* KEEPALIVE counter */
  u8_t keep_cnt_sent;

#if LWIP_WND_SCALE
  u8_t snd_scale;
  u8_t rcv_scale;
#endif
};

LwIP 中除了定義了一個完整的TCP 控制塊之外,還定義了一個刪減版的TCP 控制塊,叫tcp_pcb_listen,用於描述處於監聽狀態的連接,因爲分配完整的TCP 控制塊是比較消耗內存資源的,而TCP 協議在連接之前,是無法進行數據傳輸的,那麼在監聽的時候只需要把對方主機的相關信息得到,然後無縫切換到完整的TCP 控制塊中,這樣子就能節省不少資源,此外,LwIP 還定義了4 個鏈表來維護TCP 連接時的各種狀態,

/** the TCP protocol control block for listening pcbs */
struct tcp_pcb_listen {
/** Common members of all PCB types */
  IP_PCB;
/** Protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb_listen);

#if LWIP_CALLBACK_API
  /* Function to call when a listener has been connected. */
  tcp_accept_fn accept;
#endif /* LWIP_CALLBACK_API */

#if TCP_LISTEN_BACKLOG
  u8_t backlog;
  u8_t accepts_pending;
#endif /* TCP_LISTEN_BACKLOG */
};


/* The TCP PCB lists. */

/** List of all TCP PCBs bound but not yet (connected || listening) */
struct tcp_pcb *tcp_bound_pcbs;
/** List of all TCP PCBs in LISTEN state */
union tcp_listen_pcbs_t tcp_listen_pcbs;
/** List of all TCP PCBs that are in a state in which
 * they accept or send data. */
struct tcp_pcb *tcp_active_pcbs;
/** List of all TCP PCBs in TIME-WAIT state */
struct tcp_pcb *tcp_tw_pcbs;

tcp_bound_pcbs 鏈表上的TCP 控制塊可以看做是處於CLOSED 狀態,那些新綁定的端口初始的時候都是處於這個狀態。tcp_listen_pcbs 鏈表用於記錄處於監聽狀態的TCP 控制塊,一般就是記錄的是tcp_pcb_listen 控制塊。tcp_tw_pcbs 鏈表用於記錄連接中處於TIME_WAIT 狀態下的TCP 控制塊。而tcp_active_pcbs 鏈表用於記錄所有其他狀態的TCP控制塊,這些端口是活躍的,可以不斷進行狀態轉移。

8. 窗口的概念

窗口是個什麼東西,前面也說了,TCP 協議的發送和接收都會給每個字節的數據進行編號,這個編號可以理解爲相對序號,如圖 所示,就是每個字節的數據的編號。

8.1 接收窗口

TCP 控制塊中關於接收窗口的成員變量有rcv_nxt、rcv_wnd、rcv_ann_wnd、rcv_ann_right_edge,其中這些成員變量的定義在控制塊中也講解了,rcv_nxt 表示下次期望接收到的數據編號,rcv_wnd 表示接收窗口的大小,rcv_ann_wnd 用於告訴發送方窗口的大小,rcv_ann_right_edge 記錄了窗口的右邊界,這4 個成員變量都會在數據傳輸的過程中動態改變

比如在7 字節之前的數據,都是已經接收確認的數據,而7 字節正是主機想要接收到的下一個字節數據編號,而窗口的大小是7,它會告訴發送方“你可以發送7 個數據過來”,窗口的右邊界爲14,當主機下一次接收到N(不一定是7)字節數據的時候,窗口就會向右移動N 個字節,但是rcv_wnd、rcv_ann_wnd、rcv_ann_right_edge變量的值是不確定的,通過LwIP 內部計算得出,而下一次想要接收的數據編號就爲7+N。

8.2 發送窗口

TCP 控制塊中關於發送窗口的成員變量有lastack、snd_nxt、snd_lbb、snd_wnd,lastack 記錄了已經確認的最大序號,snd_nxt 表示下次要發送的序號,snd_lbb 是表示下一個將被應用線程緩衝的序號,而snd_wnd 表示發送窗口的大小,是由接收已方提供的。這些值也是動態變化的,當發送的數據收到確認,就會更新lastack,並且隨着數據的發送出去,窗口會向右移動,即snd_nxt 的值在增加。

 每條TCP 連接的每一端都必須設有兩個窗口——一個發送窗口和一個接收窗口,TCP的可靠傳輸機制用字節的序號(編號)進行控制,TCP 所有的確認都是基於數據的序號而不是基於報文段,發送過的數據未收到確認之前必須保留,以便超時重傳時使用,發送窗口在沒收到確認序號之前是保持不動的,當收到確認序號就會向右移動,並且更新lastack的值。

發送緩衝區用來暫時存放應用程序發送給對方的數據,這是主機已發送出但未收到確認的數據。接收緩存用來暫時存放按序到達的、但尚未被接收應用程序讀取的數據以及 不按序到達的數據。

關於窗口的概念必須強調三點:

1. 發送方的發送窗口並不總是和接收方接收窗口一樣大,因爲有一定的時間滯後。

2. TCP 標準沒有規定對不按序到達的數據應如何處理,通常是先臨時存放在接收窗口中,等到字節流中所缺少的字節收到後,再按序交付上層的應用進程。

3. TCP 要求接收方必須有確認的功能,這樣可以減小傳輸開銷。

9. TCP 報文段處理

9.1 報文段緩衝隊列

TCP 連接的每一端都有接收緩衝區與發送緩衝區(也可以稱之爲緩衝隊列,下文均用緩衝隊列),而 TCP 控制塊只是維護緩衝區隊列的指針,通過指針簡單對這些緩衝區進行管理,LwIP 爲了更好管理TCP 報文段的緩衝隊列數據,特地定義了一個數據結構,命名爲tcp_seg,使用它將所有的報文段連接起來,這些報文可能是無發送的、可能是已發送但未確認的或者是已經接收到的無序報文,都是需要緩衝在TCP 控制塊內部的,以便識別是哪個連接,而TCP 控制塊,又不可能單獨爲每個連接開闢那麼大的空間,只能使用指針來管理。

/* This structure represents a TCP segment on the unsent, unacked and ooseq queues */
struct tcp_seg {
  struct tcp_seg *next;    /* used when putting segments on a queue */
  struct pbuf *p;          /* buffer containing data + TCP header */
  u16_t len;               /* the TCP length of this segment */
#if TCP_OVERSIZE_DBGCHECK
  u16_t oversize_left;     /* Extra bytes available at the end of the last
                              pbuf in unsent (used for asserting vs.
                              tcp_pcb.unsent_oversize only) */
#endif /* TCP_OVERSIZE_DBGCHECK */
#if TCP_CHECKSUM_ON_COPY
  u16_t chksum;
  u8_t  chksum_swapped;
#endif /* TCP_CHECKSUM_ON_COPY */
  u8_t  flags;
#define TF_SEG_OPTS_MSS         (u8_t)0x01U /* Include MSS option (only used in SYN segments) */
#define TF_SEG_OPTS_TS          (u8_t)0x02U /* Include timestamp option. */
#define TF_SEG_DATA_CHECKSUMMED (u8_t)0x04U /* ALL data (not the header) is
                                               checksummed into 'chksum' */
#define TF_SEG_OPTS_WND_SCALE   (u8_t)0x08U /* Include WND SCALE option (only used in SYN segments) */
#define TF_SEG_OPTS_SACK_PERM   (u8_t)0x10U /* Include SACK Permitted option (only used in SYN segments) */
  struct tcp_hdr *tcphdr;  /* the TCP header */
};

每個已經連接的TCP 控制塊中維護了3 個是指針,分別是unsent、unacked、ooseq,unsent 指向未發送的報文段緩衝隊列,unacked 指向已發送但未收到確認的報文段緩衝隊列,ooseq 指向已經收到的無序報文段緩衝隊列,當然啦,如果都沒有這些報文段,那麼這些指針都會指向NULL。

9.2 TCP 報文段發送

一般我們在應用層使用NETCONN API 或者Socket API 進行編程的時候,會將用戶數據傳遞給傳輸層,那麼本章關於應用層是如何傳遞數據到傳輸層的就暫時先不講解,只需要知道數據到達傳輸層後是怎麼輸出的即可,如果我們使用的是NETCONN API 對已經連接的TCP 應用發送數據,那麼經過內核的一系列處理,就會調用lwip_netconn_do_writemore()函數對發送數據,但是真正處理TCP 報文段緩衝等操作是在tcp_write()函數中,在這個函數裏,LwIP 會寫入數據,但是不會立即發送,也就是存儲在緩衝區裏面,等待更多的數據進行高效的發送,這也是著名的Nagle 算法,然後在調用tcp_output()函數進行發送出去,這樣子一個應用層的數據就通過TCP 協議傳遞給IP 層了。

err_t
tcp_output(struct tcp_pcb *pcb)
{
  struct tcp_seg *seg, *useg;
  u32_t wnd, snd_nxt;
  err_t err;
  struct netif *netif;
#if TCP_CWND_DEBUG
  s16_t i = 0;
#endif /* TCP_CWND_DEBUG */

  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ASSERT("tcp_output: invalid pcb", pcb != NULL);
  /* pcb->state LISTEN not allowed here */
  LWIP_ASSERT("don't call tcp_output for listen-pcbs",
              pcb->state != LISTEN);

  /* First, check if we are invoked by the TCP input processing
     code. If so, we do not output anything. Instead, we rely on the
     input processing code to call us when input processing is done
     with. */
  if (tcp_input_pcb == pcb) {
    return ERR_OK;
  }

  wnd = LWIP_MIN(pcb->snd_wnd, pcb->cwnd);

  seg = pcb->unsent;

  if (seg == NULL) {
    LWIP_DEBUGF(TCP_OUTPUT_DEBUG, ("tcp_output: nothing to send (%p)\n",
                                   (void *)pcb->unsent));
    LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_output: snd_wnd %"TCPWNDSIZE_F
                                 ", cwnd %"TCPWNDSIZE_F", wnd %"U32_F
                                 ", seg == NULL, ack %"U32_F"\n",
                                 pcb->snd_wnd, pcb->cwnd, wnd, pcb->lastack));

    /* If the TF_ACK_NOW flag is set and the ->unsent queue is empty, construct
     * an empty ACK segment and send it. */
    if (pcb->flags & TF_ACK_NOW) {
      return tcp_send_empty_ack(pcb);
    }
    /* nothing to send: shortcut out of here */
    goto output_done;
  } else {
    LWIP_DEBUGF(TCP_CWND_DEBUG,
                ("tcp_output: snd_wnd %"TCPWNDSIZE_F", cwnd %"TCPWNDSIZE_F", wnd %"U32_F
                 ", effwnd %"U32_F", seq %"U32_F", ack %"U32_F"\n",
                 pcb->snd_wnd, pcb->cwnd, wnd,
                 lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len,
                 lwip_ntohl(seg->tcphdr->seqno), pcb->lastack));
  }

  netif = tcp_route(pcb, &pcb->local_ip, &pcb->remote_ip);
  if (netif == NULL) {
    return ERR_RTE;
  }

  /* If we don't have a local IP address, we get one from netif */
  if (ip_addr_isany(&pcb->local_ip)) {
    const ip_addr_t *local_ip = ip_netif_get_local_ip(netif, &pcb->remote_ip);
    if (local_ip == NULL) {
      return ERR_RTE;
    }
    ip_addr_copy(pcb->local_ip, *local_ip);
  }

  /* Handle the current segment not fitting within the window */
  if (lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd) {
    /* We need to start the persistent timer when the next unsent segment does not fit
     * within the remaining (could be 0) send window and RTO timer is not running (we
     * have no in-flight data). If window is still too small after persist timer fires,
     * then we split the segment. We don't consider the congestion window since a cwnd
     * smaller than 1 SMSS implies in-flight data
     */
    if (wnd == pcb->snd_wnd && pcb->unacked == NULL && pcb->persist_backoff == 0) {
      pcb->persist_cnt = 0;
      pcb->persist_backoff = 1;
      pcb->persist_probe = 0;
    }
    /* We need an ACK, but can't send data now, so send an empty ACK */
    if (pcb->flags & TF_ACK_NOW) {
      return tcp_send_empty_ack(pcb);
    }
    goto output_done;
  }
  /* Stop persist timer, above conditions are not active */
  pcb->persist_backoff = 0;

  /* useg should point to last segment on unacked queue */
  useg = pcb->unacked;
  if (useg != NULL) {
    for (; useg->next != NULL; useg = useg->next);
  }
  /* data available and window allows it to be sent? */
  while (seg != NULL &&
         lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) {
    LWIP_ASSERT("RST not expected here!",
                (TCPH_FLAGS(seg->tcphdr) & TCP_RST) == 0);
    /* Stop sending if the nagle algorithm would prevent it
     * Don't stop:
     * - if tcp_write had a memory error before (prevent delayed ACK timeout) or
     * - if FIN was already enqueued for this PCB (SYN is always alone in a segment -
     *   either seg->next != NULL or pcb->unacked == NULL;
     *   RST is no sent using tcp_write/tcp_output.
     */
    if ((tcp_do_output_nagle(pcb) == 0) &&
        ((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
      break;
    }
#if TCP_CWND_DEBUG
    LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_output: snd_wnd %"TCPWNDSIZE_F", cwnd %"TCPWNDSIZE_F", wnd %"U32_F", effwnd %"U32_F", seq %"U32_F", ack %"U32_F", i %"S16_F"\n",
                                 pcb->snd_wnd, pcb->cwnd, wnd,
                                 lwip_ntohl(seg->tcphdr->seqno) + seg->len -
                                 pcb->lastack,
                                 lwip_ntohl(seg->tcphdr->seqno), pcb->lastack, i));
    ++i;
#endif /* TCP_CWND_DEBUG */

    if (pcb->state != SYN_SENT) {
      TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);
    }

    err = tcp_output_segment(seg, pcb, netif);
    if (err != ERR_OK) {
      /* segment could not be sent, for whatever reason */
      tcp_set_flags(pcb, TF_NAGLEMEMERR);
      return err;
    }
#if TCP_OVERSIZE_DBGCHECK
    seg->oversize_left = 0;
#endif /* TCP_OVERSIZE_DBGCHECK */
    pcb->unsent = seg->next;
    if (pcb->state != SYN_SENT) {
      tcp_clear_flags(pcb, TF_ACK_DELAY | TF_ACK_NOW);
    }
    snd_nxt = lwip_ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg);
    if (TCP_SEQ_LT(pcb->snd_nxt, snd_nxt)) {
      pcb->snd_nxt = snd_nxt;
    }
    /* put segment on unacknowledged list if length > 0 */
    if (TCP_TCPLEN(seg) > 0) {
      seg->next = NULL;
      /* unacked list is empty? */
      if (pcb->unacked == NULL) {
        pcb->unacked = seg;
        useg = seg;
        /* unacked list is not empty? */
      } else {
        /* In the case of fast retransmit, the packet should not go to the tail
         * of the unacked queue, but rather somewhere before it. We need to check for
         * this case. -STJ Jul 27, 2004 */
        if (TCP_SEQ_LT(lwip_ntohl(seg->tcphdr->seqno), lwip_ntohl(useg->tcphdr->seqno))) {
          /* add segment to before tail of unacked list, keeping the list sorted */
          struct tcp_seg **cur_seg = &(pcb->unacked);
          while (*cur_seg &&
                 TCP_SEQ_LT(lwip_ntohl((*cur_seg)->tcphdr->seqno), lwip_ntohl(seg->tcphdr->seqno))) {
            cur_seg = &((*cur_seg)->next );
          }
          seg->next = (*cur_seg);
          (*cur_seg) = seg;
        } else {
          /* add segment to tail of unacked list */
          useg->next = seg;
          useg = useg->next;
        }
      }
      /* do not queue empty segments on the unacked list */
    } else {
      tcp_seg_free(seg);
    }
    seg = pcb->unsent;
  }
#if TCP_OVERSIZE
  if (pcb->unsent == NULL) {
    /* last unsent has been removed, reset unsent_oversize */
    pcb->unsent_oversize = 0;
  }
#endif /* TCP_OVERSIZE */

output_done:
  tcp_clear_flags(pcb, TF_NAGLEMEMERR);
  return ERR_OK;
}

總的來說,這個函數的流程還是很簡單的,如果控制塊的flags 字段被設置爲TF_ACK_NOW,但是此時還沒有數據發送,就只發送一個純粹的ACK 報文段,如果能發送數據,那就將ACK 應答捎帶過去,這樣子就能減少網絡中的流量,同時在發送的時候先找到未發送鏈表,然後調用tcp_output_segment()->ip_output_if()函數進行發送,直到把未發送鏈表的數據完全發送出去或者直到填滿發送窗口,並且更新發送窗口相關字段,同時將這些已發送但是未確認的數據存儲在未確認鏈表中,以防丟失數據進行重發操作,放入未確認鏈表的時候是按序號升序進行排序的。

9.3 TCP 報文段接收

IP 數據報中如果是遞交給TCP 協議的數據,就會調用tcp_input()函數往上層傳遞,而TCP 協議收到數據就會對這些數據進行一系列的處理與驗證,因此這個函數是很麻煩的一個函數,源碼足足有476 行.

tcp_input()函數會對傳遞進來的IP 數據報進行處理,做一些校驗數據報是否正確的操作,查看一下數據報中是否有數據,如果沒有就丟掉,看一下是不是多播、廣播報文,如果是就不做處理,釋放pbuf。將TCP 首部中的各字段內容提取出來,首先在tcp_active_pcbs 鏈表中尋找對應的TCP 控制塊,找到了就調用tcp_process()函數進行處理;如果找不到就去tcp_tw_pcbs 鏈表中查找,找到了就調用tcp_timewait_input()函數處理它;如果還是找不到就去tcp_listen_pcbs 鏈表中找,如果找到就調用tcp_listen_input()函數處理,如果找不到的話,就釋放pbu。

此外,還要補充,對於正常接收處理的數據,如果收到的報文段是復位報文或終止連接應答報文,那麼就釋放pbuf,終止連接;如果TCP 協議確認了報文段是新的數據,那麼就調用帶參宏TCP_EVENT_SENT(其實是一個sent 的回調函數)去處理,如果報文段中包含有效的數據,就調用TCP_EVENT_RECV 去處理 ,如果是收到FIN 報文,則調用TCP_EVENT_CLOSED 去處理它。

總結:TCP 相關的處理毫無疑問比較複雜,結合協議理解也需要一定的時間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章