深入理解TCP协议及其源代码-拥塞控制算法分析

这是我的第五篇博客,鉴于前面已经有很多人对前四个题目如三次握手等做了很透彻的分析,本博客将对拥塞控制算法做一个介绍。

首先我会简要介绍下TCP协议,其次给出拥塞控制介绍和源代码分析,最后结合源代码具体分析拥塞控制算法。

一、TCP协议

关于TCP协议,其实在我的第二篇博客中:https://www.cnblogs.com/xiaofengustc/p/12012638.html 已有简要的介绍,并且在该博客中我还拿TCP协议与HTTP协议、UDP协议做了相关对比。有兴趣的同学可以参见我的第二篇博客做进一步的了解。

网上关于TCP协议的内容介绍很多:https://www.jianshu.com/p/e916bfb27daa 这篇文章介绍的极其详细,总结一些必备的基础知识如下:

1.TCP协议产生背景:互联网络与单个网络有很大的不同,因为互联网络的不同部分可能有截然不同的拓扑结构、带宽、延迟、数据包大小和其他参数,且不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。

2.TCP是能够动态地适应互联网络的这些特性,而且具备面对各种故障时的健壮性,且能够在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。

3.TCP作用原理过程:
应用层向TCP层发送用于网间传输的、用8位字节表示的数据流,然后TCP把数据流分区成适当长度的报文段(通常受该计算机连接的网络的数据链路层的最大传输单元(MTU)的限制)。之后TCP把结果包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。TCP为了保证不发生丢包,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的包发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据包就被假设为已丢失将会被进行重传。TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。

4.TCP协议作用过程的7个要点:数据分片、到达确认、超时重发、滑动窗口、失序处理、重复处理、数据校验(具体可参见百度百科对TCP的解释)

5.TCP首部格式图:

(图片引用自:https://blog.51cto.com/11418774/1835048)

几个重要参数解释如下:

紧急 URG —— 当 URG =1 时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据)。

确认 ACK —— 只有当 ACK = 1 时确认号字段才有效。当 ACK =0 时,确认号无效。

推送 PSH (PuSH) —— 接收 TCP 收到 PSH = 1 的报文段,就尽快地交付接收应用进程,而不再等到整个缓存都填满了后再向上交付。

复位 RST (ReSeT) —— 当 RST =1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。

同步 SYN —— 同步 SYN = 1 表示这是一个连接请求或连接接受报文。

终止 FIN (FINis) —— 用来释放一个连接。FIN =1 表明此报文段的发送端的数据已发送完毕,并要求释放运输连接。

6.TCP工作方式:采用三次握手来建立连接,四次挥手来释放连接,这部分的内容网上资料极其详细,感兴趣的小伙伴可以参见百度百科TCP的内容

如下是TCP连接与终止的过程图:
                               

二、拥塞控制介绍和源代码分析

首先在控制拥塞之前,我们需要了解拥塞指代的为何物。

百度百科解释如下:拥塞现象是指到达通信子网中某一部分的分组数量过多,使得该部分网络来不及处理,以致引起这部分乃至整个网络性能下降的现象,严重时甚至会导致网络通信业务陷入停顿,即出现死锁现象。这种现象跟公路网中经常所见的交通拥挤一样,当节假日公路网中车辆大量增加时,各种走向的车流相互干扰,使每辆车到达目的地的时间都相对增加(即延迟增加),甚至有时在某段公路上车辆因堵塞而无法开动(即发生局部死锁)。

造成拥塞的原因由如下两点:

1.多条流入线路有分组到达,并需要同一输出线路,此时,如果路由器没有足够的内存来存放所有这些分组,那么有的分组就会丢失。

2.路由器的慢带处理器的缘故,以至于难以完成必要的处理工作,如缓冲区排队、更新路由表等。

在不同的层处理拥塞有不同的方法,具体如下:

1.在传输层可采用:重传策略、乱序缓存策略、确认策略、流控制策略和确定超时策略。

2.在网络层可采用:子网内部的虚电路与数据报策略、分组排队和服务策略、分组丢弃策略、路由算法和分组生存管理。

3.在数据链路层可采用:重传策略、乱序缓存策略、确认策略和流控制策略。

本博客重点介绍的是传输层下TCP协议的拥塞控制手段

TCP传统的拥塞控制AIMD算法的四个部分如下:

(1)慢启动

每当建立一个TCP连接时或一个TCP连接发生超时重传后,该连接便进入慢启动阶段。进入慢启动后,TCP实体将拥塞窗口的大小初始化为一个报文段,即:cwnd=1。此后,每收到一个报文段的确认(ACK),cwnd值加1,即拥塞窗口按指数增加。当cwnd值超过慢启动阐值(sshterhs)或发生报文段丢失重传时,慢启动阶段结束。前者进入拥塞避免阶段,后者重新进入慢启动阶段。

(2)拥塞避免

在慢启阶段,当cwnd值超过慢启动阐值(ssthresh)后,慢启动过程结束,TCP连接进入拥塞避免阶段。在拥塞避免阶段,每一次发送的cwnd个报文段被完全确认后,才将cwnd值加1。在此阶段,cwnd值线性增加。

(3)快速重传

快速重传是对超时重传的改进。当源端收到对同一个报文的三个重复确认时,就确定一个报文段已经丢失,因此立刻重传丢失的报文段,而不必等到重传定时器(RTO)超时。以此减少不必要的等待时间。

(4)快速恢复

快速恢复是对丢失恢复机制的改进。在快速重传之后,不经过慢启动过程而直接进入拥塞避免阶段。每当快速重传后,置sshtesrh=cwnd/2、ewnd=ssthresh+3。此后,每收到一个重复确认,将cwnd值加1,直至收到对丢失报文段和其后若干报文段的累积确认后,置cwnd=ssthesrh,进入拥塞避免阶段。

AIMD传统算法现在已经很少使用了,故不再贴出对它的源代码分析

下面分析目前应用最广泛且较为成熟的Reno算法,该算法所包含的慢启动、拥塞避免和快速重传、快速恢复机制,是现有的众多算法的基础。

源代码位于内核linux5.0.1/net/ipv4/tcp_cong.c中,贴出部分代码分析如下:

/*
 * TCP Reno congestion control
 * This is special case used for fallback as well.
 */
/* This is Jacobson‘s slow start and congestion avoidance.
 * SIGCOMM ‘88, p. 328.
 */
void tcp_reno_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (!tcp_is_cwnd_limited(sk))
        return;

    /* In "safe" area, increase. */
    if (tcp_in_slow_start(tp)) {
        acked = tcp_slow_start(tp, acked);
        if (!acked)
            return;
    }
    /* In dangerous area, increase slowly. */
    tcp_cong_avoid_ai(tp, tp->snd_cwnd, acked);
}
EXPORT_SYMBOL_GPL(tcp_reno_cong_avoid);

/* Slow start threshold is half the congestion window (min 2) */
u32 tcp_reno_ssthresh(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);

    return max(tp->snd_cwnd >> 1U, 2U);
}
EXPORT_SYMBOL_GPL(tcp_reno_ssthresh);

u32 tcp_reno_undo_cwnd(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);

    return max(tp->snd_cwnd, tp->prior_cwnd);
}
EXPORT_SYMBOL_GPL(tcp_reno_undo_cwnd);

struct tcp_congestion_ops tcp_reno = {
    .flags        = TCP_CONG_NON_RESTRICTED,
    .name        = "reno",
    .owner        = THIS_MODULE,
    .ssthresh    = tcp_reno_ssthresh,
    .cong_avoid    = tcp_reno_cong_avoid,
    .undo_cwnd    = tcp_reno_undo_cwnd,
};

从Reno运行机制中很容易看出,为了维持一个动态平衡,必须周期性地产生一定量的丢失,再加上AIMD机制--减少快,增长慢,尤其是在大窗口环境下,由于一个数据报的丢失所带来的窗口缩小要花费很长的时间来恢复,这样,带宽利用率不可能很高且随着网络的链路带宽不断提升,这种弊端将越来越明显。公平性方面,根据统计数据,Reno的公平性还是得到了相当的肯定,它能够在较大的网络范围内理想地维持公平性原则。

Reno算法以其简单、有效和鲁棒性成为主流,被广泛的采用。

但是它不能有效的处理多个分组从同一个数据窗口丢失的情况。这一问题在New Reno算法中得到解决。

近几年来,随着高带宽延时网络(High Bandwidth-Delay product network)的普及,针对提高TCP带宽利用率这一点上,又涌现出许多新的基于丢包反馈的TCP协议改进,这其中包括HSTCP、STCP、BIC-TCP、CUBIC和H-TCP。

总的来说,基于丢包反馈的协议是一种被动式的拥塞控制机制,其依据网络中的丢包事件来做网络拥塞判断。即便网络中的负载很高时,只要没有产生拥塞丢包,协议就不会主动降低自己的发送速度。这种协议可以最大程度的利用网络剩余带宽,提高吞吐量。然而,由于基于丢包反馈协议在网络近饱和状态下所表现出来的侵略性,一方面大大提高了网络的带宽利用率;但另一方面,对于基于丢包反馈的拥塞控制协议来说,大大提高网络利用率同时意味着下一次拥塞丢包事件为期不远了,所以这些协议在提高网络带宽利用率的同时也间接加大了网络的丢包率,造成整个网络的抖动性加剧。

下面给出BIC-TCP源代码分析如下:位于linux5.0.1/net/ipv4/tcp_bic.c中

/* BIC TCP Parameters */
struct bictcp {
    u32    cnt;        /* increase cwnd by 1 after ACKs */
    u32    last_max_cwnd;    /* last maximum snd_cwnd */
    u32    last_cwnd;    /* the last snd_cwnd */
    u32    last_time;    /* time when updated last_cwnd */
    u32    epoch_start;    /* beginning of an epoch */
#define ACK_RATIO_SHIFT    4
    u32    delayed_ack;    /* estimate the ratio of Packets/ACKs << 4 */
};

static inline void bictcp_reset(struct bictcp *ca)
{
    ca->cnt = 0;
    ca->last_max_cwnd = 0;
    ca->last_cwnd = 0;
    ca->last_time = 0;
    ca->epoch_start = 0;
    ca->delayed_ack = 2 << ACK_RATIO_SHIFT;
}

static void bictcp_init(struct sock *sk)
{
    struct bictcp *ca = inet_csk_ca(sk);

    bictcp_reset(ca);

    if (initial_ssthresh)
        tcp_sk(sk)->snd_ssthresh = initial_ssthresh;
}

/*
 * Compute congestion window to use.
 */
static inline void bictcp_update(struct bictcp *ca, u32 cwnd)
{
    if (ca->last_cwnd == cwnd &&
        (s32)(tcp_jiffies32 - ca->last_time) <= HZ / 32)
        return;

    ca->last_cwnd = cwnd;
    ca->last_time = tcp_jiffies32;

    if (ca->epoch_start == 0) /* record the beginning of an epoch */
        ca->epoch_start = tcp_jiffies32;

    /* start off normal */
    if (cwnd <= low_window) {
        ca->cnt = cwnd;
        return;
    }

    /* binary increase */
    if (cwnd < ca->last_max_cwnd) {
        __u32    dist = (ca->last_max_cwnd - cwnd)
            / BICTCP_B;

        if (dist > max_increment)
            /* linear increase */
            ca->cnt = cwnd / max_increment;
        else if (dist <= 1U)
            /* binary search increase */
            ca->cnt = (cwnd * smooth_part) / BICTCP_B;
        else
            /* binary search increase */
            ca->cnt = cwnd / dist;
    } else {
        /* slow start AMD linear increase */
        if (cwnd < ca->last_max_cwnd + BICTCP_B)
            /* slow start */
            ca->cnt = (cwnd * smooth_part) / BICTCP_B;
        else if (cwnd < ca->last_max_cwnd + max_increment*(BICTCP_B-1))
            /* slow start */
            ca->cnt = (cwnd * (BICTCP_B-1))
                / (cwnd - ca->last_max_cwnd);
        else
            /* linear increase */
            ca->cnt = cwnd / max_increment;
    }

    /* if in slow start or link utilization is very low */
    if (ca->last_max_cwnd == 0) {
        if (ca->cnt > 20) /* increase cwnd 5% per RTT */
            ca->cnt = 20;
    }

    ca->cnt = (ca->cnt << ACK_RATIO_SHIFT) / ca->delayed_ack;
    if (ca->cnt == 0)            /* cannot be zero */
        ca->cnt = 1;
}

static void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct bictcp *ca = inet_csk_ca(sk);

    if (!tcp_is_cwnd_limited(sk))
        return;

    if (tcp_in_slow_start(tp))
        tcp_slow_start(tp, acked);
    else {
        bictcp_update(ca, tp->snd_cwnd);
        tcp_cong_avoid_ai(tp, ca->cnt, 1);
    }
}

可以看出其在大大提高了自身吞吐率的同时,也严重影响了Reno流的吞吐率。基于丢包反馈的协议产生如此低劣的TCP友好性的组要原因在于这些协议算法本身的侵略性拥塞窗口管理机制,这些协议通常认为网络只要没有产生丢包就一定存在多余的带宽,从而不断提高自己的发送速率。其发送速率从时间的宏观角度上来看呈现出一种凹形的发展趋势,越接近网络带宽的峰值发送速率增长得越快。这不仅带来了大量拥塞丢包,同时也恶意吞并了网络中其它共存流的带宽资源,造成整个网络的公平性下降。

三、拥塞控制实验演示

可利用Wireshark 记录若干TCP 短流(少于5 秒,如访问web 页面,收发邮件等)和TCP长流(长于1 分钟,如FTP 下载大文件,用HTTP 观看在线视频等)。

对于每个TCP 流,可画出其congestion window 随时间的变化曲线,指出拥塞控制的慢启动、拥塞避免、快恢复等阶段。

详情可参见:https://blog.csdn.net/justice0/article/details/73697213

总结:选题之前没有意识到拥塞控制在内核中的实现实在太难了,网上对源代码的分析很少很少,本博客介绍的特别肤浅,后续还需要加强对拥塞控制的理解。

原文地址:https://www.cnblogs.com/xiaofengustc/p/12104231.html

时间: 2024-10-10 05:47:22

深入理解TCP协议及其源代码-拥塞控制算法分析的相关文章

深入理解TCP协议及其源代码

深入理解TCP协议及其源代码 前言 在前面实验我们分别实现了Socket 通信工具,探讨了Socket API.Socket 调用原理等.但是还没有针对某一实例进行讲解,在本实验我们将针对TCP协议进行详细分析,期待在Linux内核进行分析TCP原理. 1.Tcp基本原理 TCP是一种面向连接.可靠.基于字节流的传输协议,位于TCP/IP模型的传输层. 面向连接:不同于UDP,TCP协议需要通信双方确定彼此已经建立连接后才可以进行数据传输: 可靠:连接建立的双方在进行通信时,TCP保证了不会存在

深入理解TCP协议及其源代码——connect及bind、listen、accept背后的“三次握手”

一.TCP简介 TCP(Transmission Control Protocol,传输控制协议)是一个传输层(Transport Layer)协议,它在TCP/IP协议族中的位置如图1所示.它是专门为了在不可靠的互联网络上提供一个面向连接的且可靠的端到端(进程到进程)字节流而设计的.互联网络与单个网络不同,因为互联网络的不同部分可能有截然不同的拓扑.带宽.延迟.分组大小和其他参数.TCP的设计目标是能够动态地适应互联网络的这些特性,而且当面对多种失败的时候仍然足够健壮. 图1 TCP在TCP/

深入理解TCP协议及其源代码-send和recv背后数据的收发过程

send和recv背后数据的收发过程 send和recv是TCP常用的发送数据和接受数据函数,这两个函数具体在linux内核的代码实现上是如何实现的呢? ssize_t recv(int sockfd, void buf, size_t len, int flags) ssize_t send(int sockfd, const void buf, size_t len, int flags) 理论分析 对于send函数,比较容易理解,捋一下计算机网络的知识,可以大概的到实现的方法,首先TCP是

深入理解TCP协议及其源代码——三次握手

Wireshark分析报文 对TCP三次握手过程进行抓包分析,并通过Wireshark的Analyze分析出tcp握手过程,通过截图体现传输内容. 1.捕获大量的由本地主机到远程服务器的TCP分组: 2.浏览追踪信息 在显示筛选规则编辑框中输入“tcp”,可以看到在本地主机和服务器之间传输的一系列tcp和HTTP消息,你应该能看到包含SYN Segment的三次握手.通过Analyze的Follow TCP Stream分析出传输内容.写出其中某TCP数据包的源IP地址,目的IP地址,源端口,目

深入理解TCP协议及其源代码——send和recv背后数据的收发过程

TCP数据发送和接收的原理 TCP连接的建立过程 TCP Socket的连接的过程是服务端先通过socket()函数创建一个socket对象,生成一个socket文件描述符,然后通过bind()函数将生成的socket绑定到要监听的地址和端口上面.绑定好了之后,使用listen()函数来监听相应的端口.而客户端是在通过socket()函数创建一个socket对象之后,通过connect()函数向被服务端监听的socket发起一个连接请求,即发起一次TCP连接的三次握手.接下来就可以就可以通过TC

深入理解TCP协议的三次握手,分析源码并跟踪握手过程

1.TCP三次握手建立连接 在TCP中,面向连接的传输需要经过三个阶段:连接建立.数据传输和连接终止. 三次握手建立连接 在我们的例子中,一个称为客户的应用程序希望使用TCP作为运输层协议来和另一个称为服务器的应用程序建立连接. 这个过程从服务器开始.服务器程序告诉它的TCP自己已准备好接受连接.这个请求称为被动打开请求.虽然服务器的TCP已准备好接受来自世界上任何一个机器的连接,但是它自己并不能完成这个连接. 客户程序发出的请求称为主动打开.打算与某个开放的服务器进行连接的客户告诉它的TCP,

通俗大白话来理解TCP协议的三次握手和四次分手

通俗理解: 但是为什么一定要进行三次握手来保证连接是双工的呢,一次不行么?两次不行么?我们举一个现实生活中两个人进行语言沟通的例子来模拟三次握手. 引用网上的一些通俗易懂的例子,虽然不太正确,后面会指出,但是不妨碍我们理解,大体就是这么个理解法. 第一次对话: 老婆让甲出去打酱油,半路碰到一个朋友乙,甲问了一句:哥们你吃饭了么? 结果乙带着耳机听歌呢,根本没听到,没反应.甲心里想:跟你说话也没个音,不跟你说了,沟通失败.说明乙接受不到甲传过来的信息的情况下沟通肯定是失败的. 如果乙听到了甲说的话

深入理解TCP协议:三次握手详解

1.什么是三次握手? TCP协议建立连接时,需要三次发送数据包: 第一次:客户机向服务器端请求建立连接 第二次:服务器收到客户机的请求,发出响应 第三次:客户机收到响应 认为连接建立成功 详细过程: 名词解释: SYN - 标志位 只有第一次和第二次为1,第三次和其他任何情况都是0 ACK - 标志位 只有第一次不为1,第二,三次和其他任何情况都是1 Sequence Number 顺序号,初始值为随机数 Acknowledgment Number 确认号,下一次对收到的数据顺序号的期望 第一次

如何理解TCP协议是无边界的,以及粘包?

更新记录 时间 版本修改 2020年4月2日 初稿 我们从经典的计算机科学丛书上阅到的知识,都说:TCP协议是没有消息边界的.但是这个要怎么理解呢?在我没有接触底层的套接字相关逻辑时.我对此也没有特别的了解.直到阅读了套接字的相关逻辑源码,才对此有了一定的了解 TCP的发包和我们业务层所发出的协议数据是不一定吻合的.也就是说,我们发的数据库可能会被分拆成不同的包.然后再和别的协议(这里当然是只发往同一个端口)的数据封装同一个TCP包体. 因此.对于我们业务网络层而言,我们需要在一个TCP包体里面