0%

Linux中socket地址API

Linux中socket地址API

学习《Linux高性能服务器编程》第五章Linux网络编程基础API,为了印象深刻一些,多动手多实践,所以记下这个笔记。这一篇主要记录Linux中socket地址的基础,包括主机字节序和网络字节序、socket地址和IP地址转化函数。

主机字节序和网络字节序

计算机硬件有两种储存数据的方式:大端字节序(big endian)小端字节序(little endian)

  • 大端字节序:高位字节在前,低位字节在后,符合人类读写数值的方法。
  • 小端字节序:低位字节在前,高位字节在后

想要判别机器的字节序可以使用如下的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

void byteorder()
{
union
{
short value;
char union_bytes[sizeof(short)];
} test;
test.value = 0x0102;
if ((test.union_bytes[0] == 1) && (test.union_bytes[1] == 2))
{
printf("big endian\n");
}
else if ((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1))
{
printf("little endian\n");
}
else
{
printf("unknown...\n");
}
}

int main()
{

byteorder();
return 0;
}

运行结果:

image-20220809181538872

这段代码使用的原理是union变量所占用的内存长度等于最长的成员的内存长度。

所以testvalueunion_bytes是共用一段内存的。因为在c中short是16位也就是2字节,char是8位也就是1字节,所以union_bytes数组的大小是2。

我们给value赋值为0x0102。如果是机器是大端存储,那么union_bytes数组第一个元素存储0x01,第二个元素存储0x02,如果是机器是小端存储,那么union_bytes数组第一个元素存储0x02,第二个元素存储0x01

image-20220809185922374

扩展到32位,四字节来说以0x12345678为例,那么

大端字节序:0x12345678

小端字节序:0x78563412

总结来说就是大端字节序和小端字节序的区别就是以字节为单位的存储方式不同。

在网络中两台使用不同字节序的主机之间直接传递时,接收端必然会造成错误。书中说解决的方法是发送端总是把要发送的数据转化成大端字节序数据再发送,接受端知道传送过来的数据总是采用大端字节序,所以接收端根据自身采用的字节序再对数据进行一定的处理(小端进行转换,大端就不转换)。

Linux提供了4个函数来完成主机字节序和网络字节序之间的转换:

image-20220810153657086

1
2
3
4
5
#include <netinet/in.h>
uint32_t ntohl (uint32_t __netlong);
uint16_t ntohs (uint16_t __netshort);
uint32_t htonl (uint32_t __hostlong);
uint16_t htons (uint16_t __hostshort);

它们的含义是就是首字母缩写(这谁看的出来),比如”htonl”表示“host to network long”,即将长整型(32bit)的主机字节序数据转化为网络字节序数据。这四个函数中,长整型uint32_t函数通常用来转换IP地址,短整型uint16_t函数用来转化端口号。

简单示例展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <netinet/in.h>
#include <cstdio>

int main(int argc, char const *argv[])
{

uint16_t port = 258;
uint16_t p = htons(port);
port = ntohs(p);
printf("htons :%u \n", p);
printf("ntohs :%u \n", port);

return 0;
}

运行结果:

image-20220810181643405

513二进制:0000 0010 0000 0001

258二进制:0000 0001 0000 0010

可以看出两者字节序是不同的

socket地址

通用socket地址

sockaddr

socket网络接口中表示socket地址的是结构体sockaddr,他的定义在头<bits/socket.h>中,我看在我的电脑上看到的是如下的定义(各个版本不同,可能实现不同,我这里和书上就不大相同):

image-20220810185029006

1
2
3
4
5
6
7
#include <bits/socket.h>
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};

其中__SOCKADDR_COMMON定义在<bits/sockaddr.h>

image-20220810185204703

1
2
3
4
5
6
7
8
9
#include <bits/sockaddr.h>
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;

/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */

#define __SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family

__SOCKADDR_COMMON是定义的一个函数,它返回一个sa_family_t类型的数据,数据的名字是sa_prefixfamily,其中sa_prefix是你传进去的值。比如:__SOCKADDR_COMMON (sa_)其实就是返回sa_family_t sa_family

所以sockaddr其实就是两个成员,一个是sa_family_t(地址族)类型的变量sa_family,一个char数组类型的变量sa_data

sa_family_t常见的协议族(protocol family,也称domain)和对应的地址族如下表所示

协议族 地址族 描述
PF_UNIX AF_UNIX UNIX本地协议族
PF_INET AF_INET IPv4协议族
PF_INET6 AF_INET6 IPv6协议族

宏PF_*和AF_*都定义在<bits/socket.h>当中,两者的值相同,所以两者可以混用

image-20220811094342093

image-20220811094510529

sa_data成员用于存放socket地址值。不同的协议族的地址值有不同的含义和长度。

协议族 地址值含义和长度
PF_UNIX 文件的路径名,长度可达108字节
PF_INET 16bit端口号和32bit IPv4地址,共6字节
PF_INET6 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共26字节

sockaddr_storage

可以看出14字节的sa_data根本无法容纳多数协议族的地址值。所以,Linux中定义了新的通用socket地址结构体(其实就是把存放地址的数组加大了):

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <bits/socket.h>
/* Structure large enough to hold any socket address (with the historical
exception of AF_UNIX). */
#define __ss_aligntype unsigned long int
#define _SS_PADSIZE \
(_SS_SIZE - __SOCKADDR_COMMON_SIZE - sizeof (__ss_aligntype))

struct sockaddr_storage
{
__SOCKADDR_COMMON (ss_); /* Address family, etc. */
char __ss_padding[_SS_PADSIZE];
__ss_aligntype __ss_align; /* Force desired alignment. */
};

其中_SS_SIZE__SOCKADDR_COMMON_SIZE<bits/sockaddr.h>当中

1
2
3
4
5
#include <bits/sockaddr.h>
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))

/* Size of struct sockaddr_storage. */
#define _SS_SIZE 128

image-20220811135111456

image-20220811135134566

这个结构体提供了足够大的空间用于存放地址值,并且是内存对齐的。

ss_(其实是ss_family)是sa_family_t类型(介绍sockaddr有提到),即unsigned short int类型,2字节。

__ss_align__ss_aligntype类型,即unsigned long int类型,4字节

__ss_paddingchar类型数组,大小为_SS_PADSIZE,而_SS_PADSIZE=_SS_SIZE - __SOCKADDR_COMMON_SIZE - sizeof (__ss_aligntype)=128-2-4=122字节,完全足够保存地址值。

综上sockaddr_storage是128字节大小,保证了内存对齐。

专用socket地址

上面两种通用的socket地址使用起来显然不够方便,因为将IP地址和端口等信息直接放在同一个char数组中,那要得到IP地址和端口信息都得费好大劲进行操作。因此,Linux为各个协议族提供了专门的socket地址结构体。

UNIX本地协议族使用sockaddr_un,数据结构很简单,只有一个保存地址族类型的sun_(其实是sun_family)和保存文件位置的sun_path

1
2
3
4
5
6
7
#include <sys/un.h>
/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108]; /* Path name. */
};

IPv4协议族使用sockaddr_in

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <netinet/in.h>
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */

/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr)
- __SOCKADDR_COMMON_SIZE
- sizeof (in_port_t)
- sizeof (struct in_addr)];
};

其中in_port_t定义、in_addr_t结构如下

1
2
3
4
5
6
7
8
9
10
#include <netinet/in.h>
/* Type to represent a port. */
typedef uint16_t in_port_t;

/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};

image-20220811143542024

image-20220811143600470

image-20220811143622907

可以看的出来sin_(其实是sin_family)存放地址族类型,sin_port存放端口,sin_addr存放地址。sin_zero为了让sockaddr_in大小和sockaddr相同,为什么有这个成员,个人感觉这是因为所以专用socket在实际使用中都需要转化为通用socket地址类型socketaddr,因为socket编程接口使用的是参数类型是socketaddr

IPv6协议族使用sockaddr_in6

1
2
3
4
5
6
7
8
9
10
#include <netinet/in.h>
/* Ditto, for IPv6. */
struct sockaddr_in6
{
__SOCKADDR_COMMON (sin6_);
in_port_t sin6_port; /* Transport layer port # */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* IPv6 scope-id */
};

其中in6_addr如下,因为IPv6不是学习重点,这里就不过多展开介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* IPv6 address */
struct in6_addr
{
union
{
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __in6_u;
#define s6_addr __in6_u.__u6_addr8
#ifdef __USE_MISC
# define s6_addr16 __in6_u.__u6_addr16
# define s6_addr32 __in6_u.__u6_addr32
#endif
};

除此之外需要注意:所有专用socket地址(以及sockaddr_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转换即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。

IP地址转化函数

通常来说,人们更喜欢用点分十进制的字符串来表示IPv4地址,但是在编程的过程中,我们需要把这个字符串转化为整数才能使用,但是输出的时候我们又需要把整数转化成点分十进制的字符串,这样方便观察。所以系统提供了3个函数用于点分十进制的字符串IPv4地址和整数的IPv4地址之间的转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <arpa/inet.h>

/* Convert Internet host address from numbers-and-dots notation in CP
into binary data in network byte order. */
extern in_addr_t inet_addr (const char *__cp) __THROW;

/* Convert Internet host address from numbers-and-dots notation in CP
into binary data and store the result in the structure INP. */
extern int inet_aton (const char *__cp, struct in_addr *__inp) __THROW;

/* Convert Internet number in IN to ASCII representation. The return value
is a pointer to an internal array containing the string. */
extern char *inet_ntoa (struct in_addr __in) __THROW;

inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。它失败时返回 INADDR_NONE

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{

in_addr_t ip = inet_addr("192.168.167.14");
if (ip == INADDR_NONE)
printf("ip error\n");
else
printf("ip convert by inet_addr %u \n", ip);
return 0;
}

image-20220811170759741

inet_aton功能和inet_addr相同,但是将结果存在在in_addr_t指向的地址结构当中,函数成功返回1,失败返回0。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{
struct in_addr ip;
int ret = inet_aton("192.168.167.14", &ip);
if (0 == ret)
printf("ip error\n");
else
printf("ip convert by inet_aton %u \n", ip.s_addr);
return 0;
}

image-20220811171444397

inet_ntoa函数将整数的IPv4地址转化为点分十进制字符串的IPv4。成功时返回转换的字符串地址值,失败时返回-1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{
struct in_addr ip;

int ret = inet_aton("192.168.167.14", &ip);
if (0 == ret)
printf("ip error\n");
else
printf("ip convert by inet_aton %u \n", ip.s_addr);

char *ip_str = inet_ntoa(ip);
printf("address :%s \n", ip_str);
return 0;
}

image-20220811173503409

需要注意的是inet_ntoa函数内部使用一个静态变量存储转化的结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的,这一点需要多注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <arpa/inet.h>
#include <cstdio>

int main(int argc, char const *argv[])
{
struct in_addr ip;

inet_aton("192.168.167.14", &ip);
char *ip_str1 = inet_ntoa(ip);

inet_aton("192.168.167.15", &ip);
char *ip_str2 = inet_ntoa(ip);

printf("address :%s \n", ip_str1);
printf("address :%s \n", ip_str2);
return 0;
}

image-20220811180430697

除此之外,下面两个函数也能完成前三个函数的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <arpa/inet.h>
/* Convert from presentation format of an Internet number in buffer
starting at CP to the binary network format and store result for
interface type AF in buffer starting at BUF. */
extern int inet_pton (int __af, const char *__restrict __cp,
void *__restrict __buf) __THROW;

/* Convert a Internet address in binary network format for interface
type AF in buffer starting at CP to presentation form and place
result in buffer of length LEN astarting at BUF. */
extern const char *inet_ntop (int __af, const void *__restrict __cp,
char *__restrict __buf, socklen_t __len)
__THROW;

inet_pton函数将用字符串表示的P地址__cp(用点分十进制字符串表示的IPv4地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于__buf指向的内存中。其中,__af参数指定地址族,可以是AF_INET或者AF_INET6inet_pton成功时返回1,失败则返回0并设置errno

__restrictemmm目前找不到定义,但是看了下restrict关键字,是指告诉编译器传入的两个指针不指向同一数据,方便进行优化用来提升性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <arpa/inet.h>
#include <cstdio>
#include <errno.h>

int main(int argc, char const *argv[])
{
char ip_str[] = "192.168.167.42";
in_addr_t ip;
int ret = inet_pton(AF_INET, ip_str, &ip);
if (0 == ret)
perror("ip error\n");
else
printf("ip convert by inet_pton %u \n", ip);

struct in_addr in_ip;
ret = inet_pton(AF_INET, ip_str, &in_ip);
if (0 == ret)
perror("ip error\n");
else
printf("ip convert by inet_pton %u \n", in_ip.s_addr);

return 0;
}

image-20220811183002035

inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton的参数相同,最后一个参数 __len指定目标存储单元的大小。下面的两个宏能帮助我们指定这个大小(分别用于IPv4和IPv6):

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

inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <arpa/inet.h>
#include <cstdio>
#include <errno.h>

int main(int argc, char const *argv[])
{
char ip_str[] = "192.168.167.42";

struct in_addr in_ip;
int ret = inet_pton(AF_INET, ip_str, &in_ip);
if (0 == ret)
perror("ip error\n");
else
printf("ip convert by inet_pton %u \n", in_ip.s_addr);

char ip_str2[1024];

const char *ip_str3 = inet_ntop(AF_INET, &in_ip, ip_str2, sizeof(ip_str2));

if (ip_str3 == NULL)
perror("ip error \n");
else
{
printf("address :%s \n", ip_str2);
printf("address :%s \n", ip_str3);
}

return 0;
}

image-20220811220718920

值得注意的是ip_str2ip_str3的地址相同,也就是说传入参数和返回值相同,虽然不知道为啥这样设计。

image-20220811221357471