
【回头看】Linux网络编程 | Socket地址API:网络字节序和地址结构体
网络字节序
字节序
机器字节序:多字节内容在内存总排列的顺序。
发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。
上述策略可见RFC 1700中的规定。
字节序 | 别称 | 排列(32 bit) |
---|---|---|
大端字节序 | 大端字节序 | 整数的高位字节(23~31 bit)存储在内存的低地址处,低位字节(0~7 bit)存储在内存的高地址处 |
小端字节序 | 主机字节序 | 整数的高位字节存储在内存的高地址处 |
Linux字节序转换函数
#include<netinet/in.h>
// h-host n-network l-long s-short
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
Socket地址
地址结构体的定义
通用Socket地址结构体
Sokcet接口中提供通用地址结构体sockaddr
,包含地址族类型(sa_family
)和地址值(sa_data[14]
)。地址族(AF_*
)和协议族(PF_*
)定义在bits/socket.h
中,二者的值和定义完全相同,故经常混用。
协议族 | 地址包含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可达到 108 字节 |
PF_INET | 16 bit 端口号和 32 bit IPv4 地址,共 6 字节 |
PF_INET6 | 16 bit 端口号,32 bit 流标识,128 bit IPv6 地址,32 bit 范围 ID,共 26 字节 |
为了解决14字节sa_data
无法容纳上表提到的地址数据的问题,Linux定义了新的通用socket地址结构体:
#include<bits/socket.h>
struct sockaddr_storage {
sa_family_t sa_family;
unsigned long int__ss_align;
char__ss_padding[128-sizeof(__ss_align)];
}
通用地址结构体在设计上保证了其作为系统API的通用性和灵活性,但这样的地址结构体对于网络编程来说并不友好,反而非常难用,因此Linux为协议族提供了专门的socket地址结构体。
专用Socket地址结构体
专用地址结构体对于网络编程来说更加实用,包括UNIX、IPv4、IPv6的专用地址结构,其中值得注意的是,TCP/IP协议族的地址存放依然是一个结构体,我会在后文单独说。
UNIX本地协议族
struct sockaddr_un {
sa_family_t sun_family; // 地址族,通常是 AF_UNIX
char sun_path[108]; // Unix 域套接字路径
};
TCP/IP协议族
我翻阅了一些资料,表明TCP/IP协议族中的地址采用struct in_addr sinaddr
的定义方式大概有关这几点:
-
历史原因:兼容早期BSD套接字的需求,BSD套接字需要兼容多种网络协议
-
协议无关性:所有协议专用地址结构(如IPv4的
sockaddr_in
、IPv6的sockaddr_in6
)必须兼容sockaddr
的内存布局
/* IPv4 */
struct sockaddr_in {
sa_family_t sin_family; // 地址族,通常是 AF_INET
in_port_t sin_port; // 端口号
struct in_addr sin_addr; // IPv4 地址
};
// 其中包含地址结构体
struct in_addr {
u_int32_t s_addr; // IPv4地址,网络(大端)字节序
}
/* IPv6 */
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,通常是 AF_INET6
in_port_t sin6_port; // 端口号
uint32_t sin6_flowinfo; // 流信息,通常设置为 0
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 范围 ID(用于链路本地地址)
};
// 其中包含地址结构体
struct in6_addr {
unsigned char sa_addr[16]; // IPv6地址,网络(大端)字节序
}
也正由于这样的设计,我们需要在具体使用地址结构体时需要进行地址转换:
// 创建一个IPv4地址字符串并将其绑定
#include <netinet/in.h>
// socket编程中常常引入arpa/inet.h而非字节引入netinet/in.h
#include <cstring>
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY: 监听所有可用的网络接口
server_addr.sin_port = htons(8888);
Linux IP地址转换函数
在地址结构体中,我们使用二进制格式表示IP地址,但我们常用点分十进制或十六进制文本表示IPv4或IPv6地址,用以人类阅读,这里则出现了需要转换的场景。
#include<arpa/inet.h>
// presentation to numeric 文本转二进制 IPv4 & IPv6
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
// inet ASCII to address 文本转网络字节序二进制地址 仅IPv4
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
inet_aton
&inet_ntoa
这两个函数仅支持IPv4地址的文本和二进制转换。
inet_ntoa
具有不可重新入性(),在《Linux高性能服务器编程》5.1.4中的案例体现的很好:
char*szValue1=inet_ntoa(“1.2.3.4”);
char*szValue2=inet_ntoa(“10.194.71.60”);
printf(“address 1:%s\n”,szValue1);
printf(“address 2:%s\n”,szValue2);
这段代码的输出:
address1:10.194.71.60
address2:10.194.71.60
inet_pton
&inet_ntop
这两个新版函数就有意思了,他们都支持IPv4和IPv6地址的转换,以ntop
为例讲一下他们的参数:
// numerice to persentation
const char *inet_ntop(int af,const void *src, char *dst, socklen_t size);
-
af
:地址族(AF_INET
表示 IPv4,AF_INET6
表示 IPv6) -
src
:指向二进制地址的缓冲区(如struct in_addr
或struct in6_addr
) -
dst
:指向存储文本表示的缓冲区(如char[]
) -
size
:dst
缓冲区的大小
其中size
可以使用这里的宏:
#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
参考阅读
《Linux高性能服务器编程》5.1 socket地址API