Подтвердить что ты не робот

Правильный порядок закрытия соединений TcpListener и TcpClient (какая сторона должна быть активной)

Я прочитал этот ответ по предыдущему вопросу, в котором говорится:

Таким образом, партнер, который инициирует завершение, т.е. сначала вызовет функцию close(), закончит работу в состоянии TIME_WAIT. [...]

Однако это может быть проблемой с большим количеством сокетов в состоянии TIME_WAIT на сервере, поскольку это может в конечном итоге предотвратить прием новых соединений. [...]

Вместо этого создайте протокол приложения, так что завершение соединения всегда начинается с клиентской стороны. Если клиент всегда знает, когда он прочитал все остальные данные, он может инициировать последовательность завершения. Например, браузер знает из HTTP-заголовка Content-Length, когда он считывает все данные и может инициировать закрытие. (Я знаю, что в HTTP 1.1 он будет держать его открытым на некоторое время для возможного повторного использования, а затем закрыть его.)

Я хотел бы реализовать это с помощью TcpClient/TcpListener, но не ясно, как заставить его работать правильно.

Подход 1: обе стороны закрыты

Это типичный пример, который иллюстрирует большинство примеров MSDN - обе стороны, вызывающие Close(), а не только клиент:

private static void AcceptLoop()
{
    listener.BeginAcceptTcpClient(ar =>
    {
        var tcpClient = listener.EndAcceptTcpClient(ar);

        ThreadPool.QueueUserWorkItem(delegate
        {
            var stream = tcpClient.GetStream();
            ReadSomeData(stream);
            WriteSomeData(stream);
            tcpClient.Close();   <---- note
        });

        AcceptLoop();
    }, null);
}

private static void ExecuteClient()
{
    using (var client = new TcpClient())
    {
        client.Connect("localhost", 8012);

        using (var stream = client.GetStream())
        {
            WriteSomeData(stream);
            ReadSomeData(stream);
        }
    }
}

После запуска с 20 клиентами TCPView показывает много сокетов от и клиента и сервера застрял в TIME_WAIT, что занимает довольно некоторое время, чтобы исчезнуть.

enter image description here

Подход 2: закрытие клиента

В соответствии с приведенными выше цитатами я удалил вызовы Close() моего слушателя, и теперь я просто полагаюсь на закрытие клиента:

var tcpClient = listener.EndAcceptTcpClient(ar);

ThreadPool.QueueUserWorkItem(delegate
{
    var stream = tcpClient.GetStream();
    ReadSomeData(stream);
    WriteSomeData(stream);
    // tcpClient.Close();   <-- Let the client close
});

AcceptLoop();

Теперь у меня больше нет TIME_WAIT, но я получаю сокеты, оставшиеся на разных этапах CLOSE_WAIT, FIN_WAIT и т.д., которые также очень долго исчезают.

TCPView, with everything closing

Подход 3: дать клиенту время закрыть первый

На этот раз я добавил задержку перед закрытием подключения к серверу:

var tcpClient = listener.EndAcceptTcpClient(ar);

ThreadPool.QueueUserWorkItem(delegate
{
    var stream = tcpClient.GetStream();
    ReadSomeData(stream);
    WriteSomeData(stream);
    Thread.Sleep(100);      // <-- Give the client the opportunity to close first
    tcpClient.Close();      // <-- Now server closes
});

AcceptLoop();

Кажется, это лучше - теперь только клиентские сокеты находятся в TIME_WAIT; серверные сокеты полностью закрыты:

enter image description here

Это, похоже, согласуется с тем, что говорится в предыдущей статье:

Таким образом, партнер, который инициирует завершение, т.е. сначала вызовет функцию close(), - закончится в состоянии TIME_WAIT.

Вопросы:

  • Какой из этих подходов - правильный путь, и почему? (Предполагая, что я хочу, чтобы клиент был стороной "активного закрытия" ).
  • Есть ли лучший способ реализовать подход 3? Мы хотим, чтобы клиент был закрыт, чтобы клиент остался с TIME_WAIT, но когда клиент закрывается, мы также хотим закрыть соединение на сервере.
  • Мой сценарий фактически противоположный веб-серверу; У меня есть один клиент, который подключается и отключается от многих разных удаленных компьютеров. Я предпочел бы, чтобы сервер имел соединения, застрявшие в TIME_WAIT вместо этого, чтобы высвободить ресурсы на моем клиенте. В этом случае, должен ли я заставить сервер выполнить активное закрытие и поставить sleep/close на мой клиент?

Полный код, чтобы попробовать сам, находится здесь:

https://gist.github.com/PaulStovell/a58cd48a5c6b14885cf3

Изменить: еще один полезный ресурс:

http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html

Для сервера, который устанавливает исходящие соединения, а также принимает входящие соединения, золотое правило всегда должно гарантировать, что если TIME_WAIT должен произойти, что он попадает на другой узел, а не на сервер. Лучший способ сделать это - никогда не начинать активное закрытие с сервера, независимо от причины. Если ваш сверстник истечет, прекратите соединение с RST, а не закрывайте его. Если ваш партнер отправляет недействительные данные, прерывает соединение и т.д. Идея состоит в том, что если ваш сервер никогда не инициирует активное закрытие, он никогда не сможет накапливать сокеты TIME_WAIT и поэтому никогда не будет страдать от проблем с масштабируемостью, которые они вызывают. Хотя легко видеть, как вы можете прервать соединения, когда возникают ситуации с ошибками, что касается нормального завершения соединения? В идеале вы должны разработать в своем протоколе способ, чтобы сервер сообщал клиенту, что он должен отключиться, вместо того, чтобы просто заставить сервер инициировать активное закрытие. Поэтому, если серверу необходимо завершить соединение, сервер отправляет сообщение "мы закончили" на уровне приложения, которое клиент берет в качестве причины для закрытия соединения. Если клиент не сможет закрыть соединение в разумные сроки, сервер прерывает соединение.

На клиентском компьютере несколько сложнее, в конце концов, кто-то должен инициировать активное закрытие, чтобы прекратить TCP-соединение, и если это клиент, то тогда, когда TIME_WAIT закончится. Однако наличие в TIME_WAIT на клиенте имеет ряд преимуществ. Во-первых, если по какой-то причине клиент заканчивает проблемы с подключением из-за накопления сокетов в TIME_WAIT, это всего лишь один клиент. Другие клиенты не будут затронуты. Во-вторых, это неэффективно, чтобы быстро открывать и закрывать TCP-соединения с одним и тем же сервером, поэтому имеет смысл выйти за рамки TIME_WAIT, чтобы попытаться поддерживать соединения на более длительные периоды времени, а не на более короткие периоды времени. Не разрабатывайте протокол, посредством которого клиент каждую минуту подключается к серверу и делает это, открывая новое соединение. Вместо этого используйте конструкцию постоянных соединений и только повторно подключайтесь, когда соединение терпит неудачу, если посреднические маршрутизаторы отказываются поддерживать соединение открытым без потока данных, тогда вы можете либо выполнить пинг на уровне приложения, либо использовать TCP, либо просто принять, что маршрутизатор сбрасывает ваше соединение; хорошо, что вы не собираете сокеты TIME_WAIT. Если работа, которую вы выполняете при подключении, естественно недолговечна, тогда рассмотрим какую-то форму "объединения пулов соединений", при которой соединение будет оставаться открытым и повторно использоваться. Наконец, если вы абсолютно должны быстро открывать и закрывать соединения с клиентом на один и тот же сервер, возможно, вы можете создать последовательность выключения уровня приложения, которую вы можете использовать, а затем следовать этому с неудачным закрытием. Ваш клиент может отправить сообщение "Я сделан", ваш сервер может отправить сообщение "прощай", и клиент может прекратить соединение.

4b9b3361

Ответ 1

Именно так работает TCP, вы не можете этого избежать. Вы можете установить различные таймауты для TIME_WAIT или FIN_WAIT на вашем сервере, но об этом.

Причиной этого является то, что на TCP пакет может прибыть в сокет, который вы закрыли давным-давно. Если у вас уже есть другой сокет, открытый на том же IP-адресе и порту, он получит данные, предназначенные для предыдущего сеанса, что может смутить его. Особенно учитывая, что большинство людей считают TCP надежным:)

Если ваш клиент и сервер правильно реализуют TCP (например, корректно обрабатывая чистое выключение), на самом деле не имеет значения, закрыл ли клиент или сервер соединение. Поскольку это звучит так, будто вы управляете обеими сторонами, это не должно быть проблемой.

Ваша проблема, похоже, связана с надлежащим отключением на сервере. Когда одна сторона сокета закрывается, другая сторона будет Read с длиной 0 - это сообщение о завершении связи. Скорее всего, вы игнорируете это в своем коде сервера - это специальный случай, в котором говорится: "Теперь вы можете безопасно избавиться от этого сокета, сделайте это сейчас".

В вашем случае закрытие на стороне сервера похоже наилучшим образом.

Но на самом деле TCP довольно сложный. Это не помогает, что большинство образцов в Интернете сильно испорчены (особенно образцы для С# - это не слишком сложно найти хороший пример для С++, например) и игнорировать многие важные части протокола. У меня есть простой пример того, что может сработать для вас - https://github.com/Luaancz/Networking/tree/master/Networking%20Part%201 Это еще не идеальный TCP, но он намного лучше, чем образцы MSDN, например.

Ответ 2

Пол, вы сами провели большое исследование. Я тоже занимался этой областью. Одна статья, которую я нашел очень полезной по теме TIME_WAIT, такова:

http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html

В нем есть несколько проблем, связанных с Linux, но все содержимое уровня TCP является универсальным.

В конечном итоге обе стороны должны закрыть (т.е. завершить рукопожатие FIN/ACK), поскольку вы не хотите, чтобы состояния FIN_WAIT или CLOSE_WAIT задерживались, это просто "плохой" TCP. Я бы избегал использовать RST для принудительного закрытия соединений, поскольку это может вызвать проблемы в другом месте и просто чувствует себя плохой сетью.

Верно, что состояние TIME_WAIT произойдет в конце, которое сначала завершает соединение (т.е. отправляет первый пакет FIN), и что вы должны оптимизировать, чтобы закрыть соединение сначала на конце, которое будет иметь наименьшее отключение соединения.

В Windows у вас будет по меньшей мере 15 000 TCP-портов, доступных по IP, по умолчанию, поэтому вам понадобится приличный отбор для подключения к нему. Память для TCB для отслеживания состояний TIME_WAIT должна быть вполне приемлемой.

https://support.microsoft.com/kb/929851

Также важно отметить, что TCP-соединение может быть полузамкнуто. То есть, один конец может выбрать закрыть соединение для отправки, но оставить его открытым для приема. В .NET это делается следующим образом:

tcpClient.Client.Shutdown(SocketShutdown.Send);

http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.shutdown.aspx

Я нашел это необходимым при переносе части инструмента netcat из Linux в PowerShell:

http://www.powershellmagazine.com/2014/10/03/building-netcat-with-powershell/

Я должен повторно повторить совет, что если вы можете сохранить соединение открытым и простоя до тех пор, пока вам это не понадобится, это, как правило, оказывает огромное влияние на сокращение TIME_WAIT.

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

Надеюсь, что часть этого будет полезна.

Ответ 3

Какой из этих подходов - правильный путь, и почему? (Предполагая, что я хочу, чтобы клиент был стороной "активного закрытия" )

В идеальной ситуации ваш сервер отправит пакет RequestDisconnect с определенным кодом операции клиенту, который затем обрабатывает клиент, закрыв соединение после получения этого пакета. Таким образом, вы не получаете устаревших сокетов на стороне сервера (поскольку сокеты - это ресурсы, а ресурсы - конечные, поэтому наличие сталей - это плохо).

Если клиент выполняет свою последовательность разъединения, удалив сокет (или вызывающий Close(), если вы используете TcpClient, он поместит сокет в состояние CLOSE_WAIT на сервере, что означает соединение находится в процессе закрытия.

Есть ли лучший способ реализовать подход 3? Мы хотим, чтобы инициация закрытия была инициирована клиентом (чтобы клиент остался с TIME_WAIT), но когда клиент закрывается, мы также хотим закрыть соединение на сервере.

Да. Как и выше, сервер отправляет пакет, чтобы попросить клиента закрыть его подключение к серверу.

Мой сценарий фактически противоположный веб-серверу; У меня есть один клиент, который подключается и отключается от многих разных удаленных компьютеров. Я предпочел бы, чтобы сервер имел соединения, застрявшие в TIME_WAIT, вместо этого, чтобы высвободить ресурсы на моем клиенте. В этом случае, должен ли я заставить сервер выполнить активное закрытие и поставить sleep/close на мой клиент?

Да, если то, что вам нужно, не стесняйтесь вызывать Dispose на сервере, чтобы избавиться от клиентов.

На стороне примечания вы можете захотеть изучить исходные объекты Socket, а не TcpClient, так как он чрезвычайно ограничен, Если вы работаете напрямую с сокетами, у вас есть SendAsync и все другие методы async для операций сокета в вашем распоряжении. Использование ручных вызовов Thread.Sleep - это то, чего я бы избегал - эти операции носят асинхронный характер - отключение после того, как вы что-то написали в поток, должно выполняться в обратном вызове SendAsync, а не после Sleep.