之前我们在学习内存的时候可知,内存存储数据也是有大端存储和小端存储的,对于网络数据流同样有大端小端之分,那么为什么要定义网络字节序呢?
原因是让不同cpu架构的计算机进行网络通信时,字节序不会混淆,因此tcp/ip协议规定了在网络中传输的字节流数据采用大端字节序。
通常,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存(即先发出的数据是低地址,后发出的数据是高地址)。
对于有的计算机内部使用的字节序与网络字节序不同的,就需要对数据进行转换,比如ip地址,端口号,那么可以使用下面的转换函数。
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
上面这四个函数中uint32_t表示无符号32位整数,你会发现这几个函数都长得很类似,其中在这些函数中,h表示主机,n表示网络,to表示转换到,l表示32位长整数的ip地址,s表示16位短整数的端口号。
这里肯定会有一些同学有疑问,那么ip地址为什么是占32位,端口号是占16位呢?
这是因为ip地址的格式是:255.255.255.255,公共分为4个部分,每一部分占1字节,1字节占8bit位,那么1个ip地址用2机制表示就是32bit位了。
由于端口号最大能表示65535号,那么把65535换算成2进制就是2^16 - 1,也就是16个bit位了。
生活中人们通常需要知道自己的居住地址,在网络中的同样需要一个地址来唯一标识每一台主机,这样所有的设备之间才能实现全球通信,ip地址就是标识了一台主机或路由设备在因特网的位置,并且互联网中的所有主机都要遵守这个ip地址结构。ip协议就可以通过ip地址结构把数据转发到互联网中的目的主机。
一般ip地址的表示方法有以下三种:
二进制记法(1000 0001 0000 1011 0000 1011 11101111),每8bit位为一个字节。
点分十进制记法(可读性高)(129 11 11 239)
十六进制记法(注册表、编程使用)(第一种:0x810B0BEF ,第二种:810B0BEF 16)
为了符合人类的编程习惯,ip地址一般使用点分十进制来表示的,这是为了方便区分和记忆,但是由于计算机只认识0和1,所以在计算机中ip地址也是用二进制的0和1来表示,在编程中通常以十六进制来表示ip地址。由此可知,ip地址在不同使用场景下需要进行转换。另外,IPv4 地址本质上是 32 位无符号整数。
对于ip地址在网络中是以网络字节序形式传输的,有时候需要将点分十进制的ip地址转换成网络字节序的in_addr_t类型。下面这几个函数就是对ip地址进行淀粉十进制到in_addr_t类型之间的转换,在早期ip地址的转换函数只能转换IPv4的地址。
// 将点分十进制直接转换成 in_addr 类型(推荐使用)
int inet_aton(const char *cp, struct in_addr *inp);
// 将点分十进制转换成 in_addr_t 类型,返回值保存的是网络字节序(不推荐使用)
in_addr_t inet_addr(const char *cp);
// 将 in_addr 地址转换成点分十进制,返回点分十进制的ip地址。注意:这个函数是线程不安全的
char *inet_ntoa(struct in_addr in);
下面使用的ip地址转换函数是随IPv6出现的新函数,对于IPv4和IPv6都支持,可以把ip地址转换转换成点分10进制来表示,比如:点分十进制记法表示129.11. 11.239,这样可读性就显得高很多了。
inet_pton函数用来将点分十进制的ip地址转换为网络字节序,函数名中的p就表示点分十进制,n表示网络字节序
函数原型:
#include
int inet_pton(int af, const char *src, void *dst);
参数说明:
af:选择ip的版本(AF_INET代表ipv4,AF_INET6代表ipv6)
src:源ip地址
dst:转换后的目标ip地址是网络字节序(传出参数)
返回值:
成功返回1,无效格式返回0,出错返回-1
inet_ntop函数用来将网络字节序转换为点分十进制的ip地址
函数原型:
#include
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数说明:
af:选择ip的版本
src:网络字节序的ip地址
dst:存储转换后的字符串ip地址
size:指定存储字符串ip地址的空间大小
返回值:
成功返回ip地址(点分十进制形式的),出错则返回NULL
linux早期是用sockaddr结构体来描述ipv4协议的,但是现在sockaddr结构体已经不推荐使用,因为历史原因,套接字函数函数早期在设计时是使用的struct sockaddr结构体,后期又设计了struct sockaddr_in来替代struct sockaddr,所以当在调用套接字函数声明传递参数的指针类型会存在问题,ANSI C标准提供了解决办法:将sockaddr定义成void *类型,也就是说sockaddr是一个通用的套接字地址结构。
通用的套接字地址结构,sockaddr结构体信息:
struct sockaddr {
sa_family_t sa_family; /* internet协议族, AF_xxx */
char sa_data[14]; /*14字节的协议地址*/
};
在使用struct sockaddr的时候需要进行强制转换成通用的套接字地址结构,即sockaddr结构:
//使用sockaddr_in定义
struct sockaddr_in addr;
//调用bind函数传参时需要对sockaddr_in类型强转成通用的sockaddr类型
bind((struct sockaddr *)&addr);
sockaddr是早期用来表示IPv4地址的。
IPv4地址使用sockaddr_in结构体来表示,包括16位端口号,32为ip地址
IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128位IP地址和一些控制字段
unix网络编程中是用sockaddr_un结构体表示,各种socket地址结构体的开头都是相同的,前16位表示整个结构体的长度,后16位表示地址类型。
IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。
IPv4地址用sockaddr_in结构体表示,包括16位端口号和32位IP地址,其中sin_addr成员的类型是一个in_addr结构体,该结构体中的s_addr成员就是用来存储ipv4的地址。
struct in_addr{
in_addr_t s_addr //ipv4地址
};
struct sockaddr_in{
short int sin_family; //inernet地址族,如AF_INET(主机字节序)
unsigned short int sin_port; //端口号占16位(网络字节序)
struct in_addr sin_addr; //internet地址,32位ipv4地址(网络字节序)
unsigned char sin_zero[8]; //以0填充(为了格式对齐的填充位)
}
这样的话,只要取得某种sockaddr结构体的首地址,就可以根据地址类型字段确定结构体中的内容,因此我们在调用bind、accept、connect等套接字函数,应该将这些函数的参数都用通用的套接字地址struct sockaddr *类型表示,即在传递参数之前先强制转换成struct sockaddr *
类型。