标签:
TCP的逻辑是极其复杂的,其学习曲线虽然很平缓但其每一步都是异常艰难,好在这些都是体力活,只要肯花时间也就不在话下了。想彻底理解一个TCP的机制,有个四部曲:由于packetdrill是在运行时才打开一个tun(由tun.ko驱动的一个虚拟网卡设备)网卡设备,然后对其配置,为了仅仅论述TCP本身,我需要消除任何offload机制的影响,并且我不知道怎么用参数去掉offload(好像也没有!),因此我修改了packetdrill的netdev.c的代码:
/* Set the offload flags to be like a typical ethernet device */ static void set_device_offload_flags(struct local_netdev *netdev) { #ifdef linux // const u32 offload = // TUN_F_CSUM | TUN_F_TSO4 | TUN_F_TSO6 | TUN_F_TSO_ECN | TUN_F_UFO; // if (ioctl(netdev->tun_fd, TUNSETOFFLOAD, offload) != 0) // die_perror("TUNSETOFFLOAD"); #endif }
很傻比的一个修改,特此声明。
首先我给出我的第一个packetdrill脚本,注意看其中的注释。// 建立连接 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 32792 +0 accept(3, ..., ...) = 4 // 发送一个段,注意,它不会诱发拥塞窗口增加,因为初始拥塞窗口已经遵循google的建议变为10个段,此不满10个段会被限制。 // 请用tcpprobe来确认,限制窗口增加的代码请参加本脚本后面的代码。注意,在拥塞窗口从1,2或者3起步的年代,这种限制是不存在的。 +0 write(4, ..., 1000) = 1000 // 发包序列为:+0 > P. 1:1001(1000) ack 1 +.1 < . 1:1(0) ack 1001 win 32792 // 写入4个段,此目的为了触发快速重传 +0 write(4, ..., 4000) = 4000 // 我们得到3个SACK,但是请注意,FACK和标准的SACK的区别要体现了: // 标准SACK:只有等到4001-5001这一个段以及2001-4001这两个段都收被SACK了,才能触发重传 // FACK:注意到先收到的那个4001-5001 SACK,其与UNA的距离已经超过了3,不用收到2001-5001这个SACK即可触发重传 +.1 < . 1:1(0) ack 1001 win 257 <sack 4001:5001,nop,nop> +0 < . 1:1(0) ack 1001 win 257 <sack 2001:4001,nop,nop> // 一个小问题,如果SACK的序列成了以下的:+0 < . 1:1(0) ack 1001 win 257 <sack 2001:3001,nop,nop> 重传还会被触发吗? // 此时抓包的话,就会发现,重传已经发生 // 全部确认 +.1 < . 1:1(0) ack 5001 win 257
然后抓包来看重传触发的时机,如果你还不是很理解其不同,请一定要做上述的抓包确认,保证可以瞬间搞明白。关于注释里说的那个限制窗口增加的代码如下:
int tcp_is_cwnd_limited(const struct sock *sk, u32 in_flight) { const struct tcp_sock *tp = tcp_sk(sk); u32 left; if (in_flight >= tp->snd_cwnd) return 1; left = tp->snd_cwnd - in_flight; if (sk_can_gso(sk) && left * sysctl_tcp_tso_win_divisor < tp->snd_cwnd && left * tp->mss_cache < sk->sk_gso_max_size) return 1; return left <= tcp_max_burst(tp); }
这个函数会在增窗前被调用。
下面我用一个综合的复杂点的例子来作为本文接下来的内容,这个例子中不仅仅包含了快速重传,还有关于拥塞窗口的部分细节,请一定仔细分析。这个比较复杂些的例子如下:
// 建立连接 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 65535 <mss 1000,sackOK,nop,nop,nop,wscale 7> +0 > S. 0:0(0) ack 1 <...> +.1 < . 1:1(0) ack 1 win 65535 +0 accept(3, ..., ...) = 4 // 发送1个段,不会诱发拥塞窗口增加 +0 write(4, ..., 1000) = 1000 +.1 < . 1:1(0) ack 1001 win 65535 // 再发送1个段,拥塞窗口还是初始值10! +0 write(4, ..., 1000) = 1000 +.1 < . 1:1(0) ack 2001 win 65535 // ..... +0 write(4, ..., 1000) = 1000 +.1 < . 1:1(0) ack 3001 win 65535 // 不管怎么发,只要是每次发送不超过init_cwnd-reordering,拥塞窗口就不会增加,详见上述的tcp_is_cwnd_limited函数 +0 write(4, ..., 1000) = 1000 +.1 < . 1:1(0) ack 4001 win 65535 // 多发一点,结果呢?自己用tcpprobe确认吧 +0 write(4, ..., 6000) = 6000 +.1 < . 1:1(0) ack 10001 win 65535 // 好吧,我们发送10个段,可以用tcpprobe确认,在收到ACK后拥塞窗口会增加1,这正是慢启动的效果! +0 write(4, ..., 10000) = 10000 +.1 < . 1:1(0) ack 20001 win 65535 // 该步入正题了。为了触发快速重传,我们发送足够多的数据,一下子发送8个段吧,注意,此时的拥塞窗口为11! +0 write(4, ..., 8000) = 8000 // 以下为收到的SACK序列。由于我假设你已经通过上面那个简单的packetdrill脚本理解了SACK和FACK的区别,因此这里我们默认开启FACK! // sack 1的效果:确认了27001-28001,此处距离ACK字段20001为8个段,超过了reordering 3,会立即触发重传。 +.1 < . 1:1(0) ack 20001 win 257 <sack 27001:28001,nop,nop> // ----(sack 1) +0 < . 1:1(0) ack 20001 win 257 <sack 22001:23001 27001:28001,nop,nop> // ----(sack 2) +0 < . 1:1(0) ack 20001 win 257 <sack 23001:24001 22001:23001 27001:28001,nop,nop> // ----(sack 3) +0 < . 1:1(0) ack 20001 win 257 <sack 24001:25001 23001:24001 22001:23001 27001:28001,nop,nop> // ----(sack 4) // 收到了28001的ACK,注意,此时的reordering已经被更新为6了,另外,这个ACK也会尝试触发reordering的更新,但是并不成功,为什么呢?详情见下面的分析。 +.1 < . 1:1(0) ack 28001 win 65535 // 由于经历了上述的快速重传/快速恢复,拥塞窗口已经下降到了5,为了确认reordering已经更新,我们需要将拥塞窗口增加到10或者11 +0 write(4, ..., 5000) = 5000 +.1 < . 1:1(0) ack 33001 win 65535 // 由于此时拥塞窗口的值为5,我们连续写入几个等于拥塞窗口大小的数据,诱发拥塞窗口增加到10. +0 write(4, ..., 5000) = 5000 +.1 < . 1:1(0) ack 38001 win 65535 +0 write(4, ..., 5000) = 5000 +.1 < . 1:1(0) ack 43001 win 65535 +0 write(4, ..., 5000) = 5000 +.1 < . 1:1(0) ack 48001 win 65535 +0 write(4, ..., 5000) = 5000 +.1 < . 1:1(0) ack 53001 win 65535 +0 write(4, ..., 5000) = 5000 +.1 < . 1:1(0) ack 58001 win 65535 +0 write(4, ..., 5000) = 5000 +.1 < . 1:1(0) ack 63001 win 65535 // 好吧!此时重复上面发生SACK的序列,写入8个段,我们来看看同样的SACK序列还会不会诱发快速重传! +0 write(4, ..., 8000) = 8000 // 我们构造同上面sack 1/2/3/4一样的SACK序列,然而等待我们的不是重传被触发,而是... // 什么?没有触发重传?这不可能吧!你看,70001-71001这个段距离63001为8个段,而此时reordering被更新为6,8>6,依然符合触发条件啊,为什么没有触发呢? // 答案在于,在于8>6触发快速重传有个前提,那就是开启FACK,然而在reordering被更新的时候,已经禁用了FACK,此后就是要数SACK的段数而不是数最高被SACK的段值了,以下4个SACK只是选择确认了4个段,而4<6,不会触发快速重传。 +.1 < . 1:1(0) ack 63001 win 257 <sack 70001:71001,nop,nop> +0 < . 1:1(0) ack 63001 win 257 <sack 65001:66001 70001:71001,nop,nop> +0 < . 1:1(0) ack 63001 win 257 <sack 67001:68001 65001:66001 70001:71001,nop,nop> +0 < . 1:1(0) ack 63001 win 257 <sack 68001:69001 67001:68001 65001:66001 70001:71001,nop,nop> // 这里,这里到底会不会触发超时重传呢?取决于packetdrill注入下面这个ACK的时机 // 如果没有发生超时重传,下面这个ACK将会再次把reordering从6更新到8 +.1 < . 1:1(0) ack 71001 win 65535 //从这里往后,属于神的世界...
然后在我们来看抓包结果之前,允许我再次重申,如果不是为了分析协议的细节,请直接使用tcpdump的屏幕输出即可,没必要用wireshark/tshark来展现自己的高大上,我们直接看tcpdump的输出来确认细节:
很明了,如果你去仔细比Linux内核协议栈的实现,会更加清晰,那么在同一TCP连接的后面呈现相同的SACK序列时,情况就不同了,没有触发快速重传,下图分析之:
通过packetdrill构造的包序列理解TCP快速重传机制
标签:
原文地址:http://blog.csdn.net/dog250/article/details/51923079