0%

Socket(一)套接字地址结构

引言

大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义了它自己的套接字地址结构。这些结构的名字均以 sockaddr_ 开头,并以对应每个协议族的唯一后缀结尾。本篇文章将着重介绍一些我们在 socket 编程中常用的并且容易混淆的套接字地址结构。

IPv4

IPv4 套接字地址结构通常也称为 “网际套接字地址结构”,它以 sockaddr_in 命名,定义在 <netinet/in.h> 头文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 以下定义取自 macOS 

typedef __uint8_t sa_family_t;
typedef __uint16_t in_port_t;
typedef __uint32_t in_addr_t;

struct sockaddr_in
{
uint8_t sin_len; // 8 位
sa_family_t sin_family; // 8 位
in_port_t sin_port; // 16 位

struct in_addr sin_addr;
char sin_zero[8];
}

struct in_addr
{
in_addr_t s_addr; // 32 位
}
  • sin_len

    长度字段,是为增加对 OSI 协议的支持而随 4.3 BSD-Reno 添加的。并不是所有的厂家都支持套接字地址结构的长度字段,而且 POSIX 规范也不要求有这个成员。当然,也正因为有了长度字段,才简化了长度可变套接字地址结构的处理。即使有长度字段,我们也无须设置和检查它,除非涉及路由套接字。

  • sin_family

    地址族,POSIX 规范字段,IPv4 对应的地址族为 AF_INET。sin_family 可以是任何无符号整数类型。在支持长度字段的实现中,sin_family 通常是一个 8 位无符号整数,而在不支持长度字段的实现中,它则是一个 16 位的无符号整数。

  • sin_port

    TCP/UDP 端口,POSIX 规范字段,数据类型必须是一个至少 16 位无符号整数类型。

  • sin_addr

    IPv4 地址,POSIX 规范字段,必须是一个至少 32 位的无符号整数类型。

  • sin_zero

    该字段未曾使用,不过在填写这种套接字地址结构时,我们总是把该字段置为 0。按照惯例,我们总是在填写前把整个结构置为 0,而不是单单把 sin_zero 置为 0。

    📚 Tips

    端口号在该结构体中总是以网络字节序存储。对于 IPv4 地址存在两种不同的访问方法,举例来说,如果 serv 定义位某个网际套接字地址结构,那么 serv.sin_addr 将按 in_addr 结构引用其中的 32 位 IPv4 地址,而 serv.sin_addr.s_addr 将按 in_addr_t(通常是一个无符号 32 位整数)引用同一个 32 位地址。因此我们必须正确地使用 IPv4 地址,尤其是在讲它作为函数的参数时,因为编译器对传递结构和传递整数的处理是完全不同的。
    套接字地址结构仅在给定主机上使用,虽然结构中的某些字段(例如:IP 地址和端口号)用在不同主机之间的通讯中,但是结构体本身并不在主机之间传递。

通用套接字地址结构

当作为一个参数传递给任何套接字函数时,套接字地址结构总是以引用形式(也就是以指针)来传递。然而以这样的指针作为参数之一的任何套接字函数,必须处理来自所支持的任何协议族的套接字地址结构。
在如何声明所传递指针的数据类型上存在一个问题。有了 ANSI C 后解决办法很简单:void * 是通用的指针类型。然而套接字函数是在 ANSI C 之前定义的,所以在当时(1982 年)采取的办法是在 <sys/socket.h> 头文件中定义一个通用的套接字地址结构。

1
2
3
4
5
6
7
8
typedef __uint8_t    sa_family_t;

struct sockaddr
{
unit8_t sa_len;
sa_family_t sa_family;
char sa_data[4];
}

于是套接字函数被定义为以指向某个通用套接字地址结构的一个指针作为其参数之一,比如 bind 函数的 ANSI C 函数原型所示:

1
2
3
4
typedef __uint32_t              __darwin_socklen_t
typedef __darwin_socklen_t socklen_t;

int bind(int, struct sockaddr *, socklen_t);

这就要求对这些函数的任何调用都必须要将指向特定与协议的套接字地址结构的指针进行类型强制转换,变成指向某个通用套接字地址结构的指针,如:

1
2
struct sockaddr_in serv;
bind(sockfd, (struct sockaddr *)&serv, sizeof(serv));

📚 Tips

从内核角度看,使用指向通用套接字地址结构的指针另有原因:内核必须取调用者的指针,把它类型强制类型转换为 struct sockaddr * 类型,然后检查期中的 sa_family 字段的值来确定这个结构的真实类型。

IPv6

IPv6 套接字地址结构在 <netinet/in.h> 头文件中定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct in6_addr
{
unit8_t s6_addr[16]; // 128 位网络字节序的 IPv6 地址
}

#define SIN6_LEN
struct sockaddr_in6
{
unit8_t sin6_len;
sa_family_t sin6_family;
in_port_t sim6_port;

uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;

uint32_t sin6_scope_id;
}

  • SIN6_LEN

    如果系统支持套接字地址结构中的长度字段,那么 SIN6_LEN 常值必须定义。

  • sin6_family

    IPv6 的地址族为 AF_INET6。

  • sin6_flowinfo

    该字段分为两部分:

    1. 低 20 是流标(flow label)
    2. 高 12 位保留
  • sin6_scope_id

    对于具备范围的地址,该字段标识其范围,最常见的是链路局部地址的接口索引。

📚 Tips

结构中字段的先后顺序做过编排,使得如果 sockaddr_in6 结构本身是 64 位对齐的,那么 128 位的 sin6_addr 字段也是 64 位对齐的。在一些 64 位处理机上,如果 64 位数据存储在某个 64 位边界位置,那么对它的访问将得到优化处理。

新的通用套接字地址结构

作为 IPv6 套接字 API 的一部分而定义的新的通用套接字地址结构克服了现有 struct sockaddr 的一些缺点。不像 struct sockaddr,新的 struct sockaddr_storage 足以容纳系统所支持的任何套接字地址结构。sockaddr_storage 结构在 <netinet/in.h> 头文件中定义。

1
2
3
4
5
struct sockaddr_storage
{
unit8_t ss_len;
sa_family ss_family;
}

sockaddr_storage 类型提供的通用套接字地址结构相比 sockaddr 存在以下两点差异:

  1. 如果系统支持的任何套接字地址结构有对齐需要,那么 sockaddr_storage 能够满足最苛刻的对齐要求。
  2. sockaddr_storage 足够大,能够容纳系统支持的任何套接字地址结构。

📚 Tips

除了 ss_family 和 ss_len 外(如果有的话),sockaddr_storage 结构中的其他字段对用户来说是透明的。sockaddr_storage 结构必须类型强制转换或者复制到适合于 ss_family 字段所给出地址类型的套接字地址结构中,才能访问其他字段。

Unix

Unix 域套接字地址结构在 <sys/un.h> 头文件中定义。

1
2
3
4
5
struct sockaddr_un
{
sa_family_t sun_family;
char sun_path[104];
}
  • sun_family

    地址族,unix 地址族为 AF_LOCAL。

  • sun_path

    以空字符结尾的绝对路径字符串。BSD 早期版本定义 sun_path 数据的大小为 108 字节,而不是 104 字节。POSIX 规范没有定义 sun_path 数组的大小,而且明确警示应用进程不应该假设一个特定长度。应用进程应该在运行时使用 sizeof 运算符得出本结构的长度,再验证一个路径名是否适合粗放到期中的 sun_path 数组。数组长度可能在 92 到 108 之间,而不是足以存放任何路径名的更大的值。存在这些限制源于 4.2 BSD 的实现细节,要求本结构适合装入一个 128 字节的 mbuf(一种内核内存缓冲区)。

📚 Tips

存放在 sun_path 数组中的路径名必须以空字符结尾。实现提供的 SUN_LEN 宏以一个指向 sockaddr_un 结构的指针作为参数并返回该结构的长度,其中包括路径名中非空字节数。未指定地址通过以空字符串作为路径名指示,也就是一个 sun_path[0] 值为 0 的地址结构。它等价于 IPv4 的 INADDR_ANY 常值以及 IPv6 的 IN6ADDR_ANY_INIT 常值。
POSIX 把 Unix 域协议重新命名位 “本地 IPC”,以消除它对于 Unix 操作系统的依赖,历史性常值也由 AF_UNIX 变为 AF_LOCAL。尽管 POSIX 努力使它独立于操作系统,但是它的套接字地址结构仍然保留 _un 后缀。

数据链路套接字地址结构

在头文件 <net/if_dl.h> 中。

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_dl
{
unit8_t sdl_len;
sa_family_t sdl_family;
unit16_t sdl_index;
unit8_t sdl_type;
unit8_t sdl_nlen;
unit8_t sdl_alen;
unit8_t sdl_slen;
char sdl_data[12];
}
  • sdl_len

    整个地址的长度。

  • sdl_family

    地址族,这里为:AF_LINK。

  • sdl_index

    sdl_index 在内核中标识接口。其中以太网的索引为 1,SLIP 接口的索引为 2,而环回接口的索引为 3。

  • sdl_type

    接口类型,如 IFT_ETHER。

  • sdl_nlen

    接口名称长度。

  • sdl_alen

    链路层地址长度。

  • sdl_slen

    链路层选择器长度。

  • sdl_data

    保存网卡名称和链路层地址。名字从 sdl_data[0] 开始,而且不以空字符结尾。链路层地址从 sdl_data[sdl_nlen] 开始。定义该结构的头文件定义了以下这个宏,用以返回指向链路层地址的指针。

    1
    #define LLADDR(s)    ((caddr_t)((s)->sdl_data + (s)->sdl_nlen))

📚 Tips

数据链路套接字地址结构是可变长度的。如果链路层地址和名字总长度超过 12 字节,结构将大于 20 字节。在 32 位系统上,这个大小通常会向上舍入到下一个 4 字节的倍数。