Исключения NetworkStream и коды возврата

Я пытаюсь разобраться в тонкостях чтения из NetworkStream и понять различные способы возникновения проблем. У меня есть следующий код:

    public async Task ReceiveAll()
    {
        var ns = this.tcp.GetStream();
        var readBuffer = new byte[1000];
        while (true)
        {
            int bytesRead;
            try
            {
                bytesRead = await ns.ReadAsync(readBuffer, 0, readBuffer.Length);
                if (bytesRead == 0)
                {
                    // Remote disconnection A?
                    break;
                }
            }
            catch (IOException)
            {
                // Remote disconnection B?
                break;
            }
            catch (ObjectDisposedException)
            {
                // Local disconnection?
                break;
            }

            /*Do something with readBuffer */
        }
    }

Я отметил в коде три точки, где программа говорит: «Что-то пошло не так, продолжать нет смысла».

«Локальное отключение» не является чем-то неправильным, это произойдет, когда я локально закрою сокет, что является единственным способом выхода из цикла при нормальных обстоятельствах. Я не думаю, что что-то еще может вызвать это, поэтому я думаю, что могу просто проглотить исключение.

Две точки «Удаленное отключение» - это то, в чем я не уверен. Я знаю, что ReadAsync вернет 0, если соединение будет разорвано удаленно (A), но в некоторых случаях также срабатывает IOException. Если мой удаленный клиент является консолью С#, то закрытие сокета, по-видимому, приводит к «А», а закрытие окна консоли, похоже, к «Б». Я не уверен, что понимаю, в чем разница между этими сценариями?

Наконец, немного общего вопроса, но есть ли что-то явно неправильное в этом фрагменте кода или в моих предположениях выше?

Спасибо.

РЕДАКТИРОВАТЬ: В ответ на мое использование ObjectDisposedException для прерывания цикла:

Вот как выглядит мой метод «Стоп» (из того же класса, что и выше):

    public void Stop()
    {
        this.tcp.Close();
    }

Это приводит к тому, что ожидающий 'ReadAsync' за исключением ObjectDisposedException. AFAIK нет другого способа прервать это. Изменение этого на:

    public void Stop()
    {
        this.tcp.Client.Shutdown(SocketShutdown.Both);
    }

На самом деле ничего не делает с ожидающим вызовом, он просто продолжает ждать.


person Barguast    schedule 12.02.2015    source источник


Ответы (1)


Когда NetworkStream возвращает 0, это означает, что сокет получил пакет отключения от удаленной стороны. Вот как должны заканчиваться сетевые соединения.

Правильный способ разорвать соединение (особенно если вы находитесь в полнодуплексном режиме) — позвонить socket.Shutdown(SocketShutdown.Send) и дать удаленной стороне некоторое время, чтобы закрыть канал передачи. Это гарантирует, что вы получите все ожидающие данные, а не закроете соединение. ObjectDisposedException никогда не должно быть частью нормального потока приложений.

Любые выброшенные исключения указывают на то, что что-то пошло не так, и я думаю, можно с уверенностью сказать, что вы больше не можете полагаться на текущее соединение.

TL;DR

Я не вижу ничего плохого в вашем коде, но (особенно при полнодуплексной связи) я бы закрыл канал отправки и дождался 0-байтового пакета, чтобы предотвратить получение ObjectDisposedExceptions по умолчанию:

  1. используйте tcp.Shutdown(SocketShutdown.Send), чтобы сообщить удаленной стороне, что вы хотите отключиться

  2. ваша петля может по-прежнему получать данные, которые отправляла удаленная сторона

  3. ваш цикл, если все прошло правильно, получит 0-байтовый пакет, указывающий, что удаленная сторона отключается

  4. цикл завершается правильно

  5. вы можете решить избавиться от сокета через определенное время, если вы не получили 0-байтовый пакет

person C.Evenhuis    schedule 12.02.2015
comment
Спасибо. Обработчик ObjectDisposedException существует только там, поскольку я не думаю, что есть какой-либо другой способ «отменить» ReadAsync, когда этот цикл выполняется без удаления сокета. Если «удаленное отключение A» связано с «чистым» завершением работы удаленного клиента, то что именно вызывает B, то есть как локальный клиент узнает, что удаленный клиент ушел? - person Barguast; 12.02.2015
comment
Отвечая на ваше редактирование: я бы использовал двухэтапное завершение работы, чтобы предотвратить получение ObjectDisposedExceptions по умолчанию. Можете ли вы уточнить это? - person Barguast; 12.02.2015
comment
@Barguast: вы можете предотвратить ObjectDisposedException, если вы Shutdown() при чтении = 0. - person jgauffin; 12.02.2015
comment
@Barguast ситуация B возникает, когда возникла обнаруживаемая проблема или удаленная сторона не ответила в течение периода времени, настроенного в ОС. - person C.Evenhuis; 12.02.2015
comment
@jgauffin Я думаю, что проблема возникла, когда локальная сторона хочет закрыть - person C.Evenhuis; 12.02.2015
comment
@C.Evenhuis: То же самое. Используйте Shutdown() вместо Close(). Чтение также получит 0. ObjectDisposedException возможно только в том случае, если разработчик не ждет корректного завершения работы. - person jgauffin; 12.02.2015
comment
Я не уверен, правильно ли я объяснил себя. Вышеупомянутый «код сервера» появляется после того, как удаленное соединение было установлено, и серверу необходимо обработать любые отправленные данные. Сервер может быть остановлен пользователем, и в этот момент мне нужно остановить эти циклы (и их ожидающие ReadAsync-ы). Я не думаю, что на сервере можно что-то сделать, кроме удаления сокета? - person Barguast; 12.02.2015
comment
@Barguast: Как вы думаете, почему вы не можете использовать Shutdown() на стороне сервера, а затем ждать, пока Read() получит 0 прочитанных байтов? - person jgauffin; 12.02.2015
comment
@jgauffin - я обновил свой ответ, сделав его более подробным, но в основном он ничего не делает, если я вызываю «tcp.Client.Shutdown», пока ожидается ReadAsync. - person Barguast; 12.02.2015
comment
@Barguast, потому что вы также закрываете канал Receive. Используйте SocketShutdown.Send, чтобы получить подтверждение отключения. - person C.Evenhuis; 12.02.2015
comment
@C.Evenhuis — изменение метода Stop для вызова this.tcp.Client.Shutdown (SocketShutdown.Send); также, похоже, не приводит к тому, что ReadAsync возвращает (или исключает) либо - person Barguast; 12.02.2015
comment
Мне нужно сказать этим циклам (и их ожидающим ReadAsync-ам) остановиться - если ваши клиенты ведут себя хорошо, то, когда вы вызываете Shutdown(SocketShutdown.Send), они увидят 0-байтовый прием на своем конце, и как только они закончат отправку что угодно, они вызовут Shutdown(SocketShutdown.Both) (или эквивалент их платформы), и ваш сервер тогда увидит завершение приема 0 байтов. При желании вы можете запустить таймер, а затем закрыть сокет, если вы чувствуете, что клиент не ответил своевременно (т.е. ведет себя плохо). Но это не нормальный сценарий, даже если его нужно поддерживать. - person Peter Duniho; 13.02.2015
comment
@PeterDuniho - я определенно не могу полагаться на то, что клиенты будут вести себя хорошо. Мой реальный сценарий — различные типы мобильного оборудования, подключенного к моему серверу через GPRS, поэтому все ставки сняты в отношении подключения и поведения. - person Barguast; 13.02.2015
comment
@Barguast Мне трудно определить, какая информация, по вашему мнению, все еще отсутствует. Сценарий хорошего поведения повторяется несколько раз, и мы также предусмотрели возможность плохого поведения клиентов. Вам решать, закрыть соединение или дать каждому клиенту некоторое время, чтобы правильно закрыть соединение. - person C.Evenhuis; 13.02.2015
comment
@C.Evenhuis - у меня сложилось впечатление, что jgauffin говорил мне, что если я выполню Socket.Shutdown на сервере (т.е. во время остановки, вместо вызова Close), то ReadAsync выйдет, немедленно вернув 0, а не выбрасывая ObjectDisposedException как это делает Клоуз. Кажется, это не так. У меня нет проблем с вызовом Close и устранением исключения при условии отсутствия побочных эффектов, но ваш первоначальный ответ показал, что это плохой способ сделать это. - person Barguast; 13.02.2015
comment
@Barguast После Socket.Shutdown(Send) удаленной стороне будет отправлен 0-байтовый пакет, и когда удаленная сторона ответит правильно, вы прочитаете 0, так что это определенно не произойдет немедленно. При закрытии соединения с помощью Socket.Close() вы можете пропустить последнее входящее сообщение, если оно было отправлено в то время, когда вы закрывали соединение. - person C.Evenhuis; 13.02.2015
comment
Хорошо, я думаю, что в этом случае я должен признать, что клиенты не будут вести себя предсказуемо (см. мой комментарий чуть выше). Спасибо, что нашли время, чтобы пройти через это со мной. - person Barguast; 13.02.2015
comment
@Barguast: это нормально, но вы все равно должны сначала дать им презумпцию невиновности. Даже если некоторые клиенты не будут вести себя хорошо, вам нужно начать с предположения, что они будут вести себя так, чтобы ваш код был правильным. Используйте Shutdown(SocketShutdown.Send), чтобы указать удаленной конечной точке, что вы закончили отправку, а затем не закрывайте сокет до тех пор, пока ваш прием does не завершится с длиной 0 байт. Вы можете также добавить к этому таймер, который вы используете, чтобы определить, когда закрывать сокет, если и только если удаленная конечная точка не отключается своевременно. - person Peter Duniho; 13.02.2015