#TCP你学得会# 之 client重用链接之时

    好吧,原本已经写好的博文在我点击保存以后仍是莫名丢失了,人生就是这样,到处均可能充满了狗血,但连滚带爬也不能屈服,因而深夜从新来过,文思可能不如上一次那么流畅,但好在是技术文章,主要看气质吧。并发

    下面是华丽的分割线less

============================================================socket

    继续前面的话题。tcp

    咱们已经知道,当client端断开链接(close)以后,server端进入CLOSE_WAIT状态,若是server端不进行任何动做的话这个状态能够一直持续下去,而client端的链接则最终会由于超时而销毁。函数

    那么,随之而来,新的问题又来了,若是一段时间以后client端复用了以前的四元组信息发起新的链接请求,又会是什么样的情形呢?spa

    下面先经过抓包来直观地感觉一下这个过程(为了便于分析tcp报文中的sequence number,特别取消了Wireshark中的相对序列号分析功能,Edit--->Preferences--->Protocols--->TCP--->Analyze TCP sequence numbers):code

    上图的抓包能够分为这么几个阶段:
server

    1)client端向server端发起链接并发送若干数据段(No. 1  -  No. 22);ip

    2)client端关闭链接,server端对数据和FIN分别回应ACK后没有后续动做,这时server端将维持在CLOSE_WAIT状态,而client端则在FIN_WAIT_2状态下暂停一段时间后超时退出(No.23  -  No.25);
原型

    3)client端复用以前的四元组从新发起链接,server端以ACK进行回应,且应答号与SYN中的初始序列号不一致,而是延续了上一个链接的应答号,这触发了client端的RST(No.26  -  No.28);

    4)约1s以后client端协议栈尝试从新创建链接并成功;

    

    下面咱们就去Kernel里面扒一扒对应的实现吧,重点集中在如下三个场景:

  • 处于CLOSE_WAIT状态的链接收到一个复用的新链接请求时会怎么样?

  • 处于SYN_SENT状态的链接收到一个序列号不匹配的应答时会怎么样?

  • 处于CLOSE_WAIT状态的链接收到RST时会怎样?

    Kernel版本3.6.1。

    协议栈中处理tcp协议的入口函数是tcp_v4_rcv,其原型以下:

int tcp_v4_rcv(struct sk_buff *skb);

    函数tcp_v4_rcv只有一个参数,就是接收报文的sk_buff结构,在该函数中会以四元组信息(sip, sport, dip, dport)来计算hash值,并根据计算结果分别在established表和listen表中查找对应的struct sock* 结构。因为client端复用了四元组信息,且server端的链接并无销毁,所以可以匹配到原有的struct sock* 结构,该结构中的状态信息sk_state将做为后续处理的重要依据。接着流程转入tcp_v4_do_rcv函数:

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb);

    能够看到,tcp_v4_do_rcv函数的参数除了存放报文内容的sk_buff结构外,就是上一步中查到的struct sock* 结构啦,这个函数内只处理了TCP_ESTABLISHED和TCP_LISTEN两种状态,更多的内容交付给了tcp_rcv_state_process函数:

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  const struct tcphdr *th, unsigned int len)

    tcp_rcv_state_process函数中的内容可就丰富多了,全部的状态几乎都在这里等待检阅,这也是本文的重点所在,对照上面总结的三个场景来分别进行分析。

    1) 处于CLOSE_WAIT状态的链接收到一个复用的新链接请求时会怎么样:

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  const struct tcphdr *th, unsigned int len)
{
    ... ...
    /* step 7: process the segment text */
    switch (sk->sk_state) {
        case TCP_CLOSE_WAIT:
	case TCP_CLOSING:
	case TCP_LAST_ACK:
            if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt))/*报文中的序列号晚于指望接收的序列号*/
                break;
	case TCP_FIN_WAIT1:
	case TCP_FIN_WAIT2:
	    /* RFC 793 says to queue data in these states,
	    * RFC 1122 says we MUST send a reset.
	    * BSD 4.4 also does reset.
	    */
	    if (sk->sk_shutdown & RCV_SHUTDOWN) {
                if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
                    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
                    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
                    tcp_reset(sk);
                    return 1;
		}
	    }
	/* Fall through */
	case TCP_ESTABLISHED:
	    tcp_data_queue(sk, skb);
	    queued = 1;
	    break;
    }

    /* tcp_data could move socket to TIME-WAIT */
    if (sk->sk_state != TCP_CLOSE) {
        tcp_data_snd_check(sk);
        tcp_ack_snd_check(sk);
    }

    if (!queued) {
discard:
        __kfree_skb(skb);
    }
    return 0;
}

    能够看到,在处于CLOSE_WAIT状态的链接上收到一个报文的序列号晚于(大于)指望接收的序列号时,会以ACK来进行回应。

    2) 处于SYN_SENT状态的链接收到一个序列号不匹配的应答时会怎么样:

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  const struct tcphdr *th, unsigned int len)
{
    ... ...
    case TCP_SYN_SENT:
        queued = tcp_rcv_synsent_state_process(sk, skb, th, len);
	if (queued >= 0)
	    return queued;

	/* Do step6 onward by hand. */
	tcp_urg(sk, skb, th);
	__kfree_skb(skb);
	tcp_data_snd_check(sk);
	return 0;
    }
    ... ...
}


static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
					 const struct tcphdr *th, unsigned int len)
{
    ... ...
    if (th->ack) {
        /* rfc793:
	* "If the state is SYN-SENT then
	*    first check the ACK bit
	*      If the ACK bit is set
	*	  If SEG.ACK =< ISS, or SEG.ACK > SND.NXT, send
	*        a reset (unless the RST bit is set, if so drop
	*        the segment and return)"
	*/
	if (!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_una) ||
	    after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))
	    goto reset_and_undo;

	if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
            !between(tp->rx_opt.rcv_tsecr, tp->retrans_stamp,
            tcp_time_stamp)) {
            NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSACTIVEREJECTED);
            goto reset_and_undo;
        }

	/* Now ACK is acceptable.
        *
	* "If the RST bit is set
	*    If the ACK was acceptable then signal the user "error:
	*    connection reset", drop the segment, enter CLOSED state,
	*    delete TCB, and return."
	*/
    ... ...
    /* "fifth, if neither of the SYN or RST bits is set then
    * drop the segment and return."
    */

discard_and_undo:
    tcp_clear_options(&tp->rx_opt);
    tp->rx_opt.mss_clamp = saved_clamp;
    goto discard;

reset_and_undo:
    tcp_clear_options(&tp->rx_opt);
    tp->rx_opt.mss_clamp = saved_clamp;
    return 1;
}

    tcp_rcv_synsent_state_process函数中的注释很明确的说明了当链接处在SYN_SENT状态时首先检查ACK位,若是该 ACK位置位而且报文中的应答号不大于初始序列号,则触发RST。

    3) 处于CLOSE_WAIT状态的链接收到RST时会怎样:

    在tcp_rcv_state_process函数中有一段调用:

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  const struct tcphdr *th, unsigned int len)
{
    ... ...
    if (!tcp_validate_incoming(sk, skb, th, 0))
	return 0;
    ... ...
}


static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb,
				  const struct tcphdr *th, int syn_inerr)
{
    ... ...
    /* Step 2: check RST bit */
    if (th->rst) {
	/* RFC 5961 3.2 :
	* If sequence number exactly matches RCV.NXT, then
	*     RESET the connection
	* else
	*     Send a challenge ACK
	*/
	if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt)
	    tcp_reset(sk);
	else
	    tcp_send_challenge_ack(sk);
	goto discard;
	}
    ... ...
}

    能够看到,当收到RST且报文中携带的序列号与指望序列号一致时,会调用tcp_reset函数,这个函数在以前的文章中也有分析过,其中会对处于CLOSE_WAIT状态的链接赋予EPIPE错误码,并触发SIGPIPE信号。

    至此,整个流程就合拢了。


    PS.  聪明的你必定知道tcp端口号是如何复用的,没错,就是bind函数咯。

相关文章
相关标签/搜索