Razonamiento detrás de los zócalos C sockaddr y sockaddr_storage

Estoy viendo funciones como connect() y bind() en los sockets C y observo que llevan un puntero a una estructura sockaddr . He estado leyendo y para hacer que su aplicación sea independiente de AF, es útil usar el puntero de estructura sockaddr_storage y convertirlo en un puntero sockaddr debido a todo el espacio adicional que tiene para direcciones más grandes.

Lo que me pregunto es cómo funcionan las funciones como connect() y bind() que solicitan un puntero sockaddr para acceder a los datos desde un puntero que apunta a una estructura más grande que la que está esperando. Claro, le pasa el tamaño de la estructura que le está proporcionando, pero ¿cuál es la syntax real que utilizan las funciones para que la dirección IP pase de los punteros a las estructuras más grandes que haya convertido en struct *sockaddr ?

Probablemente sea porque vengo de lenguajes OOP, pero parece una especie de truco y un poco desordenado.

Las funciones que esperan un puntero a struct sockaddr probablemente encierran el puntero que les envía a sockaddr cuando les envía un puntero a struct sockaddr_storage . De esa manera, acceden a él como si fuera una struct sockaddr .

struct sockaddr_storage está diseñado para encajar tanto en struct sockaddr_in como en struct sockaddr_in6

No creas tu propia struct sockaddr , normalmente creas una struct sockaddr_in o una struct sockaddr_in6 dependiendo de la versión de IP que estés usando. Para evitar intentar saber qué versión de IP utilizará, puede usar una struct sockaddr_storage que puede contener cualquiera de las dos. A su vez, esto se tipificará en la struct sockaddr mediante las funciones connect (), bind (), etc. y se accederá de esa manera.

Puede ver todas estas estructuras a continuación (el relleno es específico de la implementación, para propósitos de alineación):

 struct sockaddr { unsigned short sa_family; // address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address }; struct sockaddr_in { short sin_family; // eg AF_INET, AF_INET6 unsigned short sin_port; // eg htons(3490) struct in_addr sin_addr; // see struct in_addr, below char sin_zero[8]; // zero this if you want to }; struct sockaddr_in6 { u_int16_t sin6_family; // address family, AF_INET6 u_int16_t sin6_port; // port number, Network Byte Order u_int32_t sin6_flowinfo; // IPv6 flow information struct in6_addr sin6_addr; // IPv6 address u_int32_t sin6_scope_id; // Scope ID }; struct sockaddr_storage { sa_family_t ss_family; // address family // all this is padding, implementation specific, ignore it: char __ss_pad1[_SS_PAD1SIZE]; int64_t __ss_align; char __ss_pad2[_SS_PAD2SIZE]; }; 

Entonces, como puede ver, si la función espera una dirección IPv4, solo leerá los primeros 4 bytes (porque se supone que la estructura es de tipo struct sockaddr . De lo contrario, leerá los 16 bytes completos para IPv6).

En C ++, las clases con al menos una función virtual reciben un TAG. Esa etiqueta le permite dynamic_cast<>() a cualquiera de las clases de las que deriva su clase y viceversa. El TAG es lo que permite que dynamic_cast<>() funcione. Más o menos, esto puede ser un número o una cadena …

En C estamos limitados a las estructuras. Sin embargo, a las estructuras también se les puede asignar un TAG. De hecho, si observa todas las estructuras que el papel publicó en su respuesta, notará que todas comienzan con 2 bytes (un corto sin firmar) que representa lo que llamamos la familia de la dirección. Esto define exactamente cuál es la estructura y, por lo tanto, su tamaño, campos, etc.

Por lo tanto puedes hacer algo como esto:

 int bind(int fd, struct sockaddr *in, socklen_t len) { switch(in->sa_family) { case AF_INET: if(len < sizeof(struct sockaddr_in)) { errno = EINVAL; // wrong size return -1; } { struct sockaddr_in *p = (struct sockaddr_in *) in; ... } break; case AF_INET6: if(len < sizeof(struct sockaddr_in6)) { errno = EINVAL; // wrong size return -1; } { struct sockaddr_in6 *p = (struct sockaddr_in6 *) in; ... } break; [...other cases...] default: errno = EINVAL; // family not supported return -1; } } 

Como puede ver, la función puede verificar el parámetro len para asegurarse de que la longitud sea suficiente para adaptarse a la estructura esperada y, por lo tanto, pueden reinterpret_cast<>() (como se llamaría en C ++) su puntero. Si los datos son correctos en la estructura depende de la persona que llama. No hay mucha elección en ese extremo. Se espera que estas funciones verifiquen todo tipo de cosas antes de usar los datos y devuelvan -1 y errno cada vez que se encuentre un problema.

Entonces, en efecto, tiene una struct sockaddr_in o struct sockaddr_in6 que (reinterpretar) convierte en una struct sockaddr y la función bind() (y otras) lanza ese puntero a una struct sockaddr_in o struct sockaddr_in6 después de que verificaron el miembro sa_family y verificado el tamaño.