在编写、运行服务端程序时,经常会遇到的一个错误是:Address already in use.
Address already in use 是在调用bind系统调用时出现的错误。
原因有两个:
例如:
当前主机已经有服务器进程调用bind以及listen,在当前主机监听12500端口:
[jiang@localhost ~]$ netstat -an | grep 12500
tcp 0 0 0.0.0.0:12500 0.0.0.0:* LISTEN
你尝试在程序中再次调用bind,将12500端口和你的socket进行绑定,此时会产生系统调用错误:
[jiang@localhost ~]$ ./server/server
socket bind error=98(Address already in use)!!!
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BACKLOG 16
int main()
{
// socket
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0)
{
printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
// bind
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(12500); // Port
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP
if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
printf("socket bind error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
// listen
if (listen(listen_fd, BACKLOG) < 0)
{
printf("socket listen error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
printf("server init ok, start to accept new connect...\n");
int connIdx = 0;
while (1)
{
// accept
int client_fd = accept(listen_fd, NULL, NULL);
if (client_fd < 0)
{
printf("socket accept error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
printf("accept one new connect(%d)!!!\n", connIdx);
static char msg[1024] = "";
memset(msg, 0, sizeof(msg));
snprintf(msg, sizeof(msg)-1, "connIdx=%d\n", connIdx);
if (write(client_fd, msg, strlen(msg)) != strlen(msg))
{
printf("send msg to client error!!!\n");
exit(1);
}
close(client_fd);
connIdx++;
}
// never
close(listen_fd);
return 0;
}
编译 && 生成可执行文件:
[jiang@localhost server]$ gcc -o server server.c
连续调用两次server:
1)第一次:
[jiang@localhost server]$ ./server
server init ok, start to accept new connect...
bind成功,服务器进程正常运行,用netstat查看12500端口状态:
[jiang@localhost server]$ netstat -an | grep 12500
tcp 0 0 0.0.0.0:12500 0.0.0.0:* LISTEN
2)第二次:
[jiang@localhost server]$ ./server
socket bind error=98(Address already in use)!!!
bind失败,进程退出。
例如:
a)主机启动一个监听服务器;
b)连接请求到达,派生一个子进程来处理这个客户端连接;
c)监听服务器终止,但是子进程继续为现有连接的客户提供服务;
d)重启监听服务器;
在d)步骤之前,已经有一条(多条)正在连接的TCP连接:
五元组=》
客户端IP:客户端PORT:TCP:服务端IP:服务端PORT(12500)
当想要再次启动监听服务器对 bind 12500 进行调用将会失败,导致重启监听服务器失败。
也就是说,本机已经有一个连接的本端socket是12500端口,要再 bind 12500 将失败。
请看一个有意思的示例程序。
服务端程序见上文。
客户端程序:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int client_fd = socket(AF_INET, SOCK_STREAM, 0);
if (client_fd < 0)
{
printf("create socket error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12500);
if (inet_pton(AF_INET, "192.168.44.144", &server_addr.sin_addr) <= 0)
{
printf("inet_pton error!!!\n");
exit(1);
}
if (connect(client_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
printf("socket connect error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
printf("connect to server ok!\n");
char msg[1024];
int rbytes = read(client_fd, msg, sizeof(msg)-1);
if (rbytes <= 0)
{
printf("read error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
msg[rbytes] = 0; // null terminate
printf("%s", msg);
int finRead = read(client_fd, msg, sizeof(msg)-1);
if (finRead < 0)
{
printf("finRead(%d): errno(%d): %s\n", finRead, errno, strerror(errno));
}
else if (finRead == 0)
{
msg[finRead] = 0;
printf("finRead(%d): read FIN-Segment\n", finRead);
}
else
{
printf("finRead(%d): %s\n", msg);
}
close(client_fd);
return 0;
}
编译 && 生成可执行文件:
[jiang@localhost client]$ gcc -o client client.c
[jiang@localhost client]$ ll
total 16
-rwxrwxr-x. 1 jiang jiang 8476 May 14 14:09 client
-rw-rw-r--. 1 jiang jiang 1374 May 14 13:18 client.c
步骤如下:
a)启动监听服务器server:
[jiang@localhost server]$ ./server
server init ok, start to accept new connect...
[jiang@localhost ~]$ netstat -an | grep 12500
tcp 0 0 0.0.0.0:12500 0.0.0.0:* LISTEN
b)重启监听服务器server:
[jiang@localhost server]$ ./server
server init ok, start to accept new connect...
^C
[jiang@localhost server]$ ./server
server init ok, start to accept new connect...
[jiang@localhost ~]$ netstat -an | grep 12500
tcp 0 0 0.0.0.0:12500 0.0.0.0:* LISTEN
我们看到,按下Ctrl+C宕掉进程,并重启监听服务器成功。
c)执行客户端程序,链接服务端的服务端口,然后宕掉服务端进程:
[jiang@localhost client]$ ./client
connect to server ok!
connIdx=0
finRead(0): read FIN-Segment
[jiang@localhost server]$ ./server
server init ok, start to accept new connect...
accept one new connect(0)!!!
^C
d)此时在服务端快速地执行:
[jiang@localhost server]$ netstat -an | grep 12500
tcp 0 0 192.168.44.144:12500 192.168.44.144:52656 TIME_WAIT
e)快速重启监听服务器:
[jiang@localhost server]$ ./server
socket bind error=98(Address already in use)!!!
重启失败,Address already in use。
f)稍等1分钟左右,重启监听服务器:
[jiang@localhost server]$ ./server
server init ok, start to accept new connect...
重启成功。
概括来说:
1)如果没有客户端连接到服务器,那么可以不间断地重启服务器;
2)如果有客户端连接到服务器,那么服务端宕掉后,需要等待一段时间才可以 bind 12500 成功,重启成功。
原因:
监听服务程序中,是服务器主动对连接进行close,主动关闭连接,那服务器一侧就将进入到TIME_WAIT状态,持续2MSL。
当监听服务器宕掉时,连接还是会被内核所记录,也就是说,四次挥手还未完成,这个连接还未完整地走完四次挥手,还处于“连接中”的状态。
这种情况,实际和一条运行中的正常的TCP连接没啥区别,都未完成四次挥手流程。
当监听服务器进程重启,12500还被内核持有,也就 bind 12500 失败,重启监听服务器失败。
直到连接在2MSL时间之后(通常2MSL=1min)连接被内核释放,再次 bind 12500 将会成功。
如果没有客户端连接到服务器,当然可以随意起宕服务器!因为服务器一侧没有连接处于TIME_WAIT状态!12500随时可以串行地 bind 12500 成功。
设置套接字选项:SO_REUSEADDR。
在《UNIX网络编程:卷一 套接字联网API》一书中写到:
修改代码,在 bind 12500 前新增:
int flag = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)) < 0)
{
printf("socket setsockopt error=%d(%s)!!!\n", errno, strerror(errno));
exit(1);
}
再次执行上面的步骤:
当有客户端连接被服务端处理并宕掉服务端进程,在服务端执行:
[jiang@localhost server]$ ./server
server init ok, start to accept new connect...
accept one new connect(0)!!!
^C
[jiang@localhost server]$ netstat -an | grep 12500
tcp 0 0 192.168.44.144:12500 192.168.44.144:52660 TIME_WAIT
此时重启服务端进程可以 bind 12500 成功吗?
可以成功。
即,SO_REUSEADDR起作用啦!