TCP應用層協議(4)
流量控制
接收端處理數據的速度是有限的. 如果發送端發的太快, 導致接收端的緩沖區被打滿, 這個時候如果發送端繼續發送, 就會造成丟包, 繼而引起丟包重傳等等一系列連鎖反應.
因此 TCP 支持根據接收端的處理能力, 來決定發送端的發送速度. 這個機制就叫做流量控制(Flow Control);
? 接收端將自己可以接收的緩沖區大小放入 TCP 首部中的 “窗口大小” 字段, 通過 ACK 端通知發送端;
? 窗口大小字段越大, 說明網絡的吞吐量越高;
? 接收端一旦發現自己的緩沖區快滿了, 就會將窗口大小設置成一個更小的值通知給發送端;
? 發送端接受到這個窗口之后, 就會減慢自己的發送速度;
? 如果接收端緩沖區滿了, 就會將窗口置為 0; 這時發送方不再發送數據, 但是需要定期發送一個窗口探測數據段, 使接收端把窗口大小告訴發送端.
接收端如何把窗口大小告訴發送端呢? 回憶我們的 TCP 首部中, 有一個 16 位窗口字段, 就是存放了窗口大小信息;
那么問題來了, 16 位數字最大表示 65535, 那么 TCP 窗口最大就是 65535 字節么?
實際上, TCP 首部 40 字節選項中還包含了一個窗口擴大因子 M, 實際窗口大小是 窗口字段的值左移 M 位;
擁塞控制
如果是大面積丟包
這時候如果我們是多個客戶端多個服務端呢
滑動窗口是擁塞窗口和對方窗口的最小值,誰小誰是主要矛盾,即考慮了網絡擁堵問題和對方的接受能力,什么是擁塞窗口,擁塞窗口就是一個臨界值,網絡不會擁塞,這是衡量是否擁塞的標準
雖然 TCP 有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題.
因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據, 是很有可能引起雪上加霜的.
TCP 引入 慢啟動 機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據;
? 此處引入一個概念稱為擁塞窗口
? 發送開始的時候, 定義擁塞窗口大小為 1;
? 每次收到一個 ACK 應答, 擁塞窗口加 1;
? 每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口;
像上面這樣的擁塞窗口增長速度, 是指數級別的. “慢啟動” 只是指初使時慢, 但是增長速度非常快.
? 為了不增長的那么快, 因此不能使擁塞窗口單純的加倍.
? 此處引入一個叫做慢啟動的閾值
? 當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長
? 當 TCP 開始啟動的時候, 慢啟動閾值等于窗口最大值;
? 在每次超時重發的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回 1;少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞;當 TCP 通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;擁塞控制, 歸根結底是 TCP 協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案.
TCP 擁塞控制這樣的過程, 就好像 熱戀的感覺
延遲應答
如果接收數據的主機立刻返回 ACK 應答, 這時候返回的窗口可能比較小
? 假設接收端緩沖區為 1M. 一次收到了 500K 的數據; 如果立刻應答, 返回的窗口就是 500K;
? 但實際上可能處理端處理的速度很快, 10ms 之內就把 500K 數據從緩沖區消費掉了;
? 在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;
? 如果接收端稍微等一會再應答, 比如等待 200ms 再應答, 那么這個時候返回的窗口大小就是 1M;
一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率;
那么所有的包都可以延遲應答么? 肯定也不是;
? 數量限制: 每隔 N 個包就應答一次;
? 時間限制: 超過最大延遲時間就應答一次;
具體的數量和超時時間, 依操作系統不同也有差異; 一般 N 取 2, 超時時間取 200ms;
捎帶應答
在延遲應答的基礎上, 我們發現, 很多情況下, 客戶端服務器在應用層也是 “一發一收” 的. 意味著客戶端給服務器說了 “How are you”, 服務器也會給客戶端回一個 “Fine,thank you”;那么這個時候 ACK 就可以搭順風車, 和服務器回應的 “Fine, thank you” 一起回給客戶端
面向字節流
創建一個 TCP 的 socket, 同時在內核中創建一個 發送緩沖區 和一個 接收緩沖區;
? 調用 write 時, 數據會先寫入發送緩沖區中;
? 如果發送的字節數太長, 會被拆分成多個 TCP 的數據包發出;
? 如果發送的字節數太短, 就會先在緩沖區里等待, 等到緩沖區長度差不多了, 或者其他合適的時機發送出去;
? 接收數據的時候, 數據也是從網卡驅動程序到達內核的接收緩沖區;
? 然后應用程序可以調用 read 從接收緩沖區拿數據;
? 另一方面, TCP 的一個連接, 既有發送緩沖區, 也有接收緩沖區, 那么對于這一個連接, 既可以讀數據, 也可以寫數據. 這個概念叫做 全雙工
由于緩沖區的存在, TCP 程序的讀和寫不需要一一匹配, 例如:
? 寫 100 個字節數據時, 可以調用一次 write 寫 100 個字節, 也可以調用 100 次write, 每次寫一個字節;
? 讀 100 個字節數據時, 也完全不需要考慮寫的時候是怎么寫的, 既可以一次read 100 個字節, 也可以一次 read 一個字節, 重復 100 次;
粘包問題
? 首先要明確, 粘包問題中的 “包” , 是指的應用層的數據包.
? 在 TCP 的協議頭中, 沒有如同 UDP 一樣的 “報文長度” 這樣的字段, 但是有一個序號這樣的字段.
? 站在傳輸層的角度, TCP 是一個一個報文過來的. 按照序號排好序放在緩沖區中.
? 站在應用層的角度, 看到的只是一串連續的字節數據.
? 那么應用程序看到了這么一連串的字節數據, 就不知道從哪個部分開始到哪個部分, 是一個完整的應用層數據包. 那么如何避免粘包問題呢? 歸根結底就是一句話, 明確兩個包之間的邊界.
? 對于定長的包, 保證每次都按固定大小讀取即可; 例如上面的 Request 結構, 是固定大小的, 那么就從緩沖區從頭開始按 sizeof(Request)依次讀取即可;
? 對于變長的包, 可以在包頭的位置, 約定一個包總長度的字段, 從而就知道了包的結束位置;
? 對于變長的包, 還可以在包和包之間使用明確的分隔符(應用層協議, 是程序猿自己來定的, 只要保證分隔符不和正文沖突即可);
思考: 對于 UDP 協議來說, 是否也存在 “粘包問題” 呢?
? 對于 UDP, 如果還沒有上層交付數據, UDP 的報文長度仍然在. 同時, UDP 是一個一個把數據交付給應用層. 就有很明確的數據邊界.
? 站在應用層的站在應用層的角度, 使用 UDP 的時候, 要么收到完整的 UDP 報文, 要么不收. 不會出現"半個"的情況.
TCP 異常情況
進程終止: 進程終止會釋放文件描述符, 仍然可以發送 FIN. 和正常關閉沒有什么區別.
機器重啟: 和進程終止的情況相同.
機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行 reset. 即使沒有寫入操作, TCP 自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放.
另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如 HTTP 長連接中, 也會定期檢測對方的狀態. 例如 QQ, 在 QQ 斷線之后, 也會定期嘗試重新連接
TCP 小結
為什么 TCP 這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能.
可靠性:
? 校驗和
? 序列號(按序到達)
? 確認應答
? 超時重發
? 連接管理
? 流量控制
? 擁塞控制
提高性能:
? 滑動窗口
? 快速重傳
? 延遲應答
? 捎帶應答
其他:
為什么 TCP 這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能.
可靠性:
? 校驗和
? 序列號(按序到達)
? 確認應答
? 超時重發
? 連接管理
? 流量控制
? 擁塞控制
提高性能:
? 滑動窗口
? 快速重傳
? 延遲應答
? 捎帶應答
其他:
? 定時器(超時重傳定時器, 保活定時器, TIME_WAIT 定時器等)