其他分享
首页 > 其他分享> > TCP-滑动窗口

TCP-滑动窗口

作者:互联网

本文主要描述来自 : https://coolshell.cn/articles/11609.html 非原创 , 只是进行总结

问题

滑动窗口的动机

    需要说明一下,如果你不了解TCP的滑动窗口这个事,你等于不了解TCP协议。我们都知道,TCP必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。

    所以,TCP引入了一些技术和设计来做网络流控,Sliding Window是其中一个技术。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

看一下滑动窗口的位置

TCP 的头格式

img

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

滑动窗口工作过程

这是一张粗略工作图
img

上图中,我们可以看到:

于是接收端在给发送端回ACK中会汇报自己的

AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;

而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。

img

这张图来自书籍<> , 需要解释就是窗口中的 , 数字表示的 segment 对应的字节 ,例如图中的第32号, 第33号, 第34号对应一个 segment , 也就是当 segment 收到 ack 的时候 ,滑动窗口向右滑动, 32,33,34 号三个就被移出滑动窗口外

上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口)

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就相当于一个飞机的最多可以装的人,如果这飞机里满载的话,带宽最高,如果一个飞机只运一个人的话,无疑成本增加了,也而相当二。

这传输效率太低了, 并且很浪费带宽. 为了解决这个问题, 可以从两方面入手 :

其中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