Какое обходное решение для подтверждения с задержкой TCP?

19

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

Во время расследования я обнаружил, что время ожидания только неожиданно велико для клиентов, работающих под управлением Microsoft Windows (в отличие от клиентов Mac OS X ). Кроме того, я обнаружил, что если клиент Windows устанавливает TcpAckFrequency=1 в реестре и перезагружает их машину, их латентность становится нормальной.

Похоже, что мой дизайн сети не учитывал задержанное подтверждение:

  

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

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

  
  • Если второй пакет данных принят до истечения таймера задержки (200 мс), отправляется ACK.
  •   
  • Если есть данные, которые должны быть отправлены в том же направлении, что и ACK до того, как будет принят второй пакет данных и истечет таймер задержки,   ACK связывается с сегментом данных и отправляется немедленно.
  •   
  • Когда таймер задержки истекает (200 мс), отправляется ACK.
  •   

( Ссылка )

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

  1. Отключите алгоритм Nagle (TCP_NODELAY).
  2. Отключите буфер отправки сокета ( SO_SNDBUF = 0), чтобы можно было ожидать, что вызов send отправит пакет.
  3. При вызове send , если дальнейшие данные не будут отправлены немедленно, вызовите send снова с однобайтовым количеством данных, которое будет отброшено получателем.

При таком подходе второй пакет данных будет приниматься приемником примерно в то же время, что и предыдущий пакет данных. В результате ACK должно немедленно отправляться от получателя к отправителю (эмулируя, что TcpAckFrequency=1 делает в реестре).

Тем не менее, из моего тестирования это улучшенная задержка составляет лишь примерно половину того, что делает редактирование реестра. Что мне не хватает?

В: Почему бы не использовать UDP?

A: Я выбрал TCP, потому что каждый отправляемый пакет должен прибыть (и быть в порядке); нет пакетов, которые не требуют повторной передачи, если они теряются (или становятся неупорядоченными). Только когда пакеты могут быть отброшены / неупорядочены, UDP может быть быстрее TCP!

    
задан Mr. Smith 22.03.2014 в 22:40
источник
  • Если вы заботитесь о задержке, лучшим вариантом будет протокол на основе протокола UDP. Существуют высокоуровневые библиотеки UDP, которые обеспечивают надежный транспорт. –  CodesInChaos 22.03.2014 в 22:43
  • Вы говорите об играх, но большинство игр полагаются на UDP. Вы исследовали, как они это делают? Я предполагаю, что вопрос в том, для чего это приложение, и предпочитаете ли вы надежность по скорости (к сожалению, вам нужно предпочесть один за другим). Если это ориентированное на реальное время / действие, я бы использовал UDP с каким-то прогнозом и коррекцией клиент / сервер. Если нет, то задержка в 200 мс не должна быть проблемой –  Brendan 22.03.2014 в 22:51
  • @nos Они абсолютно подходят для некоторых приложений (например, игр). Я не знаю, почему ты так думал. –  Mr. Smith 23.03.2014 в 01:59
  • Как отметил Брендан, игры, как правило, используют UDP для обеспечения минимальной задержки. Посмотрите документацию о том, как работает протокол Quake 3 - это действительно очень простая концепция, которая, скорее всего, будет соответствовать вашему варианту использования. Надеюсь, кто-то ответит на ваш реальный вопрос, хотя, потому что это интересно! –  Cory Nelson 23.03.2014 в 09:56
  • Почему задержка вызвала проблему с задержкой? send не дожидается ack до тех пор, пока в буфере отправки еще есть место (которое он может сделать, потому что отправка не обещает, что было / будет получено). И когда окно TCP будет заполнено, я ожидаю, что Ack будет отправлен немедленно. Можете ли вы подробно описать шаги, которые приведут к задержке? –  usr 23.03.2014 в 10:54
Показать остальные комментарии

7 ответов

14

Начиная с Windows Vista параметр TCP_NODELAY должен быть установлен до вызова connect или (на сервере) до вызова listen . Если вы установите TCP_NODELAY после вызова connect , это не будет фактически отключить алгоритм Nagle, но GetSocketOption укажет, что Nagle отключен! Все это, как представляется, недокументировано и противоречит тому, чему учат многие учебники / статьи по этому вопросу.

Когда Nagle фактически отключен, отложенные подтверждения TCP перестают вызывать задержку.

    
ответ дан Mr. Smith 16.09.2014 в 16:19
2

Вам нечего делать. Все обходные пути, которые вы предлагаете, - это помочь протоколам, которые не были должным образом разработаны для работы через TCP. Предположительно, ваш протокол был разработан для работы через TCP, правильно?

Ваша проблема почти наверняка одна или обе из них:

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

  2. Вы не реализовали подтверждения приложений на уровне приложений приложений. Реализуйте их так, чтобы ACK могли копировать их.

ответ дан David Schwartz 23.03.2014 в 10:45
  • Пожалуйста, объясните нижний предел. Если я неясен, я хотел бы знать, что уточнить. Если я ошибаюсь, я бы хотел знать, почему. –  David Schwartz 23.03.2014 в 10:48
  • Ни один из них; когда я отправляю пакеты, они находятся в очереди в байтовом буфере и отправляются, когда поток сокетов подбирает их (на заданной частоте), 50 маленьких пакетов станут 1 большим пакетом, если отправлено в течение достаточно короткого промежутка времени (только сокет-потоки звонят в сокете). И «однобайтовые данные» описываю функции как # 2, что должно привести к тому, что приемник будет ACK. –  Mr. Smith 23.03.2014 в 10:53
  • Ваши однобайтовые данные идут не в том направлении. Вам нужно, чтобы другой конец подтвердил единицы данных уровня приложения, чтобы пакет данных для его ACK был включен. Ваш дополнительный вызов для отправки просто усугубляет ситуацию - небольшая отправка - это триггер для Nagling, что делает задержку ACK намного хуже. –  David Schwartz 23.03.2014 в 11:01
  • Оба используют описанные мной шаги; сервер отправляет однобайтовый адрес после отправки, а клиент отправляет однобайтовый адрес после отправки. Для этого необходимо работать. –  Mr. Smith 23.03.2014 в 11:05
  • Вы не замечаете того факта, что эффективность не всегда переводится в малую задержку. На самом деле очень мало пакетов, которые действительно отправляются, и чаще всего они не могут быть сгруппированы (шаблон чтения-записи-чтения-записи). –  Mr. Smith 24.03.2014 в 04:53
Показать остальные комментарии
1
  

При таком подходе второй пакет данных будет принят   приемник примерно в то же время, что и предыдущий пакет данных. Как   результат, ACK должен немедленно отправляться от приемника к   отправителя (эмулируя то, что TcpAckFrequency = 1 делает в реестре).

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

Одна идея: вместо вашего «канарейного» пакета, являющегося только одним байтом, отправьте полную стоимость данных MSS (как правило, что, 1460 байт в сети 1500 MTU).

    
ответ дан antiduh 13.07.2014 в 21:54
0

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

это следующая библиотека, которую я использую для надежных переносов UDP:

Ссылка

    
ответ дан user3239282 24.03.2014 в 11:56
0

Чтобы решить проблему, необходимо понять нормальное функционирование TCP-соединений. Telnet - хороший пример для анализа.

TCP гарантирует доставку, подтверждая успешную передачу данных. «Ack» может быть отправлен как сообщение сам по себе, но это вводит довольно некоторые накладные расходы - Ack - очень маленькое сообщение, но протоколы более низкого уровня добавляют дополнительные заголовки. По этой причине TCP предпочитает копировать сообщение Ack в другой пакет, который он отправляет в любом случае. Глядя на интерактивную оболочку через Telnet, есть постоянный поток нажатий клавиш и ответов. И если есть небольшая пауза при наборе текста, на экране нечего эха. Единственный случай, когда поток останавливается, - если у вас есть выход без соответствующего ввода. Но так как вы можете читать только так быстро, нормально ждать несколько сотен миллисекунд, чтобы увидеть, есть ли нажатие клавиши, чтобы включить Ack.

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

Вернуться к протоколу: у вас, по-видимому, нет протокола запроса / ответа. Это означает, что Ack не может поддерживаться копией (проблема 1). И в то время как принимающая ОС будет отправлять отдельные Acks, это не будет спамом.

Ваше обходное решение через TCP_NODELAY и два пакета на стороне отправки (Windows) предполагает, что принимающая сторона также является Windows или, по крайней мере, ведет себя как таковая. Это желаемое за действительное, а не инженерное . Другая ОС может решить дождаться three пакетов для отправки Ack, что полностью нарушает ваше использование TCP_NODELAY , чтобы принудительно добавить дополнительный один пакет. «Подождите 3 пакета» - это всего лишь пример; существует много других правильных алгоритмов для предотвращения спама Ack, который не будет обманут вашим вторым однобайтным фиктивным пакетом.

Какое реальное решение? Отправьте ответ на уровне протокола. Вне зависимости от ОС, он будет копировать TCP Ack в ответ на ваш протокол. В свою очередь, этот ответ также заставит Ack в другом направлении (ответ тоже является TCP-сообщением), но вы не заботитесь о латентности ответа. Ответ есть только для того, чтобы принимающая ОС копировала первый Ack.

    
ответ дан MSalters 04.11.2015 в 14:43
-1

Я предлагаю вам оставить alogithm Nagle и буферы включенными, поскольку его основная цель - собрать мелкие записи в пакеты с полным / большим размером (это значительно повышает производительность), но в то же время используйте FlushFileBuffers () в сокете после того, как вы сделали отправку на некоторое время .

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

while(run_my_game)
{
    process_game_events_and_send_data_over_network();
    Sleep(20 - time_spent_processing);
};

Теперь я предлагаю вставить FlushFileBuffers () перед вызовом Sleep ():

while(run_my_game)
{
    process_game_events_and_send_data_over_network();
    FlushFileBuffers(my_socket);
    Sleep(20 - time_spent_processing);
};

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

Если это не сработает, было бы полезно, если бы вы разместили немного (псевдо) код, который объясняет, как работает ваша программа.

EDIT: Еще две вещи, которые приходили мне в голову, когда я снова думал о твоем вопросе:

a) Задержанные ACK-пакеты действительно должны NOT вызывать какое-либо отставание, поскольку они перемещаются в противоположном направлении передаваемых данных. В худшем случае они блокируют очередь отправки. Это, однако, будет разрешено TCP после нескольких пакетов, когда это разрешает полоса пропускания связи и памяти. Поэтому, если у вас нет действительно большой ОЗУ (недостаточно для хранения большей очереди сообщений), или вы действительно трассируете больше данных, чем позволяет ваше соединение, тогда отложенные паки ACK являются оптимизацией и фактически повышают производительность.

b) Вы используете выделенный поток для отправки. Интересно, почему. AFAIK является многопоточным безопасным Socket API, поэтому каждый продуктный поток может отправлять данные сам по себе - если ваше приложение не требует такой очереди, я бы предложил также удалить этот выделенный поток отправки и с ним дополнительные служебные данные синхронизации и задержать его может вызвать.

Я специально упоминаю о задержке здесь. Поскольку операционная система может решить не сразу назначить поток отправки для выполненияg еще раз, когда он будет разблокирован в своей очереди. Типичные задержки повторного планирования в диапазоне 10 мс, но под нагрузкой они могут увеличиться до 50 мс или более. В качестве рабочего стола вы можете попробовать fiddeling с приоритетами планирования. Но это не уменьшит задержку, налагаемую самой операционной системой.

Btw. вы можете легко тестировать TCP и свою сеть, просто имея один поток на клиенте и один на сервере, который просто воспроизводит пинг / понг с некоторыми данными.

    
ответ дан SIGSEGV 13.07.2014 в 17:49
-2
  

каждый отправляемый пакет должен прибыть (и быть в порядке);

Это требование является причиной задержки.

Либо у вас есть сеть с незначительной потерей пакетов, в которой UDP будет доставлять каждый пакет, либо у вас есть потеря, в которой TCP выполняет повторную передачу, задерживая все (кратным) интервал повторной передачи (который, по крайней мере, время в оба конца). Эта задержка не является последовательной, поскольку она инициируется потерянными пакетами; джиттер обычно имеет худшие последствия, чем предсказуемая задержка в подтверждении, вызванная комбинацией пакетов

  

Только когда пакеты могут быть отброшены / неупорядочены, UDP может быть быстрее TCP!

Это легкое предположение, но ошибочное.

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

    
ответ дан Ben Voigt 13.07.2014 в 22:59
  • Причина для downvote? Вы не должны понижать информацию только потому, что вам это не нравится. Если вы считаете, что что-то не так, предложите исправление в качестве комментария. –  Ben Voigt 15.09.2014 в 16:56
  • Алгоритм Нэгл не был отключен - это и стало причиной задержки. Не стоит иметь в виду, если вы отключите подключение до / после сокета Nagle, но, видимо, это было изменено в Windows с выпуском Vista. На самом деле не существует способа узнать, так как исключение не генерируется или возвращается ошибка, и GetSocketOption возвращает, что Nagle был отключен, даже если на самом деле этого не было. –  Mr. Smith 17.09.2014 в 04:10
  • @ Mr.Smith: Отключение Nagle не будет устранять латентность, вызванную падением пакетов. Мой ответ остается верным. –  Ben Voigt 17.09.2014 в 14:37
  • Это не ответ на мой вопрос; пожалуйста, перечитайте мой вопрос насквозь. Ответ на более высокую, чем ожидалось, задержку - это Nagle, который неправильно отключается. Это то, что делают другие игры TCP в жанре, что я не был. –  Mr. Smith 17.09.2014 в 14:52
  • @ Mr.Smith: Возможно, это на одном клиенте. С другой стороны, это потеря пакетов в сети. Поскольку ваше приложение чувствительно к задержке, вы не должны использовать TCP. –  Ben Voigt 17.09.2014 в 14:55