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.

总结

文章学习了滑动窗口的工作原理和窗口管理中几种常见的场景 .

参考资料

posted @ 2022-07-23 14:51  float123  阅读(662)  评论(0编辑  收藏  举报