Обнаружение отключения TCP клиента при использовании класса NetworkStream
Мой друг пришел ко мне с проблемой: при использовании класса 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 - ложным.
Поиск переполнения стека дал ответ - поскольку 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 (née NetworkStream.Read), который бы правильно обнаруживал разъединение. Тем не менее, это накладные расходы на создание и поддержку потока для каждого клиента.
У меня такое ощущение, что мне не хватает какого-то секретного, удивительного ответа, который бы сохранил ясность исходного кода, но избегал использования дополнительных потоков для выполнения асинхронного чтения. Хотя, возможно, класс NetworkStream никогда не был разработан для такого рода использования. Кто-нибудь может пролить свет?
Обновление: просто хочу уточнить, что мне интересно узнать, есть ли в платформе.NET решение, которое охватывает это использование NetworkStream (т. Е. Опрос и избежание блокировки) - очевидно, это можно сделать; NetworkStream можно легко обернуть в поддерживающий класс, который обеспечивает функциональность. Просто показалось странным, что фреймворк, по сути, требует от вас использования потоков, чтобы избежать блокировки в NetworkStream.Read, или для просмотра самого сокета, чтобы проверить наличие разрывов - почти как в случае ошибки. Или потенциальное отсутствие функции.;)
2 ответа
Ожидает ли сервер отправки нескольких объектов по одному соединению? Если так, я не вижу, как этот код будет работать, так как нет отправляемого разделителя, который указывает, где начинается первый объект и заканчивается следующий объект.
Если отправляется только один объект и после этого соединение закрывается, то оригинальный код будет работать.
Должна быть начата сетевая операция, чтобы выяснить, активно ли еще соединение или нет. Что я хотел бы сделать, так это то, что вместо десериализации непосредственно из сетевого потока я бы вместо этого поместил буфер в MemoryStream. Это позволило бы мне определить, когда связь была потеряна. Я бы также использовал кадрирование сообщений для разграничения нескольких ответов в потоке.
MemoryStream ms = new MemoryStream();
NetworkStream ns = client.GetStream();
BinaryReader br = new BinaryReader(ns);
// message framing. First, read the #bytes to expect.
int objectSize = br.ReadInt32();
if (objectSize == 0)
break; // client disconnected
byte [] buffer = new byte[objectSize];
int index = 0;
int read = ns.Read(buffer, index, Math.Min(objectSize, 1024);
while (read > 0)
{
objectSize -= read;
index += read;
read = ns.Read(buffer, index, Math.Min(objectSize, 1024);
}
if (objectSize > 0)
{
// client aborted connection in the middle of stream;
break;
}
else
{
BinaryFormatter bf = new BinaryFormatter();
using(MemoryStream ms = new MemoryStream(buffer))
{
object o = bf.Deserialize(ns);
ReceiveMyObject(o);
}
}
Да, но что, если вы потеряете соединение, прежде чем получить размер? т.е. прямо перед следующей строкой:
// message framing. First, read the #bytes to expect.
int objectSize = br.ReadInt32();
ReadInt32()
заблокирует поток на неопределенный срок.