网络编程学习7--TIME_WAIT

TIME_WAIT 发生的场景

TCP的四次挥手

image-20211211201524222

TCP连接终止时,主机 1 先发送 FIN 报文,主机 2 进入 CLOSE_WAIT 状态,并发送一个 ACK 应答,同时,主机 2 通过 read 调用获得 EOF,并将此结果通知应用程序进行主动关闭操作,发送 FIN 报文。主机 1 在接收到 FIN 报文后发送 ACK 应答,此时主机 1 进入 TIME_WAIT 状态。

并且主机1在TIME_WAIT状态停留的时间是固定的,为2MSL(即2倍的报文最大生存时间)。在Linux系统中,有一个硬编码的字段,TCP_TIMEWAIT_LEN,其值为60秒,所以Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒

 /* how long to wait to destroy TIME-WAIT state, about 60 seconds  */
 #define TCP_TIMEWAIT_LEN (60*HZ)
复制代码

注意:只有发起连接的一方会进入TIME_WAIT状态。

TIME_WAIT状态的作用

通过TIME_WAIT可以确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭。

两个理由:

  1. 如果主机1没有TIME_WAIT状态,而是直接进入CLOSED状态,此时相当于主机1已经断开连接,那么它会失去当前状态的上下文,此时如果主机1的ACK没有传输成功的话,主机2会重新传输一个FIN报文,但是由于主机1已经失去了之前状态的上下文,所以只能回复一个RST,从而导致被动关闭方出现错误。 通过TIME_WAIT状态,主机1就可以在收到FIN报文后,重新传输一个ACK报文,直到连接正确关闭为止。

  2. 为了让旧连接的所有报文都能自然消亡。

    在网络中,报文经常要过一段时间才能到达目的地,路由器重启、链路故障都可能导致这种情况的出现,当这些报文到达时,如果TCP连接的四元组(源IP,源端口,目的IP,目的端口)所代表的连接不存在,那么直接丢弃该报文即可。

    但是,如果原连接中断后,又重新创建了一个原连接的“化身”,(化身即指新连接的四元组和原来连接的四元组完全相同),并且,之前旧连接的报文经过了一段时间也到达了目的地,那么这个报文会被误认为是连接“化身”的一个 TCP 分节,这样就会对 TCP 通信产生影响。

    不过,在经过TIME_WAIT状态,即等待了2MSL时间后,足以让原来连接的报文都在网络中自然消失,那么之后的报文一定都是新连接所产生的了。

注意:2MSL时间是在主机1收到FIN,并发送ACK后开始计时的,如果ACK没有送到主机2,主机1收到了重传的FIN后,会重新开始计时2MSL,这是因为主机1重新发送了ACK报文,所以应当按照ACK报文发送的时间开始重新计时,防止该ACK报文对连接“化身”造成干扰。

TIME_WAIT的危害

  1. 对内存资源的占用。
  2. 对端口资源的占用(主要)。由于一个TCP连接至少需要消耗一个本地端口,由于端口资源有限,如果TIME_WAIT状态过多(高并发),会导致端口都被占用完毕,从而无法创建新连接。

在高并发的情况下优化TIME_WAIT

  1. net.ipv4.tcp_max_tw_buckets(暴力,不推荐)
    通过sysctl命令(该命令用于运行时配置内核参数),将系统值调小。该值默认为18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的 TIME_WAIT 连接状态重置,并且只打印出警告信息。但是这种方法带来的问题远比解决的问题多。

  2. 降低TCP_TIMEWAIT_LEN
    降低TCP_TIMEWAIT_LEN,然后重新编译内核

  3. 设置SO_LINGER
    可以通过设置socket选项,来设置调用close或者shutdown关闭连接时的行为。

    setsockopt() 函数

     int setsockopt(int s, int level, int optname, const void * optval, ,socklen_t optlen);
    复制代码

    该函数用来设置参数 s 所指定的 socket 的状态,参数 level 表示需要设置的网络层,一般设置成 SOL_SOCKET 以存取socket层,参数 optname 表示想要设置的选项,参数 optval 表示想要设置的值,参数 optlen 表示 optval 的长度。

    返回值:成功时返回0,失败返回-1.

    
     struct linger {
      int  l_onoff;    /* 0 = off, nonzero = on */
      int  l_linger;    /* linger time, POSIX specifies units as seconds */
     }
    
     struct linger so_linger;
     so_linger.l_onoff = 1;
     so_linger.l_linger = 0;
     setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
    复制代码

    设置结构体linger的参数有几种情况:

    • l_onoff = 0,表示本选项关闭,l_linger的值也被忽略。此时代表默认行为,close或shutdown会立即返回,如果在socket发送缓冲区中有数据残留,系统会将试着把这些数据发送出去。
    • l_onoff != 0,并且 l_linger = 0,此时调用close后,会立刻发送一个 RST 标志给对端,此时TCP连接将跳过四次挥手阶段,也就跳过了TIME_WAIT状态,直接关闭。这种关闭代表”强行关闭“,在这种情况下,发送缓冲区中的排队数据不会再发送,被动关闭方也不知道对端已经彻底断开。只有当被动关闭方正阻塞在recv()调用上,此时接收到RST时,会立刻得到一个“connet reset by peer”的异常。
    • l_onoff != 0, l_linger != 0,此时调用close后,调用close的线程将被阻塞,直到缓冲区的数据被发送出去,或者设置的 l_linger 计时时间到为止。

    上面第二种情况中,虽然可以跳过TIME_WAIT状态,但是十分危险,不推荐。

  4. 设置 net.ipv4.tcp_tw_reuse (安全)
    通过该选项,允许复用处于TIME_WAIT的socket为新的连接所用。但是必须满足两点要求:1. 只适用于连接发起方(C/S 模型中的客户端)。2. 对应的 TIME_WAIT 状态的连接创建时间超过 1 秒才可以被复用。

    使用这个选项时,还有一个前提,即需要打开对TCP时间戳的支持,即net.ipv4.tcp_timestamps=1(默认即为 1)。

TCP的时间戳选项timestamp

时间戳选项占10个字节,其中包含timestamp 和 timestamp echo两个值,各4个字节。

发送方在发送报文段时把当前时钟的时间值放入timestamp字段,接收方在对该报文发送确认报文段时,会把timestamp字段复制到timestamp echo 字段,因此发送方,在收到确认报文后,可以准确计算出RTT。

实例:

假设a主机和b主机之间通信,a主机向b主机发送一个报文段s1,那么在s1报文中timestamp存储的是a主机发送s1时的内核时刻ta1,b主机收到s1报文并向a主机发送含有确认ack的报文s2,在s2报文中,timestamp为b主机此时的内核时刻tb,而timestamp echo字段为从s1报文中解析出的ta1,当a主机接收到b主机发送过来的确认ack报文s2时,a主机此时内核时刻为ta2,a主机从s2报文的timestamp echo选项中可以解析出该确认ack所确认的报文的发送时刻为ta1,RTT=ta2 -ta1。ta2和ta1都来自a主机的内核,所以不需要在tcp连接的两端进行任何时钟同步的操作

TCP时间戳选项的功能

除了计算往返时延RTT这一功能外,时间戳选项还可以防止回绕的序号

由于TCP报文的序列号只有32位,而每增加2^32个序列号后就会重复使用原来用过的序列号,假设我们有一条高速网络,通信的主机双方有足够大的带宽涌来快速的传输数据。例如1Gb/s的速率发送报文,则不到35秒报文的序号就会重复。这样就会造成TCP传输混乱的情况,通过采用时间戳选项,可以很容易的分辨出相同序列号的数据报,哪个是最近发送,哪个是以前发送的。

おすすめ

転載: juejin.im/post/7041160042569007112