《LwIP协议栈源码详解——TCPIP协议的实现》TCP坚持与保
活定时器
这节讲解TCP的坚持定时器和保活定时器,先看坚持定时器。
TCP的接收⽅通过通告窗⼝⼤⼩来告诉发送⽅⾃⼰可以接收的数据字节数,接收⽅采⽤这种⽅式来进⾏流量控制。假如接收⽅通告的窗⼝⼤⼩为0会发⽣什么情况呢?这将有效地阻⽌发送⽅传送数据,直到通告窗⼝变为⾮0为⽌。
发送⽅接到0窗⼝通告时,则会停⽌数据段的发送,直到接收⽅通过⾮0的窗⼝。很重要的⼀点,TCP必须能够处理含新⾮0窗⼝通告的数据包丢失的情况,通常这个⾮0窗⼝通告是在⼀个不含任何数据的ACK包中发送的。ACK的传输并不可靠,也就是说,TCP不对ACK报⽂段进⾏确认(很明显,也就不会存在该ACK报⽂段的重发),TCP只确认那些包含有数据的ACK报⽂段。
如果⼀个确认丢失了,则双⽅就有可能因为等待对⽅⽽使连接终⽌:接收⽅等待接收数据(因为它已经向发送⽅通告了⼀个⾮ 0的窗⼝),⽽发送⽅在等待允许它继续发送数据的⾮0窗⼝更新。为防⽌这种死锁情况的发⽣,发送⽅使⽤⼀个坚持定时器 (persisttimer)来周期性地向接收⽅查询,以便发现窗⼝是否已增⼤。这些从发送⽅发出的报⽂段称为窗⼝探查 (windowprobe)。
控制块中有两个字段与坚持定时器有关:persist_cnt和persist_backoff。persist_cnt⽤于坚持定时器计数,当计数值超过某个值时,则发出窗⼝探查数据包。persist_backoff表⽰坚持定时器是否被启动(是否>0)以及已经发出去了⼏个探查数据包
(persist_backoff为⼤于0的整数时)。若坚持定时器已经被启动,则在内核500ms中断处理函数tcp_slowtmr会进⾏如下处理:
if (pcb->persist_backoff> 0) {  // 如果坚持定时器已经开启
pcb->persist_cnt++;  // 增加计数值
if (pcb->persist_cnt>=tcp_persist_backoff[pcb->persist_backoff-1]) { //计数值超过
// 某个计数值上限时则进⾏窗⼝探查
pcb->persist_cnt = 0; //复位计数值
if (pcb->persist_backoff< sizeof(tcp_persist_backoff)) { // 增加计数值上限
pcb->persist_backoff++;
}
tcp_zero_window_probe(pcb); //发送⼀个窗⼝探查包
}
}
有两点需要提及的。⼀是数组tcp_persist_backoff,它保存了⼀系列的坚持定时器的计数值上限,persist_backoff是该数组的索引。即发送第⼀个探测包的时间为3次500ms(1.5s)中断后,发送第⼆个探测包的时间为6次(3s)后,当第六次及其以上发送探测包时,时间间隔都变为120次中断(60s)。
const u8_t tcp_persist_backoff[7] = {3, 6, 12, 24, 48, 96, 120 };
再来看看函数tcp_zero_window_probe是如何进⾏窗⼝探查的。tcp_zero_window_probe函数很简单,组装⼀个含⼀字节数据的TCP报⽂段发送出去,这个字节的数据是从unacked或从unsent队列上取得的,且窗⼝探查包的数据序号字段被填成这个字节的数据序号。所以当这两个队列都为空时,则表⽰没有任何数据需要处理,当然窗⼝探查也没有必要进⾏;当这个字节的数据是从unacked队列中得到时,由于该队列是已经被发送过的,对应窗⼝探查到达接收端时,会被看做是重复报⽂⽽不进⾏
相关数据处理,只向发送⽅是返回⼀个ACK包;当这个字节的数据是从unsent队列中得到时,则这个字节数据到达接收端时会被挂接在ooseq队列或直接递交给上层应⽤,当发送⽅窗⼝允许时,unsent队列中的第⼀个数据段的第⼀个字节被发送后在接收⽅看来是重复的,接收⽅能够检测出这个重复的字节,并直接删除该字节数据。从整个过程可以看出,窗⼝探查包⾥⾯的1字节数据并不影响整个数据传输过程。
什么时候启动⼀个窗⼝探查呢?这是在函数tcp_output最后完成的。当发送完能够发送的数据段后,unsent队列还不为空,且此时窗⼝探查未启动,且当前窗⼝太⼩以⾄不能发送下⼀个数据段,此时要启动窗⼝探查。
if (seg != NULL&&pcb->persist_backoff == 0&&
ntohl(seg->tcphdr->seqno) -pcb->lastack + seg->len> pcb->snd_wnd) {
pcb->persist_cnt =0;    //复位计数值
pcb->persist_backoff =1;  // 开始窗⼝探查
}
什么时候停⽌⼀个窗⼝探查呢?从前⾯已经知道,在函数tcp_receive刚开始的部分,就会根据接收数据包的情况更新发送窗⼝,也即是在这⾥若检测到⼀个⾮0窗⼝,则停⽌窗⼝探查,如下所⽰。
if(TCP_SEQ_LT(pcb->snd_wl1, seqno)
(pcb->snd_wl1 == seqno&&TCP_SEQ_LT(pcb->snd_wl2, ackno))
(pcb->snd_wl2 == ackno&& tcphdr->wnd> pcb->snd_wnd)) { // 若满⾜窗⼝跟新条件
tcpip协议pdfpcb->snd_wnd =tcphdr->wnd;  // 窗⼝更新
pcb->snd_wl1 =seqno;
pcb->snd_wl2 =ackno;
if (pcb->snd_wnd> 0 &&pcb->persist_backoff > 0){  检测到⾮0窗⼝且探查开启
pcb->persist_backoff =0;  // 停⽌窗⼝探查
}
}
再来看保活定时器。如果⼀个已经处于稳定状态的TCP连接双⽅都没有向对⽅发送数据,则在两个TCP模块之间不交换任何信息。然⽽很多时候,连接的双⽅都希望知道对⽅的是否处于⾮活动状态。常见的状况是⼀个服务器希望知道客户主机是否崩溃并关机或者崩溃⼜重新启动,许多TCP/IP实现中提供的保活定时器可以提供这种检测功能。
保活功能主要是为服务器应⽤程序提供的。服务器应⽤程序希望知道客户主机是否崩溃,从⽽可以合理分配客户使⽤资源。如果⼀个给定的连接在两个⼩时之内没有任何动作,则服务器就向客户发送⼀个探查报⽂段。客户主机必处于以下4个状态之⼀:
1)客户主机依然正常运⾏,并从服务器可达。客户的TCP响应正常,⽽服务器也知道对⽅是正常⼯作的。服务器在两⼩时以后将保活定时器复位,并发送探查报⽂。如果在两个⼩时定时器到时间之前有应⽤程序的通信量通过此连接,则定时器在交换数据后的未来2⼩时再复位,发送探查报⽂。
2)客户主机已经崩溃,并且关闭或者正在重新启动,在这些情况下,客户的TCP都不会有任何响应。服务器将不能够收到对探查报⽂的响应,并在等待75秒后超时,以后服务器还会发送9个这样的探查报⽂,每个间隔75秒。如果服务器没有收到⼀个响应,它就认为客户主机已经关闭并终⽌连接。
3)客户主机崩溃并已经重新启动。这时服务器将收到⼀个对其保活探查的响应,但是这个响应是⼀个复位,使得服务器终⽌这个连接。
4)客户主机正常运⾏,但是从服务器不可达。这与状态2相同,因为TCP不能够区分状态4与状态2之间的区别,它所能发现的就是没有收到探查的响应。
在第1种情况下,服务器的应⽤程序没有感觉到保活探查的发⽣。TCP层负责⼀切,这个过程对应⽤程序都是不可见的。当第2、3或4种情况发⽣时,服务器应⽤程序将收到来⾃它的TCP层的差错报告(通常服务器应⽤程序向⽹络发出了读操作请求,然后等待来⾃客户的数据。如果保活功能返回⼀个差错,则该差错将作为读操作的返回值返回给应⽤程序)。在第2种情况下,差错是诸如“连接超时”之类的信息,⽽在第3种情况则为“连接被对⽅复位” 。第4种情况看起来像是连接超时,也可根据是否收到与连接有关的ICMP差错报⽂来判断是否是⽬的不可达引起的。
⾄此⼜会涉及TCP控制块中四个字段:keep_idle、keep_intvl、keep_cnt和keep_cnt_sent。其中keep_intvl和keep_cnt与编译选项LWIP_TCP_KEEPALIVE相关,当该编译选项为1时,keep_intvl和keep_cnt字段分别⽤于保存⽤户⾃定义的保活时间选项值,这点在后⾯介绍。实际应⽤中使⽤系统默认的保活时间选项值即可,所以我们将LWIP_TCP_KEEPALIVE设置为0,则keep_intvl和keep_cnt字段不会被编译,⾃然也不在我们的讨论范围之内了。
keep_idle字段记录了在多久后进⾏保活探测,⼀般为2⼩时,keep_cnt_sent字段表⽰已经发送的保活数据包的个数。除了这两个字段外,还有⼏个与默认保活时间选项值宏定义:
#define TCP_KEEPIDLE_DEFAULT    7200000UL  // 保活时间毫秒数(2⼩时)
#define TCP_KEEPINTVL_DEFAULT  75000UL //连续保活包的时间间隔毫秒数(75s)
#define TCP_KEEPCNT_DEFAULT    9U  // 保活包被重复发送的次数
#define  TCP_MAXIDLE TCP_KEEPCNT_DEFAULT * TCP_KEEPINTVL_DEFAULT
//执⾏保活探测需要消耗的时间
⽤户可以通过宏LWIP_TCP_KEEPALIVE允许keep_intvl和keep_cnt字段,它们分别⽤于记录⽤户⾃定义的保活包时间间隔与保活包个数。当不使⽤⾃定义值时,就⽤上⾯的两个DEFAULT值作为保活选项值。
在这⾥,我们使⽤系统默认的保活选项值来分析保活的整个过程,此时字段keep_idle被设置为TCP_KEEPIDLE_DEFAULT的值。保活处理也是在内核500ms中断处理函数tcp_slowtmr中进⾏的。TCP控制块中还有⼀个字段要重新提及⼀下,即tmr记录了该TCP连接上最近⼀个数据段到来时的系统时间tcp_ticks值。
if((pcb->so_options& SOF_KEEPALIVE) && // 如果开启了保活功能,稳定数据交互状态
((pcb->state ==ESTABLISHED) || (pcb->state == CLOSE_WAIT))) {
if((u32_t)(tcp_ticks -pcb->tmr) >  //2⼩时+9*75秒后断开连接
(pcb->keep_idle +TCP_MAXIDLE) / TCP_SLOW_INTERVAL)
{
tcp_abort(pcb);  // 断开连接
}
else if((u32_t)(tcp_ticks -pcb->tmr) > // 在2⼩时+9*75秒内则发送保活包
(pcb->keep_idle +pcb->keep_cnt_sent * TCP_KEEPINTVL_DEFAULT)
/ TCP_SLOW_INTERVAL)
{
tcp_keepalive(pcb);    // 发送保活包
pcb->keep_cnt_sent++;  //保活包次数加1
}
}
tcp_abort函数⽤于释放⼀个连接,主要⼯作包括将控制块从相应的TCP链表中删除,若该连接上还有数据则释放数据所占⽤的内存空间,最后向对⽅发送⼀个RST数据包。tcp_keepalive函数⽤于发送⼀个保活包,保活包只是⼀个TCP⾸部,并不包含任何数据,所以不会对对⽅的数据接收造成影响。
关于保活选项的两个⼩时的空闲时间是可以改变。⽤户只要⾃⼰定义keep_idle的值就可以了,但是系统⼀般不建议修改这些值。
Don'tchange this unless you know what you're doing!哈哈。。。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。