Флаг SO_REUSEADDR устанавливается на сетевых сокетах в ОС узлом, вызывающим это поведение. Флаг REUSEADDR имеет специальное взаимодействие с адресом IPARR_ANY (он же 0.0.0.0 для IPv4). Из справочных страниц по сокету (авторитетный источник):
SO_REUSEADDR
Indicates that the rules used in validating addresses supplied
in a bind(2) call should allow reuse of local addresses. For
AF_INET sockets this means that a socket may bind, except when
there is an active listening socket bound to the address.
When the listening socket is bound to INADDR_ANY with a spe‐
cific port then it is not possible to bind to this port for
any local address. Argument is an integer boolean flag.
Из статьи , которая касается именно этой проблемы:
Некоторым людям не нравится SO_REUSEADDR, потому что к нему прикреплено клеймо безопасности. В некоторых операционных системах он позволяет использовать один и тот же порт с разными адресами на одном компьютере разными процессами в одно и то же время. Это проблема, потому что большинство серверов привязываются к порту, но они не привязываются к указанному адресу c, вместо этого они используют INADDR_ANY (именно поэтому в выводе netstat все отображается как * .8080). Таким образом, если сервер привязан к * .8080, другой злоумышленник на локальном компьютере может подключиться к local-machine.8080, который будет перехватывать все ваши соединения, поскольку он более точен c.
Я изменил некоторый Linux тестовый код , чтобы явно продемонстрировать это (внизу). Когда вы запустите его, вы получите следующий вывод:
Opening 0.0.0.0 with no reuse flag:19999
Opening Loopback with no resuse flag:19999
bind: Address already in use
Correct: could not open lookpback with no reuse 19999
Opening 0.0.0.0 with with reuse flag:19999
Opening Loopback with with resuse flag:19999
Correct: could open lookpback with reuse 19999
Первый тестовый случай открывает сокет на адресе IPADDR_ANY без установленного флага REUSEADDR, и при попытке открыть сокет в петле EADDRINUSE ошибка выдается 'bind' (как вы и ожидали). Второй тест выполняет то же самое, но с установленным флагом REUSEADDR, и второй сокет создается без ошибки.
#include <errno.h>
#include <error.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define PORT 19999
int open_port(int any, int reuse)
{
int fd = -1;
int reuseaddr = 1;
int v6only = 1;
int addrlen;
int ret = -1;
struct sockaddr *addr;
int family = AF_INET;
struct sockaddr_in addr4 = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = any ? htonl(INADDR_ANY) : inet_addr("127.0.0.1"),
};
addr = (struct sockaddr*)&addr4;
addrlen = sizeof(addr4);
if ((fd = socket(family, SOCK_STREAM, IPPROTO_TCP)) < 0) {
perror("socket");
goto out;
}
if (reuse){
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuseaddr,
sizeof(reuseaddr)) < 0) {
perror("setsockopt SO_REUSEADDR");
goto out;
}
}
if (bind(fd, addr, addrlen) < 0) {
perror("bind");
goto out;
}
if (any)
return fd;
if (listen(fd, 1) < 0) {
perror("listen");
goto out;
}
return fd;
out:
close(fd);
return ret;
}
int main(void)
{
int listenfd;
int fd1, fd2;
fprintf(stderr, "Opening 0.0.0.0 with no reuse flag:%d\n", PORT);
listenfd = open_port(1, 0);
if (listenfd < 0)
error(1, errno, "Couldn't open listen socket");
fprintf(stderr, "Opening Loopback with no resuse flag:%d\n", PORT);
fd1 = open_port(0, 0);
if (fd1 >= 0)
error(1, 0, "Was allowed to create an loopback with no reuse");
fprintf(stderr, "Correct: could not open lookpback with no reuse %d\n", PORT);
close(listenfd);
fprintf(stderr, "Opening 0.0.0.0 with with reuse flag:%d\n", PORT);
listenfd = open_port(1, 1);
if (listenfd < 0)
error(1, errno, "Couldn't open listen socket");
fprintf(stderr, "Opening Loopback with with resuse flag:%d\n", PORT);
fd1 = open_port(0, 1);
if (fd1 < 0)
error(1, 0, "Was not allowed to create an loopback with reuse");
fprintf(stderr, "Correct: could open lookpback with reuse %d\n", PORT);
close(fd1);
close(listenfd);
return 0;
}