Как мне правильно написать Socket / NetworkStream асинхронно?

#c# #.net #sockets #asynchronous #networkstream

#c# #.net #сокеты #асинхронный #networkstream

Вопрос:

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

Документация NetworkStream.Чтение подразумевает, что оно неблокирующее («Если данные недоступны для чтения, метод чтения возвращает 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.

Ответ №1:

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

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

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

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

Комментарии:

1. Я предполагаю, что этот ответ правильный, но я решил использовать другой подход: вызвать Send() вместо BeginSend() после установки socket. Блокировка = false. Единственной проблемой с этим является недокументированный факт, что если Send () не может отправить прямо сейчас, он не возвращает ноль, как вы могли бы подумать, а вместо этого выдает исключение SocketException с ErrorCode == 0x2733, «неблокирующая операция сокета не может быть завершена немедленно».

Ответ №2:

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

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


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

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


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

Из документации MSDN по Stream.BeginWrite методу:

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

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


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

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


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

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

Комментарии:

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

2. Тот факт, что метод Read() возвращает количество прочитанных байтов и возвращает немедленно, если данные недоступны, предполагает, что он не останавливается и не ожидает поступления данных из сети. Однако разочаровывает то, что документация конкретно не гарантирует неблокирующее поведение.