• TCP 连接关闭及TIME_WAIT探究


    这里主要记录一下TCP连接在关闭的时刻,有哪些细节问题。方便在以后的程序设计中能够注意这些细节, 以避免出现这些错误。首先我们来看一下TCP的状态转换图。如《unix网络编程》卷一所示如下图:

    TCP 四次挥手:

    • 挥手时的序号问题
    • 挥手过程中状态转换问题
    • TIME_WAIT 产生原因

    挥手序号问题:

    这里可以看出FIN也占用了一个序号,例如FIN M, 对方回应ACK 确认序号为M+1。最后发送FIN也是如此。那么这里的M和N在传输数据过程中怎样得到的。看一下一个抓包的例子如下

    12:40:55.908193 IP localhost.34876 > localhost.ospf-lite: Flags [P.], seq 206:236, ack 199, win 342, length 30
    12:40:55.908606 IP localhost.ospf-lite > localhost.34876: Flags [P.], seq 199:221, ack 236, win 342, length 22
    12:40:55.908703 IP localhost.34876 > localhost.ospf-lite: Flags [.], ack 221, win 342, length 0
    12:41:00.029841 IP localhost.34876 > localhost.ospf-lite: Flags [F.], seq 236, ack 221, win 342, length 0
    12:41:00.030176 IP localhost.ospf-lite > localhost.34876: Flags [F.], seq 221, ack 237, win 342, length 0
    12:41:00.030225 IP localhost.34876 > localhost.ospf-lite: Flags [.], ack 222, win 342, length 0

    这里可以清楚的看到 发送FIN的序列号正是真实已经确认数据的序列号的下一个序号。FIN也占用一个序列号, 所以FIN的ACK序号也是加一。

    挥手过程中状态转换问题

    这里有两个测试程序如下:

     1 #!/usr/bin/env python
     2 # coding: utf-8
     3 import socket
     4 import os
     5 import sys
     6 import time
     7 def main(argv):
     8     host = (argv[1], int(argv[2]))
     9     filename = argv[3]
    10     fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    11     try:
    12         fd.connect(host)
    13     except socket.error, e:
    14         print e
    15         sys.exit(0)
    16     fp = open(filename,'rb')
    17     while True:
    18         buff = fp.read(2048)
    19         if  buff:
    20             fd.send(buff)
    21         else:
    22              break
    23 
    24 if __name__ == '__main__':
    25     if len(sys.argv) != 4:
    26         print "Like client.py 192.168.1.100 6666 a.dd"
    27         sys.exit(0)
    28     main(sys.argv)
     1 #!/usr/bin/env python
     2 # coding: utf-8
     3 import socket
     4 import os
     5 import sys
     6 import time
     7 
     8 def main(port):
     9     fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    10     host = socket.gethostname()
    11     fd.bind((host, port))
    12     fd.listen(10)
    13     while True:
    14         clifd, addr = fd.accept()
    15         print 'Client address : ', addr
    16         while True:
    17             time.sleep(30) 
    18             data =  clifd.recv(1024)
    19             if data:
    20                 print data
    21             else:#读取到0 连接断开要60s
    22                 print "client closed"
    23                 clifd.close()
    24                 break
    25 
    26 if __name__ == '__main__':
    27     port = 8888
    28     main(port)

    一个客户端 另一个是服务器端。

    1. 首先在服务器接受连接后就进入等待, 客户端连接完成后就将数据全部发送并关闭连接程序退出 抓包结果如下:

    17:13:11.825971 IP cps.59302 > cps.ddi-tcp-1: Flags [S], seq 3995218772, win 43690, options [mss 65495,sackOK,TS val 18944024 ecr 0,nop,wscale 7], length 0
    01:06:05.598183 IP cps.ddi-tcp-1 > cps.59302: Flags [S.], seq 4041698578, ack 3995218773, win 43690, options [mss 65495,sackOK,TS val 18944024 ecr 18944024,nop,wscale 7], length 0
    17:13:11.826052 IP cps.59302 > cps.ddi-tcp-1: Flags [.], ack 1, win 342, options [nop,nop,TS val 18944024 ecr 18944024], length 0
    17:13:11.826159 IP cps.59302 > cps.ddi-tcp-1: Flags [P.], seq 1:524, ack 1, win 342, options [nop,nop,TS val 18944024 ecr 18944024], length 523
    17:13:11.826170 IP cps.ddi-tcp-1 > cps.59302: Flags [.], ack 524, win 350, options [nop,nop,TS val 18944024 ecr 18944024], length 0
    17:13:11.826193 IP cps.59302 > cps.ddi-tcp-1: Flags [F.], seq 524, ack 1, win 342, options [nop,nop,TS val 18944024 ecr 18944024], length 0
    17:13:11.865650 IP cps.ddi-tcp-1 > cps.59302: Flags [.], ack 525, win 350, options [nop,nop,TS val 18944064 ecr 18944024], length 0

     在服务器暂停的30s内 已经收到了客户端发送的数据和FIN 并都得到了确认。再看一下连接状态

    tcp        0      0 192.168.24.126:8888     0.0.0.0:*               LISTEN     
    tcp        0      0 192.168.24.126:59338    192.168.24.126:8888     FIN_WAIT2  客户端的状态
    tcp      524      0 192.168.24.126:8888     192.168.24.126:59338    CLOSE_WAIT

    这里看到即使程序退出 FIN-WAIT1 FIN-WAIT2 TIME-WAIT这三种状态也不会消失  它们是由内核维护,有相关定时器控制。 如这里的FIN-WAIT2状态超时后就不再进入TIME-WAIT 这时对端再回复FIN时 就会回应RST。 若在超时时间内则正常回应并彻底断开连接。

    FIN-WAIT2超时
    17
    :18:18.401858 IP cps.59336 > cps.ddi-tcp-1: Flags [P.], seq 1:524, ack 1, win 342, options [nop,nop,TS val 19250600 ecr 19250600], length 523 17:18:18.401872 IP cps.ddi-tcp-1 > cps.59336: Flags [.], ack 524, win 350, options [nop,nop,TS val 19250600 ecr 19250600], length 0 17:18:18.401905 IP cps.59336 > cps.ddi-tcp-1: Flags [F.], seq 524, ack 1, win 342, options [nop,nop,TS val 19250600 ecr 19250600], length 0 17:18:18.441595 IP cps.ddi-tcp-1 > cps.59336: Flags [.], ack 525, win 350, options [nop,nop,TS val 19250640 ecr 19250600], length 0 17:20:18.474816 IP cps.ddi-tcp-1 > cps.59336: Flags [F.], seq 1, ack 525, win 350, options [nop,nop,TS val 19370673 ecr 19250600], length 0 17:20:18.474871 IP cps.59336 > cps.ddi-tcp-1: Flags [R], seq 223914856, win 0, length 0   FIN-WAIT2 超时消失后发送FIN 得到RST

    2. FIN_WAIT_1 状态: 假如当主动方close时, 发送FIN给对方,但是在这个过程中一直没有收到来自对方对FIN的确认, 那么主动方就会重传一定时间的FIN,当超时后就会放弃,然后不经过TIME_WAIT 直接清理缓存断开连接。可以参考: http://www.cnblogs.com/MaAce/p/8039119.html

    3. 主动方close之后,对方还有数据在发送并在路上时: 这种情况也是常常发生, 主动方close掉连接,就是把读写全部关闭并把发送缓冲区的全部数据一次性发送到对端。那么这时如果有对方发送的数据包在路上时, 当数据包达到时,刚好close已返回,那么这时主动断开的一方就会发送rst给对方。这时可以用shutdown来替换close来获取最后接收的内容。 关闭时仅仅关闭写端,然后再继续read直到读到0为止 表示收到对端的fin。当不确定关闭时还有没有未接收的数据可以这样使用。这里可以确保接收完整 直到收到断开信息 保证了对方应用进程已经读取了我们的数据。但这里要注意的是shutdown写端会把发送缓冲区清空。

    //类似这样
    shutdown(fd, FD_WR);
    while(1)
    {
        if(read() == 0)
        {
            break;
        }
    }

    4. close关闭连接后 默认情况下是立即返回以后就不再接收和发送普通数据 若发送缓冲区有数据就把数据一次性发送到对端。这里有可能并没有收到对方的对 数据和FIN  的确认,然而close已返回。 这里可以设置套接字属性SO_LINGER 延迟关闭来 确保收到对方的确认信息  在一定时间内 收到了确认 则close 返回成功。如果在延时时间内并未收到来自对端的确认,那么close就会返回错误EWOULDBLOCK 如下图:。这里还要注意此时对于非阻塞而言, 直接返回错误EWOULDBLOCK。所以验证close的返回值是很有必要的。至于so_linger的用法网上例子很多 : http://blog.csdn.net/factor2000/article/details/3929816

     

    如果发送缓冲区的数据没有发送完毕或者没有收到对端确认,close就返回,内核就放弃没有发送的数据或是不再等待B端的确认,直接发送RST复位连接不进入TIME_WAIT状态。

     TIME_WAIT 状态

    由以上可知,即使程序退出,内核也会帮其维护timewait的定时器。维持这个状态的原因如下:

    1. 假设最终的ACK丢失,server将重发FIN,client必须维护TCP状态信息以便可以重发最终的ACK,否则会发送RST,结果server认为发生错误。TCP实现必须可靠地终止连 接的两个方向(全双工关闭),client必须进入 TIME_WAIT 状态,因为client可能面 临重发最终ACK的情形。 

    2. 如果 TIME_WAIT 状态保持时间不足够长(比如小于2MSL),第一个连接就正常终止了。 第二个拥有相同相关五元组的连接出现,而第一个连接的重复报文到达,干扰了第二 个连接。TCP实现必须防止某个连接的重复报文在连接终止后出现,所以让TIME_WAIT 状态保持时间足够长(2MSL),连接相应方向上的TCP报文要么完全响应完毕,要么被 丢弃。建立第二个连接的时候,不会混淆。

    Linux下我们可以设置时检查一下time和wait的值

    #sysctl -a | grep time | grep wait
    
    net.ipv4.netfilter.ip_conntrack_tcp_timeout_time_wait = 120
    net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait = 60
    net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait = 120

    处于timewait时 内核并不会把他的相关结构清空。

    其中套接字选项中还有地址和端口重用的选项SO_REUSEADDR和SO_REUSEPORT 这两个选项就是为了避免server 重启时 端口忙的问题。


    这个套接字选项通知内核,如果端口忙,但TCP状态位于 TIME_WAIT ,可以重用 端口。如果端口忙,而TCP状态位于其他状态,重用端口时依旧得到一个错误信息,指明"地址已经使用中"。如果你的服务程序停止后想立即重启,而新套接字依旧使用同一端口,此时 SO_REUSEADDR 选项非常有用。必须意识到,此时任何非期望数据到达,都可能导致服务程序反应混乱,不过这只是一种可能,事实上很不 
    可能。

  • 相关阅读:
    JDBC操作数据库的步骤 ?
    switch 是否能作用在 byte 上,是否能作用在 long 上, 是否能作用在 String 上?
    有哪些不同类型的IOC(依赖注入)方式?
    ApplicationContext通常的实现是什么?
    如何给Spring 容器提供配置元数据?
    一个”.java”源文件中是否可以包含多个类(不是内部类)? 有什么限制?
    数据库连接(Database link)?
    你怎样定义类的作用域?
    JSP的常用指令有哪些?
    Mapper 编写有哪几种方式?
  • 原文地址:https://www.cnblogs.com/MaAce/p/8603583.html
Copyright © 2020-2023  润新知