最近做了一个项目,其中有个模块用了C/S架构,SOCKET产生了很多TIME_WAIT。虽然知道TIME_WAIT本身是不可避免的,但借此机会结合代码全面理清TCP的连接机制也好。示例代码中Client端是用C写的,Server端是用Go写的。
TCP连接
如下图这是一张完整的TCP链接交互过程,包含建立时的三步握手,建立后的数据发送,以及关闭时的四步握手
TCP的链接是从三步握手开始的,第一个SYN报文是由Cient端发起的, Client端发起请求前Server端必须已经处于Listen状态,如下如图:
[root@sndbassitapp03 ~]# netstat -an |grep 8888
tcp 0 0 :::8888 :::* LISTEN
Server端的代码:
ln, err := net.Listen("tcp", ":8887")
if err != nil {
panic(err)
return
}
defer ln.Close()
mlog.log.Informational("This is a TCP Server 8887 !!!!")
for {
conn, err := ln.Accept()
if err != nil {
panic(err)
}
//业务处理代码
time.Sleep(time.Second * 1)
}
这时候Client还没有发起请求,所以在Client系统中没有该TCP流信息
[wjm@localhost ~]$ netstat -an |grep 8888
[wjm@localhost ~]$
Client端的代码:
if((_sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
return -1;
}
if (connect(_sockfd, (struct sockaddr *)&_serveraddr, sizeof(struct sockaddr)) < 0)
{
close(_sockfd);
_sockfd = -1;
return -1;
}
Client运行后,向Server端发起请求,过程如下图:
链接建立后,TCP流状态就会变成ESTABLSHED:
Clinet端:
[root@localhost pagent]# netstat -an |grep 8887
tcp 0 0 127.0.0.1:34149 127.0.0.1:8887 ESTABLISHED
Server端:
tcp 0 0 :::8887 :::* LISTEN
tcp 0 0 ::ffff:127.0.0.1:8887 ::ffff:127.0.0.1:34149 ESTABLISHED
到此Client和Server就能分别调用send和receive函数接收,发送给数据了。
关闭连接
Client端和Server端都可以主动发起close,tcp close的四步握手流程如下图:
T
从上图看到的主动发起close的一方最后肯定是要进入TIME_WAIT状态的,如下图
[root@localhost pagent]# netstat -an |grep 8887
tcp 1 0 127.0.0.1:34149 127.0.0.1:8887 TIME_WAIT
TIME_WAIT存在的目的是为了等待对端完全关闭。如上图主动close的一方在收到对端发送的Fin Ack后,返回Ack并进入到TIME_WAIT状态。如果对端没有收到这个ACK报文,那它还会继续发送Fin Ack报文,如果没有TIME_WAIT状态,就会返回RST对对端,显然对端希望收到的是ACK,而不是RST。
TIME_WAIT持续2MSL(一个MSL是30秒),不可以修改该时间,但是Linux提供了其它的优化手段,通过proc接口提供了一些优化手段:
1.tcp_max_tw_buckets
通过设置该值控制time_wait的数量
[root@localhost ipv4]# cat /proc/sys/net/ipv4/tcp_max_tw_buckets
131072
2. tcp_tw_reuse
允许将time_wait 的流重新用于新的TCP连接
3.tcp_tw_recycle
快速回收time_wait
总结
主动close的一方不可避免的会进入到TIME_WAIT状态,优化手段治标不治本,在设计的时候应该如何规避TIME_WAIT呢
1.尽量不要在服务端主动CLOSE
2.降低创建CLOSE的次数