2011-11-03 11 views
7

Tengo una pequeña aplicación de servidor cliente en la que deseo enviar una estructura completa a través de un socket TCP en C no en C++. Supongamos que la estructura a ser las siguientes:pasando una estructura sobre el socket TCP (SOCK_STREAM) en C

struct something{ 
int a; 
char b[64]; 
float c; 
} 

he encontrado muchos mensajes diciendo que necesito utilizar el paquete pragma o para serializar los datos antes de enviarlos y recieveing.

Mi pregunta es, ¿es suficiente usar JUST pragma pack o simplemente serialzation? ¿O necesito usar ambos? Como la serialización es un proceso intensivo del procesador, esto hace que su rendimiento caiga drásticamente, entonces ¿cuál es la mejor manera de serializar una estructura SIN usar una biblioteca externa (me encantaría un ejemplo de código/algo)?

Respuesta

13

necesita lo siguiente para enviar de forma portátil de estructura través de la red:

  • paquete de la estructura. Para compiladores gcc y compatibles, haz esto con __attribute__((packed)).

  • No utilice ningún miembro que no sean enteros sin signo de tamaño fijo, otras estructuras empaquetadas que satisfagan estos requisitos o arreglos de alguno de los primeros. Los enteros con signo también son correctos, a menos que su máquina no use una representación de complemento de dos.

  • Decida si su protocolo usará codificación little-o big-endian de enteros. Haga conversiones al leer y escribir esos enteros.

  • Además, no tome los punteros de los miembros de una estructura empaquetada, excepto para aquellos con el tamaño 1 u otras estructuras empaquetadas anidadas. Ver this answer.

A continuación se muestra un ejemplo simple de codificación y decodificación.Supone que las funciones de conversión de orden de bytes hton8(), ntoh8(), hton32() y ntoh32() están disponibles (las dos anteriores son no operativas, pero hay coherencia).

#include <stdint.h> 
#include <inttypes.h> 
#include <stdlib.h> 
#include <stdio.h> 

// get byte order conversion functions 
#include "byteorder.h" 

struct packet { 
    uint8_t x; 
    uint32_t y; 
} __attribute__((packed)); 

static void decode_packet (uint8_t *recv_data, size_t recv_len) 
{ 
    // check size 
    if (recv_len < sizeof(struct packet)) { 
     fprintf(stderr, "received too little!"); 
     return; 
    } 

    // make pointer 
    struct packet *recv_packet = (struct packet *)recv_data; 

    // fix byte order 
    uint8_t x = ntoh8(recv_packet->x); 
    uint32_t y = ntoh32(recv_packet->y); 

    printf("Decoded: x=%"PRIu8" y=%"PRIu32"\n", x, y); 
} 

int main (int argc, char *argv[]) 
{ 
    // build packet 
    struct packet p; 
    p.x = hton8(17); 
    p.y = hton32(2924); 

    // send packet over link.... 
    // on the other end, get some data (recv_data, recv_len) to decode: 
    uint8_t *recv_data = (uint8_t *)&p; 
    size_t recv_len = sizeof(p); 

    // now decode 
    decode_packet(recv_data, recv_len); 

    return 0; 
} 

En lo que se refiere a las funciones de conversión de orden de bytes, de su sistema de htons()/ntohs() y htonl()/ntohl() se puede utilizar, para los enteros de 16 y 32 bits, respectivamente, para convertir a/desde big-endian. Sin embargo, no conozco ninguna función estándar para enteros de 64 bits, o para convertir a/desde little endian. Puede usar my byte order conversion functions; si lo hace, debe indicar el orden de bytes de su máquina definiendo BADVPN_LITTLE_ENDIAN o BADVPN_BIG_ENDIAN.

En lo que respecta a los enteros con signo, las funciones de conversión se pueden implementar de forma segura como las que escribí y vinculé (intercambiando bytes directamente); simplemente cambie unsigned a signed.

ACTUALIZACIÓN: si quieres un protocolo binario eficiente, pero no le gusta tocar el violín con los bytes, puede intentar algo así como Protocol Buffers (C implementation). Esto le permite describir el formato de sus mensajes en archivos separados y genera el código fuente que usa para codificar y decodificar mensajes del formato que especifique. También implementé algo similar, pero muy simplificado; vea my BProto generator y some examples (busque en los archivos .bproto, y addr.h para el ejemplo de uso).

+1

probaré este método, solo quiero preguntar qué pasa si solo uso sprintf y escribo todos los datos en cadena usando un delimitador para separar los elementos de la estructura y enviarlos a través del socket y luego uso strtok para extraer cada elemento el otro lado ? ¿Esta sería una solución viable también? – user434885

+0

sí, sprintf funcionará, pero * solo * para enteros; si quisiera enviar una cadena (es decir, una matriz de bytes sin procesar), con este método, tendría que tratarlos como una matriz de bytes y convertir cada byte en un entero, insertando espacios entre ellos. Por ejemplo, "abc" se enviaría como "97 98 99". Esto podría ser preferible porque de alguna manera es más fácil de analizar cuando se depura, pero es difícil codificar/decodificar, especialmente si desea una verificación completa de errores al decodificar. –

+0

¿Cuál es la motivación detrás de su segundo punto, solo que usa enteros sin signo? ¿Por qué no se pueden usar caracteres en la estructura (o matrices de caracteres) para enviar letras, bytes o cadenas? – aaronsnoswell

1

Se podría utilizar un union con la estructura que desea enviar y una matriz:

union SendSomething { 
    char arr[sizeof(struct something)]; 
    struct something smth; 
}; 

De esta manera usted puede enviar y recibir solo arr. Por supuesto, debe tener cuidado con los problemas de endianess y sizeof(struct something) puede variar en todas las máquinas (pero puede solucionarlo fácilmente con un #pragma pack).

2

Antes de enviar datos a través de una conexión TCP, resuelva una especificación de protocolo. No tiene que ser un documento de varias páginas lleno de jerga técnica. Pero tiene que especificar quién transmite qué y cuándo debe especificar todos los mensajes en el nivel de bytes. Debe especificar cómo se establecen los extremos de los mensajes, si hay tiempos de espera y quién los impone, y así sucesivamente.

Sin una especificación, es fácil hacer preguntas que son simplemente imposibles de responder. Si algo sale mal, ¿cuál es el fallo? Con una especificación, el extremo que no siguió la especificación tiene la culpa. (Y si ambos extremos siguen la especificación y todavía no funciona, la especificación es defectuosa).

Una vez que tiene una especificación, es mucho más fácil responder preguntas sobre cómo debe diseñarse un extremo u otro.

También recomiendo encarecidamente no diseñando un protocolo de red en torno a los detalles de su hardware. Al menos, no sin un problema de rendimiento comprobado.

1

¿Por qué harías esto cuando existen bibliotecas de serialización buenas y rápidas como Message Pack que hacen todo el trabajo por ti, y como un bono te ofrecen compatibilidad en varios idiomas del protocolo de socket?

Use Message Pack o alguna otra biblioteca de serialización para hacer esto.

+0

No tengo permitido utilizar ninguna biblioteca externa. :/ – user434885

0

El paquete Pragma se utiliza para la compatibilidad binaria de su estructura en otro extremo. Porque el servidor o el cliente al que envía la estructura puede escribirse en otro idioma o construirse con otro compilador c o con otras opciones del compilador c.

Serialización, según tengo entendido, está haciendo una secuencia de bytes desde su estructura. Cuando escribes struct en el socket haces serialización.

2

Depende de si puede estar seguro de que sus sistemas en cualquiera de los extremos de la conexión son homogéneos o no. Si está seguro, todo el tiempo (cosa que la mayoría de nosotros no podemos hacer), puede tomar algunos atajos, pero debe tener en cuenta que son accesos directos.

struct something some; 
... 
if ((nbytes = write(sockfd, &some, sizeof(some)) != sizeof(some)) 
    ...short write or erroneous write... 

y el análogo read().

Sin embargo, si hay alguna posibilidad de que los sistemas sean diferentes, entonces necesita establecer cómo se transferirán los datos formalmente. Es posible que linealices (serialices) los datos, posiblemente fancilies con algo así como ASN.1 o, probablemente, más simplemente con un formato que pueda ser releído fácilmente. Para eso, el texto suele ser beneficioso: es más fácil depurarlo cuando puede ver lo que está pasando mal. En su defecto, debe definir el orden de bytes en el que se transfiere int y asegurarse de que la transferencia sigue ese orden, y la cadena probablemente obtenga un conteo de bytes seguido de la cantidad apropiada de datos (considere transferir un terminal nulo o no), y luego alguna representación del flotador. Esto es más complicado. No es tan difícil escribir funciones de serialización y deserialización para manejar el formato. La parte difícil es diseñar (decidir sobre el protocolo).

+0

esto funcionaría en algunos casos, pero hay buenas probabilidades de que mi servidor y cliente sean máquinas de 32 y 64 bits, por lo que el tamaño de la función (struct) devolverá valores diferentes en cualquier tamaño, ya que el tamaño de int aumentará de 4 bytes a 8 bytes. – user434885

1

Por lo general, la serialización ofrece varios beneficios, por ej. enviar los bits de la estructura por el cable (con, por ejemplo, fwrite).

  1. Sucede de forma individual para cada dato atómico no agregado (por ejemplo, int).
  2. Define con precisión el formato de datos en serie enviado por el cable
  3. Por lo tanto, se trata de una arquitectura heterogénea: las máquinas de envío y recepción pueden tener diferente longitud de palabra y endianidad.
  4. Puede ser menos frágil cuando el tipo cambia un poco. Por lo tanto, si una máquina tiene una versión anterior de su código en ejecución, es posible que pueda hablar con una máquina con una versión más reciente, p. uno que tiene un char b[80]; en lugar de char b[64];
  5. Se puede tratar con estructuras de datos más complejas vectores -Variable de tamaño, o incluso de hash-Tablas- con una forma lógica (para la tabla hash, transmitir la asociación, ..)

Muy a menudo, se generan las rutinas de serialización. Incluso hace 20 años, RPCXDR ya existía para ese propósito, y las primitivas de serialización XDR todavía están en muchos libc.

0

Si necesita portabilidad, debe serializar cada miembro individualmente debido a la endianess y el relleno de la estructura.

Aquí hay un ejemplo usando Binn:

binn *obj; 

    // create a new object 
    obj = binn_object(); 

    // add values to it 
    binn_object_set_int32(obj, "id", 123); 
    binn_object_set_str(obj, "name", "Samsung Galaxy Charger"); 
    binn_object_set_double(obj, "price", 12.50); 
    binn_object_set_blob(obj, "picture", picptr, piclen); 

    // send over the network 
    send(sock, binn_ptr(obj), binn_size(obj)); 

    // release the buffer 
    binn_free(obj); 

Está a sólo 2 archivos (binn.c y binn.h) para que pueda ser compilado con el proyecto en lugar de utilizar como una biblioteca compartida.

Quizás también deba utilizar el encuadre de mensaje (también conocido como encuadre de prefijo de longitud) en las secuencias de socket.

Cuestiones relacionadas