第二次挥手
tcp_fin
在服务端进入稳定状态之后,处理数据包的顶层函数为 tcp_rcv_established ,其中该函数根据一些条件划分为了快处理和慢处理两种方式,再通过 tcp_data_queue 放入队列,当该数据包是FIN
包,那么会进入 tcp_fin 函数:
void tcp_fin(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
inet_csk_schedule_ack(sk);
sk->sk_shutdown |= RCV_SHUTDOWN; // 收到了FIN包,说明对端已经关闭了SEND端,因此从接收到这个包开始(注意前面的数据包已经处理完了),那么本端就关闭RCV端
sock_set_flag(sk, SOCK_DONE);
switch (sk->sk_state) {
case TCP_SYN_RECV:
case TCP_ESTABLISHED: // 当前是ESTABLISHED状态
/* Move to CLOSE_WAIT */
tcp_set_state(sk, TCP_CLOSE_WAIT); // 进入CLOSE_WAIT状态
inet_csk_enter_pingpong_mode(sk); // pingpong??
break;
// .........
}
/* It _is_ possible, that we have something out-of-order _after_ FIN.
* Probably, we should reset in this case. For now drop them.
*/
skb_rbtree_purge(&tp->out_of_order_queue); // 清空乱序序列队列上的数据包
if (tcp_is_sack(tp)) // 如果开启了SACK
tcp_sack_reset(&tp->rx_opt); // 关闭SACK
if (!sock_flag(sk, SOCK_DEAD)) { // 如果未处于DEAD状态
sk->sk_state_change(sk); // 唤醒正在等待该套接口的进程
/* Do not send POLL_HUP for half duplex close. */
if (sk->sk_shutdown == SHUTDOWN_MASK ||
sk->sk_state == TCP_CLOSE) // 如果在接收发送方向都关闭了,或者已经处于CLOSE状态,那么唤醒等待套接字的进程,通知链接已经停止
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_HUP);
else // 否则,通知链接可以进行写操作
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
}
}
在上面函数返回之后,在 tcp_rcv_established 函数内会根据条件判断是否需要发送ACK
报文,并进入CLOSE_WAIT
状态。
tcp_rcv_state_process
在服务端发出了第二次挥手之后,此时客户端处于FIN_WAIT_1
状态,因此此时对于数据包处理的函数为 tcp_rcv_state_process,相关部分的代码实现如下:
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
// .....
if (!th->ack && !th->rst && !th->syn) { // 这里就起到了过滤当第二次发出的ACK丢失,而服务端又直接下发第三次挥手包的情况,这里会被直接过滤掉,不作理会
SKB_DR_SET(reason, TCP_FLAGS);
goto discard;
}
// .....
switch (sk->sk_state) {
case TCP_SYN_RECV:
// ...
case TCP_FIN_WAIT1: {
int tmo;
if (req)
tcp_rcv_synrecv_state_fastopen(sk); // 处于FIN_WAIT_1状态的TCP还有request_sock结构??这里的操作时删除掉request_sock结构
if (tp->snd_una != tp->write_seq) // 序列号不对
break;
tcp_set_state(sk, TCP_FIN_WAIT2); // 进入FIN_WAIT_2状态
sk->sk_shutdown |= SEND_SHUTDOWN; // 此时说明客户端的发送要关闭这件事已经让对方知晓,这里再|=SEND_SHUTDOWN是为了关闭发送端,有一点需要注意,在tcp_close中是进入函数时就设置sk_shutdown = SHUTDOWN_MASK 即同时关闭读写端,而tcp_shutdown则是等到进入FIN_WAIT_2阶段后再关闭读端
sk_dst_confirm(sk);
if (!sock_flag(sk, SOCK_DEAD)) { // sock还没DEAD
/* Wake up lingering close() */
sk->sk_state_change(sk); // 唤醒正在等待的进程
break;
}
if (tp->linger2 < 0) { // 如果linger2<0,说明上层通过setsockopt设置了FIN_WAIT状态不等待,因此这里直接关闭socket
tcp_done(sk);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
return 1;
}
if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) { // 收到了一个超出序列号范围的FIN包???这里的处理是直接关闭
/* Receive out of order FIN after close() */
if (tp->syn_fastopen && th->fin)
tcp_fastopen_active_disable(sk);
tcp_done(sk);
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
return 1;
}
tmo = tcp_fin_time(sk);
if (tmo > TCP_TIMEWAIT_LEN) { // 如果FIN_WAIT_2阶段的等待时间还大于1分钟,那么这里通过保活定时器去等待差值时间,而在差值时间到了之后,会直接进入TIME_WAIT状态,即调用tcp_time_wait函数
inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
} else if (th->fin || sock_owned_by_user(sk)) {
/* Bad case. We could lose such FIN otherwise.
* It is not a big problem, but it looks confusing
* and not so rare event. We still can lose it now,
* if it spins in bh_lock_sock(), but it is really
* marginal case.
*/
inet_csk_reset_keepalive_timer(sk, tmo);
} else {
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); // 进入TIME_WAIT状态,这里就会释放掉重量的sock结构,转而使用inet_timewait_sock结构,因此TIME_WAIT状态占用的内存会少很多
goto consume;
}
break;
}
// ......
}
第二次挥手丢失,会发生什么?
第二次挥手实际上是 ACK 报文,而 ACK 报文是不会进行定时重发的,因此如果该数据报丢失了,那么只能等待客户端因为迟迟未能接收到 ACK 回复,那么则会定时重发FIN
报文,重发的次数上限为tcp_orphan_retries(默认0,代码处理为8)
。
如果在客户端还没有达到重发上限之前,服务端下发了FIN
报文,由于客户端还处于FIN_WAIT_1
状态,那么会直接忽略掉这个包
如果客户端因为达到重发上限而转为了CLOSE
状态,此时客户端不再存在五元组对应的 socket,然后服务端下发了FIN
报文,那么服务端会收到RST
报文,进而关闭服务端的 TCP。服务端的第三次挥手也是有超时时间的,也是 tcp_orphan_retries,因此如果第二次挥手包丢失,往往都是客户端先重发 FIN 报文到超时后转为 close 状态。