• Lwip之TCP协议实现(一)


    TCP本身是一个相对复杂的协议,Lwip中最复杂的部分也是此处。这里,我们分部分描述。

    第一部分:TCP处理

    Tcp.c该文件提供了一些通用的函数接口。该文件中的函数主要的操作对象就是tcp_pcb,包括对tcp pcb的设置,修改读取等。另外,在该文件中还实现了tcp的定时器。

    目录

    一:Tcp的一些超时变量

    二:Tcp的控制块(pcb)数据结构

    1 计时变量

    2 轮回时间评估变量

    3 快速重传和快速恢复变量

    4 拥塞避免与控制变量

    5 发送序号、窗口相关变量

    6 接收序号、窗口相关变量

    7 Tcp段

    三:定时器

    3.1 Tcp的快速定时器:

    3.2 Tcp的慢速定时器:

    3.2.1 active状态pcb的超时处理

    3.2.2 time-wait状态pcb的超时处理


    一:Tcp的一些超时变量

    1. #define TCP_TMR_INTERVAL       250 
    2. #define TCP_FAST_INTERVAL      TCP_TMR_INTERVAL
    3. #define TCP_SLOW_INTERVAL      (2*TCP_TMR_INTERVAL) 
    4. TCP处理内部时间基准为250毫秒
    5. 快速定时器为250毫秒
    6. 慢速定时器为500毫秒

    1. #define TCP_FIN_WAIT_TIMEOUT 20000 /* milliseconds */
    2. #define TCP_SYN_RCVD_TIMEOUT 20000 /* milliseconds */
    3. #define TCP_OOSEQ_TIMEOUT        6 /* x RTO */
    4. #define TCP_MSL 60000  /* The maximum segment lifetime in microseconds */
    5. MSL时间为60
    6. FIN状态超时时间20
    7. SYN_RECVD等待状态超时时间20
    8. 序号外数据超时时间6RTO

    1. #define    TCP_NODELAY       0x01     /* don't delay send to coalesce packets */
    2. 如果支持no delay,那么所有发送的数据包将不被缓存。TCP协议对一些零散的小包采取合并发送,这样可以减少交互过程,提高带宽利用率,副作用是一些包的发送将被延迟。

    1. #define TCP_KEEPALIVE  0x02    /* send KEEPALIVE probes when idle for pcb->keepalive miliseconds */
    2. /* Keepalive values */
    3. #define  TCP_KEEPDEFAULT   7200000     /* KEEPALIVE timer in miliseconds */
    4. #define  TCP_KEEPINTVL  75000  /* Time between KEEPALIVE probes in miliseconds */
    5. #define  TCP_KEEPCNT       9   /* Counter for KEEPALIVE probes */
    6. #define  TCP_MAXIDLE  TCP_KEEPCNT * TCP_KEEPINTVL  /* Maximum KEEPALIVE probe time */
    7. 如果定义了keepalive选项,则要用到上面这些变量
    8. Keepalive探测发送前等待的时间  7200120分钟 2小时
    9. 发送keepalive探测的时间间隔 751分钟多
    10. Keepalive探测发送的次数 9
    11. Keepalive总的可消耗时间 9*7567510分钟多

    二:Tcp的控制块(pcb)数据结构

    1 计时变量

    tmr内部的计时变量,该变量包存了对应连接最后一次处于活跃状态的时间

    polltmr poll定时计数

    pollinterval Poll定时器间隔

    rtime 重传计数

    2 轮回时间评估变量

    rttestrtseqsa以及sv等域被用来进行轮回时间评估。被用来进行轮回时间评估的段的序号被保存在rtseq中,该段被发送的时间保存在rttest中。平均轮回时间和轮回时间变量被保存在变量sasv中。在计算重传超时时间时这些变量就被使用,而这个基准的重传超时量保存在rto域中。Nrtx保存有重传的次数

    3 快速重传和快速恢复变量

    lastackdupacks两个域在实现快速重传与快速恢复的实现中使用。lastack域保存被接收到的最后一个ACK确认的序列号,dupacks指示有多少个这样的ACKs,这些已经被接收到的ACK序列号保存在lastack中。

    4 拥塞避免与控制变量

    当前连接的拥赛控制窗口被保存在cwnd域中,而慢启动的阈值保存在ssthresh中。

    5 发送序号、窗口相关变量

    Snd_nxt 保存有下一个要发送的序号

    Snd_max 保存有最大发送序号

    Snd_wnd 发送者窗口

    Snd_wl1 保存有窗口最近更新段的序号

    Snd_wl2 保存有窗口最近更新段的确认号

    Snd_lbb 下一个将要被缓冲的数据字节序号

    Acked 最新被确认的数据字节序号

    Snd_buf 以字节为单位的可用发送缓冲空间

    Snd_queuelen 以段为单位的可用发送缓冲空间

    以上六个域ackdsnd_nxtsnd_wndsnd_wl1snd_wl2snd_lbb在发送数据时使用。被接收者确认的最高的序列号保存在ackd中,已发送的数据的最大序号保存在snd_max中,下一个将要发送的序列号由snd_nxt保存。接收者的建议窗口由snd_wnd保存,snd_wl1snd_wl2在更新snd_wnd时使用。snd_lbb含有传输队列中最后一个字节的序号。

    6 接收序号、窗口相关变量

    rcv_nxtrcv_wnd变量在接收数据时使用。rcv_nxt域含有下一个期望从远程终端得到的字节序号,因此在发送ACKs到远程主机时使用。接收者的窗口被rcv_wnd保持,在发送TCP段中会突出这一点。tmr域被用来作为一个定时器,在特定长度时间过去之后,当前TCP连接就应当被移除,比如像处在TIME-WAIT状态的连接。链路上所允许的最大段的大小被保存在mss域中,flags域保存有链路的额外的状态信息,像连接是否属于快速恢复,或者一个延迟的ACK是否应当被发送。

    7 Tcp

    unsentunackedooseq三个队列在发送和接收数据时使用。从应用层接收到但是还没有发送的数据被加入unsent队列,已经发送但是还没有被远程主机确认的数据保存在unacked队列,接收到的序外数据缓冲在ooseq中。

    后续考虑增加与窗口扩大选项有关的变量。根据定义,当扩大因子不为零时,即使为1,也需要大约128k的缓存空间。65535*21

    三:定时器

    协议中需要的定时器  总共有7个

    连接建立定时器:syndrome发送后一定时间内没有收到响应,终止连接的建立

    重传定时器:当长时间没有收到对端的acknowledge时会触发该定时器,认为数据包可能丢失,而进行重传。该定时器的时间是动态计算的

    延迟acknowledge定时器:

    坚持定时器:有发送方发出称为窗口探查的报文段,以避免在零窗口时产生死锁。

    保活定时器:保活定时器用于在连接的双方或者其中的一方出现故障时,通过发送保活报文使客户或者服务器能够发现这种问题。此时连接可能长时间空闲,这时触发保活定时器的一个条件。

    Fin-wait-2定时器:避免进入该状态后长时间收不到对端的fin。超时后关闭连接

    Time-wait定时器:当连接的一方进入time-wait状态后启动该定时器,保证连接在time-wait时间内关闭

    实现中,Tcp的定时器有两个:一个是快速定时器,一个是慢速定时器。这两个定时器被作为基准定时器用来实现更复杂的逻辑(大部分是通过定时计数的办法来实现)。

    3.1 Tcp的快速定时器:

    该定时器每隔250毫秒调用一次。它被用来发送延迟的ack

    该函数遍历所有处于active状态的pcb,如果pcb的ack_delay标识被设置,一个空tcp确认段会被发送,之后,标识就被清除。

    3.2 Tcp的慢速定时器:

    该定时器没500毫秒调用一次。该定时器实现了tcp的超时重传定时器,以及用来移除处在time-wait状态足够长时间的pcb的定时器。它还被用来增加每个pcb中的类似于保活计数等一些计数变量

    在该定时器中,会扫描未确认段组成的链(由tcp_seg结构体中的unacked指针指向),当未确认段所在的pcb的超时量大于设定的值时,超时重传就会发生。

    对于处在TIME-WAIT状态中的连接,coarse grained timer也增加PCB结构体中的tmr域。当计数器达到2*MSL阀值后连接就被移除。

    Coarse grained timer同样增加全局TCP时钟——tcp_ticks,该时钟在轮回时间评估和重传超时中使用。

    该函数的具体逻辑如下:

    {

       首先增加全局的tcp时钟—tcp_ticks。

       扫描所有的处于active状态的pcb,进行相应的处理。

       扫描遍历处于time-wait状态的pcb,进行相应的处理。

       //因为处在listen状态的pcb都是在等待对端的连接,不需要超时处理。

    }

    3.2.1 active状态pcb的超时处理

    初始化pcb是否需要移除的变量 pcb_remove

    如果当前pcb的状态是已发送syn(SYN_SENT),并且重传次数达到了syn最大的重传次数(TCP_SYNMAXRTX),则增加pcb_remove变量

    否则,如果重传次数达到了tcp最大重传次数(TCP_MAXRTX),就增加pcb_remove变量。此时应该是某个ack的最大重传次数

    如果上述两个条件都不满足:

    pcb的重传定时器增加500个滴答。

    如果该pcb的未确认段队列不为空,也就是说存在未确认的段,并且上述重传定时器计数值大于设定的重传超时时间,说明某个段很可能丢失了,超时重传将发生

    {

        //此时,除非我们处于尝试连接某个对端,否则,将超时重传时间加倍。因为处于连接建立阶段的超时时间是固定的,典型值是75秒。

        如果当前不是在SYN_SENT状态,重新计算超时重传时间。(重传定时器的重传时间的取值是依赖与连接上测算到的RTT的)

        当超时发生时,表明发生了拥塞,tcp要执行慢启动和拥塞避免算法。(算法细节1234步骤卷一235页)。首先取拥塞窗口(cwnd)和接收者通告的窗口(snd_wnd)中的小值作为当前的窗口,将当前的阀值(ssthresh)减小到当前窗口的一半,如果阀值小于路径最大段,则设置为2个报文段的大小。(因为阀值是按照段的大小增加的,所以如果不小于一个的话就至少是两个)最后设置当前的拥塞窗口为一个段的大小。

        调用tcp_rexmit_rto()重传未确认队列上的第一个段。

    }

    //检查是否有pcbfin-wait-2状态停留了太长时间。

    如果当前pcb是在上述状态,并且tcp定时器当前的滴答数tcp_ticks与该pcb中保存的变量tmr(该变量主要用于保存连接最后一次活跃时的时间)的差值大于设定的该状态的超时时间,则pcb_remove变量加一(该变量在后续移除不用的pcb时用到)。

    //检查是否需要发送保活探测报文(KEEPALIVE)。

    如果当前的连接支持保活探测选项(SO_KEEPALIVE),并且当前的连接处于建立(ESTABLISHED)或者CLOSE-WAIT状态

    {

        如果当前时钟滴答数tcp_tickspcbtmr变量的差值大于保活探测前空闲的时间和保活探测报文发送消耗的最长时间之和,那么说明该连接空闲了太长时间,该连接不再需要。调用tcp_abort向对端发送一个RST报文,并释放与该连接相关的存储。

        否则,如果该时间在保活探测报文发送期间,还没有达到保活探测报文发送的最大次数,那么就调用tcp_keepalive继续发送保活探测报文,增加该pcb上的保活探测报文发送计数变量keep_cnt

    }

    //如果该连接上保存有序号外的数据,并且有很长时间不再活跃了,则将序号外的数据丢弃(这些数据如果最终需要传输的话还是会通过重传来完成的)

    如果该连接的序号外段数据队列不为空,并且当前时钟滴答数和tmr的差值大于等于设定的TCP_OOSEQ_TIMEOUT,也就是说该连接空闲了这么长时间,则调用tcp_seq_free释放序号外段数据,同时队列也被设置为null

    //检查确认当前连接是否在SYN-RECVD状态停留了太长时间

    如果当前pcb的状态是SYN-RECVD,并且当前的空闲时间大于设定的TCP_SYN_RECVD_TIMEOUT,则增加pcb_remove变量,在后续处理中会将该pcb从链表上移除。

    //检查确认当前连接是否在LAST-ACK状态停留了太长时间

    如果当前pcb的状态是LAST_ACK,并且连接空闲时间大于2MSL,就增加变量pcb_remove。这有点类似与TIME-WAIT状态。说明即使是服务器,如果对端主动关闭了连接,但是服务应用没有关闭该连接,我们也不会停留很长时间,最长2MST后服务器端也就会关闭该连接。

    //在这一部分的最后,我们根据pcb_remove变量的设置情况来对需要移除的pcb进行移除操作,并进行其他相关操作。

    //虽然在之前我们将该变量多次进行自加,但是实际上该变量大于1的情况并不多,因为上述判断条件有好多是互斥的,所以在某个添加满足的情况下,其他许多都是跳过的。

    如果该变量为真,说明需要移除该pcb。首先调用tcp_pcb_purge释放与该pcb相关的存储。将该pcbactive pcb链表上移除,释放其占用的内存,并将pcb指针指向下一个pcb块。

    否则,说明没有需要移除的pcb。增加polltmr变量,如果poll时间大于设定的周期,则复位该变量。执行注册的poll函数,如果该函数正确执行了,则调用tcp_output看看该连接上是否有需要发送的数据,如果有的话就发送,即使是一个单纯的ack

    pcb指针移到下一个pcb

    如果新的pcb指针不为空,也就是说active链表上还有未处理的pcb,则继续回到active pcb处理的开始处进行下一次处理。

    这样,我们处理了在active状态的pcb,并且涉及到tcp的SYN_RECVD  SYN_SENT  ESTABLISHED  CLOSE_WAIT  LAST_ACK  FIN_WAIT_2状态。从tcp的状态机来看,需要处理的状态就剩余CLOSING  TIME_WAIT  FIN_WAIT_1状态了。对于       CLOSING  FIN_WAIT_1状态不需要特殊处理。最后就剩余下面要处理的TIME_WAIT状态了。

    3.2.2 time-wait状态pcb的超时处理

    到这里就处理了active链表上的所有pcb,下面继续处理time_wait链表上的pcb。这部分的处理很简单,因为该链表上的所有pcb的状态都是time-wait的,所以只是进行2MSL超时的检查。

    首先,将pcb_remove变量复位

    如果当前连接空闲的时间大于2MSL,则将pcb_remove变量加一。

    如果该变量被增加了,那么就释放与该pcb相关的存储,并将该pcb从链表上移除,释放其占用的内存。最后使其指向链表中的下一个pcb。

    否则,说明该连接在time_wait状态还没有超时,不采取任何操作,只是将指针下移。

    同样,如果新的pcb指针不为空,说明time_wait状态链表上还有未处理的pcb,接着返回的该处理的开始继续处理下一个time_wait状态的pcb。

    接续:Lwip之TCP协议实现(二)_龙赤子的博客-CSDN博客

  • 相关阅读:
    从-99打造Sentinel高可用集群限流中间件
    自定义redission装配和集成分布式开源限流业务组件ratelimiter-spring-boot-starter的正确姿势
    基于机器学习的课堂自动点名系统
    详解联邦学习中的异构模型集成与协同训练技术
    AIGC实战——深度学习 (Deep Learning, DL)
    uniapp 获取页面来源
    【网络技术】计算机网络介绍
    【SwiftUI模块】004、SwiftUI-<探探App>喜欢手势卡片
    zookeeper本地安装启动
    PySide6应用实践 | 在PyCharm中安装、部署、启动PySide6
  • 原文地址:https://blog.csdn.net/wwwyue1985/article/details/126559147