Supongamos que tengo un cliente y un servidor que comunican números de 16 bits entre sí a través de algunos protocolos de red, por ejemplo, ModbusTCP, pero el protocolo no es relevante aquí.

Ahora sé que el endian del cliente es pequeño (mi PC) y el endian del servidor es grande (algunos PLC), el cliente está escrito completamente en C ++ con enchufes Boost Asio. Con esta configuración, pensé que tenía que cambiar los bytes recibidos del servidor para almacenar correctamente el número en una variable uint16_t, sin embargo, esto es incorrecto porque estoy leyendo valores incorrectos.

Hasta ahora, entiendo que mi abstracción de C ++ está almacenando los valores en variables correctamente sin la necesidad de que realmente me importe el intercambio o el endianness. Considera este fragmento:

// received 0x0201  (513 in big endian)
uint8_t high { 0x02 };  // first byte
uint8_t low { 0x01 };   // second byte
// merge into 16 bit value (no swap)
uint16_t val = (static_cast<uint16_t>(high)<< 8) | (static_cast<uint16_t>(low));
std::cout<<val;   //correctly prints 513

Esto me sorprendió un poco, también porque si miro la representación de la memoria con punteros, descubro que en realidad están almacenados en little endian en el cliente:

// take the address of val, convert it to uint8_t pointer
auto addr = static_cast<uint8_t*>(&val);
// take the first and second bytes and print them 
printf ("%d ", (int)addr[0]);   // print 1
printf ("%d", (int)addr[1]);    // print 2

Así que la pregunta es:

Mientras no lío con direcciones de memoria y punteros, C ++ puede garantizarme que los valores que estoy leyendo de la red son correctos sin importar el endian del servidor, ¿correcto? ¿O me falta algo aquí?

EDITAR: Gracias por las respuestas, quiero agregar que actualmente estoy usando boost::asio::write(socket, boost::asio::buffer(data)) para enviar datos desde el cliente al servidor y los datos son un std::vector<uint8_t>. Entonces, entiendo que mientras complete los datos en el orden de la red, no debería importarme la resistencia de mi sistema (o incluso del servidor para datos de 16 bits), porque estoy operando con los "valores" y no leyendo bytes directamente de memoria, ¿verdad?

Para usar la familia de funciones htons, tengo que cambiar mi capa TCP subyacente para usar memcpy o similar y un búfer de datos uint8_t*, que es más C-esque en lugar de C ++ ish, ¿Por qué debería hacerlo? ¿Hay alguna ventaja que no estoy viendo?

1
Federico Spinelli 22 jun. 2020 a las 19:04

3 respuestas

La mejor respuesta

(static_cast<uint16_t>(high)<< 8) | (static_cast<uint16_t>(low)) tiene el mismo comportamiento independientemente de la endianidad, el extremo "izquierdo" de un número siempre será el bit más significativo, la endianness solo cambia si ese bit está en el primer o el último byte.

Por ejemplo:

uint16_t input = 0x0201;
uint8_t leftByte = input >> 8; // same result regardless of endianness
uint8_t rightByte = input & 0xFF; // same result regardless of endianness
uint8_t data[2];
memcpy(data, &input, sizeof(input)); // data will be {0x02, 0x01} or {0x01, 0x02} depending on endianness

Lo mismo se aplica en la otra dirección:

uint8_t data[] = {0x02, 0x01};
uint16_t output1;
memcpy(&output1, data, sizeof(output1)); // will be 0x0102 or 0x0201 depending on endianness
uint16_t output2 = data[1] << 8 | data[0]; // will be 0x0201 regardless of endianness

Para garantizar que su código funcione en todas las plataformas, es mejor usar la familia de funciones htons y ntohs:

uint16_t input = 0x0201; // input is in host order
uint16_t networkInput = htons(input);
uint8_t data[2];
memcpy(data, &networkInput , sizeof(networkInput));
// data is big endian or "network" order
uint16_t networkOutput;
memcpy(&networkOutput, &data, sizeof(networkOutput));
uint16_t output = ntohs(networkOutput);  // output is in host order
1
Alan Birtles 22 jun. 2020 a las 16:43

Sobre Modbus:

Para palabras de 16 bits, Modbus envía primero el byte más significativo, lo que significa que usa Big-Endian, luego, si el cliente o el servidor usan Little-Endian, tendrán que intercambiar los bytes al enviar o recibir.

Otro problema es que Modbus no define en qué orden se envían los registros de 16 bits para los tipos de 32 bits.

Hay dispositivos de servidor Modbus que envían primero el registro de 16 bits más significativo y otros que hacen lo contrario. Para esto, la única solución es tener en la configuración del cliente la posibilidad de intercambiar los registros de 16 bits.

Un problema similar también puede ocurrir cuando se transmiten cadenas de caracteres, algunos servidores en lugar de enviar abcdef envían badcfe

1
Lluis Felisart 22 jun. 2020 a las 20:52

El primer fragmento de su código funciona correctamente porque no trabaja directamente con direcciones de bytes. Dicho código se compila para tener un resultado de funcionamiento correcto independientemente de la ENDIANness de su plataforma debido a la definición de los operadores '<<' y '|' por lenguaje C ++.

El segundo fragmento de su código lo demuestra, mostrando valores reales de bytes separados en su sistema little endian.

La red TCP / IP estandariza el uso del formato big-endian y proporciona las siguientes utilidades:

  • antes de enviar valores numéricos de varios bytes, utilice las funciones estándar: htonl ("host-to-network-long") y htons ("host-to-netowrk-short") para convertir sus valores en representación de red,
  • después de recibir valores numéricos de varios bytes, utilice las funciones estándar: ntohl ("red-a-host-long") y ntohs ("red-a-host-short") para convertir sus valores a su representación específica de la plataforma.

(En realidad, estas 4 utilidades hacen conversiones solo en plataformas little endian y no hacen nada en plataformas big endial. Pero usarlas siempre hace que su código sea independiente de la plataforma).

Con ASIO tiene acceso a estas utilidades utilizando: #include <boost/asio.hpp>

Puede leer más buscando el tema 'man htonl' o 'msdn htonl' en Google.

1
Daniel Z. 22 jun. 2020 a las 16:53