Правильный порядок закрытия соединений 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

согласно цитатам выше, я удалил 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, etc. это также занимает очень много времени, чтобы исчезнуть.

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.

вопросы:

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

полный код, чтобы попробовать его себе здесь:

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

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

3 ответов


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

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

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

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

в вашем случае закрытие на стороне сервера кажется наилучшим вариантом.

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


Павел, вы сделали некоторые большие исследования себя. Я тоже немного поработал в этом районе. Одна статья, которую я нашел очень полезной в теме 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 для принудительного закрытия связей, поскольку это может вызвать проблемы в другом месте и просто чувствует себя бедным netizen.

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

в Windows у вас будет чуть более 15 000 TCP порты доступны на IP по умолчанию, поэтому вам понадобится приличное соединение, чтобы поразить это. Память для TCBs для отслеживания состояний 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_WAITs.

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

Я надеюсь, что это полезно.


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

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

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

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

да. Как и выше, попросите сервер отправить пакет с просьбой к клиенту закрыть соединение с сервером.

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

да, если это то, что вы после не стесняйтесь называть Dispose на сервере, чтобы избавиться от клиентов.

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