tcp需要ack,可是為了效率,並不是每發送一個資料都要等待ack,而是儘可能利用視窗機制,積累發送ack的,當然在某些特殊情況下還是需要馬上發送ack的,比如接收到亂序的資料,這種情況下,雖然接收端可以將亂序的資料包暫存,但是接收方必鬚髮送一個ack號為按序的期望的序號的ack給發送端,另外就是接收視窗需要調整,此時就要立刻發送ack,否則則可以延遲發送ack,看一下linux的這方面的代碼:
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
struct tcp_sock *tp = tcp_sk(sk);
//rcv_mss是估算的對端的mss,它對本端接收視窗的計算也有很大意義
if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss //如果接收到了大於一個的報文,那麼就發送ack,一下子確認兩個報文
&& __tcp_select_window(sk) >= tp->rcv_wnd) || //需要調整視窗,最大化輸送量
tcp_in_quickack_mode(sk) ||
(ofo_possible && //收到亂序的包
skb_peek(&tp->out_of_order_queue))) {
tcp_send_ack(sk);
} else {
tcp_send_delayed_ack(sk);
}
}
大體上,上述的函數實現了RFC1122和RFC2581的關於ack的建議。
不是說ack可以在send資料的時候捎帶發送嗎?確實是這樣,每當發送資料的時候,ack都會被發送,但是發送資料是應用程式層的事,如果應用程式層不發送呢?那豈不是永遠都無法ack了嗎?所以還必須有傳輸層本身的一套機制來支援ack的發送,捎帶ack僅僅是一個補充,傳輸層的ack發送就是立即模式和延遲模式,正如上面的__tcp_ack_snd_check所展示的那樣。
發送ack其實很簡單,就是填寫一個tcp資料,ack欄位設定為接收視窗最左邊的那個資料的序號加1,延遲發送不怕和捎帶發送重複,RFC2581規定每個到來的報文只能產生一個ack,除非需要發送端重傳才會發送冗餘ack,如果tcp進入了等待延遲發送ack的狀態,當接收端有資料要發送的時候就會將ack捎帶到發送端,同時清除延遲ack定時器的pendding,如此延遲ack定時器到期後就不會再發送ack了。
穩定的理想情況下,接收端的視窗也是穩定的,不需要調整的,如果接收端不發送資料只是接收資料,ack幾乎全部以延遲的方式發送給發送端,如果接收端同時也發送資料,那麼ack就會以捎帶的方式發給發送端,立即ack只會在幾種特殊的異常情況才發送。
一種是收到了一個以上的完整的tcp段,並且可能要放大視窗,為了使得輸送量最大化,放大的視窗決不能浪費掉,於是需要立即發送ack給發送端,發送端接收到這個ack以後就會繼續發送其發送視窗中後面的資料了。發送端的mss和接收端的視窗大小相關聯,接收端的視窗設定為發送端mss的整數倍比較好,這樣記憶體的利用率最高,確定好接收端可以承受的視窗大小之後,如果其比當前視窗大,那麼立即發送ack使得發送端可以儘快發送資料。
另外一種是在所謂的quick模式,quick模式並不是經常的,只有在非互動的tcp串連才可能進入quick模式,因為互動的串連表明ack已經足夠快了,沒有必要立即發送ack了,一般都是捎帶ack或者延遲發送ack的,那麼如何判斷是否是互動串連呢?核心中tcp_opt結構體中有一個ack子結構體,內部有一個quick和pingpong兩個欄位,其中pingpong就是判斷互動串連的,核心會在很多地方進行抉擇,根據很多參數,比如收發間隔或者使用者配置等判斷是否一個串連是互動的,如果不是互動的,那麼就存在一系列問題:1.由於使用者進程長期不取出收到的資料導致一系列的問題,於是需要協議棧瞬間回複ack,2.積壓的ack沒有回複,影響了發送端的發送速率。此時就會給quick賦予一定的數值,每發送一個ack就會消耗掉一些quick值,直到用盡了quick而進入延遲模式,quick的值和視窗相關,因為接收端最多隻能確認接收視窗這麼大的資料。
立即ack的最後一種可能就是收到亂序包,表明資料已經可能丟失了,那麼應該儘快地進入補救階段,就是說要儘快進入快速重傳,此時ack也要立即發送(核心發現越界包[和亂序包有區別]後會調用tcp_send_dupack發送一個ack後丟次報文而返回),核心收到亂序報文後會在out_of_order_queue隊列緩衝該亂序報本,最後會調用tcp_ack_snd_check再次發送一個ack,這個ack確認的是按序報文的最後一個,以前應該發送過該ack,這樣接收端收到亂序報文後就會發送一個冗餘的ack,如果下次接收的資料仍然是亂序的,那麼就再發送一個前兩個相同的ack,這樣發送端可能就會連續接收到三個一模一樣的ack,在接收端,第三次接收到的仍然是亂序報文時,再次發送冗餘ack,只有這第4個ack被發送端收到後才會進行快速重傳。這裡的一個細節就是發送端收到了4個相同的ack(3個冗餘ack),從而作為進入快速重傳的標誌,linux是這麼實現的,符合了rfc的建議,但是這種實現所依賴的是其背後的一個思想。
一個報文談不上順序,最少兩個報文才有順序的概念,正如位元組序一樣,utf8以一個位元組為編碼單位,因此就沒有位元組序的問題,同樣的,僅僅來了一個報文也不能說它對於當前按序的報文來講是亂序的,只有當第二個報文到來的時候,如果當前按序報文,第一個報文,第二個報文拼不成順序才能說明後來的這兩個報文是亂序的,當然這也是一種權衡的結果,正如三向交握為何是三次一樣,即使接收端收到了第三個亂序報文,仍有可能被第四個填充後成為按序報文,沒完沒了等下去是不行的,必須在發送端接收到確定的,不是很大的數目冗餘ack的時候進入快速重傳,同時也不能頻繁的快速重傳,因此就選擇了3個冗餘ack,當然這個數字是可以配置的。
最後看一下亂序報文的重新調整。linux的協議棧實現中將亂序的報文按照序號大小順序插入到一個隊列當中,此隊列是基於串連的,如果該亂序隊列有報文暫存的話,每接收到一個報文都會嘗試調用tcp_ofo_queue函數,它的意義在於努力將亂序的報文順序化,正如上述冗餘ack相關的背後的思想,每個新的報文都有可能填補按序報文和亂序報文之間的缺口,換句話說,每一個新到的報文都可能直接拼接到按序報文隊列最後一個的後面,同時也有可能完成這種拼接後,和後面的亂序隊列的最前面一個或者幾個或者全部的報文拼接,最終成為一系列按序的報文:
static void tcp_ofo_queue(struct sock *sk)
{
struct tcp_opt *tp = tcp_sk(sk);
__u32 dsack_high = tp->rcv_nxt;
struct sk_buff *skb;
while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
if (after(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) //最前面的skb都拼不上
break;
...
if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
__skb_unlink(skb, skb->list); //曾經接收的報文段,繼續
__kfree_skb(skb);
continue;
}
__skb_unlink(skb, skb->list); //可以拼接,更新tp的rcv_next欄位
__skb_queue_tail(&sk->sk_receive_queue, skb);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
...
}
}