简单说说TCP(4) --- 断开连接四次握手

四次握手流程

先来一张TCP断开连接流程图:(图片来自于网络)

上图中,A是主动关闭方,B是被动关闭方,四次握手可以描述为:

  • 第一次握手:A告诉B,“我要关闭连接了”。
  • 第二次握手:B回复A,“我知道你要关闭了,但是请等一下,我还有数据没有传完,你等我消息”。
  • 第三次握手:B告诉A,“我的数据发完了,你可以关闭连接了”。
  • 第四次握手:A回复B,“好的,你先关吧,我2MSL时间后再关”。

出现大量CLOSE_WAIT的原因

被动关闭方收到主动关闭方发来的FIN,则会回应ACK,并进入CLOSE_WAIT状态。但如果被动关闭方不执行close(),就不能由CLOSE_WAIT迁移到LAST_ACK。在该状态下,recv/read会返回0。

举例来说,当主动关闭方调用closesocket的时候,被动关闭方的应用程序正在调用recv,这时候有可能应用程序没有收到主动关闭方发来的FIN包,而是由TCP代回了一个ACK,所以就陷入CLOSE_WAIT出不来了。

TCP的KeepLive功能,可以让操作系统替我们自动清理掉CLOSE_WAIT的连接。但是KeepLive在Windows操作系统下默认是7200秒,需要调整一下:

打开注册表(运行 -> 输入regedit -> 回车),在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters里修改:

KeepAliveTime              REG_DWORD  300000
KeepAliveInterval          REG_DWORD  1000
TcpMaxDataRetransmissions  REG_DWORD  5

为什么要有TIME_WAIT

  • 让4次握手关闭流程更加可靠。
    若最后一个ACK丢失,被动关闭方会重新发一个FIN。在TIME_WAIT状态内,主动关闭方收到重发的FIN后,会重发ACK。

    而倘若没有2MSL的TIME_WAIT状态,当主动关闭方收到重发的FIN后,会回复一个RST,被动关闭方在收到RST后,会将其解释成一个错误:connect reset by peer。

  • 等待老的重复分节在网络中消逝,防止lost duplicate对后续新建的incarnation connection的传输造成破坏。

    • lost duplicate:在实际的网络中非常常见,经常是由于路由器产生故障,路径无法收敛,导致一个数据包在路由器A、B、C之间做类似死循环的跳转。IP头部有个TTL,限制了一个包在网络中的最大跳数。因此这个包有两种命运,

      • TTL变为0,在网络中消失;
      • TTL在变为0之前路由器路径收敛,它凭借剩余的TTL跳数终于到达目的地。但非常可惜的是TCP通过超时重传机制在早些时候发送了一个跟它一模一样的包,并且先于它到达了目的地,因此它注定被TCP协议栈抛弃;
    • incarnation connection:跟上次的socket pair一模一样的新连接。倘若没有2MSL时间的TIME_WAIT状态来等待lost duplicate失效,那么就会出现如下情况,

      一个incarnation connection收到的seq=1000,这时来了一个lost duplicate为seq=1000,len=1000,则tcp认为这个lost duplicate合法,并存放入了receive buffer,导致传输出现错误。

TIME_WAIT状态为什么持续2MSL

MSL(Maximum Segment Lifetime)是指报文最大生存时间。RFC 793规范MSL为2min。然后,实际TCP实现中的常用值是30s、1min或2min。

之所以是2MSL而不是1MSL,是因为这里面还包括了最后一个ACK在路上的时间。

2MSL在Windows上默认是4min。

TIME_WAIT带来的问题

RFC要求socket pair在处于TIME_WAIT时,不能再起一个incarnation connection。但绝大部分TCP实现,强加了更为严格的限制,“在2MSL等待期间,socket中使用的本地端口在默认情况下不能再被使用”。

这个限制对于client来说是无所谓的,但是对于server,就严重了。一旦server端主动关闭了某个client的连接,导致server监听的端口在2MSL时间内无法接受新的连接,这显然是不行的。

  • 方案一
    保证由client主动发起关闭。

  • 方案二
    server主动关闭的时候使用RST的方式,不进入 TIME_WAIT状态。
    具体做法是设置socket的SO_LINGER选项。

  • 方案三
    给server的socket设置SO_REUSEADDR选项,这样的话就算server端口处于TIME_WAIT状态,在这个端口上依旧可以将服务启动。

    当然,“非相同socket pair”这个限制依然存在,即在2MSL时间内仍然拒绝来自与之前断掉的client相同ip和port的连接,错误信息为address already in use。

    代码如下:

int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

不过在BSD-derived实现和Windows Server 2003中,只要SYN的seq比上一次关闭时的最大seq还要大,那么TIME_WAIT状态一样接受这个SYN。

  • 方案四
    Linux实现了一个TIME_WAIT状态快速回收的机制,即无需等待2MSL这么久的时间,而是等待一个Retrans时间即释放,也就是等待一个重传时间(一般超级短,以至于你都来不及能在netstat -ant中看到TIME_WAIT状态)随即释放。
    释放了之后,一个连接的tuple元素信息就都没有了,而此时,新建立的TCP却面临着危险:

    • 可能被之前迟到的FIN包给终止
    • 可能被之前连接劫持

    于是需要有一定的手段避免这些危险。什么手段呢?虽然曾经连接的tuple信息没有了,但是在IP层还可以保存一个peer信息,注意这个信息不单单是用于TCP这个四层协议的,路由逻辑也会使用它,其字段包括但不限于:

    • 对端ip地址
    • peer最后一次被TCP触摸到的时间戳

    在快速释放掉TIME_WAIT连接之后,peer依然保留着。丢失的仅仅是端口信息。不过有了peer的IP地址信息以及TCP最后一次触摸它的时间戳就足够了,TCP规范给出一个优化,即一个新的连接除了同时触犯了以下几点,其它的均可以快速接入,即使它本应该处在TIME_WAIT状态:

    • 来自同一台机器的TCP连接携带时间戳
    • 之前同一台peer机器(仅仅识别IP地址,因为连接被快速释放了,没了端口信息)的某个TCP数据在MSL秒之内到过本机
    • 新连接的时间戳小于peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳

    看样子只有以上的3点的同时满足才能拒绝掉一个新连接,要比TIME_WAIT机制设置的障碍导致的连接拒绝几率小很多,但是要看到,上述的快速释放机制没有端口信息!这就把几率扩大了65535倍。然而,如果对于单独的机器而言,这不算什么,因为单台机器的时间戳不可能倒流的,出现上述的3点均满足时,一定是老的重复数据包又回来了。
    但是,一旦涉及到NAT设备,就悲催了,因为NAT设备将数据包的源IP地址都改成了一个地址(或者少量的IP地址),但是却基本上不修改TCP包的时间戳。

    TIME_WAIT快速回收在Linux上通过net.ipv4.tcp_tw_recycle启用,由于其根据时间戳来判定,所以必须开启TCP时间戳才有效。

    建议:如果前端部署了三/四层NAT设备,尽量关闭快速回收,以免发生NAT背后真实机器由于时间戳混乱导致的SYN拒绝问题。

Windows下调整TIME_WAIT时间

1、打开注册表(运行 -> 输入regedit -> 回车)
2、在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters里添加一项:

TcpTimedWaitDelay  REG_DWORD  0x0000001e(30)

Linux下调整内核参数

编辑文件:

vim /etc/sysctl.conf

加入以下内容:

net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30

最后执行:

/sbin/sysctl -p

使参数生效。

  • net.ipv4.tcp_syncookies = 1
    开启SYN cookies,当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击。默认为0,表示关闭。

  • net.ipv4.tcp_tw_reuse = 1
    开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭。

  • net.ipv4.tcp_tw_recycle = 1
    开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。

  • net.ipv4.tcp_fin_timeout = 30
    系統默认的 TIMEOUT 时间。

Linux下kill进程时对socket的关闭处理

Linux照顾到了一种特殊情况,即杀死进程的情况,在系统kill进程的时候,会直接调用连接的close函数单方面关闭一个方向的连接,然后并不会等待对端关闭另一个方向的连接进程即退出。现在的问题是,TCP规范和UNIX进程的文件描述符规范直接冲突!进程关闭了,套接字就要关闭,但是TCP是全双工的,你不能保证对端也在同一个时刻同意并且实施关闭动作,既然连接不能关闭,作为文件描述符,进程就不会关闭得彻底!所以,Linux使用了一种“子状态”的机制,即在进程退出的时候,单方面发送FIN,然后不等后续的关闭序列即将连接拷贝到一个占用资源更少的TW套接字,状态直接转入TIMW_WAIT,此时记录一个子状态FIN_WAIT_2,接下来的套接字就和原来的属于进程描述符的连接没有关系了。等到新的连接到来的时候,直接匹配到这个主状态为TW,子状态为FIN_WAIT_2的TW连接上,它负责处理FIN,FIN ACK等数据。

统计所有TCP连接的各种状态的数量

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

你可能感兴趣的:(tcp,四次握手)