Quiero evitar el estado TIME_WAIT al cerrar un socket TCP (soy consciente de las pros y contras de eludir TIME_WAIT).

Estoy usando Windows y WinSock2 / .Net sockets y tengo grandes dificultades para conseguir que la opción de socket SO_LINGER funcione como se describe en documentación.

Mi código de prueba con la mayor parte de la verificación de errores eliminada por brevedad es:

#include <winsock2.h>
#include <ws2tcpip.h>
#include <iostream>

int main()
{
  std::cout << "starting..." << std::endl;

  WSADATA w = { 0 };
  int error = WSAStartup(0x0202, &w);
  if (error || w.wVersion != 0x0202) {
    std::cerr << "Could not initialise Winsock2." << std::endl;
    return -1;
  }

  auto clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

  // Set socket options
  linger lingerOpt = { 1, 0 };
  setsockopt(clientSocket, SOL_SOCKET, SO_LINGER, (char*)&lingerOpt, sizeof(lingerOpt));

  linger checkLingerOpt{ 0 };
  int optLen = sizeof(checkLingerOpt);
  int getOptResult = getsockopt(clientSocket, SOL_SOCKET, SO_LINGER, (char*)&checkLingerOpt, &optLen);
  if (getOptResult < 0) {
    wprintf(L"Failed to get SO_LINGER socket option on client socket, error: %ld\n", WSAGetLastError());
  }
  else {
    std::cout << "Linger option set to onoff " << checkLingerOpt.l_onoff << ", linger seconds " << checkLingerOpt.l_linger << "." << std::endl;
  }

  // Bind local client socket.
  sockaddr_in clientBindAddr;
  clientBindAddr.sin_family = AF_INET;
  clientBindAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
  clientBindAddr.sin_port = htons(15064);
  bind(clientSocket, (SOCKADDR*)&clientBindAddr, sizeof (clientBindAddr));

  sockaddr_in serverSockAddr;
  serverSockAddr.sin_family = AF_INET;
  serverSockAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
  serverSockAddr.sin_port = htons(5060);

  // Connect to server.
  connect(clientSocket, (SOCKADDR*)&serverSockAddr, sizeof (serverSockAddr));

  std::cout << "connected." << std::endl;

  Sleep(1000);

  //shutdown(clientSocket, SD_BOTH);
  closesocket(clientSocket);

  std::cout << "finished." << std::endl;
}

Resultado:

starting...
Linger option set to onoff 1, linger seconds 0.
connected.
finished.

El ejemplo anterior evita el estado TIME_WAIT pero lo hace porque el socket del cliente envía un paquete RST.

Wireshark capture

Si la opción Linger se cambia a:

linger lingerOpt = { 1, 5 };

Resultado

starting...
Linger option set to onoff 1, linger seconds 5.
connected.
finished.

Luego, cerrar el socket da como resultado un TIME_WAIT pero de 30 s, que es el mismo resultado que no configurar la opción SO_LINGER.

netsh time wait

wireshark time wait

Otra observación es que si el socket se apaga (que es la forma recomendada de cerrar limpiamente) con shutdown(clientSocket, SD_BOTH); entonces la opción Linger de {1,0} no tendrá ningún efecto.

En resumen:

  • Establezca la opción Linger como {1,0} y cierre con closesocket => RST.
  • Establezca la opción Linger como {1,5} y cierre con closesocket => FIN-ACK & TIME_WAIT de 30s.
  • Establezca la opción Linger como {1,0} y cierre con apagado, closesocket => FIN-ACK & TIME_WAIT de 30 s.
  • Establezca la opción Linger como {1,5} y cierre con apagado, closesocket => FIN-ACK & TIME_WAIT de 30s.

Lo que me gustaría es:

Establezca la opción Linger como {1,0} y cierre con apagado, closesocket => FIN-ACK & TIME_WAIT de 0s.

Actualización: como se indica en closesocket de referencia de Remy Lebeau una opción de Linger de {distinto de cero, 0} está codificada para generar un RST.

Un TIME_WAIT corto de unos pocos segundos sería igual de bueno, es decir, una opción prolongada de {1,1} provocó que closesocket saliera elegantemente con un período TIME_WAIT de 1 s, y que según la documentación de closesocket debería ser posible.

Actualización 2: como lo señaló nuevamente Remy Lebeau, la opción de demora y el período TIME_WAIT NO están vinculados. Si está leyendo esto, probablemente cometió el mismo error que yo y estaba tratando de acortar el período TIME_WAIT a través de setsockopt y SO_LINGER.

Por todas las cuentas que no se puede hacer y en los casos en que la consideración cuidadosa juzga TIME_WAIT debe evitarse (como en mi caso, donde el protocolo de la capa de aplicación puede tratar con paquetes de datos TCP perdidos o huérfanos), la opción ideal parece ser un Linger configuración de {1,0} para forzar un cierre de socket RST duro que permitirá que la conexión se reintente inmediatamente sin que el sistema operativo bloquee el intento.

1
sipsorcery 22 oct. 2019 a las 17:43

1 respuesta

La mejor respuesta

Realmente no puede evitar TIME_WAIT cuando su aplicación es la que cierra la conexión TCP primero (TIME_WAIT no sucede cuando el par cierra la conexión primero). Ninguna cantidad de configuraciones de SO_LINGER cambiará ese hecho, aparte de realizar un cierre de socket abortivo (es decir, enviar un paquete RST). Es simplemente parte de cómo funciona TCP (consulte el diagrama de estado de TCP) . SO_LINGER simplemente controla cuánto tiempo espera closesocket() antes de cerrar una conexión activa.

La única forma de evitar que el conector entre en el estado TIME_WAIT es establecer la duración de l_linger en 0 y no llamar a shutdown(SD_SEND) o shutdown(SD_BOTH) en absoluto (llamando a { {X4}} está bien). Este es comportamiento documentado:

La llamada de closesocket solo se bloqueará hasta que todos los datos se hayan entregado al par o el tiempo de espera expire. Si la conexión se restablece porque el tiempo de espera expira, el socket no entrará en el estado TIME_WAIT. Si todos los datos se envían dentro del período de tiempo de espera, el socket puede entrar en el estado TIME_WAIT.

Si el miembro l_onoff de la estructura persistente es distinto de cero y el miembro l_linger tiene un intervalo de tiempo de espera cero en un socket de bloqueo, entonces una llamada a closesocket restablecerá la conexión. El socket no pasará al estado TIME_WAIT.

El verdadero problema con su código (aparte de la falta de manejo de errores) es que su cliente está bind() 'conectando un socket de cliente antes de connect()' enviarlo a un servidor. Por lo general, no debe bind() un socket de cliente en absoluto, debe dejar que el sistema operativo elija un enlace apropiado para usted. Sin embargo, si debe bind() un socket de cliente, es probable que deba habilitar la opción SO_REUSEADDR en ese socket para evitar ser bloqueado cuando una conexión anterior boudn a la misma IP / puerto local todavía está en { {X5}} y estás intentando connect() en un corto período de tiempo después del closesocket() anterior.

Consulte ¿Cómo evitar el estado TIME_WAIT después de closesocket ()? para más detalles. Además, el documento al que vinculó en su pregunta también explica formas de evitar TIME_WAIT sin tener que jugar con SO_LINGER.

1
Remy Lebeau 22 oct. 2019 a las 19:30