前两篇文章中我们讲到,shutdown和close方法会发送fin消息给对方,开始tcp连接的关闭流程,现在我们从源码角度看下tcp连接关闭的具体过程,以及中间发送的消息和涉及到的各种状态。
假设客户端先调用了shutdown方法,发起了tcp连接关闭请求,由之前的文章我们可以知道,此时客户端会从TCP_ESTABLISHED状态切换到TCP_FIN_WAIT1状态,shutdown方法同时会发送fin消息给服务端。
下面看下服务端如何处理fin消息的。
由之前的文章可知,ip层在收到消息之后,会通过回调tcp_v4_rcv方法将消息转给tcp层。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
int ret;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
return ret;
...
}
该方法会先根据ip、端口等信息,找到对应的sk,再调用tcp_v4_do_rcv方法,继续处理fin消息。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
...
tcp_rcv_established(sk, skb, tcp_hdr(skb));
return 0;
}
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
由于此时服务端的sk->sk_state处于TCP_ESTABLISHED状态,所以skb的处理流程会进入到tcp_rcv_established方法。
// net/ipv4/tcp_input.c
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th)
{
...
tcp_data_queue(sk, skb);
...
tcp_ack_snd_check(sk);
return;
...
}
EXPORT_SYMBOL(tcp_rcv_established);
该方法先调用tcp_data_queue处理fin消息,再调用tcp_ack_snd_check方法
// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
...
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
...
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
...
return;
}
...
}
由上可见,fin消息的处理流程会进入到tcp_fin方法。
// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
...
sk->sk_shutdown |= RCV_SHUTDOWN;
sock_set_flag(sk, SOCK_DONE);
switch (sk->sk_state) {
...
case TCP_ESTABLISHED:
/* Move to CLOSE_WAIT */
tcp_set_state(sk, TCP_CLOSE_WAIT);
...
break;
...
}
...
}
方法描述
1. 标记sk->sk_shutdown字段包含RCV_SHUTDOWN。
由上两篇文章可以知道,shutdown和close方法在发送fin消息之前,会先标记sk->sk_shutdown字段包含SEND_SHUTDOWN,即发送fin消息后,客户端是SEND_SHUTDOWN,服务端是RCV_SHUTDOWN。
2. 设置sk的flag为SOCK_DONE。
3. 由于此时服务端的sk状态还是TCP_ESTABLISHED,所以该方法会调用tcp_set_state方法,将sk->sk_state状态设置为TCP_CLOSE_WAIT,即等待应用层关闭tcp连接。
tcp_fin方法处理完之后,上面的tcp_ack_snd_check方法会发送ack给客户端,现在我们再从客户端角度看下ack消息的处理流程。
还是先看tcp_v4_rcv方法。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
}
该方法会先根据ip、端口等信息找到对应的sk,再调用tcp_v4_do_rcv方法,对这个sk继续进行ack消息的逻辑处理。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (tcp_rcv_state_process(sk, skb)) {
...
}
return 0;
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
该方法又调用了tcp_rcv_state_process方法
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT1: {
...
tcp_set_state(sk, TCP_FIN_WAIT2);
sk->sk_shutdown |= SEND_SHUTDOWN;
...
}
...
}
...
return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);
由于客户端的sk此时处于TCP_FIN_WAIT1状态,当收到ack消息后,该方法会将sk的状态修改为TCP_FIN_WAIT2。
至此,由客户端到服务端的tcp流就已经被完全关闭。
当服务端应用层调用了shutdown或close方法后,根据前两篇文章我们可以知道,shutdown或close方法会先调用tcp_close_state方法,将服务端sk的状态由TCP_CLOSE_WAIT修改为TCP_LAST_ACK,然后再调用tcp_send_fin方法,发送fin消息给客户端。
继续看下客户端fin消息的处理流程,还是从tcp_v4_rcv方法看起。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
}
处理流程和之前差不多,最终还是调用tcp_v4_do_rcv方法。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (tcp_rcv_state_process(sk, skb)) {
...
}
return 0;
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
该方法又会调用tcp_rcv_state_process方法。
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT2:
...
/* Fall through */
case TCP_ESTABLISHED:
tcp_data_queue(sk, skb);
...
break;
}
...
return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);
由于此时客户端的sk的状态为TCP_FIN_WAIT2,该方法最终会调用tcp_data_queue继续处理fin消息。
// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
...
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
...
if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
tcp_fin(sk);
...
return;
}
...
}
该方法又调用了tcp_fin方法。
// net/ipv4/tcp_input.c
void tcp_fin(struct sock *sk)
{
...
switch (sk->sk_state) {
...
case TCP_FIN_WAIT2:
/* Received a FIN -- send ACK and enter TIME_WAIT. */
tcp_send_ack(sk);
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
break;
...
}
...
}
由于此时客户端的sk是TCP_FIN_WAIT2状态,所以会执行如上代码
1. 调用tcp_send_ack方法,发送ack给服务端。
2. 调用tcp_time_wait方法,先将sk状态设置为TCP_TIME_WAIT,再开启TIME_WAIT定时,超时后销毁这个sk。
当sk处于TCP_TIME_WAIT状态时,会一直占用对应的ip和端口,防止其他连接再次使用,从而出现错误。
由上可见,谁先发起的tcp连接关闭请求,谁最终就会进入到TIME_WAIT状态,在写服务器端代码时,这个是要注意的。
现在剩下最后一步,即服务端ack消息的处理。
还是从tcp_v4_rcv方法看起。
// net/ipv4/tcp_ipv4.c
int tcp_v4_rcv(struct sk_buff *skb)
{
...
const struct tcphdr *th;
...
struct sock *sk;
...
th = (const struct tcphdr *)skb->data;
...
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
...
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
...
}
...
}
和之前一样,该方法最终还是会调用tcp_v4_do_rcv方法。
// net/ipv4/tcp_ipv4.c
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
if (tcp_rcv_state_process(sk, skb)) {
...
}
return 0;
...
}
EXPORT_SYMBOL(tcp_v4_do_rcv);
该方法又调用了tcp_rcv_state_process方法。
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
...
switch (sk->sk_state) {
...
case TCP_LAST_ACK:
if (tp->snd_una == tp->write_seq) {
...
tcp_done(sk);
goto discard;
}
break;
}
...
return 0;
}
EXPORT_SYMBOL(tcp_rcv_state_process);
由于此时服务器的sk状态为TCP_LAST_ACK,所以该方法最终会调用tcp_done方法。
// net/ipv4/tcp.c
void tcp_done(struct sock *sk)
{
...
tcp_set_state(sk, TCP_CLOSE);
...
sk->sk_shutdown = SHUTDOWN_MASK;
...
}
EXPORT_SYMBOL_GPL(tcp_done);
该方法会先将sk状态设置为TCP_CLOSE,之后再设置sk->sk_shutdown字段的值为SHUTDOWN_MASK,即SEND_SHUTDONW & RCV_SHUTDOWN。
至此,服务端的sk就已经完全关闭。
客户端的sk等TIME_WAIT状态的定时超时之后,也会自动关闭。
这样,tcp连接的关闭流程就完整了。
本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!