ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

TCP-滑动窗口

2022-07-23 15:01:22  阅读:173  来源: 互联网

标签:窗口 args TCP 发送 Window 滑动


本文主要描述来自 : 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.

总结

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

参考资料

标签:窗口,args,TCP,发送,Window,滑动
来源: https://www.cnblogs.com/Benjious/p/16511217.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有