lwip_14_TCP协议之可靠传输的实现
前言
前面章节太长了,不得不分开。
这里已源码为主,默认读者已知晓概念或原理,概念或原理可以参考前面章节,有分析。
参考:李柱明博客:https://www.cnblogs.com/lizhuming/p/17438743.html
两个时钟处理函数
lwip的时钟机制可以翻看前面章节。
lwip的TCP可靠传传输的实现离不开两个时钟处理函数:
- 快时钟:
tcp_fasttmr()
- 快时钟周期为
TCP_FAST_INTERVAL
,默认250ms。 - 主要作用:遍历处理PCB:
- 处理延迟ACK,将其发出。
- 通知应用层获取接收缓冲区中的数据。
- 快时钟周期为
- 慢时钟:
tcp_slowtmr()
- 快时钟周期为
TCP_SLOW_INTERVAL
,默认500ms。 - 主要作用:遍历处理PCB:
- 各种超时计算。如TCP4大定时器:重传定时器、保活定时器、坚持定时器、2MSL定时器。
- 上面这几个定时器也包含了各自的业务。如重传定时器中包含了拥塞发生后的算法,坚持定时器中包含了窗口探查等等。
- 还有RTT等计时。
- 快时钟周期为
RTT和RTO计算源码实现
原理参考前面大章节。
RTT和RTO相关变量
控制块中RTT和RTO相关变量:
/* RTT (round trip time) 估算 */
u32_t rttest; /* RTT测量,发送时的时间戳。精度500ms */
u32_t rtseq; /* 开始计算RTT时对应的seq号 */
/* RTT估计出的平均值和时间差。
注意:sa为算法中8倍的均值;sv为4倍的方差。再去分析LWIP实现RTO的算法。 */
s16_t sa, sv; /* @see "Congestion Avoidance and Control" by Van Jacobson and Karels */
s16_t rto; /* 重传超时时间。节拍宏:TCP_SLOW_INTERVAL。初始超时时间宏:LWIP_TCP_RTO_TIME *//* retransmission time-out (in ticks of TCP_SLOW_INTERVAL) */
u8_t nrtx; /* 重发次数 */
发送前记录发出的时间搓
在tcp_output_segment()
发送报文段时,如果需要计算RTT,就记录发送当前报文的时间搓:
/* 计算RTT */
if (pcb->rttest == 0) {
pcb->rttest = tcp_ticks; /* 记录当前时间戳 */
pcb->rtseq = lwip_ntohl(seg->tcphdr->seqno); /* 记录当前发送的起始seq号 */
}
计算RTT&RTO
在tcp_receive()
收到新的ACK,这个ACK包含了我们用于计算RTT的报文时,即可计算RTT:
- 计算RTT和RTO方法已经在TCP原理篇描述了。
- 本次RTT就是当前时间戳-当时时间戳:
(s16_t)(tcp_ticks - pcb->rttest);
tcp_ticks
会在TCP慢时钟tcp_slowtmr()
中计算(500ms),所以RTT精度也就500ms。
/* RTT测量:如果当前ACK已经把我们附带RTT测量的报文也ACK了,则可以计算RTT */
if (pcb->rttest && TCP_SEQ_LT(pcb->rtseq, ackno)) {
/* RTT值不应该超过32K,因为这是tcp计时器滴答和往返不应该那么长… */
m = (s16_t)(tcp_ticks - pcb->rttest); /* 算出RTT */
LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_receive: experienced rtt %"U16_F" ticks (%"U16_F" msec).\n",
m, (u16_t)(m * TCP_SLOW_INTERVAL)));
/* RTO算法有很多种,LWIP使用的是Jacobson提出的,具体格式如下: */
/* M:某次测量的RTT值。A:RTT平均值。D:RTT估计方差。g:常数1/8。h:常数1/4。 */
/* 说明:pcb->sa是8倍的RTT平均值。pcb->sv是4倍的方差。 */
/* ERR = M-A */
/* A = A+g*ERR */
/* D = D+h*(|ERR|-D) */
/* RTO = A+4*D */
/* 算出平滑RTT */
m = (s16_t)(m - (pcb->sa >> 3)); /* 偏差 = RTT - 均值 */
pcb->sa = (s16_t)(pcb->sa + m); /* 均值 = 原均值 + (1/8)偏差 */
/* 绝对差 = 差值取绝对值 */
if (m < 0) {
m = (s16_t) - m;
}
m = (s16_t)(m - (pcb->sv >> 2));
pcb->sv = (s16_t)(pcb->sv + m); /* 方差 = 原方差 + (1/4)(绝对差 - 原方差) */
pcb->rto = (s16_t)((pcb->sa >> 3) + pcb->sv); /* RTO = 均值 + 4*方差 */
LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_receive: RTO %"U16_F" (%"U16_F" milliseconds)\n",
pcb->rto, (u16_t)(pcb->rto * TCP_SLOW_INTERVAL)));
/* 本次RTT测量完毕,关闭本次RTT测量 */
pcb->rttest = 0;
}
RTO退避指数
上面只是每次RTT计算出来的RTO,适用于没有发送超时的情况下。
而当发生发送超时时,RTO并不是维持RTT计算的结果,而是超时后每次超时都会按照RTO退避指数来放大RTO。
RTO退避指数:
static const u8_t tcp_backoff[13] =
{ 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7, 7};
发生超时重传后的RTO计算:
- 在
tcp_slowtmr()
函数处理超时重传时,RTO会根据本次的重传次数来选择RTO退避指数来放大RTO。
/* TCP客户端发起的SYN不纳入RTO算法范围 */
if (pcb->state != SYN_SENT) {
/* RTO计算 */
u8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff) - 1);
int calc_rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[backoff_idx];
pcb->rto = (s16_t)LWIP_MIN(calc_rto, 0x7FFF);
}
超时重传&拥塞窗口变化
超时重传相关定时器
在PCB控制块:
/* 超时重传计时器值,当该值大于RTO值时,重传报文 */
s16_t rtime;
存在空中数据时,就会一直开启这个超时定时器,在慢时钟tcp_slowtmr()
中计时。
在收到新的ACK时,会复位这个定时器值。
如果在超过RTO值都还没收到新的ACK,则表示超时,需要重传。
由于lwip的特点(轻量)每条TCP只有一个重传定时器,而不是每个报文段都有一个独立的定时器,所以只要发生超时重传,就会把当前空中链表pcb->unacked
中的所有空中数据全部挪回发送缓冲区pcb->unsent
,哪怕是刚刚才发送出去的也要挪回。其源码根据参考tcp_rexmit_rto_prepare()
即可。
超时重传算法
在tcp_slowtmr()
函数中,会检查超时重传,超时值比当前RTO值大就表示超时,需要触发超时重传算法:
- 慢启动上门限值
pcb->ssthresh
减半。但是不能低于2个MSS。 - 拥塞窗口
pcb->cwnd
降到1个MSS。 - 所有空中数据迁回待发送缓冲区准备重新发送。
- 触发发送。
/* 如果开启了重传计时器,则计时 */
if ((pcb->rtime >= 0) && (pcb->rtime < 0x7FFF)) {
++pcb->rtime;
}
if (pcb->rtime >= pcb->rto) {
/* 发生超时 */
LWIP_DEBUGF(TCP_RTO_DEBUG, ("tcp_slowtmr: rtime %"S16_F
" pcb->rto %"S16_F"\n",
pcb->rtime, pcb->rto));
/* 如果unacked队列报文迁移成功
或
PCB还有unsent报文,但是没有unacked报文(这意味着存在某种原因导致发送报文段失败
(如:可追踪下tcp_output_segment(),开启RTO,但是发送失败)) */
if ((tcp_rexmit_rto_prepare(pcb) == ERR_OK) || ((pcb->unacked == NULL) && (pcb->unsent != NULL))) {
/* TCP客户端发起的SYN不纳入RTO算法范围 */
if (pcb->state != SYN_SENT) {
/* RTO计算 */
u8_t backoff_idx = LWIP_MIN(pcb->nrtx, sizeof(tcp_backoff) - 1);
int calc_rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[backoff_idx];
pcb->rto = (s16_t)LWIP_MIN(calc_rto, 0x7FFF);
}
/* 复位超时计时器 */
pcb->rtime = 0;
/* 发生重传,触发拥塞避免算法:更新慢启动上门限值为有效窗口的一半 */
eff_wnd = LWIP_MIN(pcb->cwnd, pcb->snd_wnd);
pcb->ssthresh = eff_wnd >> 1;
/* 慢启动上门限不能低于2个MSS, */
if (pcb->ssthresh < (tcpwnd_size_t)(pcb->mss << 1)) {
pcb->ssthresh = (tcpwnd_size_t)(pcb->mss << 1);
}
/* 超时引起的拥塞避免算法:拥塞窗口需要更新为一个MSS。重新进行慢启动。 */
pcb->cwnd = pcb->mss;
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_slowtmr: cwnd %"TCPWNDSIZE_F
" ssthresh %"TCPWNDSIZE_F"\n",
pcb->cwnd, pcb->ssthresh));
/* 复位上次成功发送的字节数为0(因为unacked都为NULL) */
pcb->bytes_acked = 0;
/* 调用能统计重传次数的API把数据再次发送出去 */
tcp_rexmit_rto_commit(pcb);
}
}
保活定时器&保活探测报文
相关变量
保活定时器在PCB控制块中的变量:
u32_t keep_idle
:- 其值为
TCP_KEEPIDLE_DEFAULT
,默认7200秒,即是两小时。 - 空闲的最长时间,超过这个时间都没有数据交互就会触发保活机制。
- 调用
setsocketopt()
搭配TCP_KEEPIDLE
即可修改该值。
- 其值为
u32_t keep_intvl
:- 其值为
TCP_KEEPINTVL_DEFAULT
,默认75秒。 - 触发保活机制后,每隔
keep_intvl
秒会发送一个保活探测报文。 - 调用
setsocketopt()
搭配TCP_KEEPINTVL
即可修改该值。
- 其值为
u32_t keep_cnt
:- 其值为
TCP_KEEPCNT_DEFAULT
,默认9次。 - 触发保活机制后,最多发送
keep_cnt
次保活探测报文,超过后都未收到对端响应,则断开当前连接。 - 调用
setsocketopt()
搭配TCP_KEEPCNT
即可修改该值。
- 其值为
/* keepalive计时器的上限值 */
u32_t keep_idle;
#if LWIP_TCP_KEEPALIVE
/* keepalive探测间隔 */
u32_t keep_intvl;
/* keepalive探测的上限次数 */
u32_t keep_cnt;
#endif /* LWIP_TCP_KEEPALIVE */
当然,除了上面三个参数外,还有两个关键参数:
- PCB中的最近交互时间搓:
/* 保存这控制块的TCP节拍起始值。用于当前PCB的时基初始值参考 */
/* 活动计时器,收到合法报文时自动更新。 */
u32_t tmr;
- 全局变量当前时间戳:
/* Incremented every coarse grained timer shot (typically every 500 ms). */
u32_t tcp_ticks;
tcp_ticks
- pcb->tmr
就是当前连接的持续空闲时间了。
保活机制源码实现
在tcp_slowtmr()
函数中,实现保活机制:
/* Check if KEEPALIVE should be sent */
if (ip_get_option(pcb, SOF_KEEPALIVE) &&
((pcb->state == ESTABLISHED) ||
(pcb->state == CLOSE_WAIT))) {
if ((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + TCP_KEEP_DUR(pcb)) / TCP_SLOW_INTERVAL) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: KEEPALIVE timeout. Aborting connection to "));
ip_addr_debug_print_val(TCP_DEBUG, pcb->remote_ip);
LWIP_DEBUGF(TCP_DEBUG, ("\n"));
++pcb_remove;
++pcb_reset;
} else if ((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + pcb->keep_cnt_sent * TCP_KEEP_INTVL(pcb))
/ TCP_SLOW_INTERVAL) {
err = tcp_keepalive(pcb);
if (err == ERR_OK) {
pcb->keep_cnt_sent++;
}
}
}
保活探测报文
调用tcp_keepalive()
函数即可发送保活探测报文。
保活探测报一般是包含一个字节的TCP数据,但是该字节的SEQ已经被对端ACK过了的(代码证明如下),所以发送该SEQ到对端并不影响对端的字节流,但是对端如果收到会响应一个ACK回来,我们便可判断对端主机在线,可重新计时保活探测。
tcp_keepalive()
:pcb->snd_nxt - 1
p = tcp_output_alloc_header(pcb, optlen, 0, lwip_htonl(pcb->snd_nxt - 1));
坚持定时器&零窗口探测报文
相关变量
在PCB控制块中:
u8_t persist_cnt
:- 坚持定时器节拍计数。在
tcp_slowtmr()
中计时,精度500ms。
- 坚持定时器节拍计数。在
u8_t persist_backoff
:- 坚持定时器探查报文时间间隔列表索引及开关。如果该索引值大于0,表示开启坚持定时器计时。
- 该值表示
tcp_persist_backoff[]
数组的索引,也表示本次窗口探测报文的时间间隔的节拍数。
u8_t persist_probe
:- 坚持定时器窗口0时发出的探查报文次数。
- 最大为
TCP_MAXRTX
,默认12次,超过也没收到对端响应,则关闭当前连接。
/* 坚持定时器:用于解决远端接收窗口为0时,定时询问使用 */
u8_t persist_cnt; /* 坚持定时器节拍计数值 */
u8_t persist_backoff; /* 坚持定时器探查报文时间间隔列表索引及开关 */
u8_t persist_probe; /* 坚持定时器窗口0时发出的探查报文次数 */
坚持定时器时间间隔节拍数数组:
/* 坚持定时器的阻塞时长列表,发送窗口探查报文越来越稀疏 */
static const u8_t tcp_persist_backoff[7] = { 3, 6, 12, 24, 48, 96, 120 };
零窗口探测源码实现
在tcp_slowtmr()
函数中,实现零窗口探测:
if (pcb->persist_backoff > 0) {
LWIP_ASSERT("tcp_slowtimr: persist ticking with in-flight data", pcb->unacked == NULL);
LWIP_ASSERT("tcp_slowtimr: persist ticking with empty send buffer", pcb->unsent != NULL);
if (pcb->persist_probe >= TCP_MAXRTX) {
++pcb_remove; /* max probes reached */
} else {
u8_t backoff_cnt = tcp_persist_backoff[pcb->persist_backoff - 1];
if (pcb->persist_cnt < backoff_cnt) {
pcb->persist_cnt++;
}
if (pcb->persist_cnt >= backoff_cnt) {
int next_slot = 1; /* increment timer to next slot */
/* If snd_wnd is zero, send 1 byte probes */
if (pcb->snd_wnd == 0) {
if (tcp_zero_window_probe(pcb) != ERR_OK) {
/* 发送窗口探查失败,即是本次坚持定时器相关报文发送失败,不能清空现有计时数值,因为下次进入需要马上补回窗口探查报文的发送 */
next_slot = 0; /* try probe again with current slot */
}
/* snd_wnd not fully closed, split unsent head and fill window */
} else {
/* 窗口不够大,那切割也得发送 */
if (tcp_split_unsent_seg(pcb, (u16_t)pcb->snd_wnd) == ERR_OK) {
if (tcp_output(pcb) == ERR_OK) {
/* 切割后,发送成功会关闭坚持定时器清理相关值,这里标记下后面不用刷新坚持定时器相关值了 */
next_slot = 0;
}
}
}
if (next_slot) {
/* 坚持定时器本次轮询已经成功发出相关报文了,进入下次轮询计时 */
pcb->persist_cnt = 0;
if (pcb->persist_backoff < sizeof(tcp_persist_backoff)) {
pcb->persist_backoff++;
}
}
}
}
}
零窗口探测报文
调用tcp_zero_window_probe()
即可发送零窗口探测报文。
窗口探测报文的是包含一字节TCP数据的,该字节就是待发送的下一个字节。
tcp_zero_window_probe()
函数源码就不贴了,给出大概实现的流程:
- 如果当前没有需要发送的数据,则不需要进行窗口探查。
- 如果待发送的数据中,是FIN报文,则本次窗口探查不需要附加数据字节。
- 申请一个新的pbuf,作为窗口探查报文的pbuf。
- 从待发送的数据中拷贝一个TCP数据字节,作为本次窗口探查报文的附带字节。
- 注意:是复制,原有的tcp_seg的数据区是不偏移的,下次发送这段数据时,第一个数据字节也会被重复发送到对端,对端会根据seq号进行裁剪处理的。
- 发送窗口探查报文。
整个过程中,就算携带了一字节的数据,也不会将当前数据包加入pcb->unacked队列,也就是本地不会监听这个字节的ack确认,因为没必要,等待窗口放开后,这个字节也会被正常发送过去。
2MSL定时器
在TIME_WAIT状态下会开启2MSL计时来清除当前连接的PCB。
当然,也是需要两个PCB变量来辅助:
- PCB中的最近交互时间搓:
/* 保存这控制块的TCP节拍起始值。用于当前PCB的时基初始值参考 */
/* 活动计时器,收到合法报文时自动更新。 */
u32_t tmr;
- 全局变量当前时间戳:
/* Incremented every coarse grained timer shot (typically every 500 ms). */
u32_t tcp_ticks;
tcp_ticks
- pcb->tmr
就是当前连接的持续空闲时间了。
源码也是在tcp_slowtmr()
函数中实现:
TCP_MSL
:默认为60秒。
pcb = tcp_tw_pcbs;
while (pcb != NULL) {
LWIP_ASSERT("tcp_slowtmr: TIME-WAIT pcb->state == TIME-WAIT", pcb->state == TIME_WAIT);
pcb_remove = 0;
/* Check if this PCB has stayed long enough in TIME-WAIT */
if ((u32_t)(tcp_ticks - pcb->tmr) > 2 * TCP_MSL / TCP_SLOW_INTERVAL) {
++pcb_remove;
}
/* If the PCB should be removed, do it. */
if (pcb_remove) {
struct tcp_pcb *pcb2;
tcp_pcb_purge(pcb);
/* Remove PCB from tcp_tw_pcbs list. */
if (prev != NULL) {
LWIP_ASSERT("tcp_slowtmr: middle tcp != tcp_tw_pcbs", pcb != tcp_tw_pcbs);
prev->next = pcb->next;
} else {
/* This PCB was the first. */
LWIP_ASSERT("tcp_slowtmr: first pcb == tcp_tw_pcbs", tcp_tw_pcbs == pcb);
tcp_tw_pcbs = pcb->next;
}
pcb2 = pcb;
pcb = pcb->next;
tcp_free(pcb2);
} else {
prev = pcb;
pcb = pcb->next;
}
拥塞控制
相关变量
PCB控制块中:
tcpwnd_size_t cwnd; /* 拥塞窗口大小 */
tcpwnd_size_t ssthresh; /* 拥塞避免算法启动阈值。也叫慢启动上门限值。 */
慢启动
慢启动时,拥塞窗口cwnd
起始为1MSS,收到多少ACK就扩大多少(但是一般都是以MSS为步伐、单位)(lwip实际实现得看源码,下面有),直至达到慢启动上门限ssthresh
后才进入拥塞避免,每次最大只追加1MSS。
慢启动拥塞窗口cwnd
起始为1MSS源码在SYN_SENT
状态下收到SYN和ACK时配置的,具体在tcp_process()
函数中:
/* 计算初始拥塞窗口 */
pcb->cwnd = LWIP_TCP_CALC_INITIAL_CWND(pcb->mss);
此时的拥塞窗口还是PCB初始化时配置的初始值:默认为发送缓冲区size TCP_SND_BUF
。
/* RFC 5618建议设置ssthresh值尽可能高,比如设置为最大可能的窗口通告值大小(可以理解为最大可能的发送窗口大小 )。 */
/* 这里先设置为本地发送缓冲区大小,即是最大飞行数据量。后面进行窗口缩放和自动调优时自动调整。 */
pcb->ssthresh = TCP_SND_BUF;
慢启动拥塞窗口变化是在收到新ACK中处理,即是tcp_receive()
函数:包含慢启动和拥塞避免:
- 慢启动:收到一个新的ACK后,会扩大拥塞窗口
cwnd
,如果在超时重传状态下,仅增大1MSS;如果在正常状态下,会增大2MSS。 - 拥塞避免:如果累计ACK的数据大于一个拥塞窗口,则拥塞窗口
cwnd
增大1MSS,然后重新累计ACK。
/* 更新拥塞控制字段:拥塞窗口cwnd 和 慢启动上门限ssthresh */
if (pcb->state >= ESTABLISHED) { /* 连接处于ESTABLISHED状态 */
if (pcb->cwnd < pcb->ssthresh) { /* 慢启动算法 */
tcpwnd_size_t increase;
/* 参考:RFC 3465, section 2.2 Slow Start */
/* 如果是超时重传后的慢启动,则选1MSS;
如果是正常状态下的慢启动,选2MSS */
u8_t num_seg = (pcb->flags & TF_RTO) ? 1 : 2;
/* 拥塞窗口增长:ACK新数据的量 和 nMSS 中的最小值 */
increase = LWIP_MIN(acked, (tcpwnd_size_t)(num_seg * pcb->mss));
TCP_WND_INC(pcb->cwnd, increase);
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: slow start cwnd %"TCPWNDSIZE_F"\n", pcb->cwnd));
} else { /* 拥塞避免算法 */
/* 参考:RFC 3465, section 2.1 Congestion Avoidance */
/* 如果累计ACK新数据量不少于一个拥塞窗口,
则累计ACK新数据流减一个拥塞窗口值;拥塞窗口加一个MSS */
TCP_WND_INC(pcb->bytes_acked, acked);
if (pcb->bytes_acked >= pcb->cwnd) {
pcb->bytes_acked = (tcpwnd_size_t)(pcb->bytes_acked - pcb->cwnd);
TCP_WND_INC(pcb->cwnd, pcb->mss);
}
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_receive: congestion avoidance cwnd %"TCPWNDSIZE_F"\n", pcb->cwnd));
}
}
上面pcb->bytes_acked
变量是累计ACK新数据的量。拥塞避免时,用于判断拥塞窗口cwnd
是否需要+1MSS。
拥塞避免
当拥塞窗口增大到慢开始上门限值ssthresh
时,就开始拥塞避免算法。每次只增加1MSS。这里的每次是指每累计收到一个拥塞窗口量的ACK。
其源码在tcp_receive()
函数中,慢启动中有分析。
拥塞发送:超时重传&快重传
拥塞发送包括超时重传和快重传,这两者要区别起来,因为前者的算法会严重影响性能。
超时重传参考本章前面小节,有分析,这里续上分析快重传。
快重传在PCB中的变量:
u8_t dupacks; /* 收到最大重复ACK的次数:一般收1-2次认为是重排序引起的。收到3次后,可以确认为失序,需要立即重传。然后执行拥塞避免算法中的快恢复。 */
PCB快重传标志位:TF_INFR
当收到对端连续三次ACK同一个SEQ时,我们就能判断为发送了网络丢包,这时就不用等待超时,不用执行超时重传的拥塞算法了,而是执行快速重传的拥塞发生算法:
- 拥塞窗口
cwnd
设为原来的一半:cwnd /= 2
; - 慢开始上门限值
ssthresh = cwnd
;(cwnd为减半后的拥塞窗口) - 然后拥塞窗口
cwnd = ssthresh + 3
(3:每收到1个ACK,可以认为对端收到1次TCP包,网络上就少了1个TCP包,一个包最大为1个报文段,所以快恢复的拥塞窗口就追加3个报文段) - 进入快恢复算法。
既然是需要判断收到三次重复ACK,那么源码肯定就在tcp_receive()
函数中实现:
重复ACK的判断条件、做法如下:
/* (From Stevens TCP/IP Illustrated Vol II, p970.)
* 通过以下条件可以判断是否是重复的ACK:
* 1) 没有ACK新数据;
* 2) 没有TCP数据,也没有SYN、FIN标志;
* 3) 前面更新窗口算法中,本地发送窗口没有更新;(看具体源码)
* 4) 本地还有unacked数据,并且重传计时器在跑;
* 5) 当前收到的ACK,是本次连接历史最大的ACK。
*
* 如果上面5个条件都满足,则是一个重复的ACK:
* a) 重复 < 3次:do nothing
* b) 重复 == 3次: 快重传
* c) 重复 > 3次: 拥塞窗口CWND+1MSS(拥塞避免算法)
*
* 如果只满足条件1、2、3:重置重复ACK计数器。(并添加到统计中,但是LWIP没有做这个统计)
*
* 如果只满足条件1:重置重复ACK计数器。
*
*/
具体源码:
/* Clause 1:没有ACK新数据 */
if (TCP_SEQ_LEQ(ackno, pcb->lastack)) {
/* Clause 2:报文段中没有数据,也没有SYN、FIN */
if (tcplen == 0) {
/* Clause 3:本地发送窗口没有更新 */
if (pcb->snd_wl2 + pcb->snd_wnd == right_wnd_edge) {
/* Clause 4:本地还有unacked数据,重传计时器还在跑 */
if (pcb->rtime >= 0) {
/* Clause 5:收到的ACK是本连接历史最大的ACK */
if (pcb->lastack == ackno) {
if ((u8_t)(pcb->dupacks + 1) > pcb->dupacks) { /* 防溢出 */
++pcb->dupacks; /* 收到重复的ACK */
}
if (pcb->dupacks > 3) {
/* Inflate the congestion window */
/* 拥塞避免:拥塞窗口cwnd+一个MSS */
TCP_WND_INC(pcb->cwnd, pcb->mss);
}
if (pcb->dupacks >= 3) {
/* 快重传:是检查unacked和TF_INFR标志位来确定是否触发快重传。 */
tcp_rexmit_fast(pcb);
}
}
}
}
}
}
调用的是tcp_rexmit_fast()
来实现快重传:
/**
* 收到3个及以上重复ACK才会调用当前函数实现快重传算法。
*/
void
tcp_rexmit_fast(struct tcp_pcb *pcb)
{
LWIP_ASSERT("tcp_rexmit_fast: invalid pcb", pcb != NULL);
/* 存在未被ACK的数据 && 快重传标志位没有被标记 */
if (pcb->unacked != NULL && !(pcb->flags & TF_INFR)) {
/* 重传pcb->unacked队列中第一个报文 */
LWIP_DEBUGF(TCP_FR_DEBUG,
("tcp_receive: dupacks %"U16_F" (%"U32_F
"), fast retransmit %"U32_F"\n",
(u16_t)pcb->dupacks, pcb->lastack,
lwip_ntohl(pcb->unacked->tcphdr->seqno)));
if (tcp_rexmit(pcb) == ERR_OK) {
/* 设置慢启动上门限pcb->ssthresh = MIN(当前拥塞窗口,发送窗口) 的一半。
但是不能低于2个MSS */
pcb->ssthresh = LWIP_MIN(pcb->cwnd, pcb->snd_wnd) / 2;
/* The minimum value for ssthresh should be 2 MSS */
if (pcb->ssthresh < (2U * pcb->mss)) {
LWIP_DEBUGF(TCP_FR_DEBUG,
("tcp_receive: The minimum value for ssthresh %"TCPWNDSIZE_F
" should be min 2 mss %"U16_F"...\n",
pcb->ssthresh, (u16_t)(2 * pcb->mss)));
pcb->ssthresh = 2 * pcb->mss;
}
/* 拥塞窗口更新为 = 慢启动上门限 + 3MSS */
pcb->cwnd = pcb->ssthresh + 3 * pcb->mss;
/* 标记PCB处于快重传状态 */
tcp_set_flags(pcb, TF_INFR);
/* 重置超时重传计时器 */
pcb->rtime = 0;
}
}
}
快恢复
快速重传和快速恢复算法一般同时使用。
因为快恢复算法认为,能收到三个ACK,说明网络还不是很差,没必要像RTO一样搞得那么僵。
快恢复算法:
- 收到第3个重复ACK时,先执行快速重传算法。
- 收到超过3个重复的ACK时,每次都会增大拥塞窗口:
cwnd += 1
。 - 当收到新的ACK后:
cwnd = ssthresh
。然后进入拥塞避免算法。
上面是推荐算法,下面才是lwip实际算法。
源码当然还是在tcp_receive()
函数中:
- 说明:快重传
/* 需要退出快重传状态 */
if (pcb->flags & TF_INFR) {
tcp_clear_flags(pcb, TF_INFR); /* 退出快重传状态 */
pcb->cwnd = pcb->ssthresh; /* 快恢复算法:拥塞窗口重置为慢启动上门限值 */
pcb->bytes_acked = 0; /* 重置被ACK的数据长度的统计 */
}
Nagle算法
nagle算法: 尽可能组合更多数据合到同一个报文段中。所以,该算法是在TCP出口函数中实现的,所以查看tcp_output()
函数即可:
/* 如果nagle算法生效,则延迟发送。
* 打破nagle算法生效的条件(即是nagle生效,也要马上发送的条件)之一:
* - 如果之前调用tcp_write()时有内存错误未能成功发送,为了防止延迟ACK超时,需要立即发送。
* - 如果FIN已经在队列中了,则没必要再延迟发送了,立即把数据发出,加速闭环。
* 注意:SYN一直都是单独报文段的。所以要么不存在SYN,如存在未发送数据seg->next != NULL; 要么只存在SYN,即是还没有发送数据,如pcb->unacked == NULL;。
* 注意:RST是不会通过tcp_wirte()和tcp_output()发送的。
*/
if ((tcp_do_output_nagle(pcb) == 0) &&
((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
/* nagle算法生效 && 上次发送内存正常 && 还没有FIN */
break;
}
判断Nagle是否生效实现在tcp_do_output_nagle()
函数中:Nagle失效条件如下(即是可以立即发送的条件):
- 没有飞行中的数据。
- 用户设置了
TF_NODELAY
标志。(该标志表示关闭nagle算法) - 用户设置了
TF_INFR
标志。(该标志表示正在快恢复) - 未发送的报文段不止一个,也满足立即发送条件。
- 未发送的报文段长度大于或大于一个MSS,也满足立即发送条件。
- 内存不足,nagle算法也会失效,因为可能需要告知应用层发送缓冲区内存不足。
#define tcp_do_output_nagle(tpcb) ((((tpcb)->unacked == NULL) || \
((tpcb)->flags & (TF_NODELAY | TF_INFR)) || \
(((tpcb)->unsent != NULL) && (((tpcb)->unsent->next != NULL) || \
((tpcb)->unsent->len >= (tpcb)->mss))) || \
((tcp_sndbuf(tpcb) == 0) || (tcp_sndqueuelen(tpcb) >= TCP_SND_QUEUELEN)) \
) ? 1 : 0)
延迟确认
如果Nagle算法生效,则会延迟确认,延迟的确认会在tcp_fasttmr()
发送出去,该函数周期默认为250ms,表示延迟的确认在(0:250]ms内会发送出去。
tcp_fasttmr()
:
/* 如果存在延迟发送的ACK,需要发送这个延迟的ACK */
if (pcb->flags & TF_ACK_DELAY) {
LWIP_DEBUGF(TCP_DEBUG, ("tcp_fasttmr: delayed ACK\n"));
tcp_ack_now(pcb); /* 标记立即发送ACK */
tcp_output(pcb); /* 发送数据 */
tcp_clear_flags(pcb, TF_ACK_DELAY | TF_ACK_NOW); /* 清空相关标志位 */
}
LWIP源码中有两个宏可能会导致读者混淆,所以我在这里说明下,希望能有助于你理解:
TF_ACK_NOW
:不管未发送队列中是否有无数据,也不管窗口是否满足,都必须立即响应一个ACK,哪怕是纯粹的ACK。
TF_ACK_DELAY
:如果收到数据,需要响应ACK,但是开启了拥塞控制,Nagle算法生效,就需要开启延迟ACK。lwip 开启了一个 250ms 的定时器,如果在超时前都还没满足发送,超时时必须响应ACK。是为了让lwip的tcp_fasttmr()
定时器超时时,检查当前连接是否存在延迟ACK,如果存在,则响应ACK。
- 其实就是表示当前连接存在延迟发送的ACK,如果超时了,一定要把这个延迟发送的ACK发送出去。
- 需要注意的是,这里的250ms定时器是一直在跑的,是每250ms会检查处理一次。如果某个时刻标记了
TF_ACK_DELAY
,且一直未满足发送,并不是此刻起等待250ms后才发送ACK,而是下一个250ms定时器到来时就发送这个延迟的ACK了,可能下一个ms就到了。
糊涂窗口综合症的处理
作为接收方时的解决:小窗口不通告。
在滑动更新接收窗口size时,小窗口不通告。而更新接收窗口是在应用层从TCP接收缓冲区成功提取数据时更新的,所以查看tcp_recved()
即可:
/* 更新滑动窗口。支持糊涂窗口避免算法。 */
wnd_inflation = tcp_update_rcv_ann_wnd(pcb);
tcp_update_rcv_ann_wnd()
:
- 小窗口不通告。滑动大于
LWIP_MIN((TCP_WND / 2), pcb->mss)
才通告。
/**
* 窗口滑动。窗口滑动阈值:LWIP_MIN((TCP_WND / 2), pcb->mss)
* 返回窗口滑动偏移值。
*
* 通俗点:如果窗口滑动后能接收大于等于 LWIP_MIN((TCP_WND / 2), pcb->mss) 这么多数据时,才滑动窗口通告值。
* 如果窗口滑动后,只能接收一点点数据,还不如不滑动呢。
*/
u32_t
tcp_update_rcv_ann_wnd(struct tcp_pcb *pcb)
{
u32_t new_right_edge;
LWIP_ASSERT("tcp_update_rcv_ann_wnd: invalid pcb", pcb != NULL);
/* 新的接收窗口右边沿 */
new_right_edge = pcb->rcv_nxt + pcb->rcv_wnd;
if (TCP_SEQ_GEQ(new_right_edge, pcb->rcv_ann_right_edge + LWIP_MIN((TCP_WND / 2), pcb->mss))) {
/* 新窗口右边沿比旧窗口右边沿多出一个MSS时(或1/2 宏定义接收窗口大小时),更新窗口通告值大小为当前新的接收窗口大小 */
pcb->rcv_ann_wnd = pcb->rcv_wnd;
return new_right_edge - pcb->rcv_ann_right_edge;
} else { /* 新、旧窗口右边沿还没拉开足够距离,不更新通告窗口 */
if (TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)) {
/* 接收窗口已满 */
/* 窗口通告值设为0,不允许再发送数据到本地,等待窗口滑动后再发送 */
pcb->rcv_ann_wnd = 0;
} else {
/* 窗口未满,而且滑动的长度不满足滑动阈值,保持窗口右边沿,不滑动 */
u32_t new_rcv_ann_wnd = pcb->rcv_ann_right_edge - pcb->rcv_nxt;
#if !LWIP_WND_SCALE
LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif
pcb->rcv_ann_wnd = (tcpwnd_size_t)new_rcv_ann_wnd;
}
return 0;
}
}
作为发送方:
- 参考nagle算法。
标签:lwip,14,ACK,tcp,TCP,pcb,窗口,重传 From: https://www.cnblogs.com/lizhuming/p/17438743.html