分析the address already in use产生的原因

Posted: 2009年11月5日星期四
最近在维护一个别人写的程序时遇到一个它的程序启动不了的情况,具体情况如下,服务器端按照它的方式不能正常关闭,就用kill命令把它的程序结束掉了,再启动程序时报如下的错误:the address already in use,问了公司的同事,同事也告诉不可能啊,要不就是由什么链入了系统库了,当时我也不懂也没有在意,没有去查,这几天我刚好在学习linux网络编程,也碰到了这个问题,通过查找资料,了解到,并不是什么链入了系统库了,而是下面将要说明的情况:
一般情况认为,server socket已经关闭,理应释放了ip address及port资源,但是事实并不一定是这样的,因为大部分socket连接是基于tcp协议多次握手连接的基础上,以保证数据传输的完整性,我们都知道tcp协议是一种可靠的协议,它有3次握手,四次挥手过程,下面是4次挥手过程
(1)当服务器端处于ESTABLISHED状态时, 服务器欲主动关闭连线,发送FIN至客户端,进入到FIN-WAIT-1状态,等待客户端回应ACK,表示等待确认客户端得知服务器端要关闭连线
(2)当客户端收到服务器端的FIN时,立即回应ACK给服务器端,进入到CLOSE-WAIT状态,表示须等到应用程序没有任何资料要传送给服务器端, 客户端才决定关闭连线
(3)服务器端收到客户端回应的ACK,表示确认客户端得知服务器要关闭连线,此刻等待客户端发送FIN
(4)客户端决定关闭连线,发送FIN至服务器端,进入到LAST-ACK状态,等待服务器端回应ACK,表示等待确认服务器端得知客户端要关闭连线
(5)服务器端收到客户端的FIN后,随即回应ACK,进入TIME_WAIT状态,表示服务器端得知客户端要关闭连线,且等待2MSL时间,以防客户端再次发送FIN
(6)客户端收到服务器端回应的ACK后,进入CLOSED状态,表示客户端已确认服务器端已得知它要关闭连线,才进行关闭连线
(7)服务器端等待2MSL时间,才进入CLOSED状态,关闭连线,并自连线表中移除
在上述的第(5),(7)点,此刻TIME-WAIT的用意在于,虽服务器已确认客户端要关闭连线,且回应了ACK给客户端,但不保证客户端会收到ACK ,一旦ACK遗漏,
客户端会再次发送FIN给服务器端,再次进行确认,所以服务器端须进入TIME-WAIT状态,等待2MSL时间,预防客户端会再次发送FIN,进行连线关闭的确认。

从上面的4次挥手过程得知,我们直接结束游戏的服务器端,它并没有马上释放它的ip及port资源就在于服务器端进入了time_wait状态,有可能客户端没有
收到ack资源,导致不停的发送fin请求,导致直接结束服务器端,导致重启起不来,下面是我用C写的一个程序怎么重现上面所出现的实例,以及怎么避免上面所
出现的情况
1.客户端

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main()
{
struct sockaddr_in server;
int sock;
char buf[32];
int n;

sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
exit(1);
}

server.sin_family = AF_INET;
server.sin_port = htons(1234);
server.sin_addr.s_addr = inet_addr("127.0.0.1");

if(connect(sock, (struct sockaddr *)&server, sizeof(server)) != 0){
perror("connetc");
exit(1);
}

n = read(sock, buf, sizeof(buf));
if(n < 0) {
perror("read");
return 1;
}

printf("%d, %s\n", n, buf);

close(sock);

return 0;
}

帮上面的代码保存为一个client.c文件,通过下面命令进行编译
gcc -o client client.c
2.重现出现情况的服务器端

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
int sock0;
struct sockaddr_in addr;
struct sockaddr_in client;
int len;
int sock;

sock0 = socket(AF_INET, SOCK_STREAM, 0);
if (sock0 < 0) {
perror("socket");
return 1;
}

addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
addr.sin_addr.s_addr = INADDR_ANY;
if(bind(sock0, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
perror("bind");
return 1;
}

if (listen(sock0, 5) !=0) {
perror("listen:");
return 1;
}
while (1) {
len = sizeof(client);
sock = accept(sock0, (struct sockaddr *)&client, &len);
if (sock < 0) {
perror("accept:");
break;
}

printf("accepted connection from %s\n",
inet_ntoa(client.sin_addr));
if(send(sock, "Hello", 5, 0) < 1) {
perror("write:");
return 1;
}

close(sock);
}
close(sock0);

return 0;
}

把上面的程序保存为一个server1.c文件,执行下面的命令进行编译
gcc -o server1 server1.c
3.避免上述情况发生的服务器端

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(int argc, char *argv[])
{
int sock0;
struct sockaddr_in addr;
struct sockaddr_in client;
int len;
int sock;
int yes = 1;

sock0 = socket(AF_INET, SOCK_STREAM, 0);
if (sock0 < 0) {
perror("socket");
return 1;
}

addr.sin_family = AF_INET;
addr.sin_port = htons(1234);
addr.sin_addr.s_addr = INADDR_ANY;
setsockopt(sock0,
SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
if(bind(sock0, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
perror("bind");
return 1;
}

if (listen(sock0, 5) !=0) {
perror("listen:");
return 1;
}
while (1) {
len = sizeof(client);
sock = accept(sock0, (struct sockaddr *)&client, &len);
if (sock < 0) {
perror("accept:");
break;
}

printf("accepted connection from %s\n",
inet_ntoa(client.sin_addr));
if(send(sock, "Hello", 5, 0) < 1) {
perror("write:");
return 1;
}

close(sock);
}
close(sock0);

return 0;
}

把上面的程序保存为一个server2.c文件,执行下面的命令进行编译
gcc -o server2 server2.c

启动服务器端
./server1
在另外一个终端中启动客户端
./clinet
得到如下的内容
5, Hello
在服务器的终端通过ctrl + c结束刚刚起来的服务器端,在一分钟内再启动服务器端,你就可以发现出现下面的错误信息
bind: Address already in use
过1分钟后(Linux系统默认time_wait的时间为60秒)就可以发现可以起来了。
为规避上面的情况,再启动server2,再重复上面的步骤,发现已经没有这种情况了。

上面只是一个具体的实例,并不保证数据的完整性,具体到它们的程序中,具体进行分析。

0 评论: