Обнаружение отключения 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() заблокирует поток на неопределенный срок.

Другие вопросы по тегам