1 概述
在實驗0中,您實現了一個流控制的位元組流(ByteStream)的抽象。在實驗1和實驗2中,您實現了將攜帶在不可靠資料包中的段翻譯成傳入位元組流的工具:StreamReassembler和TCPReceiver。現在,在實驗3中,您將實現連線的另一側。TCPSender是一個工具,將傳出位元組流翻譯為將成為不可靠資料包負載的段。最後,在實驗4中,您將結合之前實驗的工作,建立一個完整的TCP實現:一個包含TCPSender和TCPReceiver的TCPConnection。您將使用這個實現與網際網路上的真實伺服器進行通訊。
回想lab2,我們實現了流接收的API,還記得流接收所需要的引數嗎?
我們在接收函式中,接收了一個seg資料片段,將資料段塞入重排器,那麼資料包從何而來呢,顯然就是由傳送端而來,本lab就是來實現傳送端傳送seg的行為。
3 The TCP Sender
TCP是一種可靠地在不可靠資料包上傳輸一對流控制位元組流(每個方向一個流)的協議。TCP連線中有兩個參與方,每個參與方同時充當傳送方(傳送自己的傳出位元組流)和接收方(接收傳入位元組流)。這兩個參與方被稱為連線的端點或對等方。
本週,您將實現TCP的傳送方部分,負責從一個ByteStream中讀取(由某個傳送方應用程式建立和寫入),並將流轉換為一系列傳出的TCP段。在遠端一側,TCP接收方將這些段(可能不是全部都到達)轉換回原始位元組流,並向傳送方傳送確認和視窗廣告。
TCP傳送方和接收方各自負責TCP段的一部分。TCP傳送方寫入了對於Lab 2中的TCPReceiver相關的TCPSegment的所有欄位,即序列號、SYN標誌、有效載荷和FIN標誌。然而,TCP傳送方僅讀取接收方寫入的段中的欄位,即確認號和視窗大小。
以下是TCP段的結構,僅突出顯示傳送方將讀取的欄位:TCPSender的責任是:
跟蹤接收方的視窗(處理傳入的確認號和視窗大小)
在可能的情況下填充視窗,從ByteStream中讀取資料,建立新的TCP段(包括如果需要的SYN和FIN標誌),併傳送它們。傳送方應該保持傳送段,直到視窗滿或ByteStream為空。
跟蹤已傳送但尚未被接收方確認的段,我們稱之為未確認段
如果足夠的時間過去而它們尚未被確認,則重新傳送未確認的段
為什麼要這樣做?基本原則是傳送接收方允許我們傳送的內容(填充視窗),並保持重傳,直到接收方確認每個段。這稱為自動重複請求(ARQ)。傳送方將位元組流分成段併傳送它們,只要接收方的視窗允許。
由於上週的工作,我們知道遠端TCP接收方只要至少一次收到每個索引標記的位元組就可以重建位元組流,無論順序如何。傳送方的任務是確保接收方至少收到每個位元組一次。
到這,我們就要開始接觸TCP保證可靠性連線的基礎,我們就要開始瞭解TCP實現可靠性連線的相關傳送規則了。
我們看他介紹的傳送端需要做出的行為:
- 跟蹤接收方的視窗(處理傳入的確認號和視窗大小)
- 在可能的情況下填充視窗,從ByteStream中讀取資料,建立新的TCP段(包括如果需要的SYN和FIN標誌),併傳送它們。傳送方應該保持傳送段,直到視窗滿或ByteStream為空。
- 跟蹤已傳送但尚未被接收方確認的段,我們稱之為未確認段
- 如果足夠的時間過去而它們尚未被確認,則重新傳送未確認的段
跟蹤
正如我在lab1中提出的問題,如果我們塞入的資料包片填滿了整個capacity,再有資料包seg傳入會怎樣?答案是不會出現這種情況。TCP實現的可靠性傳輸專門維護了一個視窗大小window_size,來告訴傳送方接收方的capacity還剩多少空間(接收方在回覆確認報文的時候會攜帶這個window_size在回覆的ack頭部中,後面會做到),傳送方會根據這個大小傳送合適數量的seg避免超出這個大小造成seg丟失。這,也叫做TCP的流量控制。
當然,我們在傳送時候需要將某些段規定他特殊的部分,例如
在三次握手的時候,也就是連線的開始,我們需要對某些段標誌連線開始的段的頭部syn設為true(預設都是false),如果你還是不太瞭解TCP頭部的資訊,重新看一遍tcp_header.hh這個函式吧。與之相對應的,結束的時候需要對結束標誌進行更改以及在傳輸中某些資訊進行新增,更改。另外要知道我們的傳送端的職責是儘可能將傳送端內的出向位元組流的seg全部傳送出去,所以這裡一定要注意。
第三點他讓我們跟蹤已傳送但尚未被接收方確認的段,函式中體現位bytes_in_flight
然後最後一句表達了重傳的思想。
我們都知道,在因特網傳輸過程中,資訊是不具備糾錯能力的,但是具備檢錯能力,但是作為可靠性傳輸,檢查出錯誤我們應該怎麼辦呢?答案是重傳。至於重傳是怎麼回事,我們繼續往下看文件。
3.1 TCPSender是怎麼知道資料包是否丟失的呢?
您的TCPSender將傳送一系列TCPSegments。每個段都包含一個從傳出的ByteStream中提取的(可能為空的)子字串,使用序列號表示在流中的位置,並在流的開頭標有SYN標誌,在結尾標有FIN標誌。
除了傳送這些段之外,TCPSender還必須跟蹤其未確認的段,直到它們佔用的序列號完全被確認。定期地,TCPSender的所有者將呼叫TCPSender的tick方法,指示時間的流逝。TCPSender負責檢視其未確認的TCPSegments集合,並判斷最早傳送的段是否在沒有得到確認的情況下已經過了太長時間(即沒有全部序列號被確認)。如果是這樣,它需要被重傳(再次傳送)。
以下是未確認時間過長的規則。我們將要實現這個邏輯,儘管它有點詳細,但我們不希望您擔心隱藏的測試用例試圖使您失敗,也不要將其視為SAT上的詞彙問題。我們將在本週提供一些合理的單元測試,並在完成整個TCP實現後在Lab 4中提供更完整的整合測試。只要您百分之百透過這些測試並且您的實現是合理的,您就會透過。
為什麼要這麼做?總體目標是讓傳送方在段丟失並需要重新傳送時能夠及時檢測到。等待重新傳送的時間非常重要:您不希望傳送方等待太長時間才重新傳送一個段(因為這會延遲到達接收應用程式的位元組流),但您也不希望重新傳送一個如果傳送方稍微等待一下就會被確認的段,這會浪費網際網路的寶貴頻寬。
1. 每隔幾毫秒,將使用一個引數呼叫您的TCPSender的tick方法,該引數告訴它自上次呼叫該方法以來經過了多少毫秒。使用這個引數來維護TCPSender的總存活毫秒數。請不要嘗試從作業系統或CPU呼叫任何時間或時鐘函式,tick方法是您唯一訪問時間流逝的方法。這使事情保持可預測性和可測試性。
2. 當構造TCPSender時,它會得到一個引數,該引數告訴它重新傳輸超時(RTO)的初始值。RTO是在重新傳送未確認的TCP段之前等待的毫秒數。RTO的值會隨時間變化,但初始值保持不變。起始程式碼將初始RTO的值儲存在一個名為initial retransmission timeout的成員變數中。
3. 您將實現重新傳輸計時器:在某個時間開始的警報,一旦經過了RTO,警報就會觸發(或過期)。我們強調這種時間流逝的概念來自於呼叫tick方法而不是獲取實際的時刻。
4. 每次傳送包含資料的段(序列空間中長度非零的段)時(無論是第一次還是重新傳送),如果計時器尚未執行,則啟動它,以便在RTO毫秒後觸發。透過觸發,我們的意思是時間將在未來的一定毫秒數內耗盡。
5. 當所有未確認的資料都被確認時,停止重新傳輸計時器。
6. 如果呼叫tick且重新傳輸計時器已經過期: (a) 重新傳輸未被TCP接收方完全確認的最早(最低序列號)段。您需要在某種內部資料結構中儲存未確認的段,以便能夠執行此操作。 (b) 如果視窗大小不為零: i. 跟蹤連續重傳的次數,並增加它,因為您剛剛重新傳輸了某些內容。您的TCPConnection將使用此資訊來決定連線是否無望(連續重傳次數太多)並且需要中止。 ii. 將RTO的值加倍。這被稱為指數退避,它減緩了在差勁網路上的重新傳輸,以避免進一步阻塞網路。 (c) 重置重新傳輸計時器,並啟動它,以便在RTO毫秒後觸發(考慮到您可能剛剛加倍了RTO的值!)。
7. 當接收方向傳送方提供了確認以確認成功接收的新資料時(確認號反映了比任何先前確認的絕對序列號都要大的絕對序列號): (a) 將RTO設定回其初始值。 (b) 如果傳送方有任何未確認的資料,則重新啟動重新傳輸計時器,以便在RTO毫秒後觸發(對於當前RTO的值)。 (c) 將連續重傳的次數重置為零。
我們建議在一個單獨的類中實現重新傳輸計時器的功能,但這取決於您。如果您這樣做,請將其新增到現有檔案中((tcp_sender.hh tcp_sender.cc)。
這一大段極其極其重要,他介紹了資料包是怎麼判斷丟失的,以及什麼時候重傳,建議配合程式碼看。
他這裡給出了定時器的概念,定期器會被週期呼叫,每次呼叫會返回一個距離上次呼叫的時間,當距離上次接收到資料包的時間超過一個初始值最大重傳時間的時候,那麼就會發生重傳,我們認為第一個沒有被接收的資料段需要重傳(這個很好理解,最早傳出去但是還沒被收到的資料包肯定是最前面有問題那個),然後同時維護一個連續重傳次數。另外,為了保證我們的傳輸過程不會被各種因重傳出現的資料包過多而效能下降,TCP特地設計了擁塞控制,每發生重傳的時候,他緊跟著的下次重傳時間翻倍,來避免因為網路差的原因滯留太多資料包。後面就是一些細節了,比如最大傳輸時間和連續重傳次數的更新等等。下面就是你自己去嘗試理解了。
3.2 實現傳送端
好的!我們已經討論了TCP傳送方的基本思想(給定一個傳出的ByteStream,將其分割成段,傳送到接收方,如果它們在足夠快的時間內沒有被確認,則繼續重新傳送)。我們已經討論了何時可以得出結論,以及何時認為一個未確認的段丟失並需要重新傳送。
現在是時候介紹您的TCPSender將提供的具體介面了。有四個重要的事件需要處理,每個事件可能最終會傳送一個TCPSegment:
1. **void fill_window()** TCPSender被要求填充視窗:它從其輸入的ByteStream中讀取並儘可能多地傳送位元組,形成TCPSegments,只要有新的位元組可以讀取並且視窗中有空間。 確保每個傳送的TCPSegment完全位於接收方的視窗內。使每個單獨的TCPSegment儘可能大,但不要超過由TCPConfig::MAX_PAYLOAD_SIZE(1452位元組)給定的值。 您可以使用TCPSegment::length_in_sequence_space()方法來計算段佔用的序列號的總數。請記住,SYN和FIN標誌也各自佔用一個序列號,這意味著它們在視窗中佔用空間。 如果視窗大小為零怎麼辦?如果接收方宣佈視窗大小為零,則ll_window方法應該像視窗大小為一樣。傳送方可能最終傳送一個單位元組,被接收方拒絕(並且沒有被確認),但這也可能促使接收方傳送一個新的確認段,其中它透露出其視窗中已經開啟的更多空間。否則,傳送方將永遠無法瞭解到底可以開始重新傳送。
2. **void ack_received(const WrappingInt32 ackno, const uint16_t window_size)** 從接收方接收到一個段,傳達視窗的新左邊(= ackno)和右邊(= ackno + window_size)邊緣。TCPSender應該檢視其未確認的段的集合,並刪除任何現在已完全被確認的段(ackno大於段中的所有序列號)。如果有新的空間開啟,TCPSender應該再次填充視窗。
3. **void tick(const size_t ms_since_last_tick):** 距離上次呼叫此方法已經過去了一定數量的毫秒。傳送方可能需要重新傳輸一個未確認的段。
4. **void send_empty_segment():** TCPSender應該生成併傳送在序列空間中長度為零的TCPSegment,並正確設定序列號。如果所有者(您將在下週實現的TCPConnection)希望傳送一個空的ACK段,這將非常有用。 注意:像這樣不佔用序列號的段不需要被追蹤為未確認,也永遠不會被重新傳送。
要完成Lab 3,請查閱文件中的完整介面,網址為 https://cs144.github.io/doc/lab3/class tcp sender.html,並在tcp_sender.hh和tcp_sender.cc檔案中實現完整的TCPSender公共介面。我們期望您可能需要新增私有方法和成員變數,以及可能需要輔助類。
下面就是實現了,具體細節我這裡不想講,因為這裡的細節太多了,很多問題需要你自己debug發現才有意思,印象深刻,大體思路理解了後面都不是問題。
對了,TCP有一個很有意思的點
這裡,當視窗是0的話,我們沒必要對初始超時時間翻倍,這樣做是無意義且浪費頻寬的。
參考自p165 自頂向下
這是一個需要注意且文中沒有提到的地方,而且測試點還有,如果你面向樣例程式設計也可以透過。
tips:一定要仔細讀介面設計註釋來理解各個函式!
3.3 測試
測試您的程式碼時,測試套件將期望它透過一系列情況的演變,從傳送第一個SYN段,到傳送所有資料,再到傳送FIN段,最終直到FIN段被確認。我們認為您可能不想新增更多的狀態變數來跟蹤這些狀態——這些狀態已經由您的TCPSender類的公共介面定義。但為了幫助您理解測試輸出,這裡是TCPSender在流的生命週期中的預期演變圖表。(在Lab 4之前,您無需擔心錯誤狀態或RST標誌。)
這裡其實我個人是沒有用到的,因為我這裡對於狀態的判斷是挺精準的,但是如果你對TCP什麼情況應該處在什麼狀態有疑問,一定要看這張圖,你會受益匪淺。
3.4 Q&A
喜聞樂道的放水環節,Q&A他的核心目的就是提醒你需要注意的一些事情,所以一定要看。
• 如何傳送一個段? 將其推送到segments_out佇列上。就您的TCPSender而言,只要將其推送到該佇列上,它就被視為已傳送。很快,所有者將會過來並彈出它(使用公共的segments_out()訪問器方法),然後真正地傳送它。
• 等等,如何既傳送一個段又跟蹤同一個段作為未確認的,以便稍後知道要重傳什麼?我難道不必複製每個段嗎?這會浪費資源嗎? 當您傳送包含資料的段時,您可能希望將其推送到segments_out佇列,並在內部保留一個副本,以便在可能需要重傳時跟蹤未確認的段。這實際上並不浪費太多,因為段的負載被儲存為引用計數的只讀字串(Buffer物件)。所以別擔心,實際上並沒有複製負載資料。
• 在我從接收方得到ACK之前,我的TCPSender應該假設接收方的視窗大小是多少? 一個位元組。
• 如果確認僅部分確認了某個未確認的段,我該怎麼辦?我應該嘗試剪下已確認的位元組嗎? 一個TCP傳送方可以這樣做,但是對於這個課程來說,沒有必要搞得太複雜。將每個段視為完全未確認,直到它已完全被確認,即它佔用的所有序列號都小於ackno。
• 如果我傳送包含a、b和c的三個單獨的段,並且它們從未被確認,我能否稍後在一個包含abc的大段中重新傳輸它們?或者我必須逐個重新傳輸每個段? 同樣:一個TCP傳送方可以這樣做,但是對於這個課程來說,沒有必要搞得太複雜。只需單獨跟蹤每個未確認的段,當重傳計時器超時時,再次傳送最早的未確認段。
• 我應該在我的未確認資料結構中儲存空段並在必要時重新傳輸它們嗎? 不需要,唯一應該作為未確認並可能重新傳輸的段是那些傳遞了一些資料的段,即在序列空間中佔用一些長度的段。不需要記住或重新傳輸佔用零序列號的段(沒有負載、SYN或FIN)。
• 我在這個PDF之後還能找到更多的常見問題解答嗎? 請定期檢視網站(https://cs144.github.io/lab_faq.html)和Piazza。
程式碼奉上:
sender.hh
sender.cc
最後同樣,make check_lab4
就可以測試用例了。
做到這,lab3就結束了。
我想做到這你應該有點想法了,我們現在已經實現了幾個基礎部分,還有傳送端,接收端的角色,TCP三次握手,當傳送方傳送資料seg的時候,會考慮對方是否塞得下,同時還要定期重傳資料包,傳輸標準就是沒有確認的第一個seg,這裡就是根據reveive方所發來的ack中的ackno來確認,同時維護握手揮手中頭部特殊標記的變化,總之,lab3是一個非常考驗你debug能力的lab,其中的細節會讓你對於TCP的傳輸印象深刻。