这里主要记录一下 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 序号也是加一
挥手过程中状态转换问题
这里有两个测试程序如下:
- #!/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. 首先在服务器接受连接后就进入等待, 客户端连接完成后就将数据全部发送并关闭连接程序退出 抓包结果如下:
- 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 选项非常有用必须意识到, 此时任何非期望数据到达, 都可能导致服务程序反应混乱, 不过这只是一种可能, 事实上很不
可能
来源: https://www.cnblogs.com/MaAce/p/8603583.html