zoukankan      html  css  js  c++  java
  • 网络协议栈(18)tcp连接关闭时时序

    一、四次挥手
    三次握手是TCP协议中的一个经典问题,几乎所有的网络公司面试都会问到这个问题,以至于人们甚至忽略了更加有意义的挥手过程。无论从挥手的实现代码量还是在实际工程种可能引发的问题,挥手的过程都是比三次握手要更加的复杂。在TCP的状态转换过程中,大部分都是和连接关闭相关的操作,反过来看一下三次握手的实现,时序相对还是简单一些。
    想一个原因大概是这样的,握手的时候是一个双方你情我愿的过程,服务器在特定端口上执行侦听操作,相当于做出了海纳百川的气魄和姿态,而客户端的连接则也是主动投怀送抱,直接连接到了服务器的套接口上,然后时序完成。当然这里对于一些恶意攻击者只发送一次握手而不回复第三次握手的行为是要予以鄙视的。就像网络上2B青年视频:公交车开过来,把门打开,青年站在门口,然后过来一个人把他再背走。视频的地址现在没时间找,不过就是表现了一个至贱无敌的境界。
    对于连接断开的过程,此时两者不一定就是一个你情我愿的过程,很可能连接的一方觉得自己完事了,要断开这个连接,但是另一份很可能还与犹未尽,有消息要发送,所以连接的断开就是一个异步的过程。再极端一些说,即使一方已经发送了断链报文(FIN),而对方迟迟不发送FIN报文来响应这次断链,那么主动发起方是不是要永远的等待下去?根据我们日常生活中的经验来看,这是不可能的。
    二、内核对于TCP转换的状态的注释说明
    这是内核中的注释可能没有RFC中的说明那么严谨和形式化,但是理解起来可能会更加的简洁一些
    linux-2.6.21 etipv4 cp.c
    * Description of States:
     *
     *    TCP_SYN_SENT        sent a connection request, waiting for ack
     *
     *    TCP_SYN_RECV        received a connection request, sent ack,
     *                waiting for final ack in three-way handshake.
     *
     *    TCP_ESTABLISHED        connection established
     *
     *    TCP_FIN_WAIT1        our side has shutdown, waiting to complete
     *                transmission of remaining buffered data
     *
     *    TCP_FIN_WAIT2        all buffered data sent, waiting for remote
     *                to shutdown
     *
     *    TCP_CLOSING        both sides have shutdown but we still have
     *                data we have to finish sending
     *
     *    TCP_TIME_WAIT        timeout to catch resent junk before entering
     *                closed, can only be entered from FIN_WAIT2
     *                or CLOSING.  Required because the other end
     *                may not have gotten our last ACK causing it
     *                to retransmit the data packet (which we ignore)
     *
     *    TCP_CLOSE_WAIT        remote side has shutdown and is waiting for
     *                us to finish writing our data and to shutdown
     *                (we have to close() to move on to LAST_ACK)
     *
     *    TCP_LAST_ACK        out side has shutdown after remote has
     *                shutdown.  There may still be data in our
     *                buffer that we have to finish sending
     *
     *    TCP_CLOSE        socket is finished
     */
    三、状态图
    依照惯例,从网上借用了一个状态转换图,地址来自连接关闭时状态图,为了防止链接内容不存在,这里也备份一下

    tcp连接关闭时时序 - Tsecer - Tsecer的回音岛
    可以看到的问题是 关闭的主动方经历了更多的状态,并且它的每个状态的时间可能都会比被动方要长。这个也符合常理,比如离婚的时候,主动方一般要承担更多的责任。
    四、为什么会这样
    这里主要是因为TCP的异步实现方式引发的问题。对于TCP的报文发送和关闭,它都是异步的。所谓的异步意义就是说当send函数返回之后,close函数返回之后,这个函数本身声明的动作可能还并没有完成。例如,在send函数返回之后,可能之前通过send发送的报文还在本机的缓冲区中存放着,也可能在中间的某个路由器上存放着,存放的原因可能是网络的阻塞,而对于应用层来说,它也不可能等待一个操作真正完成之后才返回,因为这明显是违反了TCP的滑动窗口协议的。
    在于发起的主动方中,其中的FIN_WAIT1就是在close之后等待完成第一次握手的状态,而这里的FIN_WAIT1我们可以认为是对于四次挥手(两次FIN报文)中第一个FIN报文被确认的等待。
    那么为什么不能立即被确认呢?可能在close的时候,被关闭的套接口中还有大量的数据没有得到确认,甚至还没有被发送出去,即使这些数据已经全部发送,自己发送的FIN报文以及对方的ACK报文也可能丢失重传,所以这个FIN_WAIT1状态是可能持续任意长时间的,而不是一瞬间的事情。
    当主动的FIN报文被确认之后,此时主动方进入FIN_WAIT2,即等待第二次分手交互的到来,或者说对方的FIN包的到来。而当被动方收到FIN之后进入的是close_wati,也就是等待应用程序来关闭这个套接口了,因为另一方已经关闭,此时被动方也应该优雅的在一定时间之后关闭。
    当被动方真的执行close之后,发送了FIN就进入LAST_ACK状态,也就是等待对方回应的ACK报文,此时定时器依然是可以正常工作的,如果FIN包在一定时间之内没有被确认,此时FIN会被重传。而这个重传的FIN则具有断链的杀伤力,而另一方的TIME_WAIT则就是为了避免可能新生的套接口被这个杀伤力极大的FIN包命中而充当的挡箭牌。
    五、如果一切没有那么优雅
    通常大家都是想到了这个世界如此优美之后的后果,大家已经做得非常完美。但是此时如果假设说主动方close之后,对方非常痴情,迟迟不进行close操作,那么主动方岂不是永远也无法关闭连接,也就是这个链路可以在建立之后永远保持。显然这样是不合理的,否则服务器就只能给某些应用服务了。
    看一下内核中的实现,依然使我们熟悉的tcp_close函数
    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
                  struct tcphdr *th, unsigned len)
        case TCP_FIN_WAIT1:
    ……
                        } else {
                            tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
                            goto discard;
                        }
    这里可以看到的是,TCP_FIN_WAIT2 是作为一个TIME_WAIT状态来处理的,而TIME_WAIT状态的套接口是不能被事件触发之后回收,因为在TIME_WAIT之后,对方不会再发送数据过来,而2MSL则是一个固定值,它就是要傻傻的等待这么长时间,之后自动消失。
    所谓的自动永远都是不是自动而是被某个事件触发,只是说这个事件及相应对我们透明而已,例如自动洗衣机。而对于time_wati状态,它的“自动消失”则是通过定时器来实现,当进入time_wait状态之后,一个socket首先会进行瘦身操作,大量的资源被释放出去,它相当于僵尸套接口(事实上大家可以对比一下,socket的操作和进程的很多操作有相似之处),等待最后的FIN的到来,就像进程等待wait系统调用一样。
    在void tcp_time_wait(struct sock *sk, int state, int timeo)函数中,传入的sk套接口被关闭并且彻底释放,但是它的基本信息放入一个inet_timewait_sock结构的mini_sock,这个套接口将会代替重量级的socket链接入tcp_socket的hash链表中,用来等待最后一次分手消息的到来和处理。当然linger2参数可以修改这个等待时间。

    struct inet_timewait_sock *inet_twsk_alloc(const struct sock *sk, const int state)
            tw->tw_state        = TCP_TIME_WAIT;
            tw->tw_substate        = state也即是当进入FIN_WAIT2之后其实已经进入了TIME_WAIT状态,只是说它的子状态为一个FIN_WAIT2状态

     而对于FIN_WAIT2状态,内核中对于该值的说明
    #define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
                      * state, about 60 seconds    */
    #define TCP_FIN_TIMEOUT    TCP_TIMEWAIT_LEN
                                     /* BSD style FIN_WAIT2 deadlock breaker.
                      * It used to be 3min, new value is 60sec,
                      * to combine FIN-WAIT-2 timeout with
                      * TIME-WAIT timer.

    int sysctl_tcp_fin_timeout __read_mostly = TCP_FIN_TIMEOUT;

    linux-2.6.21Documentation etworkingip-sysctl.txt
    tcp_fin_timeout - INTEGER Time to hold socket in state FIN-WAIT-2, if it was closed
        by our side. Peer can be broken and never close its side,
        or even died unexpectedly. Default value is 60se
    c.
        Usual value used in 2.2 was 180 seconds, you may restore
        it, but remember that if your machine is even underloaded WEB server, you risk to overflow memory with kilotons of dead sockets,
        FIN-WAIT-2 sockets are less dangerous than FIN-WAIT-1,
        because they eat maximum 1.5K of memory, but they tend
        to live longer.    Cf. tcp_max_orphans.
    六、FIN_WAIT2到真正TIME_WAIT的转换
    enum tcp_tw_status
    tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
                   const struct tcphdr *th)
    ……
    if (tw->tw_substate == TCP_FIN_WAIT2) {
            /* Just repeat all the checks of tcp_rcv_state_process() */

            /* Out of window, send ACK */
            if (paws_reject ||
                !tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
                       tcptw->tw_rcv_nxt,
                       tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd))
                return TCP_TW_ACK;

            if (th->rst)
                goto kill;

            if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt))
                goto kill_with_rst;

            /* Dup ACK? */
            if (!after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) ||
                TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) {
                inet_twsk_put(tw);
                return TCP_TW_SUCCESS;
            }

            /* New data or FIN. If new data arrive after half-duplex close,
             * reset.
             */
            if (!th->fin ||
                TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1) {
    kill_with_rst:
                inet_twsk_deschedule(tw, &tcp_death_row);
                inet_twsk_put(tw);
                return TCP_TW_RST;
            }

            /* FIN arrived, enter true time-wait state. */
            tw->tw_substate      = TCP_TIME_WAIT;
            tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
            if (tmp_opt.saw_tstamp) {
                tcptw->tw_ts_recent_stamp = xtime.tv_sec;
                tcptw->tw_ts_recent      = tmp_opt.rcv_tsval;
            }

            /* I am shamed, but failed to make it more elegant.
             * Yes, it is direct reference to IP, which is impossible
             * to generalize to IPv6. Taking into account that IPv6
             * do not understand recycling in any case, it not
             * a big problem in practice. --ANK */
            if (tw->tw_family == AF_INET &&
                tcp_death_row.sysctl_tw_recycle && tcptw->tw_ts_recent_stamp &&
                tcp_v4_tw_remember_stamp(tw))
                inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
                           TCP_TIMEWAIT_LEN);
            else
                inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                           TCP_TIMEWAIT_LEN); 此时使用真正的TIME_WAIT时间,重新进入timewait队列
            return TCP_TW_ACK;
        }

  • 相关阅读:
    MyBatis+Oracle
    JAVA接口,json传递
    Oracle学习笔记(二)
    Oracle学习笔记(一)
    数据库事务四大特性之隔离性
    数据库事务四大特性(ACID)
    多表连接时条件放在 on 与 where 后面的区别
    tomcat request.getParamter() 乱码解决方案 Filter版本
    POI excel下载 中文名 浏览器兼容解决
    天马行空
  • 原文地址:https://www.cnblogs.com/tsecer/p/10487481.html
Copyright © 2011-2022 走看看