11读缓冲区(滑动窗口)耗尽与write阻塞、拆包、延迟(一)

本文验证window耗尽(读缓冲区、滑动窗口)

server:

package com.jds.test.bio.p6;

/**
 *
 * https://www.cnblogs.com/silyvin/articles/12019528.html
 * Created by joyce on 2019/11/26.
 */
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static final int PORT = 12123;
    public static final int BUFFER_SIZE = Client.BUFFER_SIZE;

    private static String flag = null;

    public static void main(String [] args) throws IOException, InterruptedException {

        /**
         * null 服务端不读任何字节
         * 1 sleep 12s 开始读
         */
        if (args != null && args.length > 0) {
            flag = args[0];
        }


        new Server().server();
    }

    //服务端代码
    public void server() throws IOException, InterruptedException{

     //   ServerSocket ss = new ServerSocket(PORT);
        ServerSocket ss = new ServerSocket();
        System.out.println(ss.getReceiveBufferSize());
        ss.setReceiveBufferSize(100);
        InetAddress bindAddr = null;
        ss.bind(new InetSocketAddress(bindAddr, PORT), 50);

        while(true) {
            Socket s = ss.accept();
            System.out.println(s.getRemoteSocketAddress().toString());

            // then do nothing, do not copy bytes from kernel
            if("12".equals(flag)) {
                Thread.sleep(12000);

                InputStream inputStream = s.getInputStream();
                byte [] bytes = new byte[BUFFER_SIZE];
                int i=0;
                while (inputStream.read(bytes) != -1) {
                    System.out.println(i++);
                }
            }
        }
    }
}

 

client:

package com.jds.test.bio.p6;

/**
 *
 * https://www.cnblogs.com/silyvin/articles/12019528.html
 * https://www.cnblogs.com/silyvin/articles/12037917.html
 * Created by joyce on 2019/11/26.
 */


import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;


public class Client {

    public static final int PORT = 12123;
    public static final int BUFFER_SIZE = 100; // 100 500 1000 1300

    public static void main(String []f) throws IOException {
        new Client().client();
    }

    //客户端代码
    public void client() throws UnknownHostException, IOException{

        final String s1 = "49.235.75.155";// tx
        final String s2 = "localhost";
        final String s3 = "39.100.99.222";// al


    //    Socket s = new Socket(s1,PORT);//创建socket连接
        Socket s = new Socket();
        System.out.println(s.getReceiveBufferSize());
        System.out.println(s.getSendBufferSize());
        s.setReceiveBufferSize(100);

        /**
         * 目的
         * 1 使其有包就发,避免在nagle作用下发送端沾包,若没有这句使用默认并启用nagle,则沾包
         * 而且不会阻塞,100个write会成功返回,虽然只发出去1152个字节
         * 2 与BUFFER_SIZE相同,暂时不考虑发送缓冲区拆包
         */
        s.setSendBufferSize(BUFFER_SIZE);

        /**
         * 禁用nagle,可连续发包
         */
     //   s.setTcpNoDelay(true);
        s.connect(new InetSocketAddress(s1, PORT));

        if(BUFFER_SIZE == 1300) {
            s.getOutputStream().write(new byte[500]);   // 100 500 1000
        }

        for(int i=0; i<100; ++i) {
            s.getOutputStream().write(new byte[BUFFER_SIZE]);
            System.out.println("send + " + i);
        }

    }
}

 

这里以 0+100 模式的日志为例:

server输出

43690

/183.195.35.246:21199

 

client输出

131072

131072

send + 0

send + 1

send + 2

send + 3

send + 4

send + 5

send + 6

send + 7

send + 8

send + 9

send + 10

send + 11

然后阻塞

 

jstack显示,阻塞在 s.getOutputStream().write上

 

抓包:

 

 

结论:

1 本文通过抓包+jstack实践验证了滑动窗口耗尽后对端的write阻塞,顺便看了一下滑动窗口<包大小时的拆包及延迟

可以看到,最初报文2 server端winsize 1152,与10tcp缓冲区大小设置 第1点第(2)小节同,经过12次write,11次发送出包,每次100字节,第11次发包的ack,25号报文,显示只有window52,第12次写入,100字节拷贝到发送缓冲区,发送缓冲区由于不能发送,维持在满的状态,第13次write,由于发送缓冲区满而阻塞

这证明了——导致write阻塞不是直接因为对端滑动窗口不够了,而是因为对端窗口导致本方发送缓冲区积压至满

 

2 阻塞时(更准确的说对端接收窗口不足),仍然有个包从内核发送缓冲区发出去了,26号len=52,可见这个地方由于滑动窗口是有拆包的,server端回复ZeroWindow,

3 但是请注意,这个包卡了5s(未在图中标红),经过试验,发现

初始发包/循环发包、同时为发送缓冲区大小/滑动窗口<包大小时的大小/包以滑动窗口截取第一段报文(==滑动窗口size)延迟

0  100  剩52  延迟5s

0  500  剩152  延迟5s

0  1000  剩152  延迟5s

0  1300  剩1152  不延迟

100  1300  剩1052  不延迟

500  1300  剩652  不延迟

1000  1300  剩152  延迟5s

根据数据,猜想当滑动窗口剩余h<包大小b时

1)h<h0(可能介于152与652之间)时,依据h拆包后的第一个大小为h的包延迟5s发送,即使能发送并塞满对端的接收区

2)h>h0时,会直接发送塞满对端的接收区

流程可能是这样的

客户端:卧槽win只有52,<h0,不管我的包有多大,先等等

5s后

客户端:卧槽服务端还没读完给我反应(Tcp Window Update 读缓冲区(滑动窗口)耗尽与write阻塞、拆包(二)中出现),不管了,先发52个字节,剩余的我留着

5s后

客户端:我问下他

服务端:0

客户端:额

。。。。。。

 

客户端:卧槽win只有652, >h0,还可以,先拆包发一个,发完了再等反应呗

 5s后

 客户端:我问下他

 服务端:0

客户端:额

。。。。。。

 

500+1300的配图:

 

4 之后客户端每5s一次询问服务端是否有windowsize,包len=1,服务端返回win=0(未在图中标红)

5 同时可以看到server从第2次(报文7)开始,开启delayed ack

6 同时我们验证了即使接收端不read(实际是不从kernel copy到用户态),也会返回ack

“当服务端网卡收到一个报文后,网卡驱动调用DMA engine将数据包通过ringbuffer拷贝到内核缓冲区中,拷贝成功后,发起中断通知中断处理程序,这时候ip层会处理该数据包,之后交给tcp层,最终到达tcp层的recv buffer(接收队列),这时候就会返回ack给客户端,并没有等到客户端调用read将数据从内核拷贝到用户空间 ”https://www.cnblogs.com/pigpdong/p/10899800.html《网络内核之TCP是如何发送和接收消息的》

7 本例还使用jstack定位了线程阻塞位置

8 client写入缓冲区100,每次发送包也是100,本文始终让发送缓冲区==每次发包大小

1)只考察window窗口导致的拆包和阻塞,暂不考虑写入缓冲区,写入缓冲区拆包-大于缓冲区小于mss的包是如何拆包的

2)这带来另一个好处,即使不禁用nagle,也未沾包,能够使我们方便的分析数据,而且是一来一回的数据

由于发送缓冲区==包大小,发送端只能放一个包,即使在等ack的期间,也没跟后面的包搞在一起,因为它们进不来,而且它们会write阻塞

write第1个包——第1个包塞满系统发送缓冲区——发送第1个包

write第2个包——第2个包塞满系统发送缓冲区——缓存等待,由于nagle等待第1个包的ack

write第3个包——由于发送缓冲区满了,所以write阻塞,而且也导致包2不能跟包3搞在一起

收到第1个包ack——发送缓冲区第2个包发送——write唤醒

3)当把写入缓冲区使用默认时(启用nagle),随即发生发送端沾包

另外可以看到在452下,也发生了5s延迟,那么这个延迟的阀值很可能是500字节

请注意,这个过程write是没有阻塞的,100个包write成功返回,由于对端滑动窗口1152只能最多接收这些,故有大量的包虽然经过write,但还在内核发送缓冲区中

这也证明了

——write只是将数据从用户态拷贝到了内核态,不保证能立即发送(报文7 452个字节延迟5s发送),甚至能否发送(超出1152长度的其它包)

也对第1点“导致write阻塞不是直接因为对端滑动窗口不够了,而是因为对端窗口导致本方发送缓冲区积压至满”是一个补充,只要内核发送缓冲区够大,write就能返回,不会由于对端不接收数据而直接阻塞

 

4)保持发送缓冲区默认大小,当关闭nagle后,客户端分开、且连续发包

 

可以看到,虽然禁用nagle,可以不用等ack而连续发包,但是也会受滑动窗口制约,在5中,对方告诉了客户端你最多发1052个字节

 

窗口耗尽阻塞对端写.zip

100

500

1000

1300

100+1300

500+1300

1000+1300

stack

 

窗口耗尽阻塞对端写.zip2.zip

使用默认发送缓冲区沾包

禁用nagle连续发包

 

posted on 2019-12-10 22:08  silyvin  阅读(795)  评论(0编辑  收藏  举报