您的位置:首页 > 大数据 > 人工智能

CLOSE_WAIT状态的原因与解决方法

2014-03-02 22:11 344 查看
CLOSE_WAIT状态的原因与解决方法2009-09-24 13:52:18

分类: C/C++

这个问题之前没有怎么留意过,是最近在面试过程中遇到的一个问题,面了两家公司,两家公司竟然都面到到了这个问题,不得不使我开始关注这个问题。说起CLOSE_WAIT状态,如果不知道的话,还是先瞧一下TCP的状态转移图吧。



关闭socket分为主动关闭(Active closure)和被动关闭(Passive closure)两种情况。前者是指有本地主机主动发起的关闭;而后者则是指本地主机检测到远程主机发起关闭之后,作出回应,从而关闭整个连接。将关闭部分的状态转移摘出来,就得到了下图:



产生原因

通过图上,我们来分析,什么情况下,连接处于CLOSE_WAIT状态呢?

在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。

通常来讲,CLOSE_WAIT状态的持续时间应该很短,正如SYN_RCVD状态。但是在一些特殊情况下,就会出现连接长时间处于CLOSE_WAIT状态的情况。

出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接。代码需要判断socket,一旦读到0,断开连接,read返回负,检查一下errno,如果不是AGAIN,就断开连接。

参考资料4中描述,通过发送SYN-FIN报文来达到产生CLOSE_WAIT状态连接,没有进行具体实验。不过个人认为协议栈会丢弃这种非法报文,感兴趣的同学可以测试一下,然后把结果告诉我;-)

为了更加清楚的说明这个问题,我们写一个测试程序,注意这个测试程序是有缺陷的。

只要我们构造一种情况,使得对方关闭了socket,我们还在read,或者是直接不关闭socket就会构造这样的情况。

server.c:

#include <stdio.h>

#include <string.h>

#include <netinet/in.h>

#define MAXLINE 80

#define SERV_PORT 8000

int main(void)

{

struct sockaddr_in servaddr, cliaddr;

socklen_t cliaddr_len;

int listenfd, connfd;

char buf[MAXLINE];

char str[INET_ADDRSTRLEN];

int i, n;

listenfd = socket(AF_INET, SOCK_STREAM, 0);

int opt = 1;

setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

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);

connfd = accept(listenfd,

(struct sockaddr *)&cliaddr, &cliaddr_len);

//while (1)

{

n = read(connfd, buf, MAXLINE);

if (n == 0) {

printf("the other side has been closed.\n");

break;

}

printf("received from %s at PORT %d\n",

inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),

ntohs(cliaddr.sin_port));

for (i = 0; i < n; i++)

buf[i] = toupper(buf[i]);

write(connfd, buf, n);

}

//这里故意不关闭socket,或者是在close之前加上一个sleep都可以

//sleep(5);

//close(connfd);

}

}
client.c:

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <sys/socket.h>

#include <netinet/in.h>

#define MAXLINE 80

#define SERV_PORT 8000

int main(int argc, char *argv[])

{

struct sockaddr_in servaddr;

char buf[MAXLINE];

int sockfd, n;

char *str;

if (argc != 2) {

fputs("usage: ./client message\n", stderr);

exit(1);

}

str = argv[1];

sockfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

servaddr.sin_port = htons(SERV_PORT);

connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

write(sockfd, str, strlen(str));

n = read(sockfd, buf, MAXLINE);

printf("Response from server:\n");

write(STDOUT_FILENO, buf, n);

write(STDOUT_FILENO, "\n", 1);

close(sockfd);

return 0;

}
结果如下:

debian-wangyao:~$ ./client a

Response from server:

A

debian-wangyao:~$ ./client b

Response from server:

B

debian-wangyao:~$ ./client c

Response from server:

C

debian-wangyao:~$ netstat -antp | grep CLOSE_WAIT

(Not all processes could be identified, non-owned process info

will not be shown, you would have to be root to see it all.)

tcp 1 0 127.0.0.1:8000 127.0.0.1:58309 CLOSE_WAIT 6979/server

tcp 1 0 127.0.0.1:8000 127.0.0.1:58308 CLOSE_WAIT 6979/server

tcp 1 0 127.0.0.1:8000 127.0.0.1:58307 CLOSE_WAIT 6979/server
解决方法

基本的思想就是要检测出对方已经关闭的socket,然后关闭它。

1.代码需要判断socket,一旦read返回0,断开连接,read返回负,检查一下errno,如果不是AGAIN,也断开连接。(注:在UNP 7.5节的图7.6中,可以看到使用select能够检测出对方发送了FIN,再根据这条规则就可以处理CLOSE_WAIT的连接)

2.给每一个socket设置一个时间戳last_update,每接收或者是发送成功数据,就用当前时间更新这个时间戳。定期检查所有的时间戳,如果时间戳与当前时间差值超过一定的阈值,就关闭这个socket。

3.使用一个Heart-Beat线程,定期向socket发送指定格式的心跳数据包,如果接收到对方的RST报文,说明对方已经关闭了socket,那么我们也关闭这个socket。

4.设置SO_KEEPALIVE选项,并修改内核参数

前提是启用socket的KEEPALIVE机制:

//启用socket连接的KEEPALIVE

int iKeepAlive = 1;

setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (void *)&iKeepAlive, sizeof(iKeepAlive));

tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)

The number of seconds between TCP keep-alive probes.

tcp_keepalive_probes (integer; default: 9; since Linux 2.2)

The maximum number of TCP keep-alive probes to send before giving up and killing the connection if no response is obtained from the other end.

tcp_keepalive_time (integer; default: 7200; since Linux 2.2)

The number of seconds a connection needs to be idle before TCP begins sending out keep-alive probes. Keep-alives are only sent when the SO_KEEPALIVE socket option is enabled. The default value is 7200 seconds (2 hours). An idle connec‐tion is
terminated after approximately an additional 11 minutes (9 probes an interval of 75 seconds apart) when keep-alive is enabled.

echo 120 > /proc/sys/net/ipv4/tcp_keepalive_time

echo 2 > /proc/sys/net/ipv4/tcp_keepalive_intvl

echo 1 > /proc/sys/net/ipv4/tcp_keepalive_probes

除了修改内核参数外,可以使用setsockopt修改socket参数,参考man 7 socket。

int KeepAliveProbes=1;

int KeepAliveIntvl=2;

int KeepAliveTime=120;

setsockopt(s, IPPROTO_TCP, TCP_KEEPCNT, (void *)&KeepAliveProbes, sizeof(KeepAliveProbes));

setsockopt(s, IPPROTO_TCP, TCP_KEEPIDLE, (void *)&KeepAliveTime, sizeof(KeepAliveTime));

setsockopt(s, IPPROTO_TCP, TCP_KEEPINTVL, (void *)&KeepAliveIntvl, sizeof(KeepAliveIntvl));

参考:
http://blog.chinaunix.net/u/20146/showart_1217433.html http://blog.csdn.net/eroswang/archive/2008/03/10/2162986.aspx http://haka.sharera.com/blog/BlogTopic/32309.htm http://learn.akae.cn/media/ch37s02.html http://faq.csdn.net/read/208036.html http://www.cndw.com/tech/server/2006040430203.asp http://davidripple.bokee.com/1741575.html http://doserver.net/post/keepalive-linux-1.php
man 7 tcp

1
CLOSE_WAIT状态的生成原因

CLOSE_WAIT状态的生成原因

首先我们知道,如果我们的Client程序处于CLOSE_WAIT状态的话,说明套接字是被动关闭的!

因为如果是Server端主动断掉当前连接的话,那么双方关闭这个TCP连接共需要四个packet:

Server ---> FIN ---> Client

Server <--- ACK <--- Client

这时候Server端处于FIN_WAIT_2状态;而我们的程序处于CLOSE_WAIT状态。

Server <--- FIN <--- Client

这时Client发送FIN给Server,Client就置为LAST_ACK状态。

Server ---> ACK ---> Client

Server回应了ACK,那么Client的套接字才会真正置为CLOSED状态。

我们的程序处于CLOSE_WAIT状态,而不是LAST_ACK状态,说明还没有发FIN给Server,那么可能是在关闭连接之前还有许多数据要发送或者其他事要做,导致没有发这个FIN
packet。

原因知道了,那么为什么不发FIN包呢,难道会在关闭己方连接前有那么多事情要做吗?

elssann举例说,当对方调用closesocket的时候,我的程序正在调用recv中,这时候有可能对方发送的FIN包我没有收到,而是由TCP代回了一个ACK包,所以我这边套接字进入CLOSE_WAIT状态。

所以他建议在这里判断recv函数的返回值是否已出错,是的话就主动closesocket,这样防止没有接收到FIN包。

因为前面我们已经设置了recv超时时间为30秒,那么如果真的是超时了,这里收到的错误应该是WSAETIMEDOUT,这种情况下也可以主动关闭连接的。

还有一个问题,为什么有数千个连接都处于这个状态呢?难道那段时间内,服务器端总是主动拆除我们的连接吗?

不管怎么样,我们必须防止类似情况再度发生!

首先,我们要保证原来的端口可以被重用,这可以通过设置SO_REUSEADDR套接字选项做到:

重用本地地址和端口

以前我总是一个端口不行,就换一个新的使用,所以导致让数千个端口进入CLOSE_WAIT状态。如果下次还发生这种尴尬状况,我希望加一个限定,只是当前这个端口处于CLOSE_WAIT状态!

在调用

sockConnected
= socket(AF_INET, SOCK_STREAM, 0);

之后,我们要设置该套接字的选项来重用:

///
允许重用本地地址和端口:

///
这样的好处是,即使socket断了,调用前面的socket函数也不会占用另一个,而是始终就是一个端口

///
这样防止socket始终连接不上,那么按照原来的做法,会不断地换端口。

int
nREUSEADDR = 1;

setsockopt(sockConnected,

SOL_SOCKET,

SO_REUSEADDR,

(const char*)&nREUSEADDR,

sizeof(int));

教科书上是这么说的:这样,假如服务器关闭或者退出,造成本地地址和端口都处于TIME_WAIT状态,那么SO_REUSEADDR就显得非常有用。

也许我们无法避免被冻结在CLOSE_WAIT状态永远不出现,但起码可以保证不会占用新的端口。

其次,我们要设置SO_LINGER套接字选项:

setsockopt
设置 SO_LINGER 选项


此选项指定函数close对面向连接的协议如何操作(如TCP)。内核缺省close操作是立即返回,如果有数据残留在套接口缓冲区中则系统将试着将这些数据发送给对方。

SO_LINGER选项用来改变此缺省设置。使用如下结构:

struct linger {

int l_onoff; /* 0 = off, nozero = on */

int l_linger; /* linger time */

};

有下列三种情况:

1、设置 l_onoff为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close调用会立即返回给调用者,如果可能将会传输任何未发送的数据;

2、设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;

3、设置 l_onoff 为非0,l_linger为非0,当套接口关闭时内核将拖延一段时间(由l_linger决定)。如果套接口缓冲区中仍残留数据,进程将处于睡眠状态,直 到(a)所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0)或(b)延迟时间到。此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失。close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果套接口设为非阻塞的,它将不等待close完成。

从容关闭还是强行关闭?

LINGER是“拖延”的意思。

默认情况下(Win2k),SO_DONTLINGER套接字选项的是1;SO_LINGER选项是,linger为{l_onoff:0,l_linger:0}。

如果在发送数据的过程中(send()没有完成,还有数据没发送)而调用了closesocket(),以前我们一般采取的措施是“从容关闭”:

因为在退出服务或者每次重新建立socket之前,我都会先调用

///
先将双向的通讯关闭

shutdown(sockConnected, SD_BOTH);

/// 安全起见,每次建立Socket连接前,先把这个旧连接关闭

closesocket(sockConnected);

我们这次要这么做:

设置SO_LINGER为零(亦即linger结构中的l_onoff域设为非零,但l_linger为0),便不用担心closesocket调
用进入“锁定”状态(等待完成),不论是否有排队数据未发送或未被确认。这种关闭方式称为“强行关闭”,因为套接字的虚电路立即被复位,尚未发出的所有数 据都会丢失。在远端的recv()调用都会失败,并返回WSAECONNRESET错误。

在connect成功建立连接之后设置该选项:

linger
m_sLinger;

m_sLinger.l_onoff
= 1; // (在closesocket()调用,但是还有数据没发送完毕的时候容许逗留)

m_sLinger.l_linger
= 0; // (容许逗留的时间为0秒)

setsockopt(sockConnected,

SOL_SOCKET,

SO_LINGER,

(const char*)&m_sLinger,

sizeof(linger));

总结

也许我们避免不了CLOSE_WAIT状态冻结的再次出现,但我们会使影响降到最小,希望那个重用套接字选项能够使得下一次重新建立连接时可以把CLOSE_WAIT状态踢掉。

我的意思是:当一方关闭连接后,另外一方没有检测到,就导致了CLOSE_WAIT的出现,上次我的一个朋友也是这样,他写了一个客户端和
APACHE连接,当APACHE把连接断掉后,他没检测到,出现了CLOSE_WAIT,后来我叫他检测了这个地方,他添加了调用 closesocket的代码后,这个问题就消除了。

如果你在关闭连接前还是出现CLOSE_WAIT,建议你取消shutdown的调用,直接两边closesocket试试。

另外一个问题:

比如这样的一个例子:

当客户端登录上服务器后,发送身份验证的请求,服务器收到了数据,对客户端身份进行验证,发现密码错误,这时候服务器的一般做法应该是先发送一个密码错误的信息给客户端,然后把连接断掉。

如果把

m_sLinger.l_onoff = 1;

m_sLinger.l_linger = 0;

这样设置后,很多情况下,客户端根本就收不到密码错误的消息,连接就被断了。

出现CLOSE_WAIT的原因很简单,就是某一方在网络连接断开后,没有检测到这个错误,没有执行closesocket,导致了这个状态的实现,这在TCP/IP协议的状态变迁图上可以清楚看到。同时和这个相对应的还有一种叫TIME_WAIT的。

另外,把SOCKET的SO_LINGER设置为0秒拖延(也就是立即关闭)在很多时候是有害处的。

还有,把端口设置为可复用是一种不安全的网络编程方法。

能不能解释请看这里

http://blog.csdn.net/cqq/archive/2005/01/26/269160.aspx

再看这个图:





断开连接的时候,

当发起主动关闭的左边这方发送一个FIN过去后,右边被动关闭的这方要回应一个ACK,这个ACK是TCP回应的,而不 是应用程序发送的,此时,被动关闭的一方就处于CLOSE_WAIT状态了。如果此时被动关闭的这一方不再继续调用closesocket,那么他就不会 发送接下来的FIN,导致自己老是处于CLOSE_WAIT。只有被动关闭的这一方调用了closesocket,才会发送一个FIN给主动关闭的这一 方,同时也使得自己的状态变迁为LAST_ACK。

比如被动关闭的是客户端。。。

当对方调用closesocket的时候,你的程序正在

int
nRet = recv(s,....);

if (nRet == SOCKET_ERROR)

{

// closesocket(s);

return FALSE;

}

很多人就是忘记了那句closesocket,这种代码太常见了。

我的理解,当主动关闭的一方发送FIN到被动关闭这边后,被动关闭这边的TCP马上回应一个ACK过去,同时向上面应用程序提交一个ERROR,导
致上面的SOCKET的send或者recv返回SOCKET_ERROR,正常情况下,如果上面在返回SOCKET_ERROR后调用了 closesocket,那么被动关闭的者一方的TCP就会发送一个FIN过去,自己的状态就变迁到LAST_ACK.

2.
time_wait状态

一般来说,tcp正常关闭需要四个包。比如a和b关闭连接,a先
给b发一个fin,b会进行确认ack,然后b也会发出fin,当a接受到这个fin,并发出最后一个ack后,就会处于time_wait状态。这个时 间长短跟操作系统有关,一般会在1-4分钟,也就是两倍的数据包(2msl)最大生存时间。TCP主动关闭方采用TIME_WAIT主要是为了实现终止
TCP全双工连接的可靠性及允许老的重复分节在网络中消逝,等过了2msl(大约1~4分钟)后TIME_WAIT就会消失。

TIME_WAIT状态的目的是

(1)为了防止最后a发出的ack丢失,让b处于LAST_ACK超时重发FIN,可靠的实现tcp全双工连接的终止

假设最终的ack丢失,则服务器将重新发送最终的FIN,因此客户必须维护状态信息以允许它重发最终的Ack。要是不维护状态信息,它将以RST(另外一个类型的tcp分节),而服务器则把该分节解释成一个错误。如果tcp打算执行所有必要的工作以彻底终止某个连接上两个方向上的数据流(即全双工关闭),那么它必须正确处理连接终止序列四个分节中任何一个分节的丢失情况。

(2)允许老的重复分节在网络中消失(消失前不允许启动新的化身)

假设12.105.32.254的端口1500和206.158.112.219的端口21之间有一个tcp连接,关闭这个连接之后,在以后的某个时候又建立了一个新的相同的ip和端口之间的tcp连接。后一个连接称为前一个连接的化身,因为他们的ip地址和端口号都相同.tcp必须防止来自某个连接的老的重复分组在该连接已终止后出现,从而被误解为属于同一连接的新的化身,为做到这一点,tcp将不给处于time_wait状态的连接启动新的化身。既然,time_wait状态的持续时间是2MSL,这就足够允许某个方向上的分组最多存活MSL秒即死去,另一个方向上的应答最多存活MSL也被丢弃。通过实施这个原则,我们就能保证每当成功建立一个tcp连接时,来自该连接先前化身的老的重复分组都已在网络中消失。

所以说,主动发起关闭连接的一方会进入time_wait状态,这个时候,进程所占用的端口号不能被释放。除非在你的程序中用setsockopt设置端口可重用(SO_REUSEADDR)的选项,但这不是所有操作系统都支持的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: