Как правильно написать Socket / NetworkStream асинхронно? - PullRequest
3 голосов
/ 27 апреля 2011

Я хочу использовать NetworkStream (или, возможно, Socket) для чтения / записи TCP-соединений.Я хочу использовать неблокирующие операции, чтобы мне не приходилось иметь дело с несколькими потоками или решать вопрос о том, как остановить программу, если некоторые потоки могут быть остановлены при блокировке сетевых операций.

документация NetworkStream.Read подразумевает, что она неблокирующая («Если данные не доступны для чтения, метод Read возвращает 0»), поэтому я предполагаю, что мне не нужны асинхронные обратные вызовы для чтения ...верно?

А как насчет письма?Что ж, у Microsoft есть одно из обычных низкокачественных учебных пособий по этому вопросу, и они предлагают написать громоздкий AsyncCallback метод для каждой операции.Какой смысл кода внутри этого обратного вызова?

client.BeginSend(byteData, 0, byteData.Length, SocketFlags.None,
                 new AsyncCallback(SendCallback), client);
    ...
private static void SendCallback(IAsyncResult ar)
{
    try {
        Socket client = (Socket)ar.AsyncState;

        int bytesSent = client.EndSend(ar);

        Console.WriteLine("Sent {0} bytes to server.", bytesSent);
        // Signal that all bytes have been sent.

        sendDone.Set();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

В документации AsyncCallback говорится, что это «метод обратного вызова, который вызывается после завершения асинхронной операции».Если операция уже завершена, зачем нам звонить Socket.EndSend(IAsyncResult)?Между тем, документация NetworkStream.BeginWrite гласит: «Вы должны создать метод обратного вызова, который реализует делегат AsyncCallback и передать его имя методу BeginWrite. Как минимум, ваш параметр состояния должен содержатьNetworkStream. "

Но почему я" должен "?Разве я не могу просто хранить IAsyncResult где-нибудь ...

_sendOp = client.BeginSend(message, 0, message.Length, SocketFlags.None, null, null);

... и периодически проверять, закончен ли он?

if (_sendOp.IsCompleted)
    // start sending the next message in the queue

(я знаю, это подразумевает опрос, ноон будет находиться на сервере, который, как ожидается, будет активен большую часть времени, и я планирую несколько потоков. Спит, когда простаивает.)

Другая проблема.Сообщения, которые я отправляю, разбиты на массив заголовков и массив тел.Могу ли я выпустить два BeginWrites подряд?

IAsyncResult ar1 = _stream.BeginWrite(header, 0, header.Length, null, null);
IAsyncResult ar2 = _stream.BeginWrite(msgBody, 0, msgBody.Length, null, null);

Также приветствуются любые мнения о том, использовать ли Socket или NetworkStream.

Ответы [ 2 ]

3 голосов
/ 27 апреля 2011

У меня нет опыта работы с NetworkStream, но я могу сказать это об асинхронных Socket операциях:

  • End* необходимо вызывать после завершения операции, потому что это очищаетдо ресурсов ОС, используемых асинхронной операцией, и позволяет вам получить результат операции (даже если этот результат только, был ли он успешным или неудачным).MSDN имеет хороший обзор этого распространенного асинхронного шаблона.
  • Вы не обязаны передавать любой конкретный объект в качестве параметра состояние .Когда эта документация была написана, передача была самым простым способом сохранить объект в области действия для делегата завершения.В наши дни с лямбда-выражениями такая практика встречается не так часто.
  • Вы можете поставить в очередь записи в сокет, и в большинстве случаев это будет работать.Впрочем, возможно, что одна из записей завершится только частично (я никогда не видел, чтобы это произошло, но это теоретически возможно).Я делал это годами, но больше не рекомендую.

Я рекомендую использовать делегаты завершения вместо опроса.Если вам нужна более простая в использовании оболочка, вы можете попробовать Nito.Async.Sockets , который обернет операции Begin / End и сериализует их все через один поток (например, пользовательский интерфейс).

PS Если вы хотите остановить операцию блокировки, вы можете закрыть сокет из другого потока, что приведет к завершению операции с ошибкой.Однако я не рекомендую использовать блокировку сокетов.

1 голос
/ 27 апреля 2011

Документация NetworkStream.Read подразумевает, что он неблокируемый («Если данные не доступны для чтения, метод Read возвращает 0»), поэтому я предполагаю, что мне не нужны асинхронные обратные вызовы для чтения ..да?

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


Но как насчет записи?Ну, у Microsoft есть одно из обычных низкокачественных учебных пособий по этому вопросу, и они предлагают написать громоздкий метод AsyncCallback для каждой операции.Какой смысл кода внутри этого обратного вызова?

Смысл в том, чтобы не писать для себя еще более громоздкий код управления потоками.Поскольку многопоточность идет, AsyncCallback примерно так же прост, как он получает, хотя этот пример довольно глупый.В этом примере подпрограмма обратного вызова сигнализирует ManualResetEvent (sendDone.Set()), что будет одним из способов избежать опроса;у вас будет отдельный поток, ожидающий этого ManualResetEvent, который блокирует его до тех пор, пока что-то еще не вызовет его метод Set().Но подождите, разве не было главной целью использования AsyncCallback, чтобы избежать написания собственного управления потоками?Так зачем же нить, которая ждет на ManualResetEvent?Лучший пример показал бы, что вы на самом деле делаете что-то в вашем методе AsyncCallback, но имейте в виду, что если вы взаимодействуете с какими-либо элементами пользовательского интерфейса, такими как элементы управления Forms, вам нужно будет использовать метод Invoke элемента управления, а не управлять им напрямую.


Если операция уже завершена, зачем нам вызывать Socket.EndSend (IAsyncResult)?

С документация MSDN для Stream.BeginWrite Method :

Передать IAsyncResult, возвращенный текущим методом, в EndWrite, чтобы гарантировать, что запись завершает и освобождает ресурсы соответствующим образом.EndWrite должен вызываться один раз для каждого вызова BeginWrite.Вы можете сделать это либо с помощью того же кода, который называется BeginWrite, либо с помощью обратного вызова, переданного BeginWrite.Если во время асинхронной записи возникает ошибка, исключение не будет выдано, пока EndWrite не будет вызван с IAsyncResult, возвращенным этим методом.

Вы никогда не знаете, что произойдет с операциями ввода-вывода, иВы не можете выполнять обычную обработку ошибок с асинхронной операцией, потому что выполнение происходит в отдельном потоке в пуле потоков.Таким образом, смысл вызова EndWrite состоит в том, чтобы дать вам возможность обрабатывать любые ошибки, помещая EndWrite в блок try / catch внутри вашего метода AsyncCallback.


Не могу япросто храните где-нибудь IAsyncResult ... и периодически проверяйте, завершен ли он?

Можно, если вы хотите обработать все проблемы управления потоками и безопасности, чтобы гарантировать, что разные потоки никогда не будут пытаться обработатьто же самое IAsyncResult в то же время, но это кажется мне гораздо более сложной задачей, чем написание AsyncCallback.Опять же, смысл использования асинхронных методов - сделать так, чтобы вам приходилось как можно меньше заниматься проблемами потоков.


Другая проблема.Сообщения, которые я отправляю, разбиты на массив заголовков и массив тел.Могу ли я выпустить два BeginWrites подряд?

Хотя я никогда не пробовал этого, я не знаю, сработает ли это, и это действительно зависит от конкретной реализации класса NetworkStream.Возможно, 2-й запрос подождет, пока 1-й запрос не завершится, прежде чем попытаться выполнить, но у меня есть ощущение, что он либо сгенерирует исключение, либо вы получите поврежденные данные на другом конце.Если вы хотите отправить 2 отдельных потока данных одновременно, я думаю, что лучший способ сделать это - использовать 2 отдельных NetworkStreams.

...