.net - клиент - tcplistener c#



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

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

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

Подход 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 и т. CLOSE_WAIT , CLOSE_WAIT также исчезают очень долго.

Подход 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 ; сокеты сервера все закрыты должным образом:

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

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

Вопросы:

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

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


Answer #1

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

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

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

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

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

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

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

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


Answer #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 у вас будет чуть более 15000 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.

Я надеюсь, что это поможет.





tcplistener