回环处理 - 弦外之音

/ 0评 / 0

本书 《网络协议栈入门》 采用的代码是 基于 linux 内核 4.4.4 版本的。linux 内核源码下载地址: mirrors.edge.kernel.org


在应用层网络协议里面,经常会看到 这样一个词,wraparound,回环处理。

TCP 的 sequence,RTMP 的时间戳, 等等,都需要做 handling wraparound ,回环处理。

那什么是回环处理呢?



我们知道 TCP 的 sequence number 序列号是不断根据发送数据包的长度不断 递增的,如下图:

从上图可以看出, TCP 的 sequence number 字段,只用了 4 个字节来存储,也就是说 sequence number 是一个 32位的数字,最大值 2^32 = 4294967296。

这是不是意味着 TCP 最多只能传 4294967296 字节的数据,不是的,TCP 针对这个问题做了回环处理。

TCP 的 sequence number 主要用来判断重传数据包,因为回环处理主要是为了 重传服务的,所以在这里简单讲解一下 TCP seq + ack 跟 重传的机制。

上图中 第 6 ,第 7个数据包,可以看到 服务器连续发了 两个 TCP 数据包,第6个包的 seq 是1,TCP body是 1412 字节。第7个包的 seq 是1413,TCP body是 1412 字节。

然后 第 8 个包 客户端回复了一个 ACK,第8个包的 seq 是86,ACK 等于 2825,ACK 等于 2825 这个值特别重要,他代表客户端已经拿到了 0~2825字节的数据。

这是 TCP 的优化机制,他不是每发一个 TCP 包就等一个 ACK 再发一个 TCP 包,而是在滑动窗口内,可以连续发多个TCP包,然后客户端统一确认。

从上图可以看到,客户端一个ACk 就确认了 服务器的两个TCP包都收到了,节省了开销。

就是上面这么一个情况,然后举个例子,客户端服务器已经传输了很多数据,服务器发的第1万个包 seq 等于 4294967290,TCP body是 1412 字节。

服务器发的第1万个包 seq 还差6就到达极限了,我们知道 客户端回复的 ACK 等于 seq + TCP body 的长度,也就是 4294967290+1412。

但是 4294967290+1412 已经超过 32位了,就会产生回环,客户端回复的 ACK 是 1406。 是无符号的。

服务器端维护了一个未确认的 TCP 包列表,收到相应的ACK确认 才会把TCP 包释放内存。然后服务器端收到 ACK = 1046 ,服务器怎么判断,然后知道第 1万个包已经被 客户端拿到了呢?

第1万个包的 seq 是 4294967290,4294967290 大于 1046 ,所以没法这么简单地判断。这时候就需要用到回环算法,这个算法的代码是这样的,如下:

/*
* The next routines deal with comparing 32 bit unsigned ints
* and worry about wraparound (automatic with unsigned arithmetic).
*/
#include <stdio.h>
​
static inline int before(unsigned int seq1, unsigned int seq2)
{
    return (signed int)(seq1-seq2) < 0;
}
​
int main() {
    unsigned int seq = 4294967290;
    unsigned int ack = seq + 1412 ;
    printf("ack 等于 %u \r\n",ack);
    
    if( before(seq,ack) ){
        printf("seq 对应的TCP包可以释放\n");
    }else{
        printf("seq 对应的TCP包不能释放\n");
    }
​
    return 0;
}

上面的算法,是 linux 内核的一个算法,我做了一点改善,int 是一个 4 字节的类型。然后定义了一个 before() 函数,before() 函数 的作用是这样的,

seq1 如果在 seq2 的前面,before() 就会返回 true,要不就是 false。上面代码运行结果如下:

可以看到,这个 before() 函数 确实好使。再用其他的ack 值测试一下,代码如下:

#include <stdio.h>
​
static inline int before(unsigned int seq1, unsigned int seq2)
{
    return (signed int)(seq1-seq2) < 0;
}
​
int main() {
    unsigned int seq = 4294967290;
    unsigned int ack = seq + 1412 ;
    printf("ack 等于 %u \r\n",ack);
​
    if( before(seq,ack) ){
        printf("seq 对应的TCP包可以释放\n");
    }else{
        printf("seq 对应的TCP包不能释放\n");
    }
​
    ack = 4294967293;
    printf("ack 等于 %u \r\n",ack);
    if( before(seq,ack) ){
        printf("seq 对应的TCP包可以释放\n");
    }else{
        printf("seq 对应的TCP包不能释放\n");
    }
​
    ack = 4294967285;
    printf("ack 等于 %u \r\n",ack);
    if( before(seq,ack) ){
        printf("seq 对应的TCP包可以释放\n");
    }else{
        printf("seq 对应的TCP包不能释放\n");
    }
    return 0;
}

可以看到,对于其他的值,before() 也一样好使。 那什么情况下 这个 before() 函数不好使,再来看一个例子,代码如下:

static inline int before(unsigned int seq1, unsigned int seq2)
{
    return (signed int)(seq1-seq2) < 0;
}
​
int main() {
    unsigned int seq = 2147483648 - 1 ;
    unsigned int ack = 0;
    printf("ack 等于 %u \r\n",ack);
    printf("seq 等于 %u \r\n",seq);
​
    if( before(seq,ack) ){
        printf("seq 对应的TCP包可以释放\n");
    }else{
        printf("seq 对应的TCP包不能释放\n");
    }
​
    return 0;
}

上面的 2147483648 是 2^31 计算得来的,可以看到 这种情况下 seq 就不在 ack 之前了,然后上面这种情况什么时候会出现。

举个例子,客户端服务器已经传输了很多数据,服务器发的第5千个包 seq 等于 2147483647,然后服务器继续不断发第5千零一,零二不断地发下去,一直发到 第1万个数据包,假设从第5千个包算起来,一共发了 2147483649 个字节,这期间 客户端没有发过任何 ACK 确认,然后客户端一次性确认 2147483649 个字节,然后客户端的 ACK 就等于2147483647+ 2147483647 ,等于0。

所以,上面这种情况会发生吗,不会发生。TCP 的滑动窗口机制,不会容忍发了2147483649 个字节还没收到 ACK,TCP 发几个包后如果没收到ACK ,滑动窗口就满了,没法再发数据了,所以不可能连续发 2147483649 个字节 数据。

简单理解,seq 跟ack 是在滑动窗口机制下 同步递增的,他们之间的差距不会超过 2^(32-1) - 1

再 研究一下 before() 函数的实现

static inline int before(unsigned int seq1, unsigned int seq2)
{
    return (signed int)(seq1-seq2) < 0;
}

它把一个 无符号整形,强制转成有符号的了,所以回环距离是 2^(32-1) - 1,seq1 跟 seq2 的差距只要不超过这个值就没问题。

在TCP 的滑动窗口控制下,seq1 跟 seq2 的差距不可能超过 2^(32-1) - 1,所以算法可用

相关阅读:

1,《tcp序列号回绕与解决》


由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注