今天正好帮助同事定位一个服务器上大量close_wait状态的bug,为了更清晰的定位到这个bug。
因为time_wait和close_wait状态都是在tcp四次挥手状态下触发的,所以小伙伴们直接看下图
状态变化的解释过程:
从客户端来看:
1.客户端主动断开连接时,会先发送FIN包,客户端此时进入FIN_WAIT_1状态;
2.客户端收到服务器的ACK包(对步骤1中FIN包的应答)后,客户端进入FIN_WAIT_2状态;
3.客户端接收到服务器的FIN包并回复ACK包给服务端,然后客户端进入TIME_WAIT状态,此时会等待2个MSL的时间,
确保发送的ACK包是否达到了对端。
4.客户端在等待了2个MSL的时间没有收到服务器重传的FIN包,就默认ACK数据包已经抵达了对端。
从服务端来看:
1.服务器收到客户端发送的FIN数据包后,回复ACK包给客户端,此时服务器进入CLOSE_WAIT状态
2.等待服务器将剩余的数据全部发送给客户端时,然后执行断开操作,(老夫把该做的事都做了,然后再给这小子发送FIN包来结束,哈哈,姜还是老的辣!)
服务器向客户端发送出FIN包后,服务器端进入LAST_ACK状态,等待最后一个ACK确认包。
3.服务端收到客户端发送的ACK包后,从LAST_ACK状态转为CLOSED状态,服务器正式关闭了
二、close_wait产生原因实验剖析
CLOSE_WAIT状态:
被动断开连接的一方在发送完ACK分节之后就会进入CLOSE_WAIT状态.
它需要服务器在发送完剩余数据之后,就调用close来关闭连接.此时服务器从CLOSE_WAIT状态变为LAST_ACK状态.
小伙伴我们先来看下示例代码
client端代码如下
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char str[MAXLINE] = "test ";
int sockfd, n;
while(1)
{
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.254.26", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));
close(sockfd);
sleep(2);
}
return 0;
}
while(1)
{
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.254.26", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));
close(sockfd);
sleep(2);
}
return 0;
}
server端代码
#include
#include
#include
#include
#include
#include /* For mode constants */
#include /* For O_* constants */
#include
#include
using namespace std;
#define LENGTH 128
#include "netinet/in.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc,char** argv)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
//int i, n;
int n;
//创建socket
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//设置端口重用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.254.26",&(servaddr.sin_addr.s_addr));
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1)
{
cliaddr_len = sizeof(cliaddr);
int connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
//while(1)
{
n = recv(connfd, buf, MAXLINE,0);
if (n == 0)
{
//对端主动关闭
printf("the other side has been closed.\n");
//break;
}
printf("received from %s at PORT %d len = %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port),n);
}
//测试:模拟CLOSE_WAIT状态时,将close(connfd);这句代码注释
close(connfd);
}
return 0;
}
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
//int i, n;
int n;
//创建socket
listenfd = socket(AF_INET, SOCK_STREAM, 0);
//设置端口重用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET,"192.168.254.26",&(servaddr.sin_addr.s_addr));
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1)
{
cliaddr_len = sizeof(cliaddr);
int connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
//while(1)
{
n = recv(connfd, buf, MAXLINE,0);
if (n == 0)
{
//对端主动关闭
printf("the other side has been closed.\n");
//break;
}
printf("received from %s at PORT %d len = %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port),n);
}
//测试:模拟CLOSE_WAIT状态时,将close(connfd);这句代码注释
close(connfd);
}
return 0;
}
测试代码中,当recv的返回值为0时(对端主动关闭连接),会跳出while(1)循环,此时正确做法是调用close关闭tcp连接
此处我们为了测试,故意将close(connfd)这句代码注释掉,注释后服务器对于客户端发送的FIN包不会做回应,一直保持close_wait状态。
运行截图
服务器端出现CLOSE_WAIT状态。
当
三、time_wait存在是否必要?
程序运行时的截图如下:
3.1 该状态用来防止最后一个ACK的丢失.
如果主动关闭连接的一端发送的最后一个ACK,在网络中延迟或丢失,被动关闭那么服务器将会重复发送FIN数据包,如果客户端不保留TIME_WAIT状态的话,客户端在发送完ack包后会进入closed状态,此时的状态再收到被动关闭连接一方的fin包,主动关闭方将发送一个RST分节,但是服务器将该分节解释为一个错误.
3.2 防止上一次连接中的分段延迟到达后影响新连接。
TCP连接由五元组(协议,源IP,源端口,目的IP,目的端口)唯一标识。假设没有TIME_WAIT状态,一个连接关闭后,可能使用相同的五元组的新连接被建立,这时若前一个原连接上的TCP分段因为网络延时刚刚到达,且它的序列号刚好在新连接的接收窗口,则会令新连接接收的数据混乱。尽管每次建立连接使用的序列号都是随机产生的,但是序列号的长度只有32位,在高速网络上可能很快出现序列号循环。TIME_WAIT状态持续2MSL后,原连接的数据包都已经在网络上消失,不会再干扰新连接。
如果服务器或客户端存在大量的TIME_WAIT状态,这是一种可能是正常的情况,主动断开连接的一方会进入TIME_WAIT状态.
主动连接端会占用本地端口,大量的TIME_WAIT状态的socket,会占用大量的本地端口,当本地端口不足时,tcp连接不能建立成功。可以通过以下两种方式来解决上述问题
1.调整参数net.ipv4.ip_local_port_range来增加本地端口的选择范围,但这样效果有限。
2.启用net.ipv4.tcp_tw_reuse参数来重用TIME_WAIT状态的socket。
3.linux api设置socket套接字的”端口重用“属性
通常情况下,客户端的端口资源比较充足,应该让客户端主动断开连接,但在某些场景下,如tcp连接长时间没有IO操作,应该将此空闲tcp连接踢除,否则空闲tcp会占有系统各个资源却不干事,太浪费了
参考资料
1.TCP网络关闭的状态变换时序图
https://coolshell.cn/articles/1484.html
2.tcp状态实验分析
http://www.just4coding.com/blog/2017/11/09/timewait/