¿Qué es mejor? ¿Muchos paquetes TCP pequeños o uno largo? [cerrado]

16

Estoy enviando bastantes datos hacia y desde un servidor, para un juego que estoy haciendo.

Actualmente envío datos de ubicación como este:

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

Obviamente está enviando los respectivos valores X, Y y Z.

¿Sería más eficiente enviar datos como este?

sendToClient((("UID:" + cl.uid +"|" + cl.x + "|" + cl.y + "|" + cl.z)));
joehot200
fuente
2
La pérdida de paquetes suele ser inferior al 5%, en mi experiencia limitada.
mucaho
55
¿SendToClient realmente envía un paquete? Si es así, ¿cómo hiciste que hiciera eso?
user253751
1
@mucaho Nunca lo he medido yo mismo ni nada, pero me sorprende que TCP sea tan tosco. Hubiera esperado algo más como 0.5% o menos.
Panzercrisis
1
@ Panzercrisis Debo estar de acuerdo contigo. Personalmente, creo que una pérdida del 5% sería inaceptable. Si piensas en algo como que yo envíe, digamos, una nueva nave engendrada en el juego, incluso un 1% de probabilidad de que ese paquete no sea recibido sería desastroso, porque obtendría naves invisibles.
joehot200
2
no asusten a los chicos, quise decir el 5% como límite superior :) en realidad es mucho mejor, como lo señalan otros comentarios.
mucaho

Respuestas:

28

Un segmento TCP tiene bastante sobrecarga. Cuando envía un mensaje de 10 bytes con un paquete TCP que realmente envía:

  • 16 bytes de encabezado IPv4 (aumentará a 40 bytes cuando IPv6 se vuelva común)
  • 16 bytes de encabezado TCP
  • 10 bytes de carga útil
  • sobrecarga adicional para el enlace de datos y los protocolos de capa física utilizados

resultando en 42 bytes de tráfico para transportar 10 bytes de datos. Por lo tanto, solo utiliza menos del 25% de su ancho de banda disponible. Y eso aún no tiene en cuenta la sobrecarga que consumen los protocolos de nivel inferior como Ethernet o PPPoE (pero estos son difíciles de estimar porque hay muchas alternativas).

Además, muchos paquetes pequeños ejercen más presión sobre los enrutadores, firewalls, conmutadores y otros equipos de infraestructura de red, por lo que cuando usted, su proveedor de servicios y sus usuarios no invierten en hardware de alta calidad, esto podría convertirse en otro cuello de botella.

Por esa razón, debe intentar enviar todos los datos que tenga disponibles a la vez en un segmento TCP.

Con respecto al manejo de la pérdida de paquetes : cuando usa TCP no necesita preocuparse por eso. El protocolo en sí mismo asegura que los paquetes perdidos se reenvíen y los paquetes se procesen en orden, por lo que puede suponer que todos los paquetes que envíe llegarán al otro lado, y llegarán en el orden en que los envió. El precio de esto es que cuando hay pérdida de paquetes, su reproductor experimentará un retraso considerable, porque un paquete descartado detendrá todo el flujo de datos hasta que se vuelva a solicitar y recibir.

Cuando esto es un problema, siempre puedes usar UDP. Pero entonces usted necesita para encontrar su propia solución de y fuera de orden mensajes perdidos (al menos garantiza que los mensajes que no llegan, llegan completa y en buen estado).

Philipp
fuente
1
¿La sobrecarga de la forma en que TCP recupera la pérdida de paquetes varía algo dependiendo de los tamaños de los paquetes?
Panzercrisis
@Panzercrisis Solo en la medida en que haya un paquete más grande que deba reenviarse.
Philipp
8
Me gustaría señalar que el sistema operativo casi seguramente aplicará el algoritmo de Nagles en.wikipedia.org/wiki/Nagle%27s_algorithm a los datos salientes, esto significa que no importa si en la aplicación se escriben o combinan por separado, los combinará antes de pasarlos por TCP.
Vality
1
@Vality La mayoría de las API de socket que he usado permiten activar o desactivar nagle para cada socket. Para la mayoría de los juegos, recomendaría desactivarlo, porque la baja latencia suele ser más importante que conservar el ancho de banda.
Philipp
44
El algoritmo de Nagle es uno, pero no el único motivo por el que los datos pueden almacenarse en el lado emisor. No hay forma de forzar de manera confiable el envío de datos. Además, el almacenamiento en búfer / fragmentación puede ocurrir en cualquier lugar después del envío de datos, ya sea en NAT, enrutadores, servidores proxy o incluso en el lado receptor. TCP no ofrece ninguna garantía con respecto al tamaño y el momento en que recibe los datos, solo que llegarán en orden y de manera confiable. Si necesita garantías de tamaño, use UDP. ¡El hecho de que TCP parece más fácil de entender no lo convierte en la mejor herramienta para todos los problemas!
Panda Pijama
10

Uno grande (dentro de lo razonable) es mejor.

Como dijiste, la pérdida de paquetes es la razón principal. Los paquetes generalmente se envían en cuadros de un tamaño fijo, por lo que es mejor tomar un cuadro con un mensaje grande que 10 cuadros con 10 mensajes pequeños.

Sin embargo, con TCP estándar, esto no es realmente un problema, a menos que lo desactive. (Se llama algoritmo de Nagle , y para los juegos debe deshabilitarlo). TCP esperará un tiempo de espera fijo o hasta que el paquete esté "lleno". Donde está "lleno" sería un número ligeramente mágico, determinado en parte por el tamaño del cuadro.

Elva
fuente
He oído hablar del algoritmo de Nagle, pero ¿es realmente una buena idea desactivarlo? Acabo de llegar de una respuesta de StackOverflow donde alguien dijo que es más eficiente (y por razones obvias quiero eficiencia).
joehot200
66
@ joehot200 La única respuesta correcta a eso es "depende". Es más eficiente para enviar muchos datos, sí, pero no para la transmisión en tiempo real que los juegos tienden a necesitar.
D-side
44
@ joehot200: el algoritmo de Nagle interactúa pobremente con un algoritmo de reconocimiento retrasado que algunas implementaciones de TCP a veces usan. Algunas implementaciones de TCP retrasarán el envío de un ACK después de obtener algunos datos si esperan que sigan más datos poco después (ya que reconocer el paquete posterior también reconocería implícitamente el anterior). El algoritmo de Nagle dice que una unidad no debería enviar un paquete parcial si ha enviado algunos datos pero no ha escuchado un acuse de recibo. A veces, los dos enfoques interactúan mal, con cada parte esperando que el otro haga algo, hasta ...
supercat
2
... un "temporizador de cordura" entra en acción y resuelve la situación (en el orden de un segundo).
Supercat
2
Desafortunadamente, deshabilitar el algoritmo de Nagle no hará nada para evitar el almacenamiento en búfer en el otro lado del host. Deshabilitar el algoritmo de Nagle no garantiza que reciba una recv()llamada por cada send()llamada, que es lo que la mayoría de las personas está buscando. Sin embargo, usar un protocolo que garantice esto, como UDP. "Cuando todo lo que tienes es TCP, todo parece una transmisión"
Panda Pyjama
6

Todas las respuestas anteriores son incorrectas. En la práctica, no importa si emite uno largosend() llamada o varias send()llamadas pequeñas .

Como afirma Phillip, un segmento TCP tiene cierta sobrecarga, pero como programador de aplicaciones, no tiene control sobre cómo se generan los segmentos. En lenguaje sencillo:

Uno send() llamada no necesariamente se traduce en un segmento TCP.

El sistema operativo es completamente gratuito para almacenar en búfer todos sus datos y enviarlos en un segmento, o tomar el largo y dividirlo en varios segmentos pequeños.

Esto tiene varias implicaciones, pero la más importante es que:

Una send()llamada o un segmento TCP no necesariamente se traduce en uno exitosorecv() llamada en el otro extremo

El razonamiento detrás de esto es que TCP es un protocolo de flujo . TCP trata sus datos como una secuencia larga de bytes y no tiene absolutamente ningún concepto de "paquetes". Con send()usted agrega bytes a esa secuencia, y con recv()usted obtiene bytes del otro lado. TCP almacenará en búfer agresivamente y dividirá sus datos donde lo considere conveniente para asegurarse de que sus datos lleguen al otro lado lo más rápido posible.

Si desea enviar y recibir "paquetes" con TCP, debe implementar marcadores de inicio de paquete, marcadores de longitud, etc. ¿Qué tal usar un protocolo orientado a mensajes como UDP? ¡UDP garantiza que una send()llamada se traduzca en un datagrama enviado y en una recv()llamada!

Cuando todo lo que tienes es TCP, todo parece una secuencia

Pijama Panda
fuente
1
Prefijar cada mensaje con una longitud de mensaje no es tan complicado.
ysdx
Tiene un interruptor para cambiar cuando se trata de la agregación de paquetes, ya sea que el algoritmo de Nagle esté vigente o no. No es raro que esté desactivado en la red de juegos para garantizar la pronta entrega de los paquetes con poco contenido.
Lars Viklund
Esto es completamente OS o incluso específico de la biblioteca. También tienes mucho control, si quieres. Es cierto que no tiene control total, TCP siempre puede combinar dos mensajes o dividir uno si no se ajusta a la MTU, pero aún puede insinuarlo en la dirección correcta. Establecer varias configuraciones de configuración, enviar mensajes manualmente con 1 segundo de diferencia o almacenar datos en búfer y enviarlos de una vez.
Dorus
@ysdx: no, no en el lado de envío, pero sí en el lado de recepción. Dado que no tiene garantías de dónde obtendrá exactamente los datos recv(), debe hacer su propio almacenamiento en búfer para compensar eso. Lo clasificaría en la misma dificultad que implementar la confiabilidad sobre UDP.
Panda Pyjama
@Pagnda Pyjama: Una implementación ingenua del lado receptor es: while(1) { uint16_t size; read(sock, &size, sizeof(size)); size = ntoh(size); char message[size]; read(sock, buffer, size); handleMessage(message); }(omitiendo el manejo de errores y las lecturas parciales por brevedad, pero no cambia mucho). Hacer esto selectno agrega mucha más complejidad y, si está utilizando TCP, es probable que necesite almacenar los mensajes parciales de todos modos. Implementar una fiabilidad robusta sobre UDP es mucho más complicado que eso.
ysdx
3

Muchos paquetes pequeños están bien. De hecho, si le preocupa la sobrecarga de TCP, simplemente inserte unbufferstream que recopile hasta 1500 caracteres (o lo que sea que sea su MTU de TCP, lo mejor es solicitarlo dinámicamente) y lidiar con el problema en un solo lugar. Hacerlo le ahorra la sobrecarga de ~ 40 bytes por cada paquete adicional que de otro modo hubiera creado.

Dicho esto, aún es mejor enviar menos datos, y construir objetos más grandes ayuda allí. Por supuesto, es más pequeño enviar "UID:10|1|2|3que enviar UID:10;x:1UID:10;y:2UID:10;z:3. De hecho, también en este punto no debería reinventar la rueda, use una biblioteca como protobuf que pueda disminuir datos como ese a una cadena de 10 bytes o menos.

Lo único que no debes olvidar es insertar un Flush comando en su flujo en ubicaciones relevantes, porque tan pronto como deje de agregar datos a su flujo, puede esperar infinito antes de enviar algo. Realmente problemático cuando su cliente está esperando esos datos, y su servidor no enviará nada nuevo hasta que el cliente envíe el siguiente comando.

La pérdida del paquete es algo que puede afectar aquí, marginalmente. Cada byte que envíe puede estar dañado y TCP solicitará automáticamente una retransmisión. Los paquetes más pequeños significan una menor posibilidad de que cada paquete se corrompa, pero debido a que se suman a los gastos generales, envía aún más bytes, lo que aumenta aún más las posibilidades de un paquete perdido. Cuando se pierde un paquete, TCP almacenará en el búfer todos los datos subsiguientes hasta que el paquete faltante se reenvíe y reciba. Esto da como resultado un gran retraso (ping). Si bien la pérdida total en el ancho de banda debido a la pérdida del paquete puede ser insignificante, el ping más alto sería indeseable para los juegos.

En pocas palabras: envíe la menor cantidad de datos posible, envíe paquetes grandes y no escriba sus propios métodos de bajo nivel para hacerlo, sino que confíe en bibliotecas y métodos conocidos como bufferstreamy protobuf para manejar el trabajo pesado.

Dorus
fuente
En realidad, para cosas simples como esta es fácil rodar la tuya. Mucho más fácil que pasar por una documentación de 50 páginas para usar la biblioteca de otra persona, y luego aún tiene que lidiar con sus errores y trampas.
Pacerier
Es cierto, escribir el suyo bufferstreames trivial, por eso lo llamé un método. Todavía desea manejarlo en un lugar y no integrar su lógica de búfer con su código de mensaje. En cuanto a la serialización de objetos, dudo mucho que obtenga algo mejor que las miles de horas de trabajo que otros ponen allí, incluso si lo intenta, le sugiero que compare su solución con las implementaciones conocidas.
Dorus
2

Aunque soy un neófito en la programación de redes, me gustaría compartir mi experiencia limitada agregando algunos puntos:

  • TCP implica una sobrecarga: debe medir las estadísticas relevantes
  • UDP es la solución de facto para escenarios de juegos en red, pero todas las implementaciones que dependen de él tienen un algoritmo adicional del lado de la CPU para dar cuenta de que los paquetes se pierden o se envían fuera de servicio

En cuanto a las mediciones, las métricas que deben considerarse son:

Como se mencionó, si descubres que no estás limitado en un sentido y que puedes usar UDP, hazlo. Existen algunas implementaciones basadas en UDP, por lo que no tiene que reinventar la rueda o trabajar contra años de experiencia y experiencia comprobada. Tales implementaciones que vale la pena mencionar son:

Conclusión: dado que una implementación UDP podría superar (por un factor de 3x) a una TCP, tiene sentido considerarla, una vez que haya identificado su escenario como compatible con UDP. ¡Ten cuidado! Implementar la pila TCP completa sobre UDP siempre es una mala idea.

teodron
fuente
Estaba usando UDP. Acabo de cambiar a TCP. La pérdida de paquetes de UDP era simplemente inaceptable para los datos cruciales que el cliente necesitaba. Puedo enviar datos de movimiento a través de UDP.
joehot200
Entonces, las mejores cosas que puede hacer son: de hecho, use TCP solo para operaciones cruciales O use una implementación de protocolo de software basado en UDP (con Enet simple y UDT bien probado). Pero primero, mida la pérdida y decida si UDT le brindará una ventaja.
Teodron