c# - cref - ¿Cuál es la solución para el reconocimiento TCP diferido?
remarks c# (7)
He enviado un videojuego en línea (basado en cuadrícula) que utiliza el protocolo TCP para garantizar una comunicación confiable en una topología de red cliente-servidor. Mi juego funciona bastante bien, pero tiene una latencia mayor a la esperada (los juegos TCP similares en el género parecen hacer un mejor trabajo para mantener la latencia al mínimo).
Mientras investigaba, descubrí que la latencia solo es inesperadamente alta para los clientes que ejecutan Microsoft Windows (a diferencia de los clientes de Mac OS X ). Además, descubrí que si un cliente de Windows establece TcpAckFrequency=1
en el registro y reinicia su máquina, su latencia se vuelve normal.
Parece que el diseño de mi red no tuvo en cuenta el acuse de recibo retrasado:
Un diseño que no tenga en cuenta la interacción del reconocimiento diferido , el algoritmo de Nagle y el almacenamiento en búfer de Winsock puede afectar drásticamente el rendimiento. ( http://support.microsoft.com/kb/214397 )
Sin embargo, me resulta casi imposible tener en cuenta el acuse de recibo retrasado en mi juego (o en cualquier juego). Según MSDN, la pila de Microsoft TCP usa los siguientes criterios para decidir cuándo enviar un ACK en los paquetes de datos recibidos:
- Si el segundo paquete de datos se recibe antes de que expire el temporizador de retardo (200 ms), se envía el ACK.
- Si hay datos que se enviarán en la misma dirección que el ACK antes de que se reciba el segundo paquete de datos y expire el temporizador de retardo, el ACK se lleva a cuestas con el segmento de datos y se envía de inmediato.
- Cuando el temporizador de retardo expira (200ms), se envía el ACK.
Al leer esto, se podría suponer que la solución alternativa para el acuse de recibo retrasado en la pila TCP de Microsoft es la siguiente:
- Deshabilita el algoritmo de Nagle (TCP_NODELAY).
- Desactive el búfer de envío del zócalo (
SO_SNDBUF
= 0), de modo que se pueda esperar que una llamada asend
envíe un paquete. - Cuando se
send
llamada, si no se espera que se envíen más datos de inmediato, vuelva a llamar con un byte de datos que el receptor descartará.
Con este enfoque, el receptor recibirá el segundo paquete de datos aproximadamente al mismo tiempo que el paquete de datos anterior. Como resultado, el ACK
debe enviarse inmediatamente desde el receptor al remitente (emulando lo que TcpAckFrequency=1
hace en el registro).
Sin embargo, de mis pruebas, esta latencia mejorada solo en aproximadamente la mitad de lo que hace la edición del registro. ¿Qué me estoy perdiendo?
P: ¿Por qué no usar UDP?R: Elegí TCP porque todos los paquetes que envío deben llegar (y estar en orden); no hay paquetes que no valga la pena retransmitir si se pierden (o se desordenan). Solo cuando los paquetes pueden ser descartados / desordenados, ¡UDP puede ser más rápido que TCP!
Con este enfoque, el receptor recibirá el segundo paquete de datos aproximadamente al mismo tiempo que el paquete de datos anterior. Como resultado, el ACK debe enviarse inmediatamente desde el receptor al remitente (emulando lo que hace TcpAckFrequency = 1 en el registro).
No estoy convencido de que esto siempre hará que se envíe un segundo paquete separado. Sé que tienes el búfer de Nagle desactivado y cero envío, pero he visto cosas más extrañas. Algunos vertederos de wirehark pueden ser útiles.
Una idea: en lugar de que su paquete ''canario'' sea solo de un byte, envíe los datos completos de un MSS (típicamente, qué, 1460 bytes en una red de 1500 MTU).
cada paquete que envío debe llegar (y estar en orden);
Este requisito es la causa de su latencia.
O tiene una red con pérdida de paquetes insignificante, en la que UDP estaría entregando cada paquete, o tiene una pérdida, en la que TCP está retransmitiendo, retrasando todo (múltiplos de) el intervalo de retransmisión (que es al menos el viaje de ida y vuelta). hora). Este retraso no es consistente, ya que se desencadena por paquetes perdidos; el jitter generalmente tiene peores consecuencias que el retraso predecible en el acuse de recibo causado por la combinación de paquetes
Solo cuando los paquetes pueden ser descartados / desordenados, ¡UDP puede ser más rápido que TCP!
Este es un supuesto fácil de hacer, pero erróneo.
Hay otras formas de mejorar las tasas de caída además de ARQ que proporcionan una latencia menor: los métodos de corrección de errores hacia adelante logran una latencia mejorada para la recuperación de la caída a expensas del ancho de banda adicional requerido.
Desde Windows Vista, la opción TCP_NODELAY se debe configurar antes de llamar a connect
, o (en el servidor) antes de llamar a listen
. Si configura TCP_NODELAY
después de llamar a connect
, en realidad no deshabilitará el algoritmo de Nagle, sin embargo, GetSocketOption
indicará que Nagle ha sido deshabilitado. Todo esto parece estar sin documentar, y contradice lo que enseñan muchos tutoriales / artículos sobre el tema.
Con Nagle realmente desactivado, los acuses de recibo de TCP ya no causan latencia.
Le sugiero que deje el algoritmo de Nagle y los búferes activados, ya que su propósito básico es recopilar escrituras pequeñas en paquetes completos / más grandes (esto mejora mucho el rendimiento), pero al mismo tiempo use FlushFileBuffers () en el zócalo después de que esté Enviado hace un tiempo.
Supongo que aquí, tu juego tiene una especie de bucle principal, que procesa cosas y luego espera durante un tiempo antes de pasar a la siguiente ronda:
while(run_my_game)
{
process_game_events_and_send_data_over_network();
Sleep(20 - time_spent_processing);
};
Ahora sugeriría insertar FlushFileBuffers () antes de la llamada Sleep ():
while(run_my_game)
{
process_game_events_and_send_data_over_network();
FlushFileBuffers(my_socket);
Sleep(20 - time_spent_processing);
};
De esa manera, demora el envío de paquetes a más tardar hasta el momento antes de que la aplicación entre en suspensión para esperar la próxima ronda. Debería recibir el beneficio de rendimiento del algoritmo de Nagel y minimizar el retraso.
En caso de que esto no funcione, sería útil si publicara un poco de (pseudo) código que explique cómo funciona realmente su programa.
EDITAR: Hubo dos cosas más que me vinieron a la cabeza cuando pensé en tu pregunta otra vez:
a) Los paquetes ACK retrasados no deberían causar ningún retraso, ya que viajan en la dirección opuesta a la de los datos que está enviando. En el peor de los casos bloquean la cola de envío. Sin embargo, esto será resuelto por TCP después de algunos paquetes cuando el ancho de banda de la conexión y los límites de memoria lo permitan. Entonces, a menos que su máquina tenga una RAM realmente baja (no es suficiente para mantener una cola de envío más grande), o si realmente está transmitiendo más datos de los que permite su conexión, los paquetes ACK retrasados son una optimización y realmente mejorarán el rendimiento.
b) Está utilizando un hilo dedicado para el envío. Me pregunto porque. AFAIK es el seguro multihilo de Socket API, por lo tanto, cada hilo productor podría enviar los datos por sí mismo, a menos que su aplicación requiera tal cola, sugeriría que también elimine este hilo de envío dedicado y con ello la sobrecarga de sincronización adicional y lo retrase. podría causar.
Menciono específicamente el retraso aquí. Como el sistema operativo puede decidir no programar de inmediato la ejecución del subproceso de envío, cuando se desbloquee en su cola. Los retrasos de reprogramación de Typicall están en el rango de 10 ms, pero bajo carga pueden dispararse a 50 ms o más. Como un entorno de trabajo, podría intentar fiddeling con las prioridades de programación. Pero esto no reducirá el retraso impuesto por el propio sistema operativo.
Por cierto puede comparar fácilmente el TCP y su red, con solo un hilo en el cliente y otro en el servidor, que solo juega ping / pong con algunos datos.
No debería haber nada que tengas que hacer. Todas las soluciones alternativas que sugiere son para ayudar a los protocolos que no se diseñaron correctamente para que funcionen sobre TCP. Presumiblemente tu protocolo fue diseñado para funcionar sobre TCP, ¿verdad?
Su problema es casi uno o ambos:
Está llamando a las funciones de envío de TCP con pequeños bits de datos, aunque no hay razón para que no pueda llamar con fragmentos más grandes.
No implementó acuses de recibo a nivel de aplicación de las unidades de datos del protocolo de aplicación. Implemente estos para que los ACK puedan realizarlos.
Para resolver el problema, es necesario comprender el funcionamiento normal de las conexiones TCP. Telnet es un buen ejemplo para analizar.
TCP garantiza la entrega mediante el reconocimiento de la transmisión de datos exitosa. El "Ack" se puede enviar como un mensaje por sí mismo, pero esto implica una cierta sobrecarga: un Ack es un mensaje muy pequeño en sí mismo, pero los protocolos de nivel inferior agregan encabezados adicionales. Por esta razón, TCP prefiere combinar el mensaje Ack en otro paquete que está enviando de todos modos. Al observar un shell interactivo a través de Telnet, hay un flujo constante de pulsaciones de teclas y respuestas. Y si hay una pequeña pausa al escribir, no hay nada que hacer eco en la pantalla. El único caso cuando el flujo se detiene es si tiene salida sin la entrada correspondiente. Pero como solo puedes leer tan rápido, está bien esperar unos cientos de milisegundos para ver si hay una pulsación de tecla para llevar a cuestas al Ack.
Entonces, resumiendo, tenemos un flujo constante de paquetes en ambos sentidos, y Ack usualmente lleva a cuestas. Si hay una interrupción en el flujo por motivos de aplicación, no se notará el retraso del Ack.
De vuelta a su protocolo: aparentemente no tiene un protocolo de solicitud / respuesta. Eso significa que el Ack no puede ser respaldado (problema 1). Y mientras el sistema operativo receptor enviará Acks por separado, no enviará spam a esos.
Su solución a través de TCP_NODELAY
y dos paquetes en el lado de envío (Windows) supone que el lado receptor también es Windows, o al menos se comporta como tal. Esto es ilusión, no ingeniería . El otro sistema operativo puede decidir esperar a que tres paquetes envíen un Ack, lo que interrumpe completamente su uso de TCP_NODELAY
para forzar un paquete adicional. "Esperar 3 paquetes" es solo un ejemplo; hay muchos otros algoritmos válidos para evitar el spam Ack que no se dejaría engañar por su segundo paquete ficticio de un byte.
¿Cuál es la solución real? Enviar una respuesta a nivel de protocolo. No importa el sistema operativo en ese momento, será compatible con TCP Ack en la respuesta de su protocolo. A su vez, esta respuesta también forzará un Ack en la otra dirección (la respuesta también es un mensaje TCP) pero no le importa la latencia de la respuesta. La respuesta está ahí, por lo que el sistema operativo receptor lleva a cuestas el primer Ack.
Use bibliotecas UDP confiables y escriba su propio algoritmo de control de congestión, esto definitivamente superará su problema de latencia de TCP.
esta la siguiente biblioteca, que uso para transferencias UDP confiables: