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个字节
100
500
1000
1300
100+1300
500+1300
1000+1300
stack
使用默认发送缓冲区沾包
禁用nagle连续发包