C/C++:TCP bind error:Address already in use

C/C++:TCP bind error:Address already in use

在编写、运行服务端程序时,经常会遇到的一个错误是:Address already in use.

Address already in use 是在调用bind系统调用时出现的错误。

原因有两个:

1.bind一个已经listen的端口

例如:

当前主机已经有服务器进程调用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)!!!

Code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#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失败,进程退出。

2.bind的目标端口是本主机本地socket连接中的端口

例如:

a)主机启动一个监听服务器;
b)连接请求到达,派生一个子进程来处理这个客户端连接;
c)监听服务器终止,但是子进程继续为现有连接的客户提供服务;
d)重启监听服务器;

在d)步骤之前,已经有一条(多条)正在连接的TCP连接:

五元组=》

客户端IP:客户端PORT:TCP:服务端IP:服务端PORT(12500)

当想要再次启动监听服务器对 bind 12500 进行调用将会失败,导致重启监听服务器失败。

也就是说,本机已经有一个连接的本端socket是12500端口,要再 bind 12500 将失败。

请看一个有意思的示例程序。

Code:

服务端程序见上文。

客户端程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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》一书中写到:

所有的TCP服务器都应当指定SO_REUSEADDR选项。

修改代码,在 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起作用啦!

猜你喜欢

转载自blog.csdn.net/test1280/article/details/80315741
今日推荐