Unix Socket 编程中几个地址结构简析
前言
Unix Socket 编程中与地址有关的结构有这么一些。
struct sockaddr
struct sockaddr_in
struct sockaddr_in6
struct sockaddr_storage
接下来我将对他们进行一些简单的分析和解释。后面将结合实际来展示如何在实际的 Socket 编程中使用他们。
结构
struct sockaddr
其声明在 <sys/socket.h>
头文件中:
typedef __uint8_t sa_family_t;
/*
* [XSI] Structure used by kernel to store most addresses.
*/
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
struct sockaddr_in
其声明在 <netinet/in.h>
头文件中:
typedef __uint32_t in_addr_t; /* base type for internet address */
/*
* Internet address (a structure for historical reasons)
*/
struct in_addr {
in_addr_t s_addr;
};
typedef __uint16_t in_port_t;
/*
* Socket address, internet style.
*/
struct sockaddr_in {
__uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
struct sockaddr_in6
其声明在 <netinet6/in6.h>
头文件中:
/*
* IPv6 address
*/
struct in6_addr {
union {
__uint8_t __u6_addr8[16];
__uint16_t __u6_addr16[8];
__uint32_t __u6_addr32[4];
} __u6_addr; /* 128-bit IP6 address */
};
struct sockaddr_in6 {
__uint8_t sin6_len; /* length of this struct(sa_family_t) */
sa_family_t sin6_family; /* AF_INET6 (sa_family_t) */
in_port_t sin6_port; /* Transport layer port # (in_port_t) */
__uint32_t sin6_flowinfo; /* IP6 flow information */
struct in6_addr sin6_addr; /* IP6 address */
__uint32_t sin6_scope_id; /* scope zone index */
};
struct sockaddr_storage
其声明也在 <sys/socket.h>
头文件中:
/*
* RFC 2553: protocol-independent placeholder for socket addresses
*/
#define _SS_MAXSIZE 128
#define _SS_ALIGNSIZE (sizeof(__int64_t))
#define _SS_PAD1SIZE \
(_SS_ALIGNSIZE - sizeof(__uint8_t) - sizeof(sa_family_t))
#define _SS_PAD2SIZE \
(_SS_MAXSIZE - sizeof(__uint8_t) - sizeof(sa_family_t) - \
_SS_PAD1SIZE - _SS_ALIGNSIZE)
/*
* [XSI] sockaddr_storage
*/
struct sockaddr_storage {
__uint8_t ss_len; /* address length */
sa_family_t ss_family; /* [XSI] address family */
char __ss_pad1[_SS_PAD1SIZE];
__int64_t __ss_align; /* force structure storage alignment */
char __ss_pad2[_SS_PAD2SIZE];
};
接口
对于 TCP 客户端来说,主要使到以下接口 :
socket()
-> connect()
-> write()
-> read()
-> close()
.
对于服务端来说主要用到以下接口:
socket()
-> bind()
-> listen()
-> accept()
-> read()
-> write()
-> close()
下面主要分析其中用到了地址的几个接口函数:
bind
int
bind(int socket, const struct sockaddr *address, socklen_t address_len);
connect
int
connect(int socket, const struct sockaddr *address,
socklen_t address_len);
accept
int
accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
分析
为什么需要提供 address_len
参数?
上面三个用到了 Socket 地址的函数接口中关于地址的参数的签名基本是一致的。
即 const struct sockaddr *address, socklen_t address_len
使用者需要提供一个 struct sockaddr
结构指针。 还需要提供此结构的长度 address_len
。对于刚学习 Socket 编程的人来说,这个参数还是有点让人疑惑的。因为我们从字面上看通过 sizeof( struct sockaddr)
就能得到地址结构的大小(长度)。
而且 struct sockaddr
结构本身还有一个 sa_len
字段呢。
首先来说这里与 Socket API 的发展历史有关。
在 BSD 4.3 时代时 struct sockaddr
的声明大概是这样的。
typedef __uint16_t sa_family_t;
struct sockaddr {
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
注意原来的 sa_family_t
是 16 位长的。
然后 Socket API 被设计成是通用的 API。即可以兼容当时大部分的网络协议。可以看到 <sys/socket.h>
中定义了很多 AF_XXX
的常量,也就是说地址种类是多种多样的。也就是说 sockaddr
本身应该当作一个接口类型来理解。但是 C 语言本身就没有接口类型,但是在 C 语言中对于布局相容的结构通过安全的通过指针转换来进行类型转换。
注意理解布局相容的转换。sockaddr
默认声明的 sa_data
为 14 个字节的大小。
但是如果我们访问 sa_data[15]
这样明显索引越界的操作,编译和运行时也不会报错,只不过里面的值是内存的原始值。所以 Socket API 的函数通过在函数中声明 address_len
参数来让使用者指定长度以便 Socket API 在实现时能正确的访问地址数据。 这里也利用到了 C 语言数组的另一个特别,即结构体中各字段在内存的布局中是连续,数组在内存中的布局也是连续的。
struct sockaddr
中的 sa_len
字段
后来在 BSD 4.4 中 开发者对 struct sockaddr
这个接口类型进行了升级。将 struct sockaddr
这个接口类型的子类的长度信息添加进 struct sockaddr
这个接口类型结构中。为了兼容以前的接口,可以利用 C 语言结构的内存布局特性,将 sa_family
由原来的16位长度改为 8 位长度(嗯,8位长度应该也够用了)。前面的 8位留给 sa_len
字段。
typedef __uint8_t sa_family_t;
struct sockaddr {
__uint8_t sa_len; /* total length */
sa_family_t sa_family; /* [XSI] address family */
char sa_data[14]; /* [XSI] addr value (actually larger) */
};
以sockaddr
接口实现类的眼光看 sockaddr_in
和 sockaddr_in6
sockaddr_in
结构名称中的 in
是什么意思呢,我估计是 internet
的缩写,
而 sockaddr_in6
存在的原因则是因为 sockaddr_in
原来是为存储 IPv4 地址而设计的,其大小存储不了 IPv6
的地址,所以 sockaddr_in6
就是为了存储 IPv6 地址而设计的了。(虽然 sockaddr_in 中的 sin_zero
字段保留了 64 位长度的空间。但是 IPv6 的地址长度是 128 位,所以终究是不够的。)
一般来说我们把像 struct sockaddr *
这种用作接口类型的指针叫做 Opaque Pointer
。
另外值得说明的是 sockaddr_in6
设计成是 64 位对齐的:
IPv6 addresses carried in data structures should be 64-bit
aligned. This is necessary in order to obtain optimum performance
on 64-bit machine architectures.
如果让我们用面向对象的方式来设计那大概是这样的表示形式。
interface SocketAddress{
uint8_t getLen();
uint8_t getFamily();
byte[] data();
}
class InternetSocketAddress implement SocketAddress{
uint16_t getPort();
IPv4Address getAddress();
}
class InternetSocketAddressV6 implement SocketAddress{
uint16_t getPort();
IPv6Address getAddress();
}
传入或传出参数
另外一个值得说明地方是:
-
bind()
和connect()
函数中的struct sockaddr *
指针是将数据传入到函数里面供函数使用。 - 而
accept()
中的函数的struct sockaddr *
指针则是用来提供分配好的内存结构以便accept()
函数将接收到的连接请求的客户端的地址写入到struct sockaddr *
指针所指向的内存中。
sockaddr_storage
上面介绍了, sockaddr_in
和 sockaddr_in6
,那么当一个 sockaddr
我们不知道其内存布局是 sockaddr_in
还是 sockaddr_in6
的时候怎么办呢?
当我们的函数要同时兼容 IPv4 和 IPv6 时就需要思考这些问题了。
比如上面的 accept
函数,比如域名解析时需要用到的接口 getaddrinfo
中的 struct addrinfo
,声明在 <netdb.h>
中。
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
socklen_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for hostname */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
int
getaddrinfo(const char *hostname, const char *servname,
const struct addrinfo *hints, struct addrinfo **res);
sockaddr_storage
就是为了兼容 sockaddr_in
和 sockaddr_in6
的等内存结构布局而来。因此在 struct sockaddr_storage
中只有 sa_len
和 sa_family
两个字段是公开的。其他的只是内存布局的占位而已。(其中 __ss_align
为字节对齐而生)
参考
If this article is useful to you, may be you want to buy me a candy.