Что лучше? Много маленьких пакетов TCP или один длинный? [закрыто]

16

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

В настоящее время я отправляю данные о местоположении следующим образом:

sendToClient((("UID:" + cl.uid +";x:" + cl.x)));
sendToClient((("UID:" + cl.uid +";y:" + cl.y)));
sendToClient((("UID:" + cl.uid +";z:" + cl.z)));

Очевидно, что он отправляет соответствующие значения X, Y и Z.

Будет ли эффективнее отправлять такие данные?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));
joehot200
источник
2
Потеря пакета обычно составляет менее 5%, в моем ограниченном опыте.
Mucaho
5
Действительно ли sendToClient отправляет пакет? Если да, то как ты это сделал?
user253751
1
@mucaho Я никогда не измерял это сам или что-то еще, но я удивлен, что TCP так груб по краям. Я бы надеялся на что-то более 0,5% или меньше.
Panzercrisis
1
@ Panzercrisis Я должен согласиться с вами. Я лично считаю, что потеря 5% была бы неприемлемой. Если вы думаете о чем-то вроде того, что я отправляю, скажем, новый корабль, появившийся в игре, даже 1% -ная вероятность того, что этот пакет не будет получен, будет катастрофической, потому что я получу невидимые корабли.
joehot200
2
не волнуйтесь, ребята, я имел в виду 5% в качестве верхней границы :) на самом деле это намного лучше, как отмечено в других комментариях.
Мукахо

Ответы:

28

Сегмент TCP имеет много накладных расходов. Когда вы отправляете 10-байтовое сообщение с одним TCP-пакетом, вы фактически отправляете:

  • 16 байтов заголовка IPv4 (увеличится до 40 байтов, когда IPv6 станет обычным)
  • 16 байтов заголовка TCP
  • 10 байт полезной нагрузки
  • дополнительные издержки для используемых протоколов передачи данных и физического уровня

В результате получается 42 байта трафика для транспортировки 10 байтов данных. Таким образом, вы используете только менее 25% доступной пропускной способности. И это еще не учитывает накладные расходы, которые потребляют низкоуровневые протоколы, такие как Ethernet или PPPoE (но их трудно оценить, потому что существует так много альтернатив).

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

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

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

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

Philipp
источник
1
Влияет ли издержки от способа, которым TCP восстанавливает потерю пакетов, на что-нибудь, в зависимости от размера пакетов?
Panzercrisis
@Panzercrisis Только в том случае, если существует более крупный пакет, который необходимо отправить повторно.
Филипп
8
Я хотел бы отметить, что операционная система почти наверняка будет применять алгоритм Nagles en.wikipedia.org/wiki/Nagle%27s_algorithm к исходящим данным, это означает, что не имеет значения, если в приложении вы делаете отдельные записи или объединяете их, она объединит их прежде чем фактически передать их через TCP.
Vality
1
@Vality Большинство API сокетов, которые я использовал, позволяют активировать или деактивировать nagle для каждого сокета. Для большинства игр я бы рекомендовал деактивировать его, потому что низкая задержка обычно важнее, чем сохранение полосы пропускания.
Филипп
4
Алгоритм Нэгла один, но не единственная причина, по которой данные могут буферизоваться на отправляющей стороне. Нет способа надежно форсировать отправку данных. Кроме того, буферизация / фрагментация может происходить в любом месте после отправки данных, либо на NAT, маршрутизаторах, прокси или даже на принимающей стороне. TCP не дает никаких гарантий относительно размера и времени получения данных, только то, что они будут доставлены в порядке и надежно. Если вам нужны гарантии размера, используйте UDP. Тот факт, что TCP кажется более понятным, не делает его лучшим инструментом для решения всех проблем!
Панда Пижама
10

Один большой (в пределах разумного) лучше.

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

Однако со стандартным TCP это на самом деле не проблема, если вы не отключите его. (Он называется алгоритмом Nagle , и для игр его следует отключить.) TCP будет ожидать фиксированного тайм-аута или до тех пор, пока пакет не «заполнится». Где "full" - это немного магическое число, частично определяемое размером кадра.

Эльва
источник
Я слышал об алгоритме Нейгла, но действительно ли это хорошая идея, чтобы отключить его? Я только что пришел из ответа StackOverflow, где кто-то сказал, что он более эффективен (и по понятным причинам я хочу эффективности).
joehot200
6
@ joehot200 Единственный правильный ответ - «это зависит». Да, он более эффективен для отправки большого количества данных, но не для потоковой передачи в реальном времени, которая обычно нужна играм.
D-Side
4
@ joehot200: Алгоритм Nagle плохо взаимодействует с алгоритмом отложенного подтверждения, который иногда используют некоторые реализации TCP. Некоторые реализации TCP откладывают отправку ACK после получения некоторых данных, если они ожидают, что вскоре после этого последуют другие данные (так как подтверждение более позднего пакета также косвенно подтвердит более ранний). Алгоритм Nagle говорит, что блок не должен отправлять частичный пакет, если он отправил некоторые данные, но не услышал подтверждение. Иногда эти два подхода плохо взаимодействуют, и каждая из сторон ждет, пока другая что-то сделает, - пока ...
суперкат
2
... включается "таймер здравомыслия" и разрешает ситуацию (с точностью до секунды).
суперкат
2
К сожалению, отключение алгоритма Nagle ничего не сделает для предотвращения буферизации на другой стороне хоста. Отключение алгоритма Nagle не гарантирует, что вы получите один recv()вызов для каждого send()вызова, что и ищет большинство людей. Использование протокола, который гарантирует это, как, впрочем, и UDP. «Когда все, что у вас есть, это TCP, все выглядит как поток»
Panda Pajama
6

Все предыдущие ответы неверны. На практике не имеет значения, что вы делаете один длинный send()звонок или несколько маленькихsend() вызовов.

Как утверждает Филлип, сегмент TCP имеет некоторые накладные расходы, но, как программист приложения, вы не контролируете, как создаются сегменты. Проще говоря:

Один send()вызов не обязательно переводится в один сегмент TCP.

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

Это имеет несколько последствий, но наиболее важным является то, что:

Один send()вызов или один сегмент TCP не обязательно переводится в один успешный recv()вызов на другом конце

Причина этого заключается в том, что TCP является потоковым протоколом. TCP обрабатывает ваши данные как длинный поток байтов и абсолютно не имеет понятия «пакеты». Когда send()вы добавляете байты в этот поток, recv()вы получаете байты с другой стороны. TCP будет активно буферизовать и разбивать ваши данные, где сочтет нужным, чтобы ваши данные передавались на другую сторону как можно быстрее.

Если вы хотите отправлять и получать «пакеты» по протоколу TCP, вам необходимо внедрить маркеры запуска пакетов, маркеры длины и так далее. Как насчет использования протокола, ориентированного на сообщения, такого как UDP? UDP гарантирует, что один send()вызов преобразуется в одну отправленную дейтаграмму и в один recv()вызов!

Когда все, что у вас есть, это TCP, все выглядит как поток

Панда Пижама
источник
1
Префикс каждого сообщения с длиной сообщения не так сложно.
MSK
У вас есть один переключатель, чтобы перевернуть, когда дело доходит до агрегации пакетов, независимо от того, действует алгоритм Nagle или нет. Нередко в игровой сети отключается обеспечение быстрой доставки недостаточно заполненных пакетов.
Ларс Виклунд
Это полностью зависит от ОС или даже библиотеки. Также у вас есть много контроля - если хотите. Это правда, что вы не имеете полного контроля, TCP всегда может объединять два сообщения или разделять одно, если оно не соответствует MTU, но вы все равно можете намекать на это в правильном направлении. Установка различных настроек конфигурации, ручная отправка сообщений с интервалом в 1 секунду или буферизация данных и отправка их за один раз.
Дорус
@ysdx: нет, не на отправляющей стороне, но да на принимающей. Поскольку у вас нет никаких гарантий относительно того, где именно вы будете получать данные recv(), вам необходимо создать собственную буферизацию, чтобы компенсировать это. Я бы оценил это в той же сложности, что и реализация надежности по UDP.
Панда Пижама
@Pagnda Pajama: Наивная реализация принимающей стороны такова: while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(для краткости опущена обработка ошибок и частичное чтение, но она не сильно меняется) Делать это selectне намного сложнее, и если вы используете TCP, вам, вероятно, все равно придется буферизовать частичные сообщения. Реализация надежной надежности по UDP намного сложнее, чем это.
YSDX
3

Многие небольшие пакеты в порядке. На самом деле, если вы беспокоитесь о накладных расходах TCP, просто вставьтеbufferstream до 1500 символов (или каковы бы ни были ваши MTU TCP, лучше всего запрашивать его динамически), и решите проблему в одном месте. Это сэкономит вам ~ 40 байт за каждый дополнительный пакет, который вы в противном случае создали бы.

Тем не менее, все-таки лучше отправлять меньше данных, и в этом вам помогают создание более крупных объектов. Конечно, отправлять меньше, "UID:10|1|2|3чем отправлять UID:10;x:1UID:10;y:2UID:10;z:3. На самом деле, на этом этапе вам также не следует изобретать велосипед, используйте библиотеку типа protobuf, которая может уменьшить такие данные до 10-байтовой строки или меньше.

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

Потеря посылки - это то, что вы можете повлиять здесь, незначительно. Каждый отправленный вами байт может быть поврежден, и TCP автоматически запросит повторную передачу. Меньшие пакеты означают меньшую вероятность повреждения каждого отдельного пакета, но поскольку они увеличивают накладные расходы, вы отправляете еще больше байтов, еще больше увеличивая шансы потерянного пакета. Если пакет потерян, TCP будет буферизовать все последующие данные до тех пор, пока отсутствующий пакет не будет повторно отправлен и получен. Это приводит к большой задержке (пинг). Хотя общая потеря пропускной способности из-за потери пакетов может быть незначительной, более высокий пинг нежелателен для игр.

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

Dorus
источник
На самом деле для простых вещей, таких как это легко катиться самостоятельно. Гораздо проще, чем просматривать документацию на 50 страниц, чтобы использовать библиотеку другого человека, а затем вам все еще приходится разбираться с их ошибками и ошибками.
Пейсер
Правда, писать свои собственные bufferstreamтривиально, поэтому я назвал это методом. Вы по-прежнему хотите обрабатывать это в одном месте и не интегрировать свою логику буфера с кодом сообщения. Что касается Сериализации Объектов, я очень сомневаюсь, что вы получите что-то лучше, чем тысячи человеко-часов, вложенных другими, даже если вы ДЕЛАЕТЕ попытку, я настоятельно рекомендую вам сравнить свое решение с известными реализациями.
Дорус
2

Хотя я и новичок в сетевом программировании, я хотел бы поделиться своим ограниченным опытом, добавив несколько моментов:

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

Что касается измерений, метрики, которые следует учитывать, являются:

  • средняя и мгновенная пропускная способность
  • средняя , максимальная и минимальная сквозная задержка. Для таких показателей существующие инструменты могут обеспечить быстрое решение. Например: iperf ( https://iperf.fr/ ), D-ITG ( http://traffic.comics.unina.it/software/ITG/ ). Датированный, но все еще полезный документ по настройке TCP можно найти по адресу http://dst.lbl.gov/publications/usenix-login.pdf .

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

  • надежный UDP http://sourceforge.net/projects/rudp/
  • ENET http://enet.bespin.org/ (который мог бы даже заменить обработку сообщений TCP, если было обнаружено, что она работает лучше)
  • UDT (протокол передачи данных на основе UDP http://udt.sourceforge.net/ ), который, кажется, стал нормой в вычислительных сценариях HP

Вывод: поскольку реализация UDP может превзойти (в 3 раза) TCP-реализацию, имеет смысл рассмотреть ее, как только вы определили свой сценарий как UDP-дружественный. Имейте в виду! Реализация полного стека TCP поверх UDP всегда плохая идея.

teodron
источник
Я использовал UDP. Я только что перешел на TCP. Потеря пакетов UDP была просто неприемлема для важных данных, которые нужны клиенту. Я могу отправить данные о движении через UDP.
joehot200
So, the best things that you can do are: indeed, use TCP just for crucial operations OR use an UDP based software protocol implementation (with Enet being simple and UDT being well tested). But first, measure the loss and decide whether UDT would bring you an edge.
teodron