TcpListener против SocketAsyncEventArgs

Есть ли веская причина не использовать TcpListener для реализации высокопроизводительного/высокопроизводительного сервера TCP вместо SocketAsyncEventArgs?

Я уже реализовал этот высокопроизводительный/высокопроизводительный TCP-сервер, используя SocketAsyncEventArgs, прошел через всевозможные головные боли при обработке этих закрепленных буферов, используя большой предварительно выделенный массив byte и пулы SocketAsyncEventArgs для приема и получения, собирая вместе с помощью некоторых низкоуровневых вещей. и блестящий интеллектуальный код с некоторым потоком данных TPL и некоторым Rx, и он работает отлично; почти учебник в этом начинании - на самом деле я узнал более 80% этих вещей из чужого кода.

Однако есть некоторые проблемы и опасения:

  1. Сложность: я не могу делегировать какие-либо изменения на этом сервере другому члену команды. Это привязывает меня к такого рода задачам, и я не могу уделять достаточно внимания другим частям других проектов.
  2. Использование памяти (закрепленные byte массивы): при использовании SocketAsyncEventArgs пулы должны быть предварительно выделены. Таким образом, для обработки 100000 одновременных подключений (хуже, даже на разных портах) там бесполезно зависает большая куча оперативной памяти; предварительно распределены (даже если эти условия выполняются только несколько раз, сервер должен быть в состоянии обрабатывать 1 или 2 таких пика каждый день).
  3. TcpListener на самом деле работает хорошо: я действительно протестировал TcpListener (с некоторыми уловками, такими как использование AcceptTcpClient в выделенном потоке, а не не версия async, а затем отправка принятых подключений в ConcurrentQueue и не создавать Task на месте и т.п.) и с последней версией .NET он работал очень хорошо, почти так же хорошо, как SocketAsyncEventArgs, без потери данных и с низким объемом памяти, что помогает не тратить слишком много ОЗУ на сервере и предварительное выделение не требуется.

Так почему же я нигде не вижу, чтобы TcpListener использовалось, а все (включая меня) используют SocketAsyncEventArgs? Я что-то упускаю?


person Kaveh Shahbazian    schedule 03.01.2015    source источник
comment
@usr Спасибо, вы правы, и, как я уже упоминал в пункте 3., я делаю именно то, что вы говорите! Прием происходит в простом цикле на выделенном Thread (Task создан с опцией TaskCreationOptions.LongRunning).   -  person Kaveh Shahbazian    schedule 03.01.2015
comment
Тогда я не понимаю, почему вы предлагаете выбор между SocketAsyncEventArgs и TcpListener. Почему бы не использовать оба? Слушатель выходит из игры, как только соединение было принято. На мой взгляд, это не имеет ничего общего с обработкой соединения.   -  person usr    schedule 03.01.2015
comment
SocketAsyncEventArgs приводит к закреплению буфера массива byte, поэтому не может быть эффективно собран мусор и вызывает фрагментацию памяти, что приводит к более высокому использованию ЦП и ОЗУ; плюс все дополнительные препятствия, которые необходимо преодолеть, чтобы подготовить и управлять пулами SocketAsyncEventArgs объектов; в целом требуется гораздо больше дополнительной работы и обслуживания, и, что более важно, эти знания/опыт не могут быть легко/безопасно/надежно переданы другому разработчику.   -  person Kaveh Shahbazian    schedule 03.01.2015
comment
Мне нужно разъяснение по этому поводу: этот вопрос вообще о TcpListener? Каким образом? Кажется, вы спрашиваете: могу ли я просто заменить SocketAsyncEventArgs обычным асинхронным вводом-выводом APM/TAP? Это вообще ничего не должно делать с TcpListener.   -  person usr    schedule 03.01.2015
comment
Как я уже говорил, TcpListener обеспечивает гораздо более простую модель программирования плюс те же характеристики производительности, что и SocketAsyncEventArgs. Но во всех проектах, которые я изучал (например, Fraction (F#), SocketAwaitable, SuperSocket и многих других примерах и сообщениях в блогах здесь и там), я не могу найти никого, кто использовал бы TcpListener. Теперь я спрашиваю, почему?   -  person Kaveh Shahbazian    schedule 03.01.2015
comment
Здесь нет ответов (но тем не менее обман), но для справки; stackoverflow.com/questions/21656077/   -  person Patrick    schedule 03.01.2015
comment
Потому что люди не знают, что они делают, когда дело доходит до кода сокета. Состояние примера кода и практики ужасное.   -  person usr    schedule 03.01.2015
comment
@ Патрик Я не защищаю этот вопрос, но этот вопрос просто спрашивает, какой из них использовать. На самом деле я использовал оба, провел стресс-тестирование обоих, сравнил их и высказал некоторые опасения; но если модераторы решили закрыть это, я просто принимаю их мнение.   -  person Kaveh Shahbazian    schedule 03.01.2015
comment
Я не предлагал закрыть это как дубликат, я просто разместил его для справки.. успокойтесь   -  person Patrick    schedule 03.01.2015


Ответы (1)


Я не вижу доказательств того, что этот вопрос вообще касается TcpListener. Кажется, вас интересует только код, который имеет дело с уже принятым соединением. Такое соединение не зависит от слушателя.

SocketAsyncEventArgs — это оптимизация загрузки процессора. Я убежден, что с его помощью вы сможете добиться более высокой скорости операций в секунду. Насколько существенна разница с обычным асинхронным вводом-выводом APM/TAP? Явно меньше, чем на порядок. Вероятно, между 1,2x и 3x. В прошлый раз, когда я сравнивал скорость транзакций loopback TCP, я обнаружил, что ядро ​​занимает около половины использования ЦП. Это означает, что ваше приложение может работать не более чем в 2 раза быстрее благодаря бесконечной оптимизации.

Помните, что SocketAsyncEventArgs был добавлен в BCL примерно в 2000 году, когда процессоры были гораздо менее производительными.

Используйте SocketAsyncEventArgs только тогда, когда у вас есть доказательства того, что вам это нужно. Это заставляет вас быть гораздо менее продуктивным. Больше возможностей для ошибок.

Вот шаблон, как должен выглядеть ваш цикл обработки сокета:

while (ConnectionEstablished()) {
 var someData = await ReadFromSocketAsync(socket);
 await ProcessDataAsync(someData);
}

Очень простой код. Никаких обратных вызовов благодаря await.


Если вас беспокоит фрагментация управляемой кучи: Выделите new byte[1024 * 1024] при запуске. Когда вы хотите прочитать из сокета, прочитайте один байт в некоторую свободную часть этого буфера. Когда это однобайтовое чтение завершается, вы спрашиваете, сколько байтов на самом деле есть (Socket.Available), и синхронно извлекаете остальные. Таким образом, вы закрепляете только один довольно небольшой буфер и все еще можете использовать асинхронный ввод-вывод для ожидания поступления данных.

Этот метод не требует опроса. Так как Socket.Available может только увеличиваться без чтения из сокета, мы не рискуем случайно выполнить слишком маленькое чтение.

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

Или, если вы не считаете это проблемой, вам не нужно ничего делать.

person usr    schedule 03.01.2015
comment
Я был с вами на 100%, пока вы не получили предложение использовать DataAvailable. Я не рекомендую это вообще; DataAvailable имеет классическую проблему гонки, поскольку теоретически может стать false между моментом, когда вы проверяете его, и временем, когда вы фактически пытаетесь прочитать (пока клиент никогда не видит ошибки, TCP может отбрасывать данные и принудительно отправлять повторно, однако маловероятно, что это может быть). ИМХО, гораздо лучше всегда считывать столько данных из сокета, сколько это практически возможно, и обрабатывать любую логику разделения после этого, а не пытаться использовать для этого сам сокет. - person Peter Duniho; 03.01.2015
comment
@PeterDuniho DataAvailable не может стать 0 (это целое число - оно неправильно названо), если вы не читаете, и оно было ненулевым. Повторная отправка TCP здесь неприменима. Единственная причина, по которой я рекомендую здесь DataAvailable, заключается в том, что вам не нужно блокировать весь буфер размером, скажем, 4 КБ для 100 КБ ожидающих операций чтения. Скорее, прочитайте один байт (заблокируйте крошечный буфер), а затем очень быстро очистите остальные, как только вы узнаете, что данные там. Если вам не нравится, вы можете просто не использовать DataAvailable здесь. Но вы рискуете, что действительно был получен один один байт, а 2-й прочитанный сейчас блокируется... Может быть, это просто нереально. - person usr; 03.01.2015
comment
Чувак; мой английский отстой :( - person Kaveh Shahbazian; 03.01.2015
comment
@KavehShahbazian, если у вас есть дополнительные вопросы, я с радостью отвечу на них. - person usr; 03.01.2015
comment
Спасибо @usr, я думаю, мне следует спросить в кодовой манере. Я позабочусь об этом. Спасибо; - person Kaveh Shahbazian; 03.01.2015
comment
@usr: DataAvailable — это логическое свойство класса NetworkStream и просто оболочка для свойства Socket.Available, которое на самом деле является int. Но обратите внимание на документы: Доступные данные — это общий объем данных, поставленных в очередь в сетевом буфере для чтения. Сетевой уровень не обязан хранить данные в своих буферах, если их получение уже не подтверждено. Я согласен, что наиболее вероятная реализация всегда будет подтверждать получение, как только данные будут помещены в буфер, но это не является обязательным, и я видел слишком много различий между платформами, чтобы рассчитывать на это предположение. YMMV. - person Peter Duniho; 04.01.2015
comment
Хорошо, я имел в виду Socket.Available. Кто знает, какому правильному свойству даны эти двусмысленные названия?! В любом случае, я почти уверен, что это данные, которые стабильно доступны для чтения. Почему это может быть по-другому? Местная сторона понятия не имеет, что происходит. Он может только сказать вам, что уже есть. Это Socket.Available. (Если вы не уверены, относитесь к моему ответу так, как будто этого Socket.Available трюка не было. Это не является существенным для любого аспекта этого ответа.) - person usr; 04.01.2015
comment
@PeterDuniho Я не разъяснял этого раньше: я считаю, что «Доступно» сообщает вам количество байтов, которые гарантированно будут доступны без блокировки, и я считаю, что это цель этого свойства. Я думаю, что хитрость в этом ответе - практически единственный вариант использования этого свойства. В 99% случаев это злоупотребление. - person usr; 04.01.2015
comment
@usr: предположим в качестве аргумента, что сетевой уровень никогда не отбрасывает данные. Available (оболочка для неуправляемого ioctlsocket(FIONREAD)) по-прежнему проблематична, так как требует опроса сокета и неизбежно использует сокет неэффективно, не считывая как можно больше данных. См., например. microsoft.public.platformsdk.networking.narkive.com/ jXh04uo8/. Это также решение не-проблемы; Я никогда не видел никаких проблем с закрепленными буферами, и если бы это было проблемой, можно было бы просто выделить буфер, достаточно большой для LOH, и распределять части по мере необходимости. - person Peter Duniho; 04.01.2015
comment
@usr: покопался в ресурсах, дополнительную информацию в этом ответе SO (включая то же предложение LOH, о котором я упоминал) и копия статьи Маллинза из интернет-архива (к сожалению, больше не в сети). Обратите внимание, что Маллинз не упоминает о том, что нужно беспокоиться о закрепленных буферах. И мощность ПК сейчас даже больше, чем когда была написана эта статья. Даже ответ Джерри, предлагающий сначала прочитать 1 байт, не использует Available. - person Peter Duniho; 04.01.2015
comment
@PeterDuniho Я не думаю, что у нас есть общее понимание того, что я предлагаю, потому что два ваших пункта неприменимы: число доступных возвратов никогда не может уменьшиться без чтения (вы верите в обратное? Почему?). Это дает нам минимальное значение для чтения. Мы не требуем читать все. Если вам это не нравится, просто используйте буфер фиксированного размера, например 4 КБ. На самом деле не имеет значения. Но оба варианта верны на 100%. Опрос не требуется (зачем? Ожидание чтения 1 байта.) - person usr; 04.01.2015
comment
Фрагментация кучи из-за закрепления — хорошо известная (я думаю) проблема. google.com/ Они пытались решайте эту проблему много раз с помощью новых функций GC. Да, есть и другие решения, например, использование небольших огромных буферов. Они также действительны. - person usr; 04.01.2015