精华内容
下载资源
问答
  • TCP在FIN_WAIT1状态到底能持续多久以及TCP假连接问题

    万次阅读 多人点赞 2018-08-16 08:52:35
    近期遇到一个问题,简单点说,主机A上显示一条ESTABLISHED状态的TCP连接到主机B,而主机B上却没有任何关于主机A的连接信息,经查明,这是由于主机A和主机B的发送/接收缓冲区差异巨大,导致主机B进程退出后,主机A...

    近期遇到一个问题,简单点说,主机A上显示一条ESTABLISHED状态的TCP连接到主机B,而主机B上却没有任何关于主机A的连接信息,经查明,这是由于主机A和主机B的发送/接收缓冲区差异巨大,导致主机B进程退出后,主机A暂时憋住,主机B频繁发送零窗口探测,FIN_WAIT1状态超时,进而连接被销毁,然而主机A并不知情导致。

    正好昨天也有人咨询另外一个类似的问题,那么就抽昨晚和今天早上的时间,写一篇总结吧。


    TCP处处是坑!

    不要觉得你对TCP的实现的代码烂熟于心了就能把控它的所有行为!不知道你有没有发现,目前市面上新上市的关于Linux内核协议栈的书可谓是汗牛充栋,然而无论作者是国内的还是国外,几乎都是碰到TCP就草草略过,反而对IP,ARP,DNS这些大书特书,Why?因为Linux内核里TCP的代码太乱太复杂了,很少有人能看明白80%以上的,即便真的有看过的,其中还包括只懂代码而不懂网络技术的,我就发现很多声称自己精通Linux内核TCP/IP源码,结果竟然不知道什么是默认路由…

    所以我打算写一篇文章,趁着这个FIN_WAIT1问题,顺便表达一下我是如何学习网络技术,我是如何解决网络问题的方法论观点,都是形而上,个人看法:

    • 设计覆盖全面的复现实验
    • 通读协议标准文档,理解实现建议
    • 再次实验,预测并确认问题以外的现象
    • 核对代码实现,跟踪代码的Changelog
    • 写一个自己的实现或者乱改代码

    本文聊聊TCP的FIN_WAIT1以及TCP假连接(死连接)问题。先看FIN_WAIT1。


    首先还是从状态机入手,看看和FIN_WAIT1相关的状态机转换图:

    这里写图片描述

    我们只考虑常规的从ESTABLISHED状态的转换,很简单的一个单一状态转换:

    • ESTAB状态发送FIN即切换到FIN_WAIT1状态;
    • FIN_WAIT1状态下收到针对FIN的ACK即可离开FIN_WAIT1到达FIN_WAIT2.

    看一下和上述状态机转换相关的简单时序图:

    这里写图片描述

    从状态图和时序图上,我们很明确地可以看到,FIN_WAIT1持续1个RTT左右的时间!这个时间段几乎不会被肉眼观察到,转瞬而即逝。

    然而,这是真的吗?

    我们之所以得到FIN_WAIT1持续1个RTT这个结论,基于两个假设,即:

    1. TCP的对端是一个正常的TCP端;
    2. 两端TCP之间的链路是正常的,可达的。

    OK,接下来我们来设计一个实验模拟异常的情况。准备实验拓扑如下:

    这里写图片描述

    host1和host2的系统内核版本(uname -r获取):

    3.10.0-862.2.3.el7.x86_64

    首先,我们看一下如果对端TCP针对FIN发送的ACK丢失,会发生什么。按照上述的时序图,正常应该是FIN_WAIT1将会永久持续。我们来验证一下。

    • 实验1:模拟ACK丢失
      在host1上做以下命令:
    nc -l -p 1234

    host2上完成以下命令:

    cat /dev/zero|nc 1.1.1.1 1234

    以上保证了host1和host2之间的TCP建立并且连接之间有持续的数据传输。接下来,在host2上执行下列动作:

    iptables -A INPUT -p tcp --tcp-flags ACK,FIN ACK
    killall nc

    此时在host2上:

    [root@localhost ~]# netstat  -antp|grep 1234
    tcp        0   1229 1.1.1.2:39318               1.1.1.1:1234                FIN_WAIT1   - 

    连续上翻命令,这个FIN_WAIT1均不会消失,暂时符合我们的预期…出去抽根烟,刷会儿微博…回来后,发现这个FIN_WAIT1消失了!

    它是如何消失的呢?这个时候,我们提取netstat数据,执行“ netstat -st”,会发现:

    TcpExt:
    ...
        1 connections aborted due to timeout

    多了一条timeout连接!


    我这里直接说答案吧。

    虽然说在协议上规范上看,TCP没有必要为链路或者说对端的不合常规的行为而买单,但是从现实角度,TCP的实现必须处理异常情况,TCP的实现必然要有所限制!

    我们知道,计算机是无法处理无限,无穷这种抽线的数学概念的,所有如果针对FIN的ACK迟迟不来,那么必然要有一个等待的极限,这个极限在Linux内核协议栈中由以下参数控制:

    net.ipv4.tcp_orphan_retries # 默认值是0!这里有坑...

    这个参数表示如果一直都收不到针对FIN的ACK,那么在彻底销毁这个FIN_WAIT1的连接前,等待几轮RTO退避

    所谓的orphan tcp connection,意思就是说,在Linux进程层面,创建该连接的进程已经退出销毁了,然而在TCP协议层面,它依然在遵循TCP状态机的转换规则存在着。

    注意,这个参数不是一个时间量,而是一个次数量。我们知道,TCP每一次超时,都会对下一次超时时间进行指数退避,这里的次数量就是要经过几次退避的时间。举一个例子,如果RTO是2ms,而tcp_orphan_retries 的值是4,那么所计算出的FIN_WAIT1容忍时间就是:
    T=21+22+23+24 T = 2 1 + 2 2 + 2 3 + 2 4
    还是看看Linux内核文档怎么说的吧:

    tcp_orphan_retries - INTEGER
      This value influences the timeout of a locally closed TCP connection, when RTO retransmissions remain unacknowledged.
      See tcp_retries2 for more details.
      The default value is 8.
      If your machine is a loaded WEB server,
      you should think about lowering this value, such sockets
      may consume significant resources. Cf. tcp_max_orphans.

    让我们看看tcp_retries2,以获取数值的含义:

    tcp_retries2 - INTEGER
      This value influences the timeout of an alive TCP connection,
      when RTO retransmissions remain unacknowledged.
      Given a value of N, a hypothetical TCP connection following
      exponential backoff with an initial RTO of TCP_RTO_MIN would
      retransmit N times before killing the connection at the (N+1)th RTO.
      The default value of 15 yields a hypothetical timeout of 924.6
      seconds and is a lower bound for the effective timeout.
      TCP will effectively time out at the first RTO which exceeds the hypothetical timeout.
      RFC 1122 recommends at least 100 seconds for the timeout,
      which corresponds to a value of at least 8.

    虽然说文档上默认值的建议是8,但是大多数的Linux发行版上其默认值都是0。更多详情,就自己看RFC和Linux源码吧。

    有了这个参数保底,我们知道,即便是ACK永远不来,FIN_WAIT1状态也不会一直持续下去的,这有效避免了有针对性截获ACK或者不发送ACK而导致的DDoS,退一万步讲,即便是没有DDoS,这种做法也具有资源利用率的容错性,使得资源使用更加高效。

    实验1的结论如下:

    • 如果主动断开端调用了close关掉了进程,它会进入FIN_WAIT1状态,此时如果它再也收不到ACK,无论是针对pending在发送缓冲的数据还是FIN,它都会尝试重新发送,在收到ACK前会尝试N次退避,该N由tcp_orphan_retries参数控制。

    接下来,我们来看一个更加复杂一点的问题,还是先从实验说起。

    • 实验2:模拟对端TCP不收数据,接收窗口憋死
      在host1上做以下命令:
    # 模拟小接收缓存,使得憋住接收窗口更加容易
    sysctl -w net.ipv4.tcp_rmem="16  32  32"
    nc -l -p 1234

    host2上完成以下命令:

    
    cat /dev/zero|nc 1.1.1.1 1234
    sleep 5 # 稍微等一下
    killall nc

    此时,我们发现host2的TCP连接进入了FIN_WAIT1状态。然而抓包看的话,数据传输依然在进行:

    05:15:51.674630 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 305:321, ack 1, win 5840, options [nop,nop,TS val 1210945 ecr 238593370], length 16
    05:15:51.674690 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 321, win 0, options [nop,nop,TS val 238593471 ecr 1210945], length 0
    05:15:51.674759 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 321, win 16, options [nop,nop,TS val 238593471 ecr 1210945], length 0
    05:15:51.777774 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 321:325, ack 1, win 5840, options [nop,nop,TS val 1211048 ecr 238593471], length 4
    05:15:51.777874 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 325, win 16, options [nop,nop,TS val 238593497 ecr 1211048], length 0
    05:15:52.182918 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 325:341, ack 1, win 5840, options [nop,nop,TS val 1211453 ecr 238593497], length 16
    05:15:52.182970 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 341, win 0, options [nop,nop,TS val 238593599 ecr 1211453], length 0
    05:15:52.183055 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 341, win 16, options [nop,nop,TS val 238593599 ecr 1211453], length 0
    05:15:52.592759 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 341:357, ack 1, win 5840, options [nop,nop,TS val 1211863 ecr 238593599], length 16
    05:15:52.592813 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 357, win 0, options [nop,nop,TS val 238593701 ecr 1211863], length 0
    05:15:52.592871 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 357, win 16, options [nop,nop,TS val 238593701 ecr 1211863], length 0
    05:15:52.695160 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 357:361, ack 1, win 5840, options [nop,nop,TS val 1211965 ecr 238593701], length 4
    05:15:52.695276 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 361, win 16, options [nop,nop,TS val 238593727 ecr 1211965], length 0
    05:15:53.099612 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 361:377, ack 1, win 5840, options [nop,nop,TS val 1212370 ecr 238593727], length 16
    05:15:53.099641 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 377, win 0, options [nop,nop,TS val 238593828 ecr 1212370], length 0
    05:15:53.099671 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 377, win 16, options [nop,nop,TS val 238593828 ecr 1212370], length 0
    05:15:53.505028 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 377:393, ack 1, win 5840, options [nop,nop,TS val 1212775 ecr 238593828], length 16
    05:15:53.505081 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 393, win 0, options [nop,nop,TS val 238593929 ecr 1212775], length 0
    05:15:53.505138 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 393, win 16, options [nop,nop,TS val 238593929 ecr 1212775], length 0
    05:15:53.605923 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [P.], seq 393:397, ack 1, win 5840, options [nop,nop,TS val 1212876 ecr 238593929], length 4

    这是显然的,这是因为收发两端巨大的缓存大小差异造成的,即便是host2发送端进程退出了,在退出前已经有大量数据pending到了TCP的发送缓冲区里面而脱离已经被销毁的进程了,FIN包当然是排在了缓冲区的末尾了。

    TCP的状态机运行在缓存的上层,即只要把FIN包pending排队,就切换到了FIN_WAIT1,而不是说实际发送了FIN包才切换。

    因此,我们可有的等了,数据传输依然在正常有序进行,针对小包的ACK源源不断从host1回来,这进一步促进host2发送未竟的数据包,直到所有缓冲区的数据全部发送完毕…

    不管怎样,总是有个头儿,只要有结束,就不需要担心。我们可以简单得出一个结论:

    • 如果主动断开端调用了close关掉了进程,它会进入FIN_WAIT1状态,如果接收端的接收窗口呈现打开状态,此时它的TCP发送队列中的数据包还是会像正常一样发往接收端,直到发送完,最后发送FIN包,收到FIN包ACK后进入FIN_WAIT2。

    现在,我们进行实验的下一步,把host1上的接收进程nc的接收逻辑彻底憋死。很简单,host1上执行下面的命令即可:

    killall -STOP nc

    进程并没有退出,只是暂停了,nc进程上下文的recv不再执行,然而软中断上下文的TCP协议的处理依然在进行。

    这个时候,抓包就会发现只剩下指数时间退避的零窗口探测包了:

    # 注意观察探测包发送时间的间隔
    05:15:56.444570 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1215715 ecr 238594487], length 0
    05:15:56.444602 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238594664 ecr 1214601], length 0
    05:15:57.757217 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1217027 ecr 238594664], length 0
    05:15:57.757248 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238594992 ecr 1214601], length 0
    05:16:00.283259 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1219552 ecr 238594992], length 0
    05:16:00.283483 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238595624 ecr 1214601], length 0
    05:16:05.234277 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1224503 ecr 238595624], length 0
    05:16:05.234305 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238596861 ecr 1214601], length 0
    05:16:15.032486 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1234301 ecr 238596861], length 0
    05:16:15.032532 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238599311 ecr 1214601], length 0
    05:16:34.629137 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1253794 ecr 238599311], length 0
    05:16:34.629164 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238604210 ecr 1214601], length 0
    05:17:13.757815 IP 1.1.1.2.39318 > 1.1.1.1.1234: Flags [.], ack 1, win 5840, options [nop,nop,TS val 1292784 ecr 238604210], length 0
    05:17:13.757863 IP 1.1.1.1.1234 > 1.1.1.2.39318: Flags [.], ack 465, win 0, options [nop,nop,TS val 238613992 ecr 1214601], length 0

    这个实验的现象和实验1的现象,仅有一个区别,那就是实验1是阻塞了ACK,而本实验则是FIN根本就还没有发送出去就进入了FIN_WAIT1,且针对RTO指数时间退避发送的零窗口探测的ACK持续到来,简单总结就是:
    实验1没有ACK到来,实验2有ACK到来。

    在实验结果之前,我们来看一段摘录,来自RFC 1122:https://tools.ietf.org/html/rfc1122#page-92

    4.2.2.17 Probing Zero Windows: RFC-793 Section 3.7, page 42
    .
      Probing of zero (offered) windows MUST be supported.
    .
      A TCP MAY keep its offered receive window closed
       indefinitely. As long as the receiving TCP continues to
      send acknowledgments in response to the probe segments, the
      sending TCP MUST allow the connection to stay open.

    紧接着后面是一段注解:

    DISCUSSION:
      It is extremely important to remember that ACK
      (acknowledgment) segments that contain no data are not
      reliably transmitted by TCP. If zero window probing is
      not supported, a connection may hang forever when an
      ACK segment that re-opens the window is lost.
    .
      The delay in opening a zero window generally occurs
      when the receiving application stops taking data from
      its TCP. For example, consider a printer daemon
      application, stopped because the printer ran out of
      paper.

    只要有ACK到来,连接就要保持,这会带来什么问题呢?确实会带来问题,但是在正视这些问题之前,Linux内核协议栈的实现者,也保持了缄默,我们来看一段实验主机host1和host2所用的标准内核主线版本3.10的内核源码,来自tcp_probe_timer函数内部的注释以及一小段代码:

        /* *WARNING* RFC 1122 forbids this
         *
         * It doesn't AFAIK, because we kill the retransmit timer -AK
         *
         * FIXME: We ought not to do it, Solaris 2.5 actually has fixing
         * this behaviour in Solaris down as a bug fix. [AC]
         *
         * Let me to explain. icsk_probes_out is zeroed by incoming ACKs
         * even if they advertise zero window. Hence, connection is killed only
         * if we received no ACKs for normal connection timeout. It is not killed
         * only because window stays zero for some time, window may be zero
         * until armageddon and even later. We are in full accordance
         * with RFCs, only probe timer combines both retransmission timeout
         * and probe timeout in one bottle.             --ANK
         */
         ...
            max_probes = sysctl_tcp_retries2;
    
        if (sock_flag(sk, SOCK_DEAD)) { // 如果是orphan连接的话
            const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);
            // 即获取tcp_orphan_retries参数,有微调,请详审。本实验参数默认值取0!
            max_probes = tcp_orphan_retries(sk, alive);
    
            if (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes))
                return;
        }
        // 只有在icsk_probes_out,即未应答的probe次数超过探测最大容忍次数后,才会出错清理连接。
        if (icsk->icsk_probes_out > max_probes) {
            tcp_write_err(sk);
        } else {
            /* Only send another probe if we didn't close things up. */
            tcp_send_probe0(sk);
        }

    是的,从上面那一段注释,我们看出了抱怨,一个FIN_WAIT1的连接可能会等到世界终结日之后,然而我们却只能“in full accordance with RFCs”


    这也许暗示了某种魔咒般的结果,即FIN_WAIT1将会一直持续到终结世界的大决战之日。然而非也,你会发现大概在发送了9个零窗口探测包之后,连接就消失了。netstat -st的结果中,呈现:

    1 connections aborted due to timeout

    看来想制造点事端,并非想象般容易!

    如上所述,我展示了标准主线的Linux 3.10内核的tcp_probe_timer函数,现在的问题是,为什么下面的条件被满足了呢?

    if (icsk->icsk_probes_out > max_probes) 

    只有当这个条件被满足,tcp_write_err才会被调用,进而:

    tcp_done(sk);
    // 递增计数,即netstat -st中的那条“1 connections aborted due to timeout”
    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONTIMEOUT);

    按照注释和代码的确认,只要收到ACK,icsk_probes_out 字段就将被清零,这是很明确的啊,我们在tcp_ack函数中便可看到无条件清零icsk_probes_out的动作:

    static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
    {
        ...
        sk->sk_err_soft = 0;
        icsk->icsk_probes_out = 0;
        tp->rcv_tstamp = tcp_time_stamp;
        ...
    }

    从代码上看,只要零窗口探测持续发送,不管退避到多久(最大TCP_RTO_MAX),只要对端会有ACK回来,icsk_probes_out 就会被清零,上述的条件就不会被满足,连接就会一直在FIN_WAIT1状态,而从我们抓包看,确实是零窗口探测有去必有回的!

    预期会永远僵在FIN_WAIT1状态的连接在一段时间后竟然销毁了。没有符合预期,到底发生了呢?


    如果我们看高版本4.14版的Linux内核,同样是tcp_probe_timer函数,我们会看到一些不一样的代码和注释:

    static void tcp_probe_timer(struct sock *sk)
    {
        ...
        /* RFC 1122 4.2.2.17 requires the sender to stay open indefinitely as
         * long as the receiver continues to respond probes. We support this by
         * default and reset icsk_probes_out with incoming ACKs. But if the
         * socket is orphaned or the user specifies TCP_USER_TIMEOUT, we
         * kill the socket when the retry count and the time exceeds the
         * corresponding system limit. We also implement similar policy when
         * we use RTO to probe window in tcp_retransmit_timer().
         */
        start_ts = tcp_skb_timestamp(tcp_send_head(sk));
        if (!start_ts)
            tcp_send_head(sk)->skb_mstamp = tp->tcp_mstamp;
        else if (icsk->icsk_user_timeout &&
             (s32)(tcp_time_stamp(tp) - start_ts) >
             jiffies_to_msecs(icsk->icsk_user_timeout))
            goto abort; 
    
        max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;
        if (sock_flag(sk, SOCK_DEAD)) {
            const bool alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;
    
            max_probes = tcp_orphan_retries(sk, alive);
            // 如果处在FIN_WAIT1的连接持续时间超过了TCP_RTO_MAX(这是前提)
            // 如果退避发送探测的次数已经超过了配置参数指定的次数(这是附加条件)
            if (!alive && icsk->icsk_backoff >= max_probes)
                goto abort; // 注意这个goto!直接销毁连接。
            if (tcp_out_of_resources(sk, true))
                return;
        }
    
        if (icsk->icsk_probes_out > max_probes) {
    abort:      tcp_write_err(sk);
        } else {
            /* Only send another probe if we didn't close things up. */
            tcp_send_probe0(sk);
        }
    }

    我们来看这段代码的注释,RFC1122的要求:

    RFC 1122 4.2.2.17 requires the sender to stay open indefinitely as
    long as the receiver continues to respond probes. We support this by
    default and reset icsk_probes_out with incoming ACKs.

    然后我们接着看这段注释,有一个But转折:

    But if the socket is orphaned or the user specifies TCP_USER_TIMEOUT, we
    kill the socket when the retry count and the time exceeds the corresponding system limit.

    看起来,这段注释是符合我们实验的结论的!然而我们实验的是3.10内核,而这个却是4.X的内核啊!即Linux在高版本内核上确实进行了优化,这是针对资源利用的优化,并且避免了有针对性的DDoS。


    答案揭晓了。

    *我们实验所使用的内核版本不是社区主线版本,而是Redhat的版本!***Redhat显然会事先回移上游的patch,我们来确认一下我们所所用的实验版本3.10.0-862.2.3.el7.x86_64的tcp_probe_timer的源码。

    为此,我们到下面的地址去下载Redhat(Centos…)专门的源码,我们看看它和社区同版本源码是不是在关于probe处理上有所不同:
    http://vault.centos.org/7.5.1804/updates/Source/SPackages/

    这里写图片描述

    使用下面的命令解压:

    rpm2cpio ../kernel-3.10.0-862.2.3.el7.src.rpm | cpio -idmv
    xz linux-3.10.0-862.2.3.el7.tar.xz -d
    tar xvf linux-3.10.0-862.2.3.el7.tar 

    查看net/ipv4/tcp_timer.c文件,找到tcp_probe_timer函数:
    这里写图片描述

    看来是Redhat移植了4.X的patch,导致了源码的逻辑和社区版本的出现差异,这也就解释了实验现象!


    那么这个针对orphan connection的patch最初是来自何方呢?我们不得不去patchwork去溯源,以便得到更深入的Why。

    在maillist,我找到了下面的链接:
    http://lists.openwall.net/netdev/2014/09/23/8

    Date: Mon, 22 Sep 2014 20:52:13 -0700
    From: Yuchung Cheng ycheng@...gle.com
    To: davem@…emloft.net
    Cc: edumazet@…gle.com, andrey.dmitrov@…etlabs.ru,
      ncardwell@…gle.com, netdev@…r.kernel.org,
      Yuchung Cheng ycheng@...gle.com
    Subject: [PATCH net-next] tcp: abort orphan sockets stalling on zero window probes

    摘录一段描述吧:

    Currently we have two different policies for orphan sockets
    that repeatedly stall on zero window ACKs. If a socket gets
    a zero window ACK when it is transmitting data, the RTO is
    used to probe the window. The socket is aborted after roughly
    tcp_orphan_retries() retries (as in tcp_write_timeout()).
    .
    But if the socket was idle when it received the zero window ACK,
    and later wants to send more data, we use the probe timer to
    probe the window. If the receiver always returns zero window ACKs,
    icsk_probes keeps getting reset in tcp_ack() and the orphan socket
    can stall forever until the system reaches the orphan limit (as
    commented in tcp_probe_timer()). This opens up a simple attack
    to create lots of hanging orphan sockets to burn the memory
    and the CPU, as demonstrated in the recent netdev post “TCP
    connection will hang in FIN_WAIT1 after closing if zero window is
    advertised.” http://www.spinics.net/lists/netdev/msg296539.html

    该链接最后面给出了patch:

    ...
    +   max_probes = sysctl_tcp_retries2;
        if (sock_flag(sk, SOCK_DEAD)) {
            const int alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;
    
            max_probes = tcp_orphan_retries(sk, alive);
    -
    +       if (!alive && icsk->icsk_backoff >= max_probes)
    +           goto abort;
            if (tcp_out_of_resources(sk, alive || icsk->icsk_probes_out <= max_probes))
                return;
        }
    
        if (icsk->icsk_probes_out > max_probes) {
    -       tcp_write_err(sk);
    +abort:     tcp_write_err(sk);
        } else {
    ...

    简单说一下这个patch的意义。

    在实验2中,我用kill -STOP信号故意憋死了nc接收进程,以重现现象,然而事实上在现实中,存在下面两种不太友善情况:

    • 接收端进程出现异常,或者接收端内核存在缺陷,导致进程挂死而软中断上下文的协议栈处理正常运行;
    • 接收端就是一个恶意的DDoS进程,故意不接收数据以诱导发送端在FIN_WAIT2状态(甚至ESTAB状态)发送数据不成后发送零窗口探测而不休止。

    无论哪种情况,最主动断开的发送端来讲,其后果都是消耗大量的资源,而orphan连接则占着茅坑不拉屎。这比较悲哀。


    现在给出本文的第三个结论:

    • 如果主动断开端调用了close关掉了进程,它会进入FIN_WAIT1状态,如果接收端的接收窗口呈现关闭状态(零窗口),此时它会不断发送零窗口探测包。发送多少次呢?有两种实现:
      1. 低版本内核(至少社区3.10及以下):永久尝试,如果探测ACK每次都返回,则没完没了。
      2. 高版本内核(至少社区4.6及以上):限制尝试tcp_orphan_retries次,不管是否收到探测ACK。

    当然,其实还有关于非探测包的重传限制,比如关于TCP_USER_TIMEOUT这个socket option的限制:

    else if (icsk->icsk_user_timeout &&
         (s32)(tcp_time_stamp(tp) - start_ts) >
         jiffies_to_msecs(icsk->icsk_user_timeout))
        goto abort;

    包括关于Keepalive的点点滴滴,本文就不多说了。


    在此,先有个必要的总结。我老是说在学习网络协议的时候读码无益并不是说不要去阅读解析Linux内核源码,而是一定要先有实验设计的能力重现问题,然后再去核对RFC或者其它的协议标准,最后再去核对源码到底是怎么实现的,这样才能一气呵成。否则将有可能陷入深渊。

    以本文为例,我假设你手头有3.10的源码,当你面对“FIN_WAIT1状态的TCP连接在持续退避的零窗口探测期间并不会如预期那般永久持续下去”这个问题的时候,你读源码是没有任何用的,因为这个时候你只能静静地看着那些代码,然后纠结自己是不是哪里理解错了,很多人甚至很难能想到去对比不同版本的代码,因为版本太多了。

    源码只是一种实现的方式,而已,真正重要的是协议的标准以及标准是实现的建议,此外,各个发行版厂商完全有自主的权力对社区源码做任何的定制和重构,不光是Redhat,即便你去看OpenWRT的代码,也是一样,你会发现很多不一样的东西。

    我并不赞同几乎每一个程序员都拥护的那种任何情况下源码至上,the whole world is cheap,show me the code的观点,当一个逻辑流程摆在那里没有源码的时候,当然那绝对是源码至上,否则就是纸上谈兵,逻辑至少要跑起来,而只有源码编译后才能跑起来,流程图和设计图是无法运行的,这个时候,你需要放弃讨论,潜心编码。然而,当一个网络协议已经被以各种方式实现了而你只是为了排查一个问题或者确认一个逻辑的时候,代码就退居二三线了,这时候,请“show me the standard!”


    本文原本是想解释完FIN_WAIT1能持续多久就结束的,但是这样显得有点遗憾,因为我想本文的这个FIN_WAIT1的论题可以引出一个更大的论题,如果不继续说一说,那便是不负责任的。

    是什么的?嗯,是TCP假连接的问题。那么何谓TCP假连接?

    所谓的TCP假连接就是TCP的一端已经逃逸出了TCP状态机,而另一端却不知道的连接。

    我们再看完美的TCP标准RFC793上的TCP状态图:
    这里写图片描述

    除了TIME_WAIT到CLOSED这唯一的出口,你是找不到其它出口的,也就是说,一个TCP端一旦发起了建立连接请求,暂不考虑同时打开同时关闭的情况,就一定要到其中一方的TIME_WAIT超时而结束

    然而,TCP的缺陷在于,TCP是一个端到端的协议,在协议层面上所有的端到端协议是需要底层的传送协议作为其支撑的,一旦底层永久崩坏,端到端协议将会面临状态机僵住的场景,而状态机僵住意味着对资源的永久消耗,因为连接再也释放不掉了!

    随便举一个例子,在两端ESTAB状态的时候,把IP动态路由协议停掉并把把网线剪断,那么TCP两端将永远处在ESTAB状态,直到机器重启。为了解决这个问题,TCP引入了Keepalive机制,一旦超过一定时间没有互通有无,那么就会主动销毁这个连接,事实上,按照纯粹的TCP状态机而言,Keepalive机制是一种对TCP协议的污染。

    是不是Keepalive就能完全避免假连接,死连接存在了呢?非也,Keepalive只是一种用户态按照自己的业务逻辑去检测并避免假连接的手段,而我们仔细观察TCP状态机,很多的步骤远不是用户态进程可是touch的,比如本文讲的FIN_WAIT1,一旦连接成为orphan的,将没有任何进程与之关联,虽然用户态设置的Keepalive也可以继续起作用,但万一用户态没有设置Keepalive呢??这时怎么办?

    我们执行下面的命令:

    [root@localhost ~]# sysctl -a|grep retries
    net.ipv4.tcp_orphan_retries = 0
    net.ipv4.tcp_retries1 = 3
    net.ipv4.tcp_retries2 = 15
    net.ipv4.tcp_syn_retries = 6
    net.ipv4.tcp_synack_retries = 5
    net.ipv6.idgen_retries = 3

    嗯,这些就是避免TCP协议本身的状态机转换僵死所引入的控制层Keepalive机制,详细情况就自己去查阅Linux内核文档吧。

    在具体实现上,防止状态机僵死的方法分为两类:

    • ESTABLISHED防止僵死的方法:使用用户进程设置的Keepalive机制
    • 非ESTABLISHED防止僵死的方法:使用各种retries内核参数设置的timeout机制

    这里写图片描述
    听说温州老板要来,公司楼下专门驻场高定皮鞋衬衫西裤,完全是为了迎接温州老板,不试穿,无样品,完全量身定做,皇家版。皮鞋可以下雨穿。

    展开全文
  • 使用Netty做TCP连接服务器,多个设备连接该服务器,都在局域网内业务上,大部分帧都是客户端上报,服务器不用回复,服务器发送到设备的帧设备也不用回复在使用wireshark抓包,服务器发送到客户端,有大量黑底红字...

    使用Netty做TCP长连接服务器,多个设备连接该服务器,都在局域网内

    业务上,大部分帧都是客户端上报,服务器不用回复,服务器发送到设备的帧设备也不用回复

    在使用wireshark抓包,服务器发送到客户端,有大量黑底红字的坏包

    我的ip192.168.1.168 设备的ip192.168.1.150

    抓包发现客户端每发上来一帧,虽然我业务上无需回复,但tcp自己还是有个ACK的正常包发回到设备,并紧跟着一个一模一样的坏包,显示tcp dup ack

    4271fa67f645fa35a6b34d4965a34d93.png

    我自己发给客户端的帧,也同样都是一个正常包后面跟一个坏包,坏包显示 tcp retransmission

    e40c89cc63019f366d104c1ea8a9887a.png

    而且客户端是能收到帧的

    为什么我发给客户端的都是一正常包紧跟一个坏包?而且有大量的不是我业务上的包,这些都是啥?是服务器收到帧后必须要回复一帧才行吗?

    try {

    ServerBootstrap bootstrap = new ServerBootstrap()

    .group(boss, worker)

    .channel(NioServerSocketChannel.class)

    .localAddress(inetSocketAddress)

    .childOption(ChannelOption.TCP_NODELAY, true)

    .childOption(ChannelOption.SO_KEEPALIVE, true)

    .childHandler(new HvacChannelInitializer(context));

    ChannelFuture future = bootstrap.bind().sync();

    if (future.isSuccess()) {

    log.info("[TCP Server] server started @ {}:{}", ip, tcpPort);

    }

    future.channel().closeFuture().sync();

    } catch (InterruptedException e) {

    log.error("[TCP Server] server started failed", e);

    } finally {

    boss.shutdownGracefully();

    worker.shutdownGracefully();

    preDestroy();

    }

    展开全文
  • 1 前言可能很多 Java 程序员对 TCP 的理解只有一...其实我个人对 TCP 的很多细节也并没有完全理解,这篇文章主要针对微信交流群里有人提出的长连接,心跳的问题,做一个统一的整理。在 Java 中,使用 TCP 通信,大概...

    1 前言

    可能很多 Java 程序员对 TCP 的理解只有一个三次握手,四次挥手的认识,我觉得这样的原因主要在于 TCP 协议本身稍微有点抽象(相比较于应用层的 HTTP 协议);其次,非框架开发者不太需要接触到 TCP 的一些细节。其实我个人对 TCP 的很多细节也并没有完全理解,这篇文章主要针对微信交流群里有人提出的长连接,心跳的问题,做一个统一的整理。

    在 Java 中,使用 TCP 通信,大概率会涉及到 Socket、Netty,本文会借用它们的一些 API 和设置参数来辅助介绍。

    2 长连接与短连接

    TCP 本身并没有长短连接的区别,长短与否,完全取决于我们怎么用它。短连接:每次通信时,创建 Socket;一次通信结束,调用 socket.close()。这就是一般意义上的短连接,短连接的好处是管理起来比较简单,存在的连接都是可用的连接,不需要额外的控制手段。

    长连接:每次通信完毕后,不会关闭连接,这样就可以做到连接的复用。长连接的好处便是省去了创建连接的耗时。

    短连接和长连接的优势,分别是对方的劣势。想要图简单,不追求高性能,使用短连接合适,这样我们就不需要操心连接状态的管理;想要追求性能,使用长连接,我们就需要担心各种问题:比如端对端连接的维护,连接的保活。

    长连接还常常被用来做数据的推送,我们大多数时候对通信的认知还是 request/response 模型,但 TCP 双工通信的性质决定了它还可以被用来做双向通信。在长连接之下,可以很方便的实现 push 模型。

    短连接没有太多东西可以讲,所以下文我们将目光聚焦在长连接的一些问题上。纯讲理论未免有些过于单调,所以下文我借助 Dubbo 这个 RPC 框架的一些实践来展开 TCP 的相关讨论。

    3 服务治理框架中的长连接

    前面已经提到过,追求性能的时候,必然会选择使用长连接,所以借助 Dubbo 可以很好的来理解 TCP。我们开启两个 Dubbo 应用,一个 server 负责监听本地 20880(众所周知,这是 Dubbo 协议默认的端口),一个 client 负责循环发送请求。执行 lsof-i:20880 命令可以查看端口的相关使用情况:

    86fc8d77b4c68d403ed9cd7c1551e46a.png*:20880(LISTEN) 说明了 Dubbo 正在监听本地的 20880 端口,处理发送到本地 20880 端口的请求

    后两条信息说明请求的发送情况,验证了 TCP 是一个双向的通信过程,由于我是在同一个机器开启了两个 Dubbo 应用,所以你能够看到是本地的 53078 端口与 20880 端口在通信。我们并没有手动设置 53078 这个客户端端口,他是随机的,但也阐释了一个道理:即使是发送请求的一方,也需要占用一个端口。

    稍微说一下 FD 这个参数,他代表了文件句柄,每新增一条连接都会占用新的文件句柄,如果你在使用 TCP 通信的过程中出现了 open too many files 的异常,那就应该检查一下,你是不是创建了太多的连接,而没有关闭。细心的读者也会联想到长连接的另一个好处,那就是会占用较少的文件句柄。

    4 长连接的维护

    因为客户端请求的服务可能分布在多个服务器上,客户端端自然需要跟对端创建多条长连接,使用长连接,我们遇到的第一个问题就是要如何维护长连接。//客户端

    publicclassNettyHandlerextendsSimpleChannelHandler{

    privatefinalMapchannels=newConcurrentHashMap();//

    }

    //服务端

    publicclassNettyServerextendsAbstractServerimplementsServer{

    privateMapchannels;//

    }

    在 Dubbo 中,客户端和服务端都使用 ip:port 维护了端对端的长连接,Channel 便是对连接的抽象。我们主要关注 NettyHandler 中的长连接,服务端同时维护一个长连接的集合是 Dubbo 的设计,我们将在后面提到。

    5 连接的保活

    这个话题就有的聊了,会牵扯到比较多的知识点。首先需要明确一点,为什么需要连接的报活?当双方已经建立了连接,但因为网络问题,链路不通,这样长连接就不能使用了。需要明确的一点是,通过 netstat,lsof 等指令查看到连接的状态处于 ESTABLISHED 状态并不是一件非常靠谱的事,因为连接可能已死,但没有被系统感知到,更不用提假死这种疑难杂症了。如果保证长连接可用是一件技术活。

    6 连接的保活:KeepAlive

    首先想到的是 TCP 中的 KeepAlive 机制。KeepAlive 并不是 TCP 协议的一部分,但是大多数操作系统都实现了这个机制。KeepAlive 机制开启后,在一定时间内(一般时间为 7200s,参数 tcp_keepalive_time)在链路上没有数据传送的情况下,TCP 层将发送相应的KeepAlive探针以确定连接可用性,探测失败后重试 10(参数 tcp_keepalive_probes)次,每次间隔时间 75s(参数 tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可用。

    在 Netty 中开启 KeepAlive:bootstrap.option(ChannelOption.TCP_NODELAY, true)

    Linux 操作系统中设置 KeepAlive 相关参数,修改 /etc/sysctl.conf 文件:net.ipv4.tcp_keepalive_time=90net.ipv4.tcp_keepalive_intvl=15net.ipv4.tcp_keepalive_probes=2

    KeepAlive 机制是在网络层面保证了连接的可用性,但站在应用框架层面我们认为这还不够。主要体现在两个方面:KeepAlive 的开关是在应用层开启的,但是具体参数(如重试测试,重试间隔时间)的设置却是操作系统级别的,位于操作系统的 /etc/sysctl.conf 配置中,这对于应用来说不够灵活。

    KeepAlive 的保活机制只在链路空闲的情况下才会起到作用,假如此时有数据发送,且物理链路已经不通,操作系统这边的链路状态还是 ESTABLISHED,这时会发生什么?自然会走 TCP 重传机制,要知道默认的 TCP 超时重传,指数退避算法也是一个相当长的过程。

    KeepAlive 本身是面向网络的,并不是面向于应用的,当连接不可用时,可能是由于应用本身 GC 问题,系统 load 高等情况,但网络仍然是通的,此时,应用已经失去了活性,所以连接自然应该认为是不可用的。

    看来,应用层面的连接保活还是必须要做的。

    7 连接的保活:应用层心跳

    终于点题了,文题中提到的心跳便是一个本文想要重点强调的另一个 TCP 相关的知识点。上一节我们已经解释过了,网络层面的 KeepAlive 不足以支撑应用级别的连接可用性,本节就来聊聊应用层的心跳机制是实现连接保活的。

    如何理解应用层的心跳?简单来说,就是客户端会开启一个定时任务,定时对已经建立连接的对端应用发送请求(这里的请求是特殊的心跳请求),服务端则需要特殊处理该请求,返回响应。如果心跳持续多次没有收到响应,客户端会认为连接不可用,主动断开连接。不同的服务治理框架对心跳,建连,断连,拉黑的机制有不同的策略,但大多数的服务治理框架都会在应用层做心跳,Dubbo 也不例外。

    8 应用层心跳的设计细节

    以 Dubbo 为例,支持应用层的心跳,客户端和服务端都会开启一个 HeartBeatTask,客户端在 HeaderExchangeClient 中开启,服务端将在 HeaderExchangeServer 开启。文章开头埋了一个坑:Dubbo 为什么在服务端同时维护 Map 呢?主要就是为了给心跳做贡献,心跳定时任务在发现连接不可用时,会根据当前是客户端还是服务端走不同的分支,客户端发现不可用,是重连;服务端发现不可用,是直接 close。// HeartBeatTaskif (channel instanceof Client) {    ((Client) channel).reconnect();} else {    channel.close();}

    熟悉其他 RPC 框架的同学会发现,不同框架的心跳机制真的是差距非常大。心跳设计还跟连接创建,重连机制,黑名单连接相关,还需要具体框架具体分析。

    除了定时任务的设计,还需要在协议层面支持心跳。最简单的例子可以参考 nginx 的健康检查,而针对 Dubbo 协议,自然也需要做心跳的支持,如果将心跳请求识别为正常流量,会造成服务端的压力问题,干扰限流等诸多问题。

    6498fe75c4a5054c55e3bcd3ff24c4fa.png

    其中 Flag 代表了 Dubbo 协议的标志位,一共 8 个地址位。低四位用来表示消息体数据用的序列化工具的类型(默认 hessian),高四位中,第一位为1表示是 request 请求,第二位为 1 表示双向传输(即有返回response),第三位为 1 表示是心跳事件。心跳请求应当和普通请求区别对待。

    9 注意和 HTTP 的 KeepAlive 区别对待HTTP 协议的 KeepAlive 意图在于连接复用,同一个连接上串行方式传递请求-响应数据

    TCP 的 KeepAlive 机制意图在于保活、心跳,检测连接错误。

    这压根是两个概念。

    10 KeepAlive 常见异常

    启用 TCP KeepAlive 的应用程序,一般可以捕获到下面几种类型错误ETIMEOUT 超时错误,在发送一个探测保护包经过 (tcpkeepalivetime + tcpkeepaliveintvl * tcpkeepaliveprobes)时间后仍然没有接收到 ACK 确认情况下触发的异常,套接字被关闭 java java.io.IOException:Connectiontimedout

    EHOSTUNREACH host unreachable(主机不可达)错误,这个应该是 ICMP 汇报给上层应用的。 java java.io.IOException:Noroute to host

    链接被重置,终端可能崩溃死机重启之后,接收到来自服务器的报文,然物是人非,前朝往事,只能报以无奈重置宣告之。 java java.io.IOException:Connectionresetbypeer

    11 总结

    有三种使用 KeepAlive 的实践方案:默认情况下使用 KeepAlive 周期为 2 个小时,如不选择更改,属于误用范畴,造成资源浪费:内核会为每一个连接都打开一个保活计时器,N 个连接会打开 N 个保活计时器。 优势很明显:TCP 协议层面保活探测机制,系统内核完全替上层应用自动给做好了

    内核层面计时器相比上层应用,更为高效

    上层应用只需要处理数据收发、连接异常通知即可

    数据包将更为紧凑

    关闭 TCP 的 KeepAlive,完全使用应用层心跳保活机制。由应用掌管心跳,更灵活可控,比如可以在应用级别设置心跳周期,适配私有协议。

    业务心跳 + TCP KeepAlive 一起使用,互相作为补充,但 TCP 保活探测周期和应用的心跳周期要协调,以互补方可,不能够差距过大,否则将达不到设想的效果。

    各个框架的设计都有所不同,例如 Dubbo 使用的是方案三,但阿里内部的 HSF 框架则没有设置 TCP 的 KeepAlive,仅仅由应用心跳保活。和心跳策略一样,这和框架整体的设计相关

    展开全文
  • TCP连接建立 TCP运输连接有以下三个阶段: 建立TCP连接 数据传送 释放TCP连接 三报文握手 一开始,两端的TCP进程都处于关闭状态。 然后,TCP服务器首先创建传输控制块,用来存储控制块的信息,例如TCP连接表...

    TCP的连接建立

    TCP运输连接有以下三个阶段:

    1. 建立TCP连接
    2. 数据传送
    3. 释放TCP连接

    三报文握手

    在这里插入图片描述

    在这里插入图片描述
    一开始,两端的TCP进程都处于关闭状态。

    然后,TCP服务器首先创建传输控制块,用来存储控制块的信息,例如TCP连接表……

    在这里插入图片描述
    之后,TCP服务器就进去监听状态,等待TCP客服的连接请求。

    在这里插入图片描述

    1. TCP发送连接请求,并将自己的状态改为同步已发送状态。
      SYN设置为1,表示这是1个连接请求报文。
      seq设置为x,作为TCP客服所选择的初始序号。

    (TCP规定SYN设置为1的报文段不能携带数据但要消耗掉一个序号)

    在这里插入图片描述

    1. 如果TCP服务器同意连接,则向TCP客户端发送一个针对连接请求的确认。
      SYN和ACK设置为1,表明这是一个连接请求确认报文段。
      seq设置为y,是TCP客户端的初始序号。
      == ack为x+1(这是对TCP客户进程所选择的初始序号的确认)==
      ack总是上一个seq的值加1
      在这里插入图片描述
    2. TCP客户端在收到服务器端的连接请求报文段后,还要向服务器进程发送一个普通的TCP确认报文段,并进入连接已建立状态。(即发送一个连接请求确认的确认)
      该报文段ACK=1,表示是一个连接确认。seq设置为x+1,是第一个序号x加一个,(消耗一个序号),ack=y+1表示是对上一个seq的确认。

    在这里插入图片描述
    整个过程就是

    1. 客户端:我喜欢你

    2. 服务器端:我也喜欢你

    3. 客户端:那我们在一起吧

    4. 本来都处于关闭状态,但是服务器知道客户端可能喜欢它,所以一直在等,处于一个监听状态。

    5. 然后客户端就说我喜欢你,说完自己处于同步发送状态。

    6. 然后服务器接收后也发了我喜欢你,然后表示已接收,处于同步接收状态。

    7. 然后客户端收到后,觉得事情成了,就发送我们在一起吧,然后自己就处于连接状态了。

    8. 然后服务器收到我们在一起吧后也懂了,也处于连接状态。

    为什么TCP最后还要发送一个连接请求确认的确认呢?这是否多余?

    答案肯定是不多余

    假若是两报文握手

    假设TCP客户端发送一个连接请求,但是却因为某些原因在网络中滞留了,这必将造成超时重传。

    于是乎就重传了一个TCP连接请求。

    然后服务器收到后,就发送一个TCP连接请求的确认,并进入连接已建立状态。(因为是两报文握手,所以直接连接已建立,而不是同步已接收状态)
    然后向客户端发送一个确认连接的请求,TCP客户端接收到后进入连接已建立状态。

    此时可以进行数据传输。然后数据传输完毕后就通过“四报文挥手”来释放连接。

    此时TCP双方都进入关闭状态,一段时间后,之前滞留在网络中的那个失效的TCP连接请求报文段到达了TCP服务器,此时TCP服务器进入连接已建立状态,并给TCP客户端发送来连接请求,并进入连接已建立状态。

    该报文段进入TCP客户端,但TCP客户端仍为关闭转台,不会理睬。

    但TCP服务已建立连接状态,它会一直等待客户端发送数据,所以会消耗很多资源。

    四报文挥手

    在这里插入图片描述
    1.FIN=1和ACK=1表示是一个终止连接报文,
    seq设置为u,它等于TCP客户进程之前已传送过 的数据的最后一个字节的序号加1。
    确认号ack的序号为v,它等于TCP客户进程之前已收到的数据的最后一个字节的序号加1。

    2.TCP服务器收到一个连接释放后,会弗萨松一个普通确认报文。并处于关闭等待状态。

    在这里插入图片描述
    seq为上一个ack,而ack为上一个seq+1;

    在这里插入图片描述
    然后TCP服务器通知上层的应用进程说TCP客户端要与自己断开连接,此时TCP服务端到TCP服务器这个进程就断了。

    1. 所以第一个过程就是客户端告诉服务器自己想跟它分手,以后不会再主动找它了,然后服务器确认了客户端以后不再找它的请求。

    此时连接处于半关闭状态,也就是客户端不再主动找服务器。但是服务器想找客户端还是可以的。
    (服务器给客户端发送数据,客户端还是得接收)

    1. 第二个过程就是服务器也告诉客户端我以后也不主动找你了,然后客户端答应了服务器,于是两人就都进入关闭状态,从此不再往来。

    但是TCP客户端也知道肯定要完全诀别了。

    在这里插入图片描述

    于是进入一个终止等待状态2,等待TCP服务器最后还有没有话想说。

    如果服务器已经无话想说了,TCP服务器就会发起一个连接释放报文段并进去最后确认状态。

    在这里插入图片描述
    FIN和ACK的值为1,表示这是一个连接释放报文,seq是重新是上次接收数据号加1。
    而ack=上次反方向传输的seq=u +1。

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    为什么需要一个时间等待呢?

    是怕客户端发送最终确认的过程中数据报丢了,这时候服务器会超时重传。

    为了使客户端不是立刻处于关闭状态(还能接收到超时重传的数据报文),所以设置一个时间等待状态,确保能服务器和客户端都能接收到最后确认。

    而且可以使本次连接持续时间内所产生的所有报文段从网络中消失,从而使下一个新的TCP连接中没有旧的报文段。

    TCP的保活措施

    如何客户端出现了故障,无法再给服务器发送数据,就应该有措施让服务器知道,而不是让服务器白白地等待下去。

    在这里插入图片描述

    展开全文
  •  假如应用服务器A上有若干模块连接某数据库服务机器B,当B异常死,需要将B的请求切换到备份系统,这样已经建立的连接就遗留了下来。如果A上hang住的连接占用的服务线程较多,就可能造成业务系统受到影响,因此...
  • HTTP的TCP连接管理

    2021-08-13 01:59:55
    因为TCP连接可靠,用其它协议也是允许的1. 最开始HTTP连接一个服务器需要服务很多请求,这些请求如果连接以后不断开,就会有大量处于等待状态的连接需要维护,而进程、内存资源、带宽资源、文件描述符数量有上限,...
  • netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' Windows下使用以下命令查看网络连接状态 netstat -n |find /i "time_wait" /c netstat -n |find /i "close_wait" /c netstat -...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 21,993
精华内容 8,797
热门标签
关键字:

tcp假连接