Я прочитал этот ответ по предыдущему вопросу, в котором говорится:
Таким образом, партнер, который инициирует завершение, т.е. сначала вызовет функцию 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
и т.д., которые также очень долго исчезают.
Подход 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.
Вопросы:
- Какой из этих подходов - правильный путь, и почему? (Предполагая, что я хочу, чтобы клиент был стороной "активного закрытия" ).
- Есть ли лучший способ реализовать подход 3? Мы хотим, чтобы клиент был закрыт, чтобы клиент остался с TIME_WAIT, но когда клиент закрывается, мы также хотим закрыть соединение на сервере.
- Мой сценарий фактически противоположный веб-серверу; У меня есть один клиент, который подключается и отключается от многих разных удаленных компьютеров. Я предпочел бы, чтобы сервер имел соединения, застрявшие в
TIME_WAIT
вместо этого, чтобы высвободить ресурсы на моем клиенте. В этом случае, должен ли я заставить сервер выполнить активное закрытие и поставить sleep/close на мой клиент?
Полный код, чтобы попробовать сам, находится здесь:
https://gist.github.com/PaulStovell/a58cd48a5c6b14885cf3
Изменить: еще один полезный ресурс:
Для сервера, который устанавливает исходящие соединения, а также принимает входящие соединения, золотое правило всегда должно гарантировать, что если TIME_WAIT должен произойти, что он попадает на другой узел, а не на сервер. Лучший способ сделать это - никогда не начинать активное закрытие с сервера, независимо от причины. Если ваш сверстник истечет, прекратите соединение с RST, а не закрывайте его. Если ваш партнер отправляет недействительные данные, прерывает соединение и т.д. Идея состоит в том, что если ваш сервер никогда не инициирует активное закрытие, он никогда не сможет накапливать сокеты TIME_WAIT и поэтому никогда не будет страдать от проблем с масштабируемостью, которые они вызывают. Хотя легко видеть, как вы можете прервать соединения, когда возникают ситуации с ошибками, что касается нормального завершения соединения? В идеале вы должны разработать в своем протоколе способ, чтобы сервер сообщал клиенту, что он должен отключиться, вместо того, чтобы просто заставить сервер инициировать активное закрытие. Поэтому, если серверу необходимо завершить соединение, сервер отправляет сообщение "мы закончили" на уровне приложения, которое клиент берет в качестве причины для закрытия соединения. Если клиент не сможет закрыть соединение в разумные сроки, сервер прерывает соединение.
На клиентском компьютере несколько сложнее, в конце концов, кто-то должен инициировать активное закрытие, чтобы прекратить TCP-соединение, и если это клиент, то тогда, когда TIME_WAIT закончится. Однако наличие в TIME_WAIT на клиенте имеет ряд преимуществ. Во-первых, если по какой-то причине клиент заканчивает проблемы с подключением из-за накопления сокетов в TIME_WAIT, это всего лишь один клиент. Другие клиенты не будут затронуты. Во-вторых, это неэффективно, чтобы быстро открывать и закрывать TCP-соединения с одним и тем же сервером, поэтому имеет смысл выйти за рамки TIME_WAIT, чтобы попытаться поддерживать соединения на более длительные периоды времени, а не на более короткие периоды времени. Не разрабатывайте протокол, посредством которого клиент каждую минуту подключается к серверу и делает это, открывая новое соединение. Вместо этого используйте конструкцию постоянных соединений и только повторно подключайтесь, когда соединение терпит неудачу, если посреднические маршрутизаторы отказываются поддерживать соединение открытым без потока данных, тогда вы можете либо выполнить пинг на уровне приложения, либо использовать TCP, либо просто принять, что маршрутизатор сбрасывает ваше соединение; хорошо, что вы не собираете сокеты TIME_WAIT. Если работа, которую вы выполняете при подключении, естественно недолговечна, тогда рассмотрим какую-то форму "объединения пулов соединений", при которой соединение будет оставаться открытым и повторно использоваться. Наконец, если вы абсолютно должны быстро открывать и закрывать соединения с клиентом на один и тот же сервер, возможно, вы можете создать последовательность выключения уровня приложения, которую вы можете использовать, а затем следовать этому с неудачным закрытием. Ваш клиент может отправить сообщение "Я сделан", ваш сервер может отправить сообщение "прощай", и клиент может прекратить соединение.