Unix网络编程(一):套接字地址结构

1. 简介

在ubuntu下写socket程序时,经常会看到那几个sockaddr的struct, 总是反应不过来它们的区别是什么,因此需要整理并归纳一下这些结构。

2. 特定IP协议的套接字地址结构

1. IPv4套接字地址结构

struct sockaddr_in
{
    uint8_t sin_len;  
    sa_family_t sin_family;    //这是套接的协议族,sin_family这里的值只能是 AF_INET,说明它是IPv4的
    in_port_t sin_port;    //端口号,这是一个16-bit的变量。为了可移植性用了in_port_t对16-bit类型进行了重定义
    struct in_addr sin_addr;  //这个是IPv4的地址结构,长度是4bytes,32 bits

    char sin_zero[8];
};  //共16-byte

这是IPv4套接字的地址结构,名字为sockaddr_in。其最后的sin_zero[] 变量是未曾使用的,但我们一般都要将它置为0。这个可以通过在定义了sockaddr_in类型的变量时,使用bzero函数将其整个结构体都清零。

其中要注意的是sockaddr_in里存放套接字ip地址的成员,即 sin_addr ,其类型为 struct in_addr

struct in_addr
{
    in_addr_t s_addr;
};

这个结构体只有一个成员,其长度为32-bit,所以这个套接字结构只能用于 IPv4(IPv6的地址长度为128-bit,4倍于IPv4长度)。至于为什么要将地址封装在这个结构体里呢?

2. IPv6 套接字地址结构

struct sockaddr_in6
{
    uint8_t sin6_len;
    sa_family_t sin6_family;
    in_port_t sin6_port;
    uint32_t sin6_flowintfo;
    struct in6_addr sin6_addr;   //16-byte, 128-bit IPv6地址
    uint32_t sin6_scope_id;
};  //共28-byte

struct in6_addr
{
    uint8_t s6_addr[16];
}

对于IPv6我还不是很熟悉,但对比一下前面IPv4的地址结构,可以发现它们的前三个成员的类型是一样的。C语言定义结构体时按成员定义的顺序为它们分配位置,所以如果有一个地址p,但不知道其指向的是sockaddr_in 还是 sockaddr_in6结构体,我们还是可以通过指针取其前三个成员的值,获得这个套接字的端口或协议族。

3. 通用套接字地址结构

我们希望能写出一些能与协议族(IPv4/v6)无关的代码。POSIX给出的接口已经实现了和协议无关了。我们来看一下 bind函数,这个函数的作用是绑定IP地址与端口号到socket中。

int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);

struct sockaddr
{
    uint8_t sa_len;
    sa_family_t fa_family;
    char sa_data[14];
}

注意到bind函数粒的第二个参数是一个新的类型struct sockaddr,不属于以上的任何一种。sockaddr类型只有三个成员,前两个成员和sockaddr_in 和 sockaddr_in6 *的前两个成员相同,而第三个成员是一个14-byte长度的数组。

为什么说这个是通用套接字地址结构呢?我们看两段代码,分别是ipv4和ipv6使用bind时的情形:

//ipv4
int sockfd4 = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addrIp4;
...  //这里对sockip4进行了正确的设置
...
bind(sockfd4, (struct sockaddr *)&AddrIp4, sizeof(AddrIp));

//ipv6
int sockfd6 = socket(AF_INET6, SOCK_STREAM, 0);
struct sockaddr_in addrIp6;
...  //这里对sockip6进行了正确的设置
...
bind(sockfd6, (struct sockaddr *)&AddrIp6, sizeof(AddrIp6));

在调用bind时上面的AddrIp6 和 AddrIp4的地址都被转换成了sockaddr类型,而区别是ipv4地址还是ipv6地址的则是第3个长度参数。明显ipv6的长度比ipv4要大许多。目前我还不够水平去研究bind的源码,但我有足够理由猜测在bind里面处理ipv6地址时会进行“越界”。

这里的所谓的“越界”的意思是,对AddrIp6取地址然后转换成sockaddr *,而明显sockaddr的长度只有16-byte,是不够存放ipv6的地址信息的(端口+地址至少18-byte)。所以bind函数会根据第三个参数对变成了sockaddr *类型的 &AddrIp6 进行完全合法的“越界访问”(换一个角度想,如果在bind里判断出是ipv6地址,再将其指针转换成sockaddr_in6 * 的话,那就不是“越界”了)所以bind函数会根据第三个参数对变成了sockaddr *类型的 &AddrIp6 进行完全合。

还有一个新的通用套接字的地址结构 struct sockaddr_storage,定义在 <netinet/in.h> 头文件中,其对IPv6有更好的支持,不过我对其不甚了解,便不在这里献丑了。

你可能感兴趣的:(unix,socket,网络编程)