TCP-滑动窗口
本文主要描述来自 : https://coolshell.cn/articles/11609.html 非原创 , 只是进行总结
问题
- 发送的segment 乱序了怎么办?
答 : 有对应的序列号(sequ)
滑动窗口的动机
需要说明一下,如果你不了解TCP的滑动窗口这个事,你等于不了解TCP协议。我们都知道,TCP必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。
所以,TCP引入了一些技术和设计来做网络流控,Sliding Window是其中一个技术。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
看一下滑动窗口的位置
TCP 的头格式
可以看到 window
的位置就是滑动窗口
滑动窗口工作过程
这是一张粗略工作图
上图中,我们可以看到:
- 接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。
- 发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。
这里可以联想到这种场景(上层应用读取buffer中的动作)就是我们之前讲的零拷贝的应用场景 ,假如没有其他机制 ,那么最原始的情况是 CPU 会到这里拷贝数据到内核内存区 ,然后再拷贝到用户内存区.
于是接收端在给发送端回ACK中会汇报自己的
AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。
这张图来自书籍<
上图中分成了四个部分,分别是:(其中那个黑模型就是滑动窗口)
-
1已收到ack确认的数据。
-
2发还没收到ack的。
-
3在窗口中还没有发出的(接收方还有空间), 也就是说这个窗口的大小是事前和对方协商出来的, 所以我才知道窗口的边界在哪。
-
4窗口以外的数据(接收方没空间)
结合图不难理解这个过程
TCP 滑动窗口管理场景
窗口收缩
图片有点模糊, 可以看到服务端和客户端原本的窗口大小都有 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
抓包如下 :
可以看到只要第一个给ack后, 后面就有个包连续发了 4个1
.
总结
文章学习了滑动窗口的工作原理和窗口管理中几种常见的场景 .