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

TCP 四次挥手:

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

挥手序号问题:

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

::55.908193 IP localhost. > localhost.ospf-lite: Flags [P.], seq :, ack , win , length
::55.908606 IP localhost.ospf-lite > localhost.: Flags [P.], seq :, ack 236, win , length
::55.908703 IP localhost. > localhost.ospf-lite: Flags [.], ack , win , length
::00.029841 IP localhost. > localhost.ospf-lite: Flags [F.], seq 236, ack , win , length
::00.030176 IP localhost.ospf-lite > localhost.: Flags [F.], seq , ack , win , length
::00.030225 IP localhost. > localhost.ospf-lite: Flags [.], ack , win , length

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

挥手过程中状态转换问题

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

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

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

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

::11.825971 IP cps. > cps.ddi-tcp-: Flags [S], seq , win , options [mss ,sackOK,TS val  ecr ,nop,wscale ], length
::05.598183 IP cps.ddi-tcp- > cps.: Flags [S.], seq , ack , win , options [mss ,sackOK,TS val ecr ,nop,wscale ], length
::11.826052 IP cps. > cps.ddi-tcp-: Flags [.], ack , win , options [nop,nop,TS val ecr ], length
::11.826159 IP cps. > cps.ddi-tcp-: Flags [P.], seq :, ack , win , options [nop,nop,TS val ecr ], length
::11.826170 IP cps.ddi-tcp- > cps.: Flags [.], ack , win , options [nop,nop,TS val ecr ], length
::11.826193 IP cps. > cps.ddi-tcp-: Flags [F.], seq , ack , win , options [nop,nop,TS val ecr ], length
::11.865650 IP cps.ddi-tcp- > cps.: Flags [.], ack , win , options [nop,nop,TS val ecr ], length

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

tcp               192.168.24.126:     0.0.0.0:*               LISTEN
tcp 192.168.24.126: 192.168.24.126: FIN_WAIT2 客户端的状态
tcp 192.168.24.126: 192.168.24.126: CLOSE_WAIT

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

FIN-WAIT2超时
17::18.401858 IP cps. > cps.ddi-tcp-: Flags [P.], seq :, ack , win , options [nop,nop,TS val ecr ], length
::18.401872 IP cps.ddi-tcp- > cps.: Flags [.], ack , win , options [nop,nop,TS val ecr ], length
::18.401905 IP cps. > cps.ddi-tcp-: Flags [F.], seq , ack , win , options [nop,nop,TS val ecr ], length
::18.441595 IP cps.ddi-tcp- > cps.: Flags [.], ack , win , options [nop,nop,TS val ecr ], length
::18.474816 IP cps.ddi-tcp- > cps.: Flags [F.], seq , ack , win , options [nop,nop,TS val ecr ], length
::18.474871 IP cps. > cps.ddi-tcp-: Flags [R], seq , win , 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()
{
if(read() == )
{
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 =
net.ipv4.netfilter.ip_conntrack_tcp_timeout_close_wait =
net.ipv4.netfilter.ip_conntrack_tcp_timeout_fin_wait =

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

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

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

TCP 连接关闭及TIME_WAIT探究的更多相关文章

  1. socket使用TCP协议时,send、recv函数解析以及TCP连接关闭的问题

    Tcp协议本身是可靠的,并不等于应用程序用tcp发送数据就一定是可靠的.不管是否阻塞,send发送的大小,并不代表对端recv到多少的数据. 在阻塞模式下, send函数的过程是将应用程序请求发送的数 ...

  2. [转]socket使用TCP协议时,send、recv函数解析以及TCP连接关闭的问题

    Tcp协议本身是可靠的,并不等于应用程序用tcp发送数据就一定是可靠的.不管是否阻塞,send发送的大小,并不代表对端recv到多少的数据. 在阻塞模式下, send函数的过程是将应用程序请求发送的数 ...

  3. TCP连接关闭总结

    由于涉及面太广,只作简单整理,有兴趣的可参考<UNIX Networking Programming>volum 1, Section 5.7, 5.12, 5.14, 5.15, 6.6 ...

  4. TCP 连接中的TIME_WAIT

    原文:http://blog.csdn.net/wangpengqi/article/details/17245349 这就有个细节,一次http请求,谁会先断开TCP连接?什么情况下客户端先断,什么 ...

  5. tcp连接的状态变迁以及如何调整tcp连接中处于time_wait的时间

    一.状态变迁图 二.time_wait状态 针对time_wait和close_wait有个简单的描述帮助理解: Due to the way TCP/IP works, connections ca ...

  6. socket 连接关闭的TIME_WAIT状态的理由

    MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间将在网络中消失 TIME_WAIT两点原因: 1.TCP分节可能 ...

  7. Linux 内核协议栈之TCP连接关闭

    Close行为: 当应用程序在调用close()函数关闭TCP连接时,Linux内核的默认行为是将套接口发送队列里的原有数据(比如之前残留的数据)以及新加入 的数据(比如函数close()产生的FIN ...

  8. ubuntu系统TCP连接参数优化-TIME_WAIT过多解决办法

    状态:描述CLOSED:无连接是活动的或正在进行LISTEN:服务器在等待进入呼叫SYN_RECV:一个连接请求已经到达,等待确认SYN_SENT:应用已经开始,打开一个连接ESTABLISHED:正 ...

  9. TCP连接的建立与关闭

    TCP是主机对主机层的传输控制协议:建立连接要三个握手,断开连接要四次挥手. 位码即TCP标志位,有6种标示:SYN(synchronous建立联机),ACK(acknowledgement 确认), ...

随机推荐

  1. hihoCoder Demo Day dp

    题意:有一个机器人被困在一个的迷宫中,机器人的初始位置是,目的地是,并且它的移动方式很奇怪:只能一直向右,直到不能再向右才能把方向变成向下:只能一直向下,直到不能再向下才能把方向变成向右.迷宫中的每个 ...

  2. uva140

    全排列回溯剪枝. 题目数据很水.记录当前最小带宽,边回溯边计算当前序列最大的距离(也就是带宽),如果当前带宽超过了当前的最小带宽就剪枝. 注意下,数据读入时的字符串处理. AC代码 #include& ...

  3. ClientURL库-curl_setopt()

    这是一个出现得比较突兀的问题: 好好学习使用一下这个库:http://php.net/manual/zh/book.curl.php curl_setopt函数:curl_setopt - 设置一个c ...

  4. python高阶函数式编程

    from functools import reduce def str2int(s): def fn(x, y): return x * 10 + y def char2num(s): return ...

  5. 关于 Java 面试,你应该准备这些知识点

    来源:占小狼, www.jianshu.com/p/1b2f63a45476 马老师说过,员工的离职原因很多,只有两点最真实: 钱,没给到位 心,受委屈了 当然,我是想换个平台,换个方向,想清楚为什么 ...

  6. struts2标签库----数据标签详解

    上篇文章我们介绍struts2标签库中的控制标签的基本使用和部分原理,本篇文章接着了解下标签库中有关数据标签的使用和原理.主要涉及以下数据标签: action标签:用于在视图页面跳转到一个Action ...

  7. 【前端】Vue2全家桶案例《看漫画》之七、webpack插件开发——自动替换服务器API-URL

    转载请注明出处:http://www.cnblogs.com/shamoyuu/p/vue_vux_app_7.html 项目github地址:https://github.com/shamoyuu/ ...

  8. 使用poi和jfreechart生成excel图表图片

    最近项目在频繁的操作excel,里边涉及到很多和图表有关的东西.有时候需要使用java操作excel自带的图标,比较复杂的我们都是使用excel模板的形式实现. 除此之外,也有一些功能只需要生成对应的 ...

  9. Struts2实现文件上传报错(二)

    1.具体报错如下 usage: java org.apache.catalina.startup.Catalina [ -config {pathname} ] [ -nonaming ] { -he ...

  10. JavaScript的几种常见的创建方式

    1.通过Object构造函数或者对象字面量创建单个对象 使用字面量方法创建对象:var stut = {name: "张三"}; 使用内置构造函数创建对象:var stu = ne ...