C语言中sockets的sockaddr和sockaddr_storage背后的原理

63

我正在研究C sockets中的connect()bind()等函数,注意到它们需要一个指向sockaddr结构的指针。阅读了一些资料后发现,为了让应用程序与AF无关,使用sockaddr_storage结构体指针并将其转换为sockaddr指针是有用的,因为它具有更大的地址空间。

我想知道的是,像connect()bind()这样要求sockaddr指针的函数如何访问来自指向比其预期的结构体更大的结构体的指针数据。当然,您会将提供给它的结构体的大小传递给它,但这些函数实际上使用的语法是什么,以便从您转换为struct * sockaddr的较大结构体的指针中获取IP地址?

也许是因为我来自面向对象编程语言,所以这似乎有点像黑客技巧并且有点凌乱。

2个回答

66

如果你给那些需要指向struct sockaddr指针的函数发送了一个struct sockaddr_storage指针,那么这些函数可能会将你发送的指针强制类型转换为sockaddr指针,以便像访问struct sockaddr一样访问它。

struct sockaddr_storage被设计用于适应struct sockaddr_instruct sockaddr_in6两种结构体。

你不需要创建自己的struct sockaddr,通常你会根据使用的IP版本创建一个struct sockaddr_instruct sockaddr_in6。为避免尝试知道将要使用的IP版本,你可以使用struct sockaddr_storage来保存二者。然后由connect()、bind()等函数将其强制转换为struct sockaddr并进行访问。

以下是所有这些结构体(填充是实现特定的,用于对齐):

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];
};

如您所见,如果该函数期望 IPv4 地址,则它将仅读取前 4 个字节(因为它假定结构体为类型 struct sockaddr)。否则,它将读取完整的 16 个字节以获取 IPv6 地址。


1
所以假设我有一个指向 struct sockaddr_storage 的指针,名为 sas,并且结构中的所有字段都已经正确填充了地址和地址族。现在我执行以下操作:struct sockaddr *s = (struct sockaddr*)sas。现在我要如何从 s 中获取地址呢? - Matt Vaughan
1
你不会直接使用 struct sockaddr。你需要将其强制转换回 sockaddr_storagesockaddr_in,然后再读取它。正如你在我的帖子中看到的那样,sockaddr 有足够的空间来容纳 IPv4 或 v6 地址。话虽如此,我不知道为什么你想要将其强制转换为 struct sockaddr,除了那些需要该类型参数的函数之外。struct sockaddr 不是用于“程序员使用”的。 - theprole
5
我正在阅读一本书,它说 sockaddr 不够大以容纳 sockaddr_in6。 - Matt Vaughan
3
sockaddr的大小为16字节,而sockaddr_storage的大小为128字节。由于IPv6地址为16字节,很难看出sockaddr如何容纳IPv6地址。 - piedpiper
1
@ashu 因为 sockaddr 的 2 个字节被用于存储 sa_family,所以剩下的 14 个字节无法容纳 IPv6 地址。 - Prakhar Agrawal
显示剩余2条评论

11

在C++中,至少有一个虚函数的类会被赋予一个标签(TAG)。该标签允许您对任何派生自您的类的类进行dynamic_cast<>()操作,反之亦然。这个标签是使dynamic_cast<>()工作的关键。大体上来说,这可以是一个数字或字符串...

在C语言中,我们只能使用结构体。然而,结构体也可以被赋予一个标签(TAG)。事实上,如果你看一下theprole在他的答案中发布的所有结构体,你会注意到它们都以2个字节(一个无符号短整型)开始,表示我们所谓的地址族(family)。这定义了结构体的确切内容,因此也就定义了它的大小、字段等。

因此,您可以像这样做:

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;

  }
}

正如你所看到的,该函数可以检查len参数以确保长度足够适应预期的结构,因此它们可以对指针进行reinterpret_cast<>()转换(在C++中称为这样)。数据在结构中是否正确由调用方决定,在这方面没有太多选择余地。这些函数在使用数据之前都需要验证各种事情,并在发现问题时返回-1和errno

因此,实际上你有一个struct sockaddr_instruct sockaddr_in6,你将其(reinterpret)转换为struct sockaddr,而bind()函数(及其他函数)会在检查了sa_family成员并验证了大小后将该指针重新转换为struct sockaddr_instruct sockaddr_in6


虽然这不是一个C++问题,但也许你可以澄清一下,并非所有的C++类都有TAG,只有至少有一个虚函数的类才有。 - MikeMB
1
@MikeMB,原帖作者写道:“可能是因为我来自面向对象编程语言”,我想这意味着他想了解C语言与他之前学习的内容有何不同。 - Alexis Wilke
抱歉,我可能应该表达得更清楚:你提到C++方面我没有任何问题。我只是想确保,尽管这不是针对C++程序员的问题,但涉及到C++的部分尽可能精确(以防像我一样的C++初学者会遇到困难)。 - MikeMB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接