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