如何使用有效的C代码将struct sockaddr *转换为struct sockaddr_in6 *的正确方法?

3

这是一个简单的程序,展示了我们在编写套接字程序时通常如何将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,并在最后一刻进行强制类型转换!

但从另一个问题的讨论中,似乎这只是一个hack,不符合C标准的有效C代码。

特别是请参见AnT的答案,其中提到:

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

因此,如果我们用于套接字编程的这种技术(并且也被书籍推荐)是无效的,那么重写上述代码的有效C代码方式是什么?


《Beej's网络编程指南》通常在网络编程技术方面非常可靠。如果其中的内容是明显不当的,那将是罕见的情况。 - undefined
你报价中提到的方式正是接口所期望的。这种方式已经持续了三十多年。没有其他必要的东西。 - undefined
4个回答

4
如果我们目前所采用的套接字编程方式(以及书籍中推荐的方式)是一种hack,那么重写上述代码的正确方式是什么?以满足C标准并且避免未定义行为?
简而言之:请继续按照您示例中的方式做。
您提供的代码在语法上似乎是正确的。在某些情况下,它可能会表现出未定义行为。它是否会这样取决于getaddrinfo()的行为。
在C中,没有方法既可以满足所有的功能要求,又比您所示范的标准技术更好地保护免受未定义的行为。这就是为什么它是标准技术的原因。问题在于该函数必须支持所有可想象的地址类型,包括尚未定义的类型。它可以将套接字地址指针声明为void *,这样就不需要转换,但这实际上不会改变任何给定程序是否表现出未定义行为的事实。
对于getaddrinfo ()本身而言,它设计时考虑到了这种用法,因此如果使用预期的强制转换结果导致错误行为,则是它自己的问题。此外,getaddrinfo()不是C标准库的一部分,它只是由POSIX标准化,而后者也包含了C标准。因此,仅根据C分析该函数会导致不适当的过度关注。尽管强制转换在C中引起一些关注,但是可以预期,在getaddrinfo()和其他使用struct sockaddr*的POSIX网络函数的上下文中,将其转换为正确的特定地址类型并访问所引用的对象能够产生可靠的结果。
此外,我认为AnT对您另一个问题的答案过于简化和过于消极。我正在考虑是否要写一个对比回答。

下面对AnT的回答的评论完全支持这一点,并引用了我引用的透明联合体来解决所有struct sockaddr *类型。也许你可以写一个对比的答案,因为它显然引起了误解。(我不敢说我理解得足够好以做出公正的评价) - undefined
@DavidC.Rankin,对于其他问题,添加了一个新答案 - undefined

1
POSIX标准保证了任何类型的套接字指针都可以转换为struct sockaddr*。因此,您可以将任何类型的套接字指针转换为struct sockaddr*以在bind()connect()中使用;库知道要检查哪些位。您还可以检查套接字的sa_family字段以查看它实际上是什么,假设它包含有效数据,然后将其转换为适当的指针类型。如果需要分配足够大的内存块来安全地存储任何类型的套接字,请使用sockaddr_storage。从sockaddr_storage*到任何其他套接字指针的转换保证对齐正确,并且包含套接字族的字段保证仍然起作用。
要从sockaddr_in获取IPv6套接字,可以将IPv4地址转换为IPv6表示法并使用getaddrinfo()。但是,现代查找函数可能会给您提供包括IPv4和IPv6套接字的链表。

0
答案在man getaddrinfosys/socket.h中。 man 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

参考这个链接和其他链接在不同的结构类型(例如: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个字节与基类完全重叠。这段代码能够正常工作并且具有完全可移植性。 不同的问题有不同的解决方案。根据我的经验,我更喜欢基类包含派生类,而不是相反。这样可以实现“多态”结构。但是,如果它能够正常工作、其他人能够理解代码并且你觉得有用...为什么不呢?这完全取决于你。可能C++就是按照这种方式来实现继承的!谁能说得准呢?
只需小心处理数组,使用正确的类型进行索引,并始终将其放在第一位。 (但是C++对多态对象数组也有问题,只能存储指针)


而且它能正常工作吗?据我所知,你不能简单地重叠相同的数据类型。由于对齐要求,一个结构体可能比包含它的数据类型更大。例如,如果我有一个包含两个char字段的结构体,sizeof()将返回4而不是2。如果你在另一个结构体中重新定义一个结构体的头部,该结构体包含另外两个char,它将始终返回4。它不会完全重叠其他部分...但是,这很奇怪。我的主要疑问是你不能声明它们的数组(但这也适用于C++,这就是为什么我告诉你多态对象的基类)。 - undefined
你必须在基类中声明所有的变量和函数,否则无法声明它们的数组。这并不直观,总之看起来不够健壮... - undefined
不,它不起作用。或者更好的说,其中隐藏了一些非常糟糕的错误,并且它绝对不可移植。他们可能运气好,因为没有要求对齐。但是如果你使用一个必须对齐的结构体,并且调用memcpy(derived_ptr, base_ptr, sizeof(struct base)),你将会弄乱derived_obj。同样适用于memset()和mem...函数。你也会弄乱所有东西,包括(struct base)derived_obj=base_obj。所以使用它的人必须非常小心。 - undefined
我告诉过你哪些是弱点。C语言的哲学是将全部责任交给程序员。所以我也会做同样的事情... - undefined
所以你失去了另一种优化代码的方式。如果有人要使用这段代码,必须意识到它的弱点。所以我会小心地使用这种编码方式。可能sockaddr只是在代码中声明了一个结构。所以这些操作是不必要的。但是如果它们必须包含5k个图形渲染的形状,或者20k个解析树的方程,如果你不能使用数组或者内存拷贝,那么性能问题就会出现。 - undefined
显示剩余8条评论

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