Мой друг обратился ко мне с проблемой: при использовании класса NetworkStream на стороне сервера соединения, если клиент отключается, NetworkStream не может его обнаружить.
Урезанный, его код на C # выглядел так:
List<TcpClient> connections = new List<TcpClient>();
TcpListener listener = new TcpListener(7777);
listener.Start();
while(true)
{
if (listener.Pending())
{
connections.Add(listener.AcceptTcpClient());
}
TcpClient deadClient = null;
foreach (TcpClient client in connections)
{
if (!client.Connected)
{
deadClient = client;
break;
}
NetworkStream ns = client.GetStream();
if (ns.DataAvailable)
{
BinaryFormatter bf = new BinaryFormatter();
object o = bf.Deserialize(ns);
ReceiveMyObject(o);
}
}
if (deadClient != null)
{
deadClient.Close();
connections.Remove(deadClient);
}
Thread.Sleep(0);
}
Код работает в том смысле, что клиенты могут успешно подключаться, а сервер может читать отправленные ему данные. Однако, если удаленный клиент вызывает tcpClient.Close (), сервер не обнаруживает разъединение - client.Connected остается истинным, а ns.DataAvailable ложным.
Поиск Stack Overflow предоставил ответ - поскольку Socket.Receive не вызывается, сокет не обнаруживает разъединение. Справедливо. Мы можем обойти это:
foreach (TcpClient client in connections)
{
client.ReceiveTimeout = 0;
if (client.Client.Poll(0, SelectMode.SelectRead))
{
int bytesPeeked = 0;
byte[] buffer = new byte[1];
bytesPeeked = client.Client.Receive(buffer, SocketFlags.Peek);
if (bytesPeeked == 0)
{
deadClient = client;
break;
}
else
{
NetworkStream ns = client.GetStream();
if (ns.DataAvailable)
{
BinaryFormatter bf = new BinaryFormatter();
object o = bf.Deserialize(ns);
ReceiveMyObject(o);
}
}
}
}
(Для краткости я оставил код обработки исключений.)
Этот код работает, однако я бы не назвал это решение «элегантным». Другое изящное решение проблемы, о которой я знаю, - это создать поток для каждого TcpClient и разрешить вызову BinaryFormatter.Deserialize (в девичестве NetworkStream.Read) блокироваться, что позволит правильно обнаружить отключение. Тем не менее, это накладные расходы на создание и обслуживание потока для каждого клиента.
У меня такое ощущение, что мне не хватает какого-то секретного, удивительного ответа, который сохранил бы ясность исходного кода, но избегал использования дополнительных потоков для выполнения асинхронного чтения. Хотя, возможно, класс NetworkStream никогда не был предназначен для такого использования. Кто-нибудь может пролить свет?
Обновление: просто хочу уточнить, что мне интересно узнать, есть ли у .NET framework решение, которое охватывает это использование NetworkStream (т. е. опрос и избежание блокировки) - очевидно это может быть сделано; NetworkStream можно легко обернуть в вспомогательный класс, обеспечивающий эту функциональность. Просто казалось странным, что фреймворк по существу требует, чтобы вы использовали потоки, чтобы избежать блокировки в NetworkStream.Read, или заглядывать в сам сокет для проверки отключений - почти как ошибка. Или потенциальное отсутствие функции. ;)