参考《TCP-IP详解卷 1:协议》"第23章 TCP的保活定时器"。
1、TCP的保活定时器介绍
TCP的保活定时器主要是在双方都没有数据收发过程中,用于确认对方是否仍然在线(keep alive;网络断开可能导致连接断开,服务器重启也可能导致连接断开,很多没有收到FIN报文就已经断开了的情况,因此需要保活定时器检测连接是否已经断开)。
2、TCP的保活定时器实现
2.1、KEEPALIVE发送次数重置
连接设置了SOF_KEEPALIVE,那么tcp keep alive就会计时,与超时重传不一样,收到报文时KEEPALIVE发送次数keep_cnt_sent重置为0(收到报文表明对方仍然有保持连接;tcp_input调用tcp_process重置KEEPALIVE发送次数)。
/* Update the PCB (in)activity timer. */
pcb->tmr = tcp_ticks; // tmr设置为收到报文的时间
pcb->keep_cnt_sent = 0; // KEEPALIVE发送次数重置为0
2.2、KEEPALIVE超时检测
tcp定时器周期性调用tcp_slowtmr,对定时器计时、检查是否有定时器超时。
KEEPALIVE超时检测是在tcp_slowtmr函数中。
KEEPALIVE总时间是否超时。tcp_ticks为当前时间,pcb->tmr为连接最后一次收到对方报文的时间,"tcp_ticks - pcb->tmr"即为多久没有收到对方的报文;pcb->keep_idle第一次发送KEEPALIVE前保持idle状态的时间,最后一次收到报文之后的keep_idle时间内不发送KEEPALIVE报文,pcb->keep_cnt*pcb->keep_intvl为允许发送KEEPALIVE的总时间(可以发送的次数乘以每次发送时间间隔)。
如果pcb->keep_cnt*pcb->keep_intvl时间没有收到对方的数据,那么认为对方已经断开连接或者不可达,直接关闭本地连接。
/* Check if KEEPALIVE should be sent */
if((pcb->so_options & SOF_KEEPALIVE) && // 连接设置了KEEPALIVE选项
((pcb->state == ESTABLISHED) || // ESTABLISHED连接状态
(pcb->state == CLOSE_WAIT))) { // CLOSE_WAIT等待关闭状态
#if LWIP_TCP_KEEPALIVE
if((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + (pcb->keep_cnt*pcb->keep_intvl))
/ TCP_SLOW_INTERVAL) // KEEPALIVE总时间超时,关闭本地连接
#else
if((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + TCP_MAXIDLE) / TCP_SLOW_INTERVAL)
#endif /* LWIP_TCP_KEEPALIVE */
{
LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: KEEPALIVE timeout. Aborting connection to %"U16_F".%"U16_F".%"U16_F".%"U16_F".\n",
ip4_addr1(&pcb->remote_ip), ip4_addr2(&pcb->remote_ip),
ip4_addr3(&pcb->remote_ip), ip4_addr4(&pcb->remote_ip)));
tcp_abort(pcb); // 关闭本地连接
}
如果KEEPALIVE总时间没有超时,检查距离上一次发送KEEPALIVE报文的时间,确定是否需要再次发送KEEPALIVE报文。keep_cnt_sent为已经发送KEEPALIVE报文的次数。
#if LWIP_TCP_KEEPALIVE
else if((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + pcb->keep_cnt_sent * pcb->keep_intvl)
/ TCP_SLOW_INTERVAL)
#else
else if((u32_t)(tcp_ticks - pcb->tmr) >
(pcb->keep_idle + pcb->keep_cnt_sent * TCP_KEEPINTVL_DEFAULT)
/ TCP_SLOW_INTERVAL)
#endif /* LWIP_TCP_KEEPALIVE */
{
tcp_keepalive(pcb); // 发送KEEPALIVE报文
pcb->keep_cnt_sent++; // KEEPALIVE发送次数keep_cnt_sent加1
}
3、KEEPALIVE报文的发送
lwip调用tcp_keepalive发送KEEPALIVE报文。tcp_keepalive发送一个已经发送但是没有数据的报文(seqno已经发送过,seqno为最后一个报文的最后一个字节序号,与坚持定时器不一样,坚持定时器发送的是已经发送报文第一个字节的序号,KEEPALIVE报文发送时,正常情况下发送队列没有数据,对方如果还没断开连接,对方收到一个重发序号的报文时,将发送ACK,发送方收到ACK报文则认为对方仍处于alive状态,KEEPALIVE重传次数重置为0)。
void
tcp_keepalive(struct tcp_pcb *pcb)
{
struct pbuf *p;
struct tcp_hdr *tcphdr;
LWIP_DEBUGF(TCP_DEBUG, ("tcp_keepalive: sending KEEPALIVE probe to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\n",
ip4_addr1(&pcb->remote_ip), ip4_addr2(&pcb->remote_ip),
ip4_addr3(&pcb->remote_ip), ip4_addr4(&pcb->remote_ip)));
LWIP_DEBUGF(TCP_DEBUG, ("tcp_keepalive: tcp_ticks %"U32_F" pcb->tmr %"U32_F" pcb->keep_cnt_sent %"U16_F"\n",
tcp_ticks, pcb->tmr, pcb->keep_cnt_sent));
p = pbuf_alloc(PBUF_IP, TCP_HLEN, PBUF_RAM);
if(p == NULL) {
LWIP_DEBUGF(TCP_DEBUG,
("tcp_keepalive: could not allocate memory for pbuf\n"));
return;
}
LWIP_ASSERT("check that first pbuf can hold struct tcp_hdr",
(p->len >= sizeof(struct tcp_hdr)));
tcphdr = tcp_output_set_header(pcb, p, 0, htonl(pcb->snd_nxt - 1)); // pcb->snd_nxt - 1已经发送报文的序号
#if CHECKSUM_GEN_TCP
tcphdr->chksum = inet_chksum_pseudo(p, &pcb->local_ip, &pcb->remote_ip,
IP_PROTO_TCP, p->tot_len);
#endif
TCP_STATS_INC(tcp.xmit);
/* Send output to IP */
#if LWIP_NETIF_HWADDRHINT
ip_output_hinted(p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl, 0, IP_PROTO_TCP,
&(pcb->addr_hint)); // 报文发送(不缓存到unacked队列,KEEPALIVE超时会再次发送KEEPALIVE报文)
#else /* LWIP_NETIF_HWADDRHINT*/
ip_output(p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl, 0, IP_PROTO_TCP);
#endif /* LWIP_NETIF_HWADDRHINT*/
pbuf_free(p);
LWIP_DEBUGF(TCP_DEBUG, ("tcp_keepalive: seqno %"U32_F" ackno %"U32_F".\n",
pcb->snd_nxt - 1, pcb->rcv_nxt));
}