Creo que la idea de que el socket no esté disponible para un programa es permitir que lleguen los segmentos de datos TCP aún en tránsito y que el núcleo los descarte. Es decir, es posible que una aplicación llame close(2)
a un zócalo, pero los retrasos en el enrutamiento o los percances para controlar los paquetes o lo que tenga puede permitir que el otro lado de una conexión TCP envíe datos por un tiempo. La aplicación ha indicado que ya no quiere tratar con segmentos de datos TCP, por lo que el núcleo debería descartarlos a medida que entran.
He pirateado un pequeño programa en C que puedes compilar y usar para ver cuánto dura el tiempo de espera:
#include <stdio.h> /* fprintf() */
#include <string.h> /* strerror() */
#include <errno.h> /* errno */
#include <stdlib.h> /* strtol() */
#include <signal.h> /* signal() */
#include <sys/time.h> /* struct timeval */
#include <unistd.h> /* read(), write(), close(), gettimeofday() */
#include <sys/types.h> /* socket() */
#include <sys/socket.h> /* socket-related stuff */
#include <netinet/in.h>
#include <arpa/inet.h> /* inet_ntoa() */
float elapsed_time(struct timeval before, struct timeval after);
int
main(int ac, char **av)
{
int opt;
int listen_fd = -1;
unsigned short port = 0;
struct sockaddr_in serv_addr;
struct timeval before_bind;
struct timeval after_bind;
while (-1 != (opt = getopt(ac, av, "p:"))) {
switch (opt) {
case 'p':
port = (unsigned short)atoi(optarg);
break;
}
}
if (0 == port) {
fprintf(stderr, "Need a port to listen on\n");
return 2;
}
if (0 > (listen_fd = socket(AF_INET, SOCK_STREAM, 0))) {
fprintf(stderr, "Opening socket: %s\n", strerror(errno));
return 1;
}
memset(&serv_addr, '\0', sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(port);
gettimeofday(&before_bind, NULL);
while (0 > bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr))) {
fprintf(stderr, "binding socket to port %d: %s\n",
ntohs(serv_addr.sin_port),
strerror(errno));
sleep(1);
}
gettimeofday(&after_bind, NULL);
printf("bind took %.5f seconds\n", elapsed_time(before_bind, after_bind));
printf("# Listening on port %d\n", ntohs(serv_addr.sin_port));
if (0 > listen(listen_fd, 100)) {
fprintf(stderr, "listen() on fd %d: %s\n",
listen_fd,
strerror(errno));
return 1;
}
{
struct sockaddr_in cli_addr;
struct timeval before;
int newfd;
socklen_t clilen;
clilen = sizeof(cli_addr);
if (0 > (newfd = accept(listen_fd, (struct sockaddr *)&cli_addr, &clilen))) {
fprintf(stderr, "accept() on fd %d: %s\n", listen_fd, strerror(errno));
exit(2);
}
gettimeofday(&before, NULL);
printf("At %ld.%06ld\tconnected to: %s\n",
before.tv_sec, before.tv_usec,
inet_ntoa(cli_addr.sin_addr)
);
fflush(stdout);
while (close(newfd) == EINTR) ;
}
if (0 > close(listen_fd))
fprintf(stderr, "Closing socket: %s\n", strerror(errno));
return 0;
}
float
elapsed_time(struct timeval before, struct timeval after)
{
float r = 0.0;
if (before.tv_usec > after.tv_usec) {
after.tv_usec += 1000000;
--after.tv_sec;
}
r = (float)(after.tv_sec - before.tv_sec)
+ (1.0E-6)*(float)(after.tv_usec - before.tv_usec);
return r;
}
Probé este programa en 3 máquinas diferentes, y obtengo un tiempo variable, entre 55 y 59 segundos, cuando el núcleo se niega a permitir que un usuario no root vuelva a abrir un socket. Compilé el código anterior en un ejecutable llamado "abridor", y lo ejecuté así:
./opener -p 7896; ./opener -p 7896
Abrí otra ventana e hice esto:
telnet otherhost 7896
Eso hace que la primera instancia de "abridor" acepte una conexión y luego la cierre. La segunda instancia de "abridor" intenta al bind(2)
puerto TCP 7896 cada segundo. "abridor" reporta 55 a 59 segundos de retraso.
Buscando en Google, encuentro que la gente recomienda hacer esto:
echo 30 > /proc/sys/net/ipv4/tcp_fin_timeout
para reducir ese intervalo. No me funcionó. De las 4 máquinas Linux a las que tenía acceso, dos tenían 30 y dos tenían 60. También establecí ese valor tan bajo como 10. No hay diferencia con el programa "abridor".
Haciendo esto:
echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle
Cambió las cosas. El segundo "abridor" solo tardó unos 3 segundos en obtener su nuevo zócalo.
man 2 bind
si no me crees. Es cierto que probablemente no es lo primero que piensan las personas de Unix cuando alguien dice "atar", por lo que es bastante justo.