Обнаружение отключения 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 ложным.

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


person Blair Holloway    schedule 13.12.2009    source источник


Ответы (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);
            }
        }
person feroze    schedule 13.12.2009
comment
Это позволяет обнаруживать разъединение, интерпретируя результат вызовов чтения. Однако мое любопытство проистекает из очевидного отсутствия у фреймворка другого способа сделать это, особенно с учетом элегантности, которую он дает исходной реализации. Кроме того, исходный код правильно разграничивал объекты, поэтому можно было отправлять несколько сообщений. Я предполагаю, что это связано с тем, как BinaryFormatter сериализует объекты - он по своей сути знает, когда это будет сделано. (Хотя в представленном вами коде есть необходимость в разделителе.) - person Blair Holloway; 14.12.2009
comment
Можете ли вы сказать мне, как бы вы хотели, чтобы это было реализовано во фреймворке? Framework - это просто оболочка поверх сокетов, которая является транспортным механизмом. Приложение должно реализовать другую семантику. Кроме того, BinaryFormatter принимает буфер в качестве входных данных для десериализации, поэтому вам необходимо предоставить ему точный буфер, необходимый для возврата объекта. В противном случае он не будет работать должным образом. - person feroze; 14.12.2009
comment
Вы правы - для десериализации нужен точный буфер, иначе она болезненно умрет, и BinaryFormatter не несет ответственности за обеспечение завершения потока; в данном случае это не NetworkStream. Я думаю, что, возможно, я неправильно думал об этом, и что это не ответственность Framework, а моя. ;) - person Blair Holloway; 17.12.2009

Да, но что, если вы потеряете соединение до того, как получите размер? т.е. прямо перед следующей строкой:

// message framing. First, read the #bytes to expect. 

int objectSize = br.ReadInt32(); 

ReadInt32() заблокирует поток на неопределенный срок.

person Kiforl    schedule 14.08.2010