(首更,2021年11.28)
现代CPU的累加器一次都能装载8字节的数据,这八字节在内存中排列的顺序将影响到它被累加器装载成长整型/浮点型/异质数据结构的值,这就是字节序列,字节序分为大端字节序和小端字节序,大端字节序就是指一个浮点/长整型的高位字节存储在机器内存中的低地址处,而小端字节序的高位字节存储在机器内存中的高地址处。(低位字节相反)
现代PC大多采用小端法,所以小端字节序又叫主机字节序。
ps:问题引入:
当两个都是小端字节序的主机互相直接传递数据(流式传输),必然会错误的解释流向自己的数据。怎样才能避免都是小端字节序带来的错误解释 ?
解决方法:
发送端将数据转换成大端字节序数据之后发送,而接收端知道对端传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换,(大端机器转换,小端机器不转换)由此可以正确的在网际直接传输信息。
所以大端法又叫网络字节序。
#include
unsigned long int htonl(unsigned long int hostlong) //host to net long 主机字节序转换网络字节序,比如长整型的IP 地址
unsigned long int ntohl(unsigned long int netlong) //net to host long 网络字节序转换主机字节序,比如长整型的IP地址
usigned short int htons(unsigned short int hostlong) //host to net short 主机字节序转换网络字节序,比如短整形的端口号
usigned short int ntohs(unsigned short int nettlong) //net to host short 网络字节序转换主机字节序,比如短整形的端口号
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include
struct sockaddr
{
sa_family_t sa_family; //sa_family是地址族类型
char sa_data[14]; //存放socket地址值
}
地址族类型和协议族对应,常见协议族(protocol family也称为domain)和对应的地址族如下
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AD_INET | TCP/IPV4协议族 |
PF_INET6 | AF_INET6 | TCP/IPV6协议族 |
PF与AF都存在bits/socket.h头文件中,两者有完全相同的值,故可混用。
用于存放socket地址,但是不同协议族的地址长度和含义不同:
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可达到108字节 |
PF_INET | 16bit的端口号和32BIT的IPV4地址,共6字节 |
PF_INET6 | 16BIT端口号,32bit流标识,128bit IPV6地址,32bit范围ID 共26字节 |
可以看到sa_data[14]成员只有14个字节,在UNIX本地协议和IPV6协议族中不能达到存储的条件,所以linux os 定义了如下通用的socket地址结构体:
#include
struct sockaddr_storage
{
sa_family_t sa_family; //sa_family是地址族类型
unsigned long int __ss_align //内存对齐
char __ss_padding[128_sizeof(__ss_slign)]//存储socket地址
}
如上这两个通用socket结构体显然不好用,比如在设置与获取IP地址和端口号的时候需要执行繁琐的位操作,所以linux专门为各个协议族提供了专门的socket地址结构体.
unix本地域协议族使用如下专用socket地址结构体
#include
struct sockaddr_un
{
sa_family_t sa_family; //sa_family是地址族类型
char sun_path[108]; //文件路径名
}
ipv4使用入下结构体:
struct sockaddr_in
{
sa_family_t sin_family; //地址族
u_int16_t sin_port; //端口号 用网络字节序表示
struct in_addr sin_addr;//ipv4地址结构体
}
struct in_addr
{
u_int32_t s_addr;//ipv4地址,用网络字节序表示
}
ipv6使用如下结构体
struct sockaddr_in16
{
sa_family sin6_family;//地址族
u_int16_t sin6_port;//端口号,用网络字节序表示
u_int32_t sin6_flowwinfo;//流信息,设置为0
struct in6_addr sin6_addr;//ipv6地址结构体,
u_int32_t sin6_scope_id;//scope ID 现在处于实验阶段
}
struct in6_addr {
unsigned char sa_addr[16];//ipv6地址,用网络字节序表示
}
所有专用socket地址与sockaddr_storage类型的变量在使用中都必须转换为通用socket地址类型sockaddr(强转即可),因为所有socket 编程接口使用的地址参数的类型都是sockaddr。
通常人们使用点分十进制字符串或者16进制字符串来表示ipv4地址,但是在编程中我们需要先把它们转化为整数(二进制数)方能使用,而记录日志却相反,我们需要把整数表示的IP转化为可读的字符串,下面的三个函数课用于点分十进制字符串表示的IPv4地址和用网络字节序整数表示的ipv4地址之间的转换:
#include
in_addr_t inet_addr(const char* strptr); //点分十进制ip---->网络字节序整数ip 失败返回INADDR_NONE
int inet_aton(const char *cp,struct in_addr* inp);//点分十进制ip----->网络字节序整数IP,将得到的IP存贮于inp指向的地址,失败返回0 ,成功返回1
char* inet_ntoa(struct in_addr in);
inet_ntoa函数用网络字节序整数表示的IPV4地址转换成点分十进制字符串IP地址。该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。
char *szValue1=inet_ntoa("1.2.3.4");
char *szValue2=inet_ntoa("10.194.71.60");
printf("address1:%s\n",szValue1);
printf("address2: %s\n",szValue2);
运行结果:
address1:1.2.3.4
address2:10.194.71.60
下面这对函数能完成以上三个函数的所有功能,且同样适用IPV4与ipv6地址
#include
int inet_pton(int af,const char* src,void *dst)//字符串转网络字节序整数
const char* inet_ntop(int af,const void src,char *dst,socklen_t cnt)//网络字节序整数转字符串
inet_pton函数中af参数表示协议族,src表示传入的字符串ip地址,转化后的字节序整数存储在dst指针指向的内存中。(成功返回1,失败返回0,设置errno)
inet_ntop函数中源操作,目标操作与pton相反,cnt表示制定目标存储单元的大小用此区分ipv4和ipv6(成功返回目标存储单元的地址,失败返回NULL并设置ERRNO)
linux/unix关于系统中任何部分都可以抽象为文件,文件都是可读写,可打开关闭,可控制的文件,socket也不例外。下面用一个socket系统调用创建一个socket
#include
#include
int socket(int domain,int type,int protocol);
socket函数成功创建套接字后返回1,失败返回0并且设置ERRNO,
domain参数说明套接字使用什么协议族,若是TCP/IP协议族,则可设置为PF_INET(IPV4)或者PF_INET6(IPV6),若是unix本地域协议族,则设置为PF_UNIX协议。
type参数表明服务类型,在TCP/IP中有两种传输协议:TCP流式传输,设置为SOCK_STREAM, UDP数据报文传输,设置为SOCK_DGRAM.
protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议,不过这个值通常唯一,在前面两个参数已经确定的情况下,应该把本参数设置为0,表示使用默认前两个参数的协议。