网络字节序

字节序

机器字节序:多字节内容在内存总排列的顺序。

发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。

上述策略可见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_INET16 bit 端口号和 32 bit IPv4 地址,共 6 字节
PF_INET616 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_addrstruct in6_addr

  • dst:指向存储文本表示的缓冲区(如 char[]

  • sizedst 缓冲区的大小

其中size可以使用这里的宏:

#include<netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46

参考阅读

  • 《Linux高性能服务器编程》5.1 socket地址API

  • RFC 1700