本文主要描述來自 : https://coolshell.cn/articles/11609.html 非原創 , 只是進行總結
問題
- 發送的segment 亂序了怎麼辦?
答 : 有對應的序列號(sequ)
滑動窗口的動機
需要說明一下,如果你不瞭解TCP的滑動窗口這個事,你等於不瞭解TCP協議。我們都知道,TCP必需要解決的可靠傳輸以及包亂序(reordering)的問題,所以,TCP必需要知道網絡實際的數據處理帶寬或是數據處理速度,這樣纔不會引起網絡擁塞,導致丟包。
所以,TCP引入了一些技術和設計來做網絡流控,Sliding Window是其中一個技術。這個字段是接收端告訴發送端自己還有多少緩衝區可以接收數據。於是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。
看一下滑動窗口的位置
TCP 的頭格式
可以看到 window
的位置就是滑動窗口
滑動窗口工作過程
這是一張粗略工作圖
上圖中,我們可以看到:
- 接收端LastByteRead指向了TCP緩衝區中讀到的位置,NextByteExpected指向的地方是收到的連續包的最後一個位置,LastByteRcved指向的是收到的包的最後一個位置,我們可以看到中間有些數據還沒有到達,所以有數據空白區。
- 發送端的LastByteAcked指向了被接收端Ack過的位置(表示成功發送確認),LastByteSent表示發出去了,但還沒有收到成功確認的Ack,LastByteWritten指向的是上層應用正在寫的地方。
這裏可以聯想到這種場景(上層應用讀取buffer中的動作)就是我們之前講的零拷貝的應用場景 ,假如沒有其他機制 ,那麼最原始的情況是 CPU 會到這裏拷貝數據到內核內存區 ,然後再拷貝到用戶內存區.
於是接收端在給發送端回ACK中會彙報自己的
AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
而發送方會根據這個窗口來控制發送數據的大小,以保證接收方可以處理。
這張圖來自書籍<
上圖中分成了四個部分,分別是:(其中那個黑模型就是滑動窗口)
-
1已收到ack確認的數據。
-
2發還沒收到ack的。
-
3在窗口中還沒有發出的(接收方還有空間), 也就是說這個窗口的大小是事前和對方協商出來的, 所以我才知道窗口的邊界在哪。
-
4窗口以外的數據(接收方沒空間)
結合圖不難理解這個過程
TCP 滑動窗口管理場景
窗口收縮
圖片有點模糊, 可以看到服務端和客戶端原本的窗口大小都有 360 bytes, 當客戶端第一次發送 140 個 bytes 後 , 服務端 360-140=220 ,此時應該返回 220 bytes大小的窗口 ,但是由於內存不足需要回收內存 buffer 的原因 ,服務端返回了 100 bytes 大小的窗口 ,可是由於交換髮送數據的原因 ,此時的客戶端在服務端返回100bytes 窗口前 ,又發送了 180個字節過去 ,這下就尷尬了, 因爲 100 < 180 , 服務端buffer 不足以接受客戶端的數據, 只好選擇丟棄 ,而客戶端由於收到了窗口改爲 100 bytes 的報文, 自身的窗口已經擴大到 180 的那部分(100-180這部分)就會被丟棄掉 .
爲解決這個問題 , TCP 給滑動窗口機制加了一條簡單的規則 : 一個設備不允許收縮窗口大小 .
關閉窗口 --- Zero Window (來自酷殼, 非原創 )
上圖,我們可以看到一個處理緩慢的Server(接收端)是怎麼把Client(發送端)的TCP Sliding Window給降成0的。此時,你一定會問,如果Window變成0了,TCP會怎麼樣?是不是發送端就不發數據了?是的,發送端就不發數據了,你可以想像成“Window Closed”,那你一定還會問,如果發送端不發數據了,接收方一會兒Window size 可用了,怎麼通知發送端呢?
解決這個問題,TCP使用了Zero Window Probe技術,縮寫爲ZWP,也就是說,發送端在窗口變成0後,會發ZWP的包給接收方,讓接收方來ack他的Window尺寸,一般這個值會設置成3次,第次大約30-60秒(不同的實現可能會不一樣)。如果3次過後還是0的話,有的TCP實現就會發RST把鏈接斷了。
注意:只要有等待的地方都可能出現DDoS攻擊,Zero Window也不例外,一些攻擊者會在和HTTP建好鏈發完GET請求後,就把Window設置爲0,然後服務端就只能等待進行ZWP,於是攻擊者會併發大量的這樣的請求,把服務器端的資源耗盡。(關於這方面的攻擊,大家可以移步看一下Wikipedia的SockStress詞條)
另外,Wireshark中,你可以使用tcp.analysis.zero_window來過濾包,然後使用右鍵菜單裏的follow TCP stream,你可以看到ZeroWindowProbe及ZeroWindowProbeAck的包。
Silly Window Syndrome
Silly Window Syndrome翻譯成中文就是“糊塗窗口綜合症”。假如一種情況服務端的處理速度跟不上客戶端發送的速度, 很快窗口大小的就會變成 0 , 當服務端處理 1 byte後, 窗口
返回給客戶端 1byte 窗口 ,然後客戶端繼續發送 1byte 大小的數據過來
你需要知道網絡上有個MTU,對於以太網來說,MTU是1500字節,除去TCP+IP頭的40個字節,真正的數據傳輸可以有1460,這就是所謂的MSS(Max Segment Size)注意,TCP的RFC定義這個MSS的默認值是536,這是因爲 RFC 791裏說了任何一個IP設備都得最少接收576尺寸的大小(實際上來說576是撥號的網絡的MTU,而576減去IP頭的20個字節就是536)。
如果你的網絡包可以塞滿MTU,那麼你可以用滿整個帶寬,如果不能,那麼你就會浪費帶寬。(大於MTU的包有兩種結局,一種是直接被丟了,另一種是會被重新分塊打包發送) 你可以想像成一個MTU就相當於一個飛機的最多可以裝的人,如果這飛機裏滿載的話,帶寬最高,如果一個飛機只運一個人的話,無疑成本增加了,也而相當二。
這傳輸效率太低了, 並且很浪費帶寬. 爲了解決這個問題, 可以從兩方面入手 :
- 對於接收端 (接收端處理不過來了) , 收到的數據導致window size小於某個值,可以直接ack(0)回sender,這樣就把window給關閉了,也阻止了sender再發數據過來,等到receiver端處理了一些數據後windows size 大於等於了MSS,或者,receiver buffer有一半爲空,就可以把window打開讓send 發送數據過來。
- 對於發送端Sender引起的,那麼就會使用著名的 Nagle’s algorithm。這個算法的思路也是延時處理,他有兩個主要的條件:
- 1)要等到 Window Size>=MSS 或是 Data Size >=MSS
- 2)收到之前發送數據的ack回包,他纔會發數據,否則就是在攢數據。
言外之意就是把小包攢成大包再發出.
其中NagleAlg 算法是可以交由客戶端設置的 , 我寫了一個 java 程序 ,使用的 TcpNoDelay 標識
客戶端
public class TimeClient {
/**
* @param args
*/
public static void main(String[] args) {
int port = 8765;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 採用默認值
}
}
Socket socket = null;
BufferedReader in = null;
PrintWriter out = null;
try {
socket = new Socket("192.168.1.101", port);
// 這一句是 NagleAlg 算法
// socket.setTcpNoDelay(false);
in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);
for (int i=0; i<5; i++) {
out.println("1");
System.out.println("發送字符成功!");
}
String resp = in.readLine();
System.out.println("獲取響應 : " + resp);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
out = null;
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
in = null;
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
socket = null;
}
}
}
}
服務端
public class TimeServer {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
int port = 8765;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 採用默認值
}
}
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("服務器啓動在端口 : " + port);
Socket socket = null;
while (true) {
socket = server.accept();
new Thread(new TimeServerHandler(socket)).start();
}
} finally {
if (server != null) {
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
然後通過 wireshark 抓本地包 , wireshark 如何抓本地包參見 : https://blog.csdn.net/qq_31362767/article/details/100849246
抓包如下 :
可以看到只要第一個給ack後, 後面就有個包連續發了 4個1
.
總結
文章學習了滑動窗口的工作原理和窗口管理中幾種常見的場景 .