一、TCP流控
之前我們介紹過TCP是基於窗口的流量控制,在TCP的發送端會維持一個發送窗口,我們假設發送窗口的大小為N比特,網絡環回時延為RTT,那么在網絡狀況良好沒有發生擁塞的情況下,發送端每個RTT就可以發送N比特的數據,發送端的速率應該與N/RTT成正比,因此通過改變發送窗口的大小就可以控制發送端的發送速率,那么接收端就可以通過控制發送端發送窗口的大小來控制發送速率。這樣接收端需要有一種方式通告發送端接收端期望的發送窗口大小,這種方式就叫做窗口通告(window advertisement),或者叫做窗口更新(window update)。另外窗口更新(window update)一般特指窗口大小發生變化的時候通告新窗口的事件。TCP是雙向通信,因此TCP兩端都會同時維護一個發送窗和一個接收窗。
二、TCP窗口信息交互
如下圖所示,在client發送至server方向(C->S)的數據傳輸用下方的淺色箭頭表示,而server發送至client方向(S->C)的數據傳輸用上方的深色箭頭表示。圖中深色和淺色字段也分別標示了不同數據傳輸方向的信息。與C->S方向淺色表示的數據傳輸相對應的,在client端會維護一個發送窗口,在server端會維護一個接收窗口,client端發送窗口的滑窗操作需要依靠S->C方向數據段的ACK和WIN信息來更新(在圖中同樣用淺色標示了出來),server端接收窗口的滑窗操作則需要C->S方向數據段中Seq和Data信息來更新。與S->C方向深色表示的數據傳輸相對應的,在server端會維護一個發送窗口,在client端會維護一個接收窗口,server端發送窗口的滑窗操作需要依靠C->S方向數據段的ACK和WIN信息來更新(在圖中用深色標示了出來),client端接收窗口的滑窗操作則需要S->C方向數據段中Seq和Data信息來更新。這樣可以看到一個TCP連接共有四個窗口,TCP會用窗口結構(window structures)來維護這些相關信息。
除了連接建立過程中的TCP報文外,每個TCP報文都會帶有一個有效的系列號Seq、確認號Ack Number、窗口大小Window size字段。其中系列號和確認號我們之前已經接觸介紹過多次。而窗口大小Window size字段則表示接收端還有多少預留空間來接收數據,即接收端願意接收的最大系列號為(Ack number + Window size)。一般來說接受端的應用層在接收到TCP數據后都會立即讀取出來,這樣實際上通過Window size通告的預留空間並不會發生太大變化,但是如果接收端應用層沒有及時讀取新接收到的TCP數據的話,這些數據就會在緩存空間中積壓,進而Window size慢慢變小(實際上linux會自動調整接收緩存和發送緩存,這里暫時按照固定緩存大小的情況來說明,后續文章在介紹緩存自動調整),如果應用層一直沒有讀取數據,那么最終沒有緩存空間后,Window size就會變為0,發送端也就不能繼續發送新數據了。Window Size字段最大為16bit,但是通過TCP的WIndow Scale選項可以擴大到大約1GB大小(請參考前面文章中關於WSOPT選項的介紹)。
三、發送端滑窗
我們首先來看一下發送窗口結構(send window structure),如下圖所示在發送端的系列號空間中,可以划分為四部分,分別是已經發送並且已經收到ACK確認包的數據,已經發送但是還沒有收到ACK確認包的數據,還未發送但是對端可以接收的數據,還未發送的對端還未准備接收的數據。其中SND.UNA、SND.NXT、(SND.UNA+SND.WND)三個構成了這四部分的邊界。而我們說的發送窗則是中間兩部分數據,即SND.UNA構成發送窗口的左邊沿,(SND.UNA+SND.WND)構成發送窗的右邊沿。左右邊沿之間的數據也叫做Offered Window,SND.NXT和(SND.UNA+SND.WND)之間的數據也叫做Usable Window。
當接收端回復ACK確認包並確認新數據的時候(同時帶有Window size字段),發送窗口左邊沿就會向右移動,右邊沿一般也會向右滑動,但是也可能保持位置不變甚至向左移動,我們一般用Closes、Shrinks、Opens等來描述窗口邊沿的變化情況
Closes:當發送窗口的左邊沿向右移動的時候,我們一般稱呼為窗口關閉。這一般發生在對端ACK確認了新數據,但是整個window size大小變小。
Shrinks:當發送窗的右邊向左移動的時候我們稱呼為窗口收縮。RFC1122強烈建議避免這種情況,但是接收端TCP要等處理這種情況。
Opens:當發送窗口的右邊沿向右移動的時候,我們稱呼為窗口打開。這一般發生在應用層讀取了新數據或者緩存自動調整的時候,對端TCP可以接收更多的新數據場景。
因為ACK是累積確認的,所以發送窗口的左邊沿不會向左移動。一個常見的情況是ACK number確認了新數據,而Window size大小並沒有變化(原因是應用層即使讀取了TCP接收的數據或者緩存自動調整),這時候我們就說向前推進(advance)了這個發送窗口,或者說這個發送窗向前滑動(slide forward)了。當ACK number確認了新數據,但是Window size慢慢變小,最后變為0的時候,這種窗口稱呼為zero window,這時候發送端就不能發送任何的數據了,需要定時探測對端的窗口大小,這種場景我們后面會進行進一步的介紹。
四、接收端滑窗
接着我們看一下接收窗口結構(receive window structure),如下圖所示接收窗口要簡單一些,主要分為三個部分,已經接收的系列號連續的並且回復了ACK報文的數據,未接收的但是正准備接收的數據或者是已經接收的落在了接收窗口內部但是系列號不連續的數據,還未准備接收的數據。其中RCV.NXT指向了接收窗的左邊沿,(RCV.NXT+RCV.WND)指向了接收窗的右邊沿。RCV.WND指定了接收窗口的大小。通過這個接收窗口結構可以讓接收端避免接收到重復的報文。按照協議超過(RCV.NXT+RCV.WND)的數據是可以丟棄的,但是在后面文章中我們將會看到linux實現一般並不會直接丟掉窗口右邊的報文。
五、wireshark示例
在linux內部會維護幾個與發送窗口相關的變量如下:snd_una、snd_wnd、snd_nxt。與接收窗口相關的變量如下rcv_nxt、rcv_wnd。另外選擇窗口的時候還會涉及一個rcv_mss的變量,這個變量是linux對對端MSS的一個保守估計值,在講解延遲ACK的時候我們已經介紹過這個變量是如何初始化和調整的,這里不再介紹。下面我們通過一個TCP通信實例來看一下server端這幾個變量的變化,在測試過程中,我們通過SO_RCVBUF選項設置server端為3000,client端為3500。並把tcp_adv_win_scale的值由默認的1改為2。SO_RCVBUF選項與Window size的關系我們后面內容會介紹。
1、server端收到No3這個數據包之后,即在三次握手之后,server端這幾個變量分別為snd_una=1065017116、 snd_wnd=5250、 snd_nxt=1065017116、 rcv_nxt=188998934、 rcv_wnd=4500、 rcv_mss=536
2、server端收到No4回復No5確認包之后,server端這幾個變量分別為snd_una=1065017116、 snd_wnd=5250、 snd_nxt=1065017116、 rcv_nxt=188999034、 rcv_wnd=4400、 rcv_mss=536,實際上這幾個變量是在收到No4變量的時候更新的,並不是在發出No5之后。
3、server端發出No6數據包之后,server端這幾個變量分別為snd_una=1065017116、 snd_wnd=5250、 snd_nxt=1065017316、 rcv_nxt=188999034、 rcv_wnd=4400、 rcv_mss=536
4、server端收到No7確認包之后,server端這幾個變量分別為snd_una=1065017316、 snd_wnd=5050、 snd_nxt=1065017316、 rcv_nxt=188999034、 rcv_wnd=4400、 rcv_mss=536
5、server端收到No8數據包回復No9確認包之后,server端這幾個變量分別為snd_una=1065017316、 snd_wnd=5050、 snd_nxt=1065017316、 rcv_nxt=189001185、 rcv_wnd=2249、 rcv_mss=2151
6、接着server端應用層把接收到的2251bytes全部讀取出來
7、server端接着發出No11報文,在應用層把緩存的TCP數據讀取出來后,此時接收緩存對應的最大接收窗口已經可以到4500bytes,但是linux在處理的時候會取rcv_mss的整數倍,因此在No11報文中可以看到Window size=4502。
補充說明:
1、Window size的選擇流程比較復雜,並不是都選為rcv_mss的整數倍,示例中因為Window scale選項為0,且滿足一些條件所以才會把Window size選擇為4302。詳細的流程可以參考代碼tcp_select_window。