TCP(六) -- 重传与确认

1,313 阅读6分钟

一:摘要概述

TCP是可靠的传输层协议,网络层采用不靠谱的IP协议导致其自身必须保证数据传输的可靠。其中最终要的就是将丢失的数据包进行重传,当数据包发送后TCP就会开启计时器,当计时器达到阈值且发送的数据包未被数据接收方确认就会重新传递丢失的数据包。当然,重传的前提就是需要确认机制。本文将详细介绍重传与确认,也会涉及到快速重传与延迟确认的概念

二:超时重传模拟

如下编辑一段packetdrill脚本,将ACK响应部分注释,当数据发送方无法获取响应时就会进行数据重传。然后使用tcpdump抓包保存文件,Wireshark进行抓包分析。脚本中就是简单经典的三次握手环节模拟以及数据传输

// 三次握手
0   socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0  setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0  bind(3, ..., ...) = 0
+0  listen(3, 1) = 0
+0  < S 0:0(0) win 4000 <mss 1000>
+0  > S. 0:0(0) ack 1 <...>
+.1 < . 1:1(0) ack 1 win 4000
+0  accept(3, ..., ...) = 4
// 数据发送
+0  write(4, ..., 1000) = 1000
// 注释掉数据接收方ACK响应
// +.1 < . 1:1(0) ack 1001 win 1000
+0 `sleep 1000000`
// 抓包命令
tcpdump -i any -nn -vv -w /home/retry.pacp port 8080

三:超时重传分析

首先关心的就是超时重传次数,这个次数由参数/pro/sys/net/ipv4/tcp_retries2控制,一般默认数值为15。使用命令查看:

[root@zsl home]# cat /proc/sys/net/ipv4/tcp_retries2
15

其次关系的应该就是重传时间,TCP超时重传使用指数避让策略,可以看Wireshark分析图中的红框部分。3-6-12-24......

在这里插入图片描述
还有一点必须要纠正,重传的次数并不会由tcp_retries2完全控制,底层函数实现仅仅只是将其作为参数参考,还会随着RTO波动动态调整

四:快速重传模拟

与超时重传一致编写packetdrill脚本,tcpdump转包,Wireshark分析

--tolerance_usecs=100000
  // 常规操作:初始化
0  socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0  < S 0:0(0) win 32792 <mss 1000,sackOK,nop,nop,nop,wscale 7>
+0  > S. 0:0(0) ack 1 <...>
+.1 < . 1:1(0) ack 1 win 257
+0 accept(3, ... , ...) = 4
// 往客户端写 5000 字节数据
+0.1 write(4, ..., 5000) = 5000
+.1 < . 1:1(0) ack 1001 win 257 <sack 1:1001,nop,nop>
 // 三次重复 ack
+0  < . 1:1(0) ack 1001 win 257 <sack 1:1001 2001:3001,nop,nop>
+0  < . 1:1(0) ack 1001 win 257 <sack 1:1001 2001:4001,nop,nop>
+0  < . 1:1(0) ack 1001 win 257 <sack 1:1001 2001:5001,nop,nop>
// 回复确认包,让服务端不再重试
+.1 < . 1:1(0) ack 5001 win 257
+0 `sleep 1000000`
// 抓包命令
tcpdump -i any -nn -vv -w /home/sack.pacp port 8080

五:快速重传分析

在这里插入图片描述
首先如图看抓包的Wireshark展示,其过程如下:

  • 1-3:三次握手
  • 4-8:5000字节数据包传输
  • 9-12:数据接收方确认
  • 13:执行快速重传

其中最主要的过程就在于9-12这四次数据接收方的确认,数据发送方发送了0-1000、1001-2001、2001-3001、3001-4001、4001-5001的五个数据包。但是确认的时候可以看到仅仅只是确认了四个数据包,打开其中的10、11、12查看详情,其中10如下图所示。ACK的序列号都是1001,但是在Options中携带了已经接收到的数据包序列号

在这里插入图片描述
其过程如下描述:

  • 9号:兄弟俺已经接收到0-1001的数据包了
  • 10号:兄弟俺接收到最大连续序列号1001的数据包,但是2001-3001也到了
  • 11号:兄弟俺接收到最大连续序列号1001的数据包,但是2001-4001也到了
  • 12号:兄弟俺接收到最大连续序列号1001的数据包,但是2001-5001也到了
    在这里插入图片描述
    图片来源于张师傅掘金小册:超时重传、快速重传与SACK,已经取得过张师傅同意,有兴趣的朋友购买小册真的物有所值。快速重传的条件就是当数据发送方接收到3个及以上相同的ACK数据包时就会判定数据包丢失,即使超时重传计时器未达到时间也会进行数据重传

六:延迟确认

正常的思路是当数据接收方接收到数据包后确认无误就需要发送ACK确认,但是实际上TCP会采用延迟确认的策略减少性能开销。当接收到数据后会稍微等待下看是否有数据包需要返回,如果有数据包返回就会顺带一起进行ACK确认,当然若等待过程中没有数据包传输最后也会单独进行ACK确认,这就是延迟确认。延迟确认有以下场景不适用:

// tcp.input.c文件源码
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
	struct tcp_sock *tp = tcp_sk(sk);

	    /* More than one full frame received... */
	if (((tp->rcv_nxt - tp->rcv_wup) > tp->ack.rcv_mss
	     /* ... and right edge of window advances far enough.
	      * (tcp_recvmsg() will send ACK otherwise). Or...
	      */
	     && __tcp_select_window(sk) >= tp->rcv_wnd) ||
	    /* We ACK each frame or... */
	    tcp_in_quickack_mode(tp) ||
	    /* We have out of order data. */
	    (ofo_possible &&
	     skb_peek(&tp->out_of_order_queue))) {
		/* Then ack it now */
		tcp_send_ack(sk);
	} else {
		/* Else, send delayed ack. */
		tcp_send_delayed_ack(sk);
	}
}
  • 如果接收到了大于一个frame 的报文,且需要调整窗口大小
  • 处于 quickack 模式(tcp_in_quickack_mode)
  • 收到乱序包(We have out of order data.)

如下使用packetdrill脚本模拟场景

--tolerance_usecs=100000
0.000 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
0.000 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
0.000 bind(3, ..., ...) = 0
0.000 listen(3, 1) = 0

0.000 < S 0:0(0) win 32792 <mss 1000, sackOK, nop, nop, nop, wscale 7>
0.000 > S. 0:0(0) ack 1 <...>

0.000 < . 1:1(0) ack 1 win 257

0.000 accept(3, ..., ...) = 4

+ 0 setsockopt(4, SOL_TCP, TCP_NODELAY, [1], 4) = 0

// 模拟往服务端写入 HTTP 头部: POST / HTTP/1.1
+0 < P. 1:11(10) ack 1 win 257

// 模拟往服务端写入 HTTP 请求 body: {"id": 1314}
+0 < P. 11:26(15) ack 1 win 257

// 往 fd 为4 的 模拟服务器返回 HTTP response {}
+ 0 write(4, ..., 100) = 100


// 第二次模拟往服务端写入 HTTP 头部: POST / HTTP/1.1
+0 < P. 26:36(10) ack 101 win 257

// 抓包看服务器返回

+0 `sleep 1000000`

因为个人的Wireshark设置的是距离上次抓包间隔时长,所以可以看到其中就复现了延迟确认的40ms

在这里插入图片描述