web-dev-qa-db-fra.com

Raisonnement derrière les sockets C sockaddr et sockaddr_storage

J'examine des fonctions telles que connect() et bind() dans les sockets C et je remarque qu'elles prennent un pointeur vers une structure sockaddr. J'ai lu et pour rendre votre application indépendante de l'AF, il est utile d'utiliser le pointeur de structure sockaddr_storage Et de le convertir en un pointeur sockaddr en raison de tout l'espace supplémentaire dont il dispose pour une plus grande adresses.

Ce que je me demande, c'est comment des fonctions comme connect() et bind() qui demandent un pointeur sockaddr vont accéder aux données à partir d'un pointeur qui pointe vers une structure plus grande que la celui qu'il attend. Bien sûr, vous lui transmettez la taille de la structure que vous lui fournissez, mais quelle est la syntaxe réelle que les fonctions utilisent pour obtenir l'adresse IP des pointeurs vers des structures plus grandes que vous avez castées en struct *sockaddr?

C'est probablement parce que je viens de OOP langues, mais cela semble être une sorte de hack et un peu désordonné.

47
Matt Vaughan

Fonctions qui attendent un pointeur sur struct sockaddr probablement transtypé le pointeur vers lequel vous les envoyez sockaddr lorsque vous leur envoyez un pointeur vers struct sockaddr_storage. De cette façon, ils y accèdent comme s'il s'agissait d'un struct sockaddr.

struct sockaddr_storage est conçu pour s'adapter à la fois à struct sockaddr_in et struct sockaddr_in6

Vous ne créez pas votre propre struct sockaddr, vous créez généralement un struct sockaddr_in ou un struct sockaddr_in6 selon la version IP que vous utilisez. Afin d'éviter d'essayer de savoir quelle version IP vous utiliserez, vous pouvez utiliser un struct sockaddr_storage qui peut contenir l'un ou l'autre. Cela sera à son tour transtypé en struct sockaddr par les fonctions connect (), bind (), etc. et accessible de cette façon.

Vous pouvez voir toutes ces structures ci-dessous (le remplissage est spécifique à l'implémentation, à des fins d'alignement):

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;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. 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];
};

Donc, comme vous pouvez le voir, si la fonction attend une adresse IPv4, elle lira simplement les 4 premiers octets (car elle suppose que la structure est de type struct sockaddr. Sinon, il lira les 16 octets complets pour IPv6).

45
theprole

Dans les classes C++ avec au moins une fonction virtuelle reçoivent un TAG. Cette balise vous permet de dynamic_cast<>() vers n'importe quelle classe dont votre classe dérive et vice versa. Le TAG est ce qui permet à dynamic_cast<>() de fonctionner. Plus ou moins, cela peut être un nombre ou une chaîne ...

En C, nous sommes limités aux structures. Cependant, les structures peuvent également se voir attribuer un TAG. En fait, si vous regardez toutes les structures que theprole postées dans sa réponse, vous remarquerez qu'elles commencent toutes par 2 octets (un court non signé) qui représente ce que nous appelons la famille de l'adresse. Cela définit exactement ce qu'est la structure et donc sa taille, ses champs, etc.

Par conséquent, vous pouvez faire quelque chose comme ceci:

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;

  }
}

Comme vous pouvez le voir, la fonction peut vérifier le paramètre len pour vous assurer que la longueur est suffisante pour s'adapter à la structure attendue et donc ils peuvent reinterpret_cast<>() (comme on l'appellerait en C++) votre aiguille. La question de savoir si les données sont correctes dans la structure dépend de l'appelant. Il n'y a pas beaucoup de choix à cette fin. Ces fonctions sont censées vérifier toutes sortes de choses avant d'utiliser les données et renvoyer -1 et errno chaque fois qu'un problème est détecté.

Donc, en fait, vous avez un struct sockaddr_in Ou struct sockaddr_in6 Que vous (réinterprétez) cast en un struct sockaddr Et la fonction bind() (et d'autres) cast ce pointeur revenir à un struct sockaddr_in ou struct sockaddr_in6 après avoir vérifié le membre sa_family et vérifié la taille.

10
Alexis Wilke