【问题标题】:What is the correct way to convert a struct sockaddr * to struct sockaddr_in6 * with valid C code?使用有效的 C 代码将 struct sockaddr * 转换为 struct sockaddr_in6 * 的正确方法是什么?
【发布时间】:2017-01-18 20:32:35
【问题描述】:

这是一个简单的程序,展示了我们在编写套接字程序时通常如何将 struct sockaddr * 类型转换为 struct sockaddr_in *struct sockaddr_in6 *

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int main()
{
    struct addrinfo *ai;

    printf("sizeof (struct sockaddr): %zu\n", sizeof (struct sockaddr));
    printf("sizeof (struct sockaddr_in): %zu\n", sizeof (struct sockaddr_in));
    printf("sizeof (struct sockaddr_in6): %zu\n", sizeof (struct sockaddr_in6));

    if (getaddrinfo("localhost", "http", NULL, &ai) != 0) {
        printf("error\n");
        return EXIT_FAILURE;
    }

    if (ai->ai_family == AF_INET) {
        struct sockaddr_in *addr = (struct sockaddr_in *) ai->ai_addr;
        printf("IPv4 port: %d\n", addr->sin_port);
    } else if (ai->ai_family == AF_INET6) {
        struct sockaddr_in6 *addr = (struct sockaddr_in6 *) ai->ai_addr;
        printf("IPv6 port: %d\n", addr->sin6_port);
    }

    return 0;
}

Beej's Guide to Network Programming 也在第 10 页推荐了这个。

为了处理 struct sockaddr,程序员创建了一个并行结构:struct sockaddr_in(“in”代表“Internet”)用于 IPv4。

这是重要的一点:指向 struct sockaddr_in 的指针可以转换为指向 struct sockaddr 的指针,反之亦然。所以即使 connect() 想要一个 struct sockaddr*,你仍然可以使用一个 struct sockaddr_in 并在最后一刻强制转换它!

但从another question 的讨论来看,这似乎只是一个 hack,不是 C 标准中有效的 C 代码。

特别是,请参阅提到的AnT's answer

至于在 struct sockaddr *、struct sockaddr_in * 和 struct sockaddr_in6 * 之间进行强制转换的流行技术——这些只是与 C 语言无关的技巧。它们只是在实践中起作用,但就 C 语言而言,该技术是无效的。

因此,如果我们用来进行套接字编程的这种技术(以及书籍也推荐的技术)是无效的,那么重写上述代码使其也是符合 C 标准的有效 C 代码的有效方法是什么?

【问题讨论】:

  • Beej 的网络编程指南 在网络编程技术方面通常是坚如磐石的。如果包含的内容完全不正确,这种情况很少见。
  • 你的报价中提到的方式是接口的意图。它已经持续了三十多年。没有其他必要了。

标签: c sockets struct casting


【解决方案1】:

POSIX 标准保证指向任何类型套接字的指针都可以转换为struct sockaddr*。因此,您可以将指向任何类型套接字的指针转换为struct sockaddr*,以便在bind()connect() 中使用它;图书馆知道要检查哪些位。您还可以检查套接字的sa_family 字段以查看它的真实内容,假设它包含有效数据,然后转换为适当的指针类型。如果您需要分配足够大的内存块来安全地存储任何类型的套接字,请使用sockaddr_storage。从sockaddr_storage* 到任何其他套接字指针的转换保证正确对齐,并且包含套接字系列的字段保证仍然有效。

要从 sockaddr_in 获取 IPv6 套接字,您可以将 IPv4 地址转换为 IPv6 表示法并使用 getaddrinfo()。但是,现代查找函数可能会为您提供一个包含 IPv4 和 IPv6 套接字的链表。

【讨论】:

    【解决方案2】:

    答案在man getaddrinfosys/socket.hman getaddrinfo 提供了使用常见 struct sockaddr 的原因:

    Given node and service, which identify an Internet host and a service, 
    getaddrinfo() returns one or more addrinfo structures, each of which 
    contains an Internet address that can be specified in a call to bind(2) 
    or connect(2). The getaddrinfo() function combines the functionality 
    provided by the gethostbyname(3) and getservbyname(3) functions into a 
    single interface, but unlike the latter functions, getaddrinfo() is 
    reentrant and allows programs to eliminate IPv4-versus-IPv6 dependencies.
    

    只有一个struct sockaddr。似乎各种类型都只是在透明联合中使用,以提供所需的任何struct sockaddr_X。例如:

    /* This is the type we use for generic socket address arguments.
    
       With GCC 2.7 and later, the funky union causes redeclarations or
       uses with any of the listed types to be allowed without complaint.
       G++ 2.7 does not support transparent unions so there we want the
       old-style declaration, too.  */
    #if defined __cplusplus || !__GNUC_PREREQ (2, 7) || !defined __USE_GNU
    # define __SOCKADDR_ARG         struct sockaddr *__restrict
    # define __CONST_SOCKADDR_ARG   const struct sockaddr *
    #else
    /* Add more `struct sockaddr_AF' types here as necessary.
       These are all the ones I found on NetBSD and Linux.  */
    # define __SOCKADDR_ALLTYPES \
      __SOCKADDR_ONETYPE (sockaddr) \
      __SOCKADDR_ONETYPE (sockaddr_at) \
      __SOCKADDR_ONETYPE (sockaddr_ax25) \
      __SOCKADDR_ONETYPE (sockaddr_dl) \
      __SOCKADDR_ONETYPE (sockaddr_eon) \
      __SOCKADDR_ONETYPE (sockaddr_in) \
      __SOCKADDR_ONETYPE (sockaddr_in6) \
      __SOCKADDR_ONETYPE (sockaddr_inarp) \
      __SOCKADDR_ONETYPE (sockaddr_ipx) \
      __SOCKADDR_ONETYPE (sockaddr_iso) \
      __SOCKADDR_ONETYPE (sockaddr_ns) \
      __SOCKADDR_ONETYPE (sockaddr_un) \
      __SOCKADDR_ONETYPE (sockaddr_x25)
    
    # define __SOCKADDR_ONETYPE(type) struct type *__restrict __##type##__;
    typedef union { __SOCKADDR_ALLTYPES
                } __SOCKADDR_ARG __attribute__ ((__transparent_union__));
    # undef __SOCKADDR_ONETYPE
    # define __SOCKADDR_ONETYPE(type) const struct type *__restrict __##type##__;
    typedef union { __SOCKADDR_ALLTYPES
                } __CONST_SOCKADDR_ARG __attribute__ ((__transparent_union__));
    # undef __SOCKADDR_ONETYPE
    #endif
    

    虽然我没有涉足所有的宏汤,但看起来你对任何一种类型都是安全的。

    【讨论】:

      【解决方案3】:

      因此,如果我们进行套接字编程的方式(以及书籍也推荐的方式)是一种 hack,那么根据 C 标准重写上述代码以使其也是有效的 C 代码的正确方法是什么?

      TL;DR:继续执行您在示例中介绍的内容。

      您提供的代码在语法上似乎是正确的。在某些情况下,它可能会或可能不会表现出未定义的行为。会不会,取决于getaddrinfo()的行为。

      在 C 中没有办法做到这一点,既能满足所有功能要求,又能比您介绍的标准技术更好地防止未定义的行为。这就是为什么它是标准技术。这里的问题是该函数必须支持所有可能的地址类型,包括尚未定义的类型。它可以将套接字地址指针声明为void *,这不需要强制转换,但这实际上不会改变任何给定程序是否表现出未定义行为的任​​何事情。

      就其本身而言,getaddrinfo() 的设计正是考虑到了这种用法,因此如果在结果上使用预期的强制转换会导致不当行为,这就是 它的 问题。此外,getaddrinfo() 不是 C 标准库的一部分——它(仅)由 POSIX 标准化,它也包含 C 标准。因此,仅根据 C 分析该功能表明了不适当的超焦点。尽管转换仅针对 C 引起了一些关注,但您应该期望在 getaddrinfo() 和其他使用 struct sockaddr * 的 POSIX 网络函数的上下文中,转换为正确的特定地址类型并访问引用的对象会产生可靠的结果。

      此外,我认为 AnT 对您的另一个问题的回答过于简单且过于消极。我正在考虑是否写一个对比鲜明的答案。

      【讨论】:

      • AnT 答案下方的 cmets 完全支持这一点,并指出所有 struct sockaddr * 类型都是通过我引用的透明联合解决的。考虑到它显然引起的误解,可能值得你写一个对比鲜明的答案。 (我并没有声称对它理解得足够好来公正地对待它)
      • @DavidC.Rankin, new answer 添加到另一个问题。
      【解决方案4】:

      参考此链接和其他链接 Is it legal to type-cast pointers of different struct types (e.g. struct sockaddr * to struct sockaddr_in6 *)? 。 这些不完全是黑客。 做你想做的事,如果理解正确,我会做类似的事情:

      struct base
      {
          int a;
          char b;
          double *n;
      }
      struct derived 
      {
        struct base b; //(no pointer, but the whole struct)
        int c;
        int d;
      }
      

      这样,当您从派生转换为基时,您可以确定派生的前 n 个字节与基完全重叠。该代码有效并且完全可移植。 不同的问题不同的解决方案。实际上,根据我的经验,我曾经更喜欢 base 包含派生,而不是反之亦然。所以要有一个“多态”的结构。但是 1)如果它有效, 2)人们会阅读代码会理解 3)你觉得有用......为什么不呢?听你的。可能c ++正是以这种方式实现了阻碍!谁能说出来?
      请注意它们的数组,使用正确的类型进行索引,并小心地将它们放在首位。 (但 C++ 对多态对象数组也有问题,它可以只使用它们的指针)

      【讨论】:

      • 我不明白这与我的问题有什么关系。 sockaddr_in6 不包含 sockaddr 对象作为其第一个成员。 sockaddr_in 也没有。
      • 它有效吗?据我所知,您不能只与相同的数据类型重叠。由于对齐要求,结构可以大于包含的数据类型。例如,如果我有一个包含两个字段的结构 char sizeof() 将返回 4 而不是 2。如果您在另一个结构中重写结构的头部,则包含另外 2 个字符的结构将返回 4。它不会完全重叠另一个......但是,所以......这很奇怪。我的主要问题是你不能声明它们的数组(但这也在 C++ 中,这就是为什么我告诉你多态 obj 的基类
      • 你必须在基类中声明所有的变量和函数,否则你不能声明它们的数组)这不直观,总之它看起来并不健壮......
      • 是的,它有效。这就是整个问题的全部内容。
      • 不,这是行不通的。或者更好的是,其中隐藏了一些非常糟糕的错误,而且它绝对不可移植。他们可能很幸运,因为不需要对齐。但是如果你使用一个必须对齐的结构,并且如果你调用 memcpy(derived_ptr, base_ptr, sizeof(struct base)) 你会搞砸derived_obj。还有 memset() 和 mem... 函数。你也会用 (struct base)derived_obj=base_obj 搞砸一切。所以使用它的人一定很清楚。
      猜你喜欢
      • 1970-01-01
      • 2014-06-17
      • 2016-10-07
      • 2023-03-05
      • 1970-01-01
      • 1970-01-01
      • 2011-04-11
      • 1970-01-01
      • 2013-09-07
      相关资源
      最近更新 更多