Как применить опцию Linger с winsock2 - PullRequest
0 голосов
/ 22 октября 2019

Я хочу избежать состояния TIME_WAIT при закрытии сокета TCP (мне известны плюсы и минусы обхода TIME_WAIT).

Я использую Windows и WinSock2 / .Netя испытываю большие затруднения при работе опции сокета SO_LINGER, как описано в документации .

Мой тестовый код с большей частью удаленной проверки ошибок для краткости:

#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;
}

Результат:

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

Приведенный выше пример избегает состояния TIME_WAIT, но делает это потому, что клиентский сокет отправляет пакет RST.

Wireshark capture

Если параметр Linger изменен на:

linger lingerOpt = { 1, 5 };

Результат

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

Тогда закрытие сокета приведет к TIME_WAIT, но к 30 с, чтотот же результат, что и не установка опции SO_LINGER.

netsh time wait

wireshark time wait

Другойнаблюдение состоит в том, что если сокет отключается (что является рекомендуемым способом чистого закрытия) с помощью shutdown(clientSocket, SD_BOTH);, то Lingerопция {1,0} не окажет влияния.

В итоге:

  • Установите опцию Linger как {1,0} и закройте с помощью closesocket => RST.
  • Установите опцию Linger как {1,5} и закройте с помощью closesocket => FIN-ACK & TIME_WAIT 30 с.
  • Установите опцию Linger как {1,0} и закройте с выключением, closesocket => FIN-ACK& TIME_WAIT - 30 с.
  • Установить опцию задержки в {1,5} и закрыть с выключением, closesocket => FIN-ACK & TIME_WAIT - 30 с.

Что бы я хотел:

Установить опцию Linger как {1,0} и закрыть при выключении, closesocket => FIN-ACK & TIME_WAIT, равном 0 с.

Обновление: Как указано в ссылке closesocket от Remy Lebeau параметр Linger {ненулевой, 0} жестко задан для генерации RST.

Короткое значение TIME_WAIT в несколько секунд будет таким же хорошим, то есть длительная опция {1,1} заставит closesocket изящно завершиться с периодом TIME_WAIT в 1 с, что в соответствии с closesocket документация должна быть возможной.

Обновление 2: Как еще раз отмечено Remy Lebeau, опция Linger и период TIME_WAIT НЕ связаны. Если вы читаете это, вы, вероятно, совершили ту же ошибку, что и я, и пытались сократить период TIME_WAIT с помощью setsockopt и SO_LINGER.

Для всех учетных записей, которые не могут быть выполнены, и в случаях, когда необходимо тщательно оценить судей, нужно избегать TIME_WAIT (например, в моем случае, когда протокол прикладного уровня может работать с паразитными или потерянными пакетами данных TCP), идеальныйопция выглядит как установка Linger {1,0} для принудительного закрытия жесткого сокета RST, что позволит немедленно повторить попытку подключения без ОС, блокирующей попытку.

1 Ответ

1 голос
/ 22 октября 2019

Вы не можете избежать TIME_WAIT, когда ваше приложение первым закрывает TCP-соединение (TIME_WAIT не происходит, когда одноранговый узел сначала закрывает соединение). Никакое количество настроек SO_LINGER не изменит этот факт, кроме выполнения неудачного закрытия сокета (т.е. отправки пакета RST). Это просто часть того, как работает TCP (смотрите диаграмму состояний TCP ). SO_LINGER просто контролирует, как долго closesocket() ожидает, прежде чем фактически закрыть активное соединение.

Единственный способ предотвратить переход сокета в состояние TIME_WAIT - установить длительность l_linger в 0, ине звоните shutdown(SD_SEND) или shutdown(SD_BOTH) (вызов shutdown(SD_RECEIVE) в порядке). Это задокументированное поведение :

Вызов closesocket будет блокироваться только до тех пор, пока все данные не будут доставлены одноранговому узлу или не истечет время ожидания. Если соединение будет сброшено из-за истечения времени ожидания, сокет не перейдет в состояние TIME_WAIT. Если все данные отправляются в течение периода ожидания, сокет может перейти в состояние TIME_WAIT.

Если элемент l_onoff структуры linger не равен нулю, а элемент l_linger представляет собой нулевой интервал времени ожидания на блокирующем сокете, товызов closesocket сбросит соединение. Сокет не перейдет в состояние TIME_WAIT.

Реальная проблема с вашим кодом (за исключением отсутствия обработки ошибок) заключается в том, что ваш клиент bind() использует клиентский сокет до connect() на сервере. Как правило, вы вообще не должны bind() клиентский сокет, вы должны позволить ОС выбрать подходящую привязку для вас. Однако, если вам необходимо bind() клиентский сокет, вам, вероятно, потребуется включить параметр SO_REUSEADDR на этом сокете, чтобы избежать блокировки, когда предыдущее соединение с тем же локальным IP / портом все еще находится в состоянии TIME_WAIT иВы пытаетесь connect() в короткий промежуток времени после предыдущего closesocket().

См. Как избежать состояния TIME_WAIT после closesocket ()? для получения дополнительной информации. Кроме того, документ, который вы указали на в своем вопросе, также объясняет способы избежать TIME_WAIT, не прибегая к связям с SO_LINGER.

...