Подтвердить что ты не робот

Использование IOCP с UDP?

Я хорошо знаком с тем, что Порты ввода/вывода относятся к TCP.

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

Скажем, я нацелен на производительность, которая будет применяться к игре MMOFPS, которая позволяет встретить сотни игроков в одном, постоянном мире, и помимо борьбы с оружием, это позволяет им общаться через сообщения чата и т.д. - что-то вроде это действительно существует и работает хорошо - проверьте PlanetSide 2.

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

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

Или, может быть, я просто делаю еще одну преждевременную оптимизацию, и на данный момент не требуется никакого размышления?

Мысль о размещении на gamedev.stackexchange.com, но этот вопрос лучше подходит для сетей общего назначения, я думаю.

4b9b3361

Ответ 1

Я не рекомендую использовать это, но технически наиболее эффективным способом получения UDP-дейтаграмм было бы просто заблокировать в recvfrom (или WSARecvFrom, если хотите). Конечно, для этого вам понадобится выделенный поток, иначе во время блокировки не произойдет ничего.

Кроме TCP, у вас нет соединения, встроенного в протокол, и у вас нет потока без определенных границ. Это означает, что вы получаете адрес отправителя с каждой дейтаграммой, которая приходит, и вы получаете цельное сообщение или ничего. Всегда. Никаких исключений.
Теперь блокировка на recvfrom означает один контекстный переключатель в ядро, а один контекстный переключатель обратно, когда что-то было получено. Это не будет происходить быстрее, если в полете будет отображаться несколько перекрывающихся прочтений, поскольку только одна датаграмма может поступать на провод одновременно, что на сегодняшний день является самым ограничивающим фактором (время процессора не является узким местом!). Использование IOCP означает как минимум 4 переключателя контекста, два для приема и два для уведомления. В качестве альтернативы, перекрывающийся прием с обратным вызовом завершения не намного лучше, потому что вы должны NtTestAlert или SleepEx запускать очередь APC, поэтому снова у вас есть как минимум 2 дополнительных контекстных переключателя (хотя это всего лишь +2 для всех уведомлений вместе, и вы можете случайно уже спать в любом случае).

Однако:
Использование IOCP и перекрывающихся чтений, тем не менее, лучший способ сделать это, даже если он не самый эффективный. Завершающие порты не зависят от использования TCP, они отлично работают с UDP. До тех пор, пока вы используете перекрываемое чтение, не имеет значения, какой протокол вы используете (или даже его сеть или диск, или какой-либо другой ожидаемый или предупреждающий объект ядра).
Это также не имеет особого значения ни для латентности, ни для загрузки процессора, если вы сжигаете несколько сотен циклов для порта завершения. Здесь мы говорим о "нано" и "милли", что составляет один-один миллион. С другой стороны, порты завершения в целом - очень удобная, звуковая и эффективная система.

Вы можете, например, тривиально реализовать логику для повторной отправки, когда вы не получили ACK вовремя (что вы должны делать, когда желательна форма надежности, UDP не делает этого для вас), а также keepalive.
Для keepalive добавьте ожидаемый таймер (возможно, после 15 или 20 секунд), когда вы reset каждый раз, когда получаете что-либо. Если ваш порт завершения сообщает вам, что этот таймер вышел, вы знаете, что соединение мертво.
Для повторных попыток вы можете, например, установите тайм-аут на GetQueuedCompletionStatus, и каждый раз, когда вы просыпаетесь, найдите все пакеты, которые больше, чем такие-то старые, и еще не были ACKed.
Вся логика происходит в одном месте, что очень приятно. Это универсальный, эффективный и трудно сделать неправильно.

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

Порт завершения просыпается до N потоков в порядке "последний-в-первом-порядке", N - количество ядер, если вы не скажете ему сделать что-то другое. Если какой-либо из этих потоков блокируется, другой разбуждается для обработки выдающихся событий. Это означает, что в худшем случае дополнительный поток может работать в течение короткого времени, но это допустимо. В среднем случае он поддерживает использование процессора почти на 100%, пока есть какая-то работа и нуль в противном случае, что очень приятно. Пробуждение LIFO выгодно для кэшей процессора и не позволяет переключать контексты потоков на низком уровне.

Это означает, что вы можете блокировать и ждать входящую дейтаграмму и обрабатывать ее (дешифровать, распаковывать, выполнять логику, читать сокеты с диска и т.д.), а другой поток будет немедленно готов к обработке следующей дейтаграммы, которая может появиться в следующем микросекунды. Вы также можете использовать перекрывающийся диск с одним и тем же портом завершения. Если вы выполняете работу по вычислению (например, AI), чтобы сделать это, можно разделить на задачи, вы можете вручную отправить (PostQueuedCompletionStatus) те, что находятся на порт завершения, и у вас есть параллельный планировщик задач бесплатно. Все, что вам нужно сделать, это обернуть OVERLAPPED в структуру, у которой есть дополнительные данные после нее, и использовать ключ, который вы узнаете. Не волнуйтесь о синхронизации потоков, он просто волшебным образом работает (вам даже не нужно иметь OVERLAPPED в своей пользовательской структуре при отправке собственных уведомлений, он будет работать с любой структурой, которую вы проходите, но мне не нравится лгать к операционной системе, вы никогда не знаете...).

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

О TCP, вызывающем потерю пакетов в UDP, это то, что я склонен назвать городским мифом (хотя это несколько правильно). То, как формулируется эта общая мантра, однако вводит в заблуждение. Возможно, это когда-то было правдой (существует исследование по этому вопросу, которое, однако, близко к десятилетнему давнему), что маршрутизаторы откажутся от UDP в пользу TCP, тем самым вызывая потерю пакетов. То есть, конечно, это не так. Более правдивая точка зрения заключается в том, что все, что вы отправляете, вызывает потерю пакетов. TCP индуцирует потерю пакетов в TCP и UDP вызывает потерю пакетов на TCP и наоборот, это нормальное условие (как TCP реализует контроль перегрузки, кстати). Маршрутизатор, как правило, пересылает один входящий пакет, если кабель на другом штепселе "неактивен", он будет стоять в очереди на несколько пакетов с жестким сроком (буферы часто преднамеренно малы), возможно, он может применять некоторую форму QoS, и он будет просто и молча бросить все остальное.
Многие приложения с довольно жесткими требованиями в реальном времени (VoIP, потоковое видео, вы называете это) в настоящее время используют UDP, и, хотя они хорошо справляются с потерянным пакетом или двумя, они совсем не похожи на значительную повторяющуюся потерю пакетов. Тем не менее, они явно работают в сетях с большим количеством трафика TCP. Мой телефон (например, телефоны миллионов людей) работает исключительно над VoIP, данные идут по тому же маршрутизатору, что и интернет-трафик. Я не могу спровоцировать отказ от TCP, как бы я ни старался.
Из этого повседневного наблюдения можно с уверенностью сказать, что UDP окончательно не отбрасывается в пользу TCP. Во всяком случае, QoS может поддержать UDP через TCP, но это, безусловно, не отменяет его.
В противном случае службы, такие как VoIP, будут заикаться, как только вы откроете веб-сайт и будете недоступны, если вы загрузите что-то размером файла ISO DVD.

EDIT:
Чтобы дать некоторое представление о том, как простая жизнь с IOCP может быть (несколько урезана, функции полезности отсутствуют):

for(;;)
{
    if(GetQueuedCompletionStatus(iocp, &n, &k, (OVERLAPPED**)&o, 100) == 0)
    {
        if(o == 0) // ---> timeout, mark and sweep
        {
            CheckAndResendMarkedDgrams();  // resend those from last pass
            MarkUnackedDgrams();           // mark new ones
        } 
        else
        {   // zero return value but lpOverlapped is not null:
            // this means an error occurred
            HandleError(k, o);
        }
        continue;
    }

    if(n == 0 && k == 0 && o == 0)
    {
        // zero size and zero handle is my termination message
        // re-post, then break, so all threads on the IOCP will
        // one by one wake up and exit in a controlled manner
        PostQueuedCompletionStatus(iocp, 0, 0, 0);
        break;
    }
    else if(n == -1) // my magic value for "execute user task"
    {
        TaskStruct *t = (TaskStruct*)o;
        t->funcptr(t->arg);
    }
    else
    {
        /* received data or finished file I/O, do whatever you do */
    }
}

Обратите внимание на то, как вся логика для сообщений о завершении обработки, пользовательских задач и управления потоком происходит в одном простом цикле, без скрытого материала, без сложных путей, каждый поток выполняет этот же идентичный цикл. Тот же код работает для 1 потока, обслуживающего 1 сокет, или для 16 потоков из пула из 50, обслуживающих 5000 сокетов, 10 перекрывающихся передач файлов и выполнения параллельных вычислений.

Ответ 2

Я видел код для многих игр FPS, которые используют UDP в качестве сетевого протокола.

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

Обычно пакет UDP содержит позиции и возможности для каждого объекта рядом с игроком, любые отправленные сообщения чата и все последние изменения состояния. (например, новый объект создан, сущность разрушена и т.д.)

Затем клиент прослушивает UDP-пакеты. Он будет использовать только пакет с самым высоким номером кадра. Поэтому, если появляются пакеты с порядком, старшие пакеты просто игнорируются.

Любые пакеты с неправильными контрольными суммами также игнорируются.

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

Сообщения чата отправляются повторно по нескольким пакетам, и каждое сообщение имеет уникальный идентификатор сообщения. Например, вы повторно передаете одно и то же сообщение чата, скажем, полную стоимость кадров. Если клиент пропустил сообщение чата после получения его 60 раз - тогда качество сетевого канала слишком низкое, чтобы играть в игру. Клиенты будут отображать любые сообщения, которые они получают в UDP-пакете, у которых есть идентификатор сообщения, который они еще не отображали.

Аналогично для создаваемых или уничтожаемых объектов. Все созданные или уничтоженные объекты имеют уникальный идентификатор объекта, заданный сервером. Объекты создаются или уничтожаются, если ранее идентификатор объекта, к которому они относятся, ранее не выполнялся.

Итак, ключом здесь является избыточность отправки данных и переключение всех переходов состояний на уникальный идентификатор, заданный сервером.

@edit: еще один плакат сказал, что для сообщений чата вы можете использовать другой протокол на другом порту. И они могут быть правы в том, что, вероятно, оптимальны. То есть для типов сообщений, где латентность не является критичной, но надежность важнее, вы можете открыть другой порт и использовать TCP. Но я оставлю это в качестве более позднего упражнения. Это, конечно, проще и чище сначала для вашей игры использовать только один канал, а позже выяснять капризы нескольких портов, несколько каналов, с их различными режимами сбоев. (например, что происходит, если канал UDP работает, но канал чата идет вниз? Что делать, если вам удастся открыть один порт, а не другой?)

Ответ 3

Когда я сделал это для клиента, мы использовали ENet как базовый надежный протокол UDP и повторно реализовал это с нуля, чтобы использовать IOCP для серверной части, используя свободно доступный код ENet для клиентской стороны.

IOCP отлично работает с UDP и прекрасно интегрируется с любыми TCP-соединениями, которые вы также можете обрабатывать (у нас есть TCP, WebSocket или UDP-клиентские соединения и TCP-соединения между узлами сервера и возможность подключить их все в один поток пул, если мы хотим, удобен).

Если абсолютная латентность и скорость обработки пакетов UDP являются наиболее важными (и маловероятно, что это действительно так), то использование нового API RIO Server 2012 может стоить того, но я еще не убежден (см. здесь для некоторых предварительных тестов производительности и некоторых серверов примеров).

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

Ответ 4

Несколько вещей:

1) Как правило, если вам нужна надежность, вам лучше всего использовать TCP. Возможно конкурентное и, возможно, даже превосходное решение поверх UDP, но очень сложно получить право и выполнить его правильно. Главное, чтобы люди, внедряющие надежность поверх UDP, не беспокоились о правильном управлении потоком. У вас должно быть управление потоком, если вы намереваетесь отправлять большие объемы данных и хотите, чтобы он изящно воспользовался имеющейся на данный момент пропускной способностью (которая постоянно меняется с условиями маршрута). На практике реализация любого другого, кроме, по сути, одного и того же алгоритма использования TCP, скорее всего, будет недружелюбным к другим протоколам в сети. Вряд ли вы будете лучше выполнять этот алгоритм, чем TCP.

2) Что касается одновременного запуска TCP и UDP, в наши дни это не так много, как отмечали другие. Когда-то я слышал, что перегруженные маршрутизаторы по пути были смещением, снижающим UDP-пакеты перед пакетами TCP, что имеет смысл в некоторых отношениях, так как упавший TCP-пакет просто будет resent в любом случае, а потерянный UDP-пакет часто не является. Тем не менее, я скептически отношусь к тому, что это происходит на самом деле. В частности, удаление TCP-пакета приведет к тому, что отправитель закроется обратно, поэтому может возникнуть больше смысла отказаться от пакета TCP.

Один случай, когда TCP может мешать UDP, заключается в том, что TCP по своей природе алгоритм постоянно пытается идти все быстрее и быстрее, если только он не достигает точки, где он теряет пакеты, затем он дросселирует назад и повторяет процесс. Поскольку TCP-соединение постоянно сталкивается с этим потолком пропускной способности, оно также может привести к потере UDP как потери TCP, что теоретически будет выглядеть так, как если бы трафик TCP спорадически вызывал потерю UDP.

Однако это проблема, с которой вы столкнетесь, даже если вы установите свой собственный надежный механизм поверх UDP (при условии, что вы правильно управляете потоком). Если вы хотите избежать этого условия, вы можете намеренно дросселировать надежные данные на уровне приложения. Как правило, в игре надежная скорость передачи данных ограничена скоростью, с которой клиент или сервер действительно должен отправлять надежные данные, что часто значительно ниже пропускной способности канала, и, таким образом, интерференция никогда не возникает, независимо от того, На основе протокола TCP или UDP.

Если ситуация становится немного сложнее, если вы делаете игру с потоковыми активами. Для игры, такой как FreeRealms, которая делает это, активы загружаются из CDN через HTTP/TCP и будут пытаться использовать всю доступную полосу пропускания, что увеличит потерю пакетов на основном игровом канале (который обычно является UDP). Я обычно обнаружил, что вмешательство достаточно низкое, и я не думаю, что вы должны слишком беспокоиться об этом.

3) Что касается IOCP, то мой опыт работы с ними очень ограничен, но в прошлом я занимался обширной сетевой игрой, я скептически отношусь к тому, что они повышают ценность в случае UDP. Обычно сервер будет иметь один UDP-сокет, который обрабатывает все входящие данные. С сотнями подключенных пользователей скорость, с которой данные поступают на сервер, очень высока. Наличие фонового потока, выполняющего блокирующий вызов в сокете, как предлагали другие, а затем быстро перемещающие данные в очередь для основного потока приложений, чтобы найти это разумное решение, но несколько ненужное, поскольку на практике данные поступают так быстро, когда под нагрузкой нет особого смысла в том, что когда-либо спал поток, когда он блокируется.

Позвольте мне поместить это другим способом, если вызов блокирующего сокета опрашивал один пакет, а затем помещал поток в режим ожидания до тех пор, пока не поступит следующий пакет, он будет переключать контекст на этот поток тысячи раз в секунду, когда данные ставка была высокой. Либо это, либо к тому моменту, когда незаблокированный поток выполнил и очистил данные, уже были готовы дополнительные данные, готовые к обработке. Вместо этого я предпочитаю вставлять сокет в неблокирующий режим, а затем использовать фоновый поток со скоростью около 100 кадров в секунду, обрабатывая его (спящий между опросами по мере необходимости для достижения частоты кадров). Таким образом, бункер сокетов будет наращивать входящие пакеты на 10 мс, а затем фоновый поток будет разбуживаться один раз и обрабатывать все эти данные навалом, а затем возвращаться спать, тем самым предотвращая бесплатные контекстные переключатели. Тогда у меня тот же фоновый поток выполняет другую обработку, связанную с отправкой, когда она просыпается. Быть полностью управляемым событиями потеряет многие из преимуществ, когда объем данных достигает наименьшего бита.

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

Итак, в случае UDP с самодельным UDP-надежным механизмом поверх него я обычно имею фоновый поток, играющий ту же роль, что и ОС... тогда как ОС получает данные из сети а затем распространяет его на различные логические TCP-соединения для обработки, мой фоновый поток получает данные из одиночного UDP-сокета (посредством периодического опроса) и распространяет его на мои собственные внутренние логические объекты соединения для обработки. Затем эти внутренние логические соединения помещают пакетные данные на уровне приложения в потокобезопасную мастер-очередь, помеченную логическим соединением, из которого они были получены. Затем основной поток приложения обрабатывает эту мастер-очередь, маршрутизируя пакеты непосредственно на объекты уровня игры, связанные с этим соединением. С точки зрения основных приложений приложения, он просто имеет очередь, управляемую событиями, которая обрабатывает.

Суть в том, что при условии, что вызов опроса в одиночном гнезде UDP редко появляется пустым, трудно представить, что будет более эффективный способ решить эту проблему. Единственное, что вы теряете с помощью этого метода, - это подождать до 10 мс, чтобы проснуться, когда теоретически вы можете проснуться в тот момент, когда данные впервые появились, но это имеет смысл только в том случае, если вы были в очень легкой нагрузке. Кроме того, основной поток приложения не будет не будет использование данных до следующего цикл кадра в любом случае, так что разница является спорной, и я думаю, что общая производительность системы повышается с помощью этого метода.

Ответ 5

Я бы не стал играть так же стара, как PlanetSide, как образец современной сетевой реализации. Особенно не увидели внутренности своей сетевой библиотеки.:)

Различные типы связи требуют разных методологий. Один из ответов выше говорит о различиях между обновлениями кадров/позиций и сообщениями чата, не признавая, что использование одного и того же транспорта для обоих, вероятно, является глупым. Вы должны определенно использовать подключенный TCP-сокет между вашей реализацией чата и чат-сервером для текстового чата. Не спорьте, просто сделайте это.

Итак, для вашего игрового клиента, выполняющего обновления через поступающие UDP-пакеты, наиболее эффективный путь от сетевого адаптера через ядро ​​и в ваше приложение (скорее всего) будет блокировкой recv. Создайте поток, который вырывает пакеты из сети, проверяет их достоверность (совпадение chksum, увеличение номера последовательности, любые другие проверки, которые у вас есть), де-сериализует данные во внутренний объект, а затем помещает объект во внутреннюю очередь в поток приложения который обрабатывает те виды обновлений.

Но не верьте мне на слово: протестируйте! Напишите небольшую программу, которая может получать и десериализовать 3 или 4 вида пакетов, используя блокирующий поток и очередь для доставки объектов, а затем переписывать их с помощью одного потока и IOCP с десериализацией и очередью в процедуре завершения. Паунд достаточно пакетов через него, чтобы получить время выполнения в минутном диапазоне и проверить, какой из них самый быстрый. Убедитесь, что что-то (то есть какой-то поток) в вашем тестовом приложении потребляет объекты из очереди, чтобы получить полную картину относительной производительности.

Отправляйтесь сюда, когда у вас есть две тестовые программы, и сообщите нам, какая из них была лучшей, mm'kay? Это был самый быстрый, который вы бы предпочли сохранить в будущем, что заняло самое длинное, чтобы заставить его работать и т.д.

Ответ 6

Если вы хотите поддерживать много одновременных подключений, вам необходимо использовать сетевой подход, основанный на событиях. Я знаю две хорошие библиотеки: libev (используется nodeJS) и libevent. Они очень портативны и просты в использовании. Я успешно использовал libevent в приложении, поддерживающем сотни параллельных соединений TCP/UDP (DNS).

Я считаю, что использование управляемых событиями сетевых подключений не является преждевременной оптимизацией на сервере - это должен быть шаблон дизайна по умолчанию. Если вы хотите выполнить быструю прототипную реализацию, лучше начать с языка более высокого уровня. Для JavaScript есть nodeJS, а для Python есть Twisted. И я лично рекомендую.

Ответ 7

Как насчет NodeJS Он поддерживает UDP и обладает высокой масштабируемостью.