前言
最近在学习网络相关的知识,虽然之前代码写了不少,但是长时间不写难免会忘记,简单地复习了一下IO多路复用的方式,对比了解了一下epoll模式和select模式的异同,不过写代码的时候发现,这个socket连接中有几个结构还是挺让人头大的,用着用着突然就强转成其他的类型了,加上年前改了半天IPv6的连接,这几个结构体更加混乱,所以今天角色放到一起,从源码的角度看一下sockaddr、sockaddr_in、sockaddr_in6这三个结构体之间的联系,以及为什么有些情况可以直接强转。
代码分析
-
看一下这三个结构的定义,先说明一下版本,操作系统为CentOS,头文件版本应该挺古老了,在’/usr/include/netinet/in.h’ 中发现版权信息:Copyright (C) 1991, 1992, 1994-2001, 2004, 2006, 2007, 2008, 2009, 2010,看着很古老,但之后的版本应该没有改动很大吧,反正不太清楚,我们就分析当前这一个版本吧。
/* /usr/include/bits/socket.h */ /* Structure describing a generic socket address. */ struct sockaddr { __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ }; /* /usr/include/netinet/in.h */ /* Structure describing an Internet socket address. */ struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port; /* Port number. */ struct in_addr sin_addr; /* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; /* /usr/include/netinet/in.h */ #ifndef __USE_KERNEL_IPV6_DEFS /* Ditto, for IPv6. */ struct sockaddr_in6 { __SOCKADDR_COMMON (sin6_); in_port_t sin6_port; /* Transport layer port # */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* IPv6 scope-id */ }; #endif /* !__USE_KERNEL_IPV6_DEFS */
-
看到3个结构的定义想到了什么?只是看着有点像吧,真正的区别我们往下看,其中3个结构里都包含了
__SOCKADDR_COMMON
这个宏,我们先把它的定义找到,最后在’usr/inlcue/bits/sockaddr.h’中找到如下代码,/* POSIX.1g specifies this type name for the `sa_family' member. */ typedef unsigned short int sa_family_t; /* This macro is used to declare the initial common members of the data types used for socket addresses, `struct sockaddr', `struct sockaddr_in', `struct sockaddr_un', etc. */ #define __SOCKADDR_COMMON(sa_prefix) \ sa_family_t sa_prefix##family #define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
由此我们知道,这三个结构的第一个字段都是一个
unsigned short int
类型,只不过用宏来定义了三个不同的名字,至此第一个结构就清楚了,在一般环境下(short一般为2个字节),整个结构占用16个字节,变量
sa_family
占用2个字节,变量
sa_data
保留14个字节用于保存IP地址信息。 -
接着我们发现第二个结构中还有
in_port_t
和
struct in_addr
两个类型没有定义,继续找下去吧,在文件
‘/usr/include/netinet/in.h’发现以下定义/* Type to represent a port. */ typedef uint16_t in_port_t; /* Internet address. */ typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; };
这么看来
sockaddr_in
这个结构也不复杂,除了一开始的2个字节表示sin_family,然后是2个字节的变量
sin_port
表示端口,接着是4个字节的变量
sin_addr
表示IP地址,最后是8个字节变量
sin_zero
填充尾部,用来与结构
sockaddr
对齐 -
现在我们该分析结构
sockaddr_in6
了,这里边只有一个未知的结构
in6_addr
,经过寻找发现其定义也在’/usr/include/netinet/in.h’中#ifndef __USE_KERNEL_IPV6_DEFS /* IPv6 address */ struct in6_addr { union { uint8_t __u6_addr8[16]; #if defined __USE_MISC || defined __USE_GNU uint16_t __u6_addr16[8]; uint32_t __u6_addr32[4]; #endif } __in6_u; #define s6_addr __in6_u.__u6_addr8 #if defined __USE_MISC || defined __USE_GNU # define s6_addr16 __in6_u.__u6_addr16 # define s6_addr32 __in6_u.__u6_addr32 #endif }; #endif /* !__USE_KERNEL_IPV6_DEFS */
这个结构看起来有点乱,但是如果抛开其中的预编译选项,其实就是8个字节,用来表示IPV6版本的IP地址,一共128位,只不过划分字节的段数有些不同,每段字节多一点那么段数就少一点,反义亦然。
-
那接下来我们整理一下,为了看的清楚,部分结构使用伪代码,不能通过编译,主要是方便对比,整理如下
/* Structure describing a generic socket address. */ struct sockaddr { uint16 sa_family; /* Common data: address family and length. */ char sa_data[14]; /* Address data. */ }; /* Structure describing an Internet socket address. */ struct sockaddr_in { uint16 sin_family; /* Address family AF_INET */ uint16 sin_port; /* Port number. */ uint32 sin_addr.s_addr; /* Internet address. */ unsigned char sin_zero[8]; /* Pad to size of `struct sockaddr'. */ }; /* Ditto, for IPv6. */ struct sockaddr_in6 { uint16 sin6_family; /* Address family AF_INET6 */ uint16 sin6_port; /* Transport layer port # */ uint32 sin6_flowinfo; /* IPv6 flow information */ uint8 sin6_addr[16]; /* IPv6 address */ uint32 sin6_scope_id; /* IPv6 scope-id */ };
这么来看是不是就清晰多了,由此我们发现结构
sockaddr
和
sockaddr_in
字节数完全相同,都是16个字节,所以可以直接强转,但是结构
sockaddr_in6
有28个字节,为什么在使用的时候也是直接将地址强制转化成(sockaddr*)类型呢?
强转的可能性
其实
sockaddr
和
sockaddr_in
之间的转化很容易理解,因为他们开头一样,内存大小也一样,但是
sockaddr
和
sockaddr_in6
之间的转换就有点让人搞不懂了,其实你有可能被结构所占的内存迷惑了,这几个结构在作为参数时基本上都是以指针的形式传入的,我们拿函数
bind()
为例,这个函数一共接收三个参数,第一个为监听的文件描述符,第二个参数是
sockaddr*
类型,第三个参数是传入指针原结构的内存大小,所以有了后两个信息,无所谓原结构怎么变化,因为他们的头都是一样的,也就是
uint16 sa_family
,那么我们也能根据这个头做处理,原本我没有看过
bind()
函数的源代码,但是可以猜一下:
int bind(int socket_fd, sockaddr* p_addr, int add_size)
{
if (p_addr->sa_family == AF_INET)
{
sockaddr_in* p_addr_in = (sockaddr_in*)p_addr;
//...
}
else if (p_addr->sa_family == AF_INET6)
{
sockaddr_in6* p_addr_in = (sockaddr_in6*)p_addr;
//...
}
else
{
//...
}
}
由以上代码完全可以实现IPv4和IPv6的版本区分,所以不需要纠结内存大小的不同
总结
-
通过等价替换的方式我们可以更好的了解
sockaddr
、
sockaddr_in
、
sockaddr_in6
之间的异同。 - 网路接口函数针对于IPv4和IPv6虽然有不同的结构,但是接口基本相同,主要是为了用户(开发者)使用方便吧。
-
有时间可以看一下
bind()
、
accept()
等函数,看看其中对于结构的使用到底是怎样的。