¡Este artículo le explica qué paquetes de datos TCP son fijos y cómo solucionarlo!

Descripción general de los problemas de las bolsas adhesivas

Describir antecedentes

En el diseño de software que utiliza el protocolo TCP para la transmisión de datos de red, existe un problema común de paquetes adhesivos. Esto se debe principalmente al mecanismo de transmisión de red de los sistemas operativos modernos.

Sabemos que la tecnología de socket utilizada en la comunicación de red en realidad la implementa el núcleo del sistema, que proporciona un caché continuo (búfer de flujo) para implementar la función de transferencia entre el programa de la capa de aplicación y la interfaz de la tarjeta de red.

Múltiples paquetes de datos se almacenan continuamente en buffers continuos. Al leer paquetes de datos, dado que no se puede determinar el límite de envío del remitente, se utiliza un cierto tamaño de valor estimado para leer los datos. Si los tamaños de ambas partes son inconsistentes, esto causará los límites de los paquetes de datos se desalinean, lo que lleva a la lectura de paquetes de datos erróneos, malinterpretando así el significado de los datos originales.

El concepto de bolsas adhesivas.

La esencia del problema pegajoso es causada por errores en los límites de lectura de datos. La siguiente figura puede comprender visualmente el fenómeno.

Como se muestra en la Figura 1, han llegado 6 paquetes de datos al caché del socket actual y sus tamaños son los que se muestran en la figura. Cuando la aplicación recopila datos (como se muestra en la Figura 2), adopta el requisito de 300 bytes para leer y, por error, recopilará pkg1 y pkg2 juntos como un solo paquete.

De hecho, es muy probable que pkg1 sea el contenido de un archivo de texto, mientras que pkg2 puede ser un contenido de audio. Estos dos paquetes de datos no relacionados se agrupan en un solo paquete para su procesamiento, lo que obviamente es inapropiado. En casos graves, la pérdida de pkg2 puede hacer que el software caiga en una rama anormal y provoque un incidente propio.

Por lo tanto, el problema de los paquetes adhesivos debe atraer la gran atención de todos los diseñadores de software (gerentes de proyectos).

Entonces, algunos lectores pueden preguntar, ¿por qué no dejar que el programa receptor lea según 100 bytes? Creo que si sabes algo de programación TCP no tendrás ese problema.

En los programas de comunicación de red, el tamaño de los paquetes de datos generalmente no se puede determinar, especialmente en la etapa de diseño del software, que en realidad no se puede determinar como un valor fijo. Por ejemplo, si el cliente del software de chat usa TCP para transmitir un nombre de usuario y contraseña al servidor para su verificación e inicio de sesión, creo que el paquete de datos tiene solo unas pocas decenas de bytes, o como máximo unos cientos de bytes, y se puede enviar. A veces es necesario transmitir un paquete muy grande. Incluso si el archivo de vídeo se envía en paquetes, debe tener varios kilobytes por paquete. (Se dice que en el MW de la plataforma de telecomunicaciones de un determinado país se han enviado 15.000 bytes de datos telefónicos a la vez).

En este caso, el tamaño del paquete de los datos enviados no se puede arreglar y el extremo receptor no se puede arreglar. Por lo tanto, generalmente se utiliza una estimación más razonable para la recepción de las encuestas. (La MTU de la tarjeta de red es de 1500 bytes, por lo que este valor estimado es generalmente de 1 a 3 veces la MTU).

Creo que los lectores deberían tener una comprensión preliminar del problema de las bolsas adhesivas.

Diseño para evitar bolsas pegajosas

1: Enviar con longitud fija

Se utiliza un diseño de longitud fija al enviar datos. Es decir, no importa cuántos datos se envíen, se empaquetan en una longitud fija (para facilitar la descripción, aquí la longitud fija se registra como LEN), es decir, cuando el remitente envía datos, se empaquetan con LEN como la longitud.

De esta forma, el receptor recibe con una LEN fija, de modo que el envío y la recepción pueden corresponderse uno a uno. Al subempaquetar, es posible que no se divida completamente en varios paquetes LEN completos. El último paquete generalmente será más pequeño que LEN. En este momento, el último paquete puede llenar la parte faltante con bytes en blanco.

Por supuesto, este enfoque tiene desventajas:

1. La longitud insuficiente del último paquete se llena con partes en blanco, lo que no es un orden de bytes válido. Entonces el receptor puede tener dificultades para identificar esta parte no válida, ya que es sólo para completar y no tiene ningún significado real. Esto crea problemas para que el receptor procese su significado. Por supuesto, existen soluciones que se pueden compensar agregando bits de bandera, es decir, agregando un encabezado de longitud fija al frente de cada paquete de datos y luego enviando la marca final del paquete de datos juntos. El receptor confirma la secuencia de bytes no válida según esta marca, logrando así la recepción completa de los datos.

2. Cuando la longitud de los paquetes enviados se distribuye aleatoriamente, se desperdiciará ancho de banda. Por ejemplo, la longitud de envío puede ser 1100, 1000, 4000 bytes, etc., y todos deben enviarse de acuerdo con la longitud máxima fija, que es 4000. Otros paquetes con paquetes de datos de menos de 4000 bytes también se completarán para 4000, provocando un desperdicio ineficaz de la carga de la red.

En resumen, esta solución es adecuada para situaciones en las que la longitud del paquete de datos enviado es relativamente estable (tiende a un cierto valor fijo) y tiene mejores resultados.

2: secuencia de marcas de cola

Establezca una secuencia de bytes especial al final de cada paquete de datos que se enviará. Esta secuencia tiene un significado especial, que es el mismo que el identificador de carácter final "\0" de la cadena. Se utiliza para marcar el final de los datos. paquete Sólo entonces se pueden analizar los datos recibidos y confirmar los límites de los paquetes de datos a través de la secuencia de cola.

Las deficiencias de este método son más obvias:

1. El receptor necesita analizar los datos e identificar secuencias de cola.

2. La determinación de la secuencia de la cola en sí es un problema. ¿Qué secuencia se puede utilizar como terminador como "\0"? Esta secuencia debe ser una secuencia de datos que no tenga ningún significado que sea generalmente reconocido por humanos o programas, al igual que "\0" es un contenido de cadena no válido y, por lo tanto, puede usarse como marca de final de cadena. Entonces, ¿cuál es esta secuencia en la comunicación de red ordinaria? Creo que es difícil encontrar la respuesta correcta por un tiempo.

Tres: recepción paso a paso de las marcas de encabezado

Este método es el mejor método basado en el conocimiento limitado del autor. No pierde eficiencia y resuelve perfectamente el problema de los límites de paquetes de cualquier tamaño.

La implementación de este método es la siguiente:

1. Defina un encabezado de usuario e indique el tamaño de cada paquete de datos enviado en el encabezado.

2. Cada vez que el receptor recibe, primero lee los datos con el tamaño del encabezado, lo que inevitablemente solo leerá los datos de un encabezado y obtendrá el tamaño de los datos del paquete de datos del encabezado.

3. Lea nuevamente de acuerdo con este tamaño y podrá leer el contenido de los datos.

De esta manera, cada paquete de datos se encapsula con un encabezado cuando se envía, y luego el receptor recibe un paquete dos veces, la primera vez para recibir el encabezado y la segunda para recibir el contenido de los datos según el tamaño de el encabezado.

(La esencia de datos [0] aquí es un puntero que apunta a la parte de texto de los datos, o puede ser la posición inicial de un área de datos continua. Por lo tanto, se puede diseñar como datos [tamaño_usuario], en este caso .)

El siguiente es un diagrama para mostrar la idea de diseño.

Como se puede observar en la figura, los datos se envían con la acción de encapsular el encabezado, el receptor divide la recepción de cada paquete en dos tiempos.

Este plan parece elegante, pero en realidad también tiene defectos:

1. Aunque el encabezado es pequeño, cada paquete necesita encapsular más datos de tamaño (_data_head) y el efecto acumulativo no se puede ignorar por completo.

2. La acción de recepción del receptor se divide en dos tiempos, es decir, la operación de lectura de datos se duplica, y la recepción o lectura de la operación de lectura de datos son llamadas al sistema, lo cual es una sobrecarga inaceptable para el kernel. se ignora por completo y el impacto en el rendimiento del programa es insignificante (las llamadas al sistema son muy rápidas).

Ventajas: evita la complejidad del diseño del programa, su efectividad es fácil de verificar y es más fácil cumplir con los requisitos de estabilidad del diseño del software.

 Information Direct: ruta de aprendizaje de tecnología del código fuente del kernel de Linux + video tutorial sobre el código fuente del kernel

Learning Express: Código fuente del kernel de Linux Ajuste de memoria Sistema de archivos Gestión de procesos Controlador de dispositivo/Pila de protocolo de red

Reponer

¿Cuándo debería considerar pegar bolsas?

1. Si usa tcp para enviar datos cada vez, establecerá una conexión con la otra parte y luego ambas partes cerrarán la conexión después de enviar un dato, para que no haya problemas con los paquetes fijos (porque hay sólo una estructura de paquete, similar al protocolo http).

Para cerrar la conexión, ambas partes deben enviar una conexión cercana (consulte el protocolo de cierre tcp). Por ejemplo: A necesita enviar una cadena a B, luego A establece una conexión con B y luego envía los caracteres de protocolo que ambas partes han predeterminado, como "hola, dame algo sobre ti", y luego, después de que B reciba el mensaje, los datos del búfer se reciben y luego se cierra la conexión, de modo que no es necesario considerar el problema de los paquetes adhesivos, porque todos saben que se está enviando un párrafo de caracteres.

2. Si los datos enviados no tienen estructura, como la transferencia de archivos, entonces el remitente solo necesita enviar y el receptor solo necesita recibir y almacenar, estará bien y no es necesario considerar la adherencia del paquete.

3. Si ambas partes establecen una conexión, dentro de un período de tiempo después de la conexión, deben enviar datos de diferentes estructuras, por ejemplo, después de la conexión, hay varias estructuras:

1) "hola, dame algo sobre ti"

2) "No me des nada de ti"

En este caso, si el remitente envía estos dos paquetes continuamente, el receptor puede recibir "hola, dame algo sobre ti, no me digas algo sobre ti".

Esto volverá estúpido a la parte receptora ¿Qué diablos está pasando? No lo sé, porque el protocolo no estipula una cadena tan extraña, por lo que debe dividirse en paquetes. Cómo dividirlo también requiere que ambas partes organicen una mejor estructura de paquetes, por lo que generalmente un paquete como la longitud de los datos puede se agregará al encabezado. Asegúrate de recibirlo.

Razones para los paquetes adhesivos: ocurre en la transmisión de streaming, UDP no tendrá paquetes adhesivos porque tiene límites de mensajes.

1 El extremo emisor debe esperar hasta que el búfer esté lleno antes de enviar, lo que provoca paquetes pegajosos;

2 El receptor no recibe los paquetes en el búfer a tiempo, lo que provoca que se reciban varios paquetes;

Solución:

Para evitar el fenómeno de adherencia, se pueden tomar las siguientes medidas.

Primero, los usuarios pueden evitar el fenómeno de bloqueo de paquetes causado por el remitente a través de la configuración de programación. TCP proporciona el comando de operación de inserción que obliga a que los datos se transmitan inmediatamente. Después de recibir el comando de operación, el software TCP envía inmediatamente estos datos, y no es necesario espere a que el búfer de envío esté lleno;

En segundo lugar, para los paquetes pegajosos causados ​​por el receptor, se pueden utilizar medidas como optimizar el diseño del programa, simplificar la carga de trabajo del proceso de recepción y mejorar la prioridad del proceso de recepción para recibir datos de manera oportuna, minimizando así el fenómeno pegajoso;

El tercero es controlado por el receptor, que controla manualmente un paquete de datos que se recibirá varias veces según el campo de la estructura y luego se fusionará, lo que puede evitar el bloqueo de paquetes.

Las tres medidas mencionadas anteriormente tienen todas sus deficiencias.

Aunque el primer método de configuración de programación puede evitar los paquetes adhesivos causados ​​por el remitente, desactiva el algoritmo de optimización, reduce la eficiencia del envío de la red, afecta el rendimiento de la aplicación y, en general, no se recomienda.

El segundo método solo puede reducir la posibilidad de paquetes adhesivos, pero no puede evitarlos por completo. Cuando la frecuencia de envío es alta o debido a ráfagas de red, los paquetes de datos en un cierto período de tiempo pueden llegar más rápido al receptor y el receptor Puede que todavía sea demasiado tarde para recibirlo, lo que provocará paquetes pegajosos.

Aunque el tercer método evita los paquetes adhesivos, la eficiencia de la aplicación es baja y no es adecuada para aplicaciones en tiempo real.

Por qué los programas de comunicación basados ​​en TCP necesitan empaquetar y desempaquetar

TCP es un protocolo de "flujo". El llamado flujo es una cadena de datos sin límites. Puede pensar en el agua que fluye en un río, que está conectada en una sola pieza sin líneas divisorias. Pero en el desarrollo general de programas de comunicación, debe definir cada paquete de datos independiente, como el paquete de datos para iniciar sesión y cerrar sesión. Debido a las características del "flujo" de TCP y las condiciones de la red, ocurrirán las siguientes situaciones durante la transmisión de datos.

Supongamos que llamamos a enviar dos veces seguidas para enviar dos datos, datos 1 y datos 2 respectivamente, existen las siguientes situaciones de recepción en el extremo receptor (por supuesto, hay más situaciones que estas, aquí solo se enumeran situaciones representativas).

A. Primero se reciben los datos1 y luego los datos2.

B. Reciba parte de los datos de datos1 primero, luego reciba la parte restante de datos1 y todos los datos2.

C. Primero recibió todos los datos de datos1 y parte de los datos de datos2, y luego recibió los datos restantes de datos2.

D. Todos los datos de data1 y data2 se reciben al mismo tiempo.

Para la situación A, esto es exactamente lo que necesitamos y no se discutirá más. Para B, C y D, la situación es lo que todo el mundo suele llamar "paquetes adhesivos", que requiere que descomprimamos los datos recibidos en paquetes de datos independientes. Para desempaquetar, el paquete debe estar encapsulado en el extremo emisor.

Otro: para UDP, no hay problema de desempacar, porque UDP es un protocolo de "paquete de datos", es decir, hay un límite entre dos datos, y el extremo receptor no puede recibir los datos o recibe un fragmento completo. No se recibirán menos ni más datos.

¿Por qué ocurre el BCD?

Los "paquetes pegados" pueden ocurrir en el extremo emisor o en el extremo receptor.

1. Paquetes adhesivos en el remitente causados ​​por el algoritmo de Nagle:

El algoritmo de Nagle es un algoritmo que mejora la eficiencia de transmisión de la red. En pocas palabras, cuando enviamos un dato a TCP para su envío, TCP no envía los datos inmediatamente, sino que espera un corto período de tiempo para ver si aún quedan datos por enviar durante el período de espera. enviará los datos a la vez. Se envían dos datos.

Esta es una explicación simple del algoritmo de Nagle. Lea libros relacionados para obtener más detalles. Situaciones como C y D pueden deberse al algoritmo de Nagle.

2. El extremo receptor no recibe los paquetes a tiempo debido a los paquetes pegajosos del extremo receptor:

TCP almacenará los datos recibidos en su propio búfer y luego notificará a la capa de aplicación para que recupere los datos. Cuando la capa de aplicación no puede extraer datos TCP a tiempo por algún motivo, se almacenarán varios datos en el búfer TCP.

Cómo empacar y desempacar

Cuando encontré por primera vez el problema de los "paquetes adhesivos", lo resolví llamando al modo de suspensión durante un corto período de tiempo entre dos envíos.

Las desventajas de esta solución son obvias: reduce en gran medida la eficiencia de la transmisión y no es confiable. Posteriormente se resolvió mediante el método de respuesta, que aunque es factible la mayor parte del tiempo, no puede resolver la situación como B. Además, el método de respuesta aumenta el volumen de comunicación y aumenta la carga de la red. El siguiente paso es empaquetar y desempaquetar los paquetes de datos.

El empaquetado consiste en agregar un encabezado a un fragmento de datos, de modo que el paquete de datos se divida en dos partes: el encabezado y el cuerpo del paquete (más adelante, al filtrar paquetes ilegales, el paquete agregará contenido de "cola de paquete").

El encabezado es en realidad una estructura con un tamaño fijo. Hay una variable miembro de la estructura que representa la longitud del paquete. Esta es una variable muy importante. Se pueden definir otros miembros de la estructura según sea necesario. Según la longitud fija del encabezado del paquete y la variable que contiene la longitud del cuerpo del paquete en el encabezado del paquete, se puede dividir correctamente un paquete de datos completo.

Para desempaquetar , actualmente utilizo los dos métodos siguientes con mayor frecuencia.

1. Método de almacenamiento temporal en búfer dinámico. La razón por la que el búfer es dinámico es que cuando la longitud de los datos que se almacenarán en el búfer excede la longitud del búfer, la longitud del búfer aumentará. El proceso aproximado se describe a continuación:

A. Asigne dinámicamente un búfer para cada conexión y asocie el búfer con SOCKET, generalmente a través de una estructura. B. Al recibir datos, primero almacene estos datos en el búfer C. Juzgue si la longitud de los datos en el área del búfer es suficiente para la longitud del encabezado de un paquete, de lo contrario, no se realizará ninguna operación de desempaquetado. D. Analizar las variables que representan la longitud del cuerpo del paquete en función de los datos del encabezado del paquete. E. Determinar la longitud de los datos en el área del buffer excepto para el encabezado del paquete. ¿Es suficiente la longitud del cuerpo de un paquete? De lo contrario, no se realizará la operación de desempaquetado. F, extraiga todo el paquete de datos. "Obtener" aquí significa no solo copiar el paquete de datos del búfer, sino también eliminar el paquete de datos del búfer. El método de eliminación es mover los datos detrás del paquete a la dirección inicial del búfer.

Este método tiene dos desventajas: 1. La asignación dinámica de un búfer para cada conexión aumenta el uso de la memoria. 2. Hay tres lugares donde es necesario copiar los datos: uno es almacenar los datos en el búfer, otro es sacar el paquete de datos completo del búfer y el otro es eliminar el paquete de datos del búfer.

Las desventajas de este enfoque se mencionaron anteriormente. A continuación se proporciona un método de mejora, es decir, el uso de un búfer circular. Sin embargo, este método de mejora aún no puede resolver la primera deficiencia y la primera copia de datos. Solo puede resolver la copia de datos en el tercer lugar (este lugar es el lugar donde se copia la mayor cantidad de datos). ) El segundo método de descompresión resolverá estos dos problemas.

La implementación del búfer en anillo consiste en definir dos punteros, que apuntan al principio y al final de los datos válidos, respectivamente. Al almacenar y eliminar datos, solo se mueven los punteros principal y final.

2. Utilice el búfer subyacente para descomprimir

Dado que TCP también mantiene un búfer, podemos usar completamente el búfer TCP para almacenar en caché nuestros datos, de modo que no necesitemos asignar un búfer para cada conexión. Por otro lado, sabemos que recv o wsarecv tiene un parámetro para indicar la longitud de datos que queremos recibir. Usando estas dos condiciones podemos optimizar el primer método.

Para bloquear SOCKET, podemos usar un bucle para recibir los datos de la longitud del encabezado del paquete, luego analizar la variable que representa la longitud del cuerpo del paquete y luego usar un bucle para recibir los datos de la longitud del cuerpo del paquete. El código relevante es el siguiente:

char PackageHead[1024];
char PackageContext[1024*20];

int len;
PACKAGE_HEAD *pPackageHead;
while( m_bClose == false )
{
    memset(PackageHead,0,sizeof(PACKAGE_HEAD));
    len = m_TcpSock.ReceiveSize((char*)PackageHead,sizeof(PACKAGE_HEAD));
    if( len == SOCKET_ERROR )
    {
        break;
    }
    if(len == 0)
   {
      break;
   }
    pPackageHead = (PACKAGE_HEAD *)PackageHead;
    memset(PackageContext,0,sizeof(PackageContext));
    if(pPackageHead->nDataLen>0)
    {
    len = m_TcpSock.ReceiveSize((char*)PackageContext,pPackageHead->nDataLen);
    }
 }

m_TcpSock es una variable de una clase que encapsula SOCKET. ReceiverSize se usa para recibir datos de una cierta longitud y no regresará hasta que se reciba una cierta longitud de datos o se produzca un error de red.

int winSocket::ReceiveSize( char* strData, int iLen )
{
    if( strData == NULL )
        return ERR_BADPARAM;
    char *p = strData;
    int len = iLen;
    int ret = 0;
    int returnlen = 0;
    while( len > 0)
    {
        ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0 );
        if ( ret == SOCKET_ERROR || ret == 0 )
        {
            return ret;
        }
        len -= ret;
        returnlen += ret;
    }
    return returnlen;
}

Para SOCKET sin bloqueo, como puertos de finalización, podemos enviar una solicitud para recibir datos de la longitud del encabezado. Cuando GetQueuedCompletionStatus regresa, determinamos si la longitud de los datos recibidos es igual a la longitud del encabezado del paquete. Si es igual, se envía una solicitud para recibir los datos de la longitud del cuerpo del paquete; si no es igual, se envía una solicitud para recibir los datos restantes se envía. Se utiliza un enfoque similar al recibir el cuerpo del paquete.

Autor original: Aprenda integrado juntos

Supongo que te gusta

Origin blog.csdn.net/youzhangjing_/article/details/132832826
Recomendado
Clasificación