TCP-滑動窗口

本文主要描述來自 : https://coolshell.cn/articles/11609.html 非原創 , 只是進行總結

問題

  • 發送的segment 亂序了怎麼辦?
    答 : 有對應的序列號(sequ)

滑動窗口的動機

    需要說明一下,如果你不瞭解TCP的滑動窗口這個事,你等於不瞭解TCP協議。我們都知道,TCP必需要解決的可靠傳輸以及包亂序(reordering)的問題,所以,TCP必需要知道網絡實際的數據處理帶寬或是數據處理速度,這樣纔不會引起網絡擁塞,導致丟包。

    所以,TCP引入了一些技術和設計來做網絡流控,Sliding Window是其中一個技術。這個字段是接收端告訴發送端自己還有多少緩衝區可以接收數據。於是發送端就可以根據這個接收端的處理能力來發送數據,而不會導致接收端處理不過來。

看一下滑動窗口的位置

TCP 的頭格式

img

可以看到 window 的位置就是滑動窗口

滑動窗口工作過程

這是一張粗略工作圖
img

上圖中,我們可以看到:

  • 接收端LastByteRead指向了TCP緩衝區中讀到的位置,NextByteExpected指向的地方是收到的連續包的最後一個位置,LastByteRcved指向的是收到的包的最後一個位置,我們可以看到中間有些數據還沒有到達,所以有數據空白區。
  • 發送端的LastByteAcked指向了被接收端Ack過的位置(表示成功發送確認),LastByteSent表示發出去了,但還沒有收到成功確認的Ack,LastByteWritten指向的是上層應用正在寫的地方。
    這裏可以聯想到這種場景(上層應用讀取buffer中的動作)就是我們之前講的零拷貝的應用場景 ,假如沒有其他機制 ,那麼最原始的情況是 CPU 會到這裏拷貝數據到內核內存區 ,然後再拷貝到用戶內存區.

於是接收端在給發送端回ACK中會彙報自己的

AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;

而發送方會根據這個窗口來控制發送數據的大小,以保證接收方可以處理。

img

這張圖來自書籍<> , 需要解釋就是窗口中的 , 數字表示的 segment 對應的字節 ,例如圖中的第32號, 第33號, 第34號對應一個 segment , 也就是當 segment 收到 ack 的時候 ,滑動窗口向右滑動, 32,33,34 號三個就被移出滑動窗口外

上圖中分成了四個部分,分別是:(其中那個黑模型就是滑動窗口)

  • 1已收到ack確認的數據。

  • 2發還沒收到ack的。

  • 3在窗口中還沒有發出的(接收方還有空間), 也就是說這個窗口的大小是事前和對方協商出來的, 所以我才知道窗口的邊界在哪。

  • 4窗口以外的數據(接收方沒空間)

img

結合圖不難理解這個過程

TCP 滑動窗口管理場景

窗口收縮

img

圖片有點模糊, 可以看到服務端和客戶端原本的窗口大小都有 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
抓包如下 :

img

可以看到只要第一個給ack後, 後面就有個包連續發了 4個1.

總結

文章學習了滑動窗口的工作原理和窗口管理中幾種常見的場景 .

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章