socket地址API
想学习socket地址API,要先理解主机字节序和网络字节序。
主机字节序和网络字节序
现代CPU的累加器一次至少装载一个整形数据。但是!内存中的排列顺序会影响他们被累加成的整数的值。也就是字节序的影响。
字节序分为大端字节序和小端字节序字节序。
大端字节序是指一个整数的高字节位存储在内存的低地址处,低字节位存储在高地址处。小端则相反。
以下是一段检查用来检查机器的字节序的代码:
#include<iostream>
using namespace std;
void ByteOrder()
{
union
{
short value;
char union_bytes[sizeof(short)]; //每八位组成一个union_bytes
}test;
test.value = 0x0102; //二进制: 0000 0001 0000 0010
if((test.union_bytes[0] == 1) && test.union_bytes[1] == 2)
{
cout<< "big endian"<< endl;
}
else if((test.union_bytes[0] == 2) && test.union_bytes[1] == 1)
{
cout<< "little endian"<< endl;
}
else
{
cout<< "i also don't konw"<< endl;
}
}
现代PC大多采用小端字节序,所以小端字节序又称主机字节序。
但是!仍然有机器采用大端字节序。
解决方法:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道传送的数据是大端字节序,可以根据自身的字节序来决定是否转换。所以大端字节序又称网络字节序。
字节序转换函数
Linux提供以下四个函数来完成字节序的转换。
#include <netinet/in.h>
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);
别看这里函数这么多,但是也是有记忆方法的:
h 指代 host(主机),n 指代 net(网络),l 是 long,s 是 short
所以htonl
就是host to network long
通用socket地址(一般不用)
socket 网络编程接口中用结构体sockaddr
表示:
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family; //地址族类型
char sa_data[14]; //存放socket地址
}
常见协议族:
AF_UNIX
UNIX本地域协议族
AF_INET
TCP/IPv4 协议族
AF_INET6
TCP/IPv6协议族
但是,随着技术的发展,14字节的sa_data
无法容纳多数的协议族的地址值。所以,当下一般使用这个结构体:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align; //用于内存对齐
char __ss_padding[128-sizeof(__ss_align)];
}
专用socket地址
通用虽然兼容性好,但是在设置与获取IP地址和端口号就需要执行繁琐的操作。所以就需要以下Linux提供的专门的地址结构体。
UNIX本地域协议族专用地址结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; //AF_UNIX
char sun_path [108]; //文件路径名
}
TCP/IP 协议族则使用sockaddr_in
和sockaddr_in6
两个结构体,分别用于IPv4和IPv6:
struct sockaddr_in
{
sa_family_t sin_family; //AF_INET
u_int16_t sin_port; //端口号
struct in_addr sin_addr; //IPv4地址结构体
}
struct in_addr
{
u_int32_t s_addr; //IPv4地址
}
struct sockaddr_in6
{
sa_family_t sin6_family; //AF_INET6
u_int16_t sin6_port; //端口号
u_int32_t sin6_flowinfo; //流信息,一般设置为0
struct in6_addr sin6_addr; //IPv6地址结构体
u_int32_t sin6_scope_id; //scope ID,用于标识网络接口
}
struct in6_addr
{
unsigned char sa_addr[16]; //IPv6地址
}
scope_id作用:
用于标识网络接口的,因为多个网络接口可以具有相同的本地链接IPv6地址。目的地址决定使用哪个接口,如果目的地址是本地链接地址,主机将无法确定要使用哪个接口,因为它们都具有相同的网络。来自stack overflow
IP地址转换函数
我们看到的IP地址通常是<stron>来表示的。但是对于计算机来说,它是不识别的。所以就需要把IP地址进行二进制的转换。</stron>
比如说我的云服务器地址就是点分十进制的:47.92.194.18
以下函数用于把点分十进制转换成二进制:
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr); //失败返回INADDDR_NONE
int inet_aton(const char* cp, struct in_addr* inp); //这个函数将转换结果存储结构体中,而不是返回。成功时返回1,失败返回0
下面这个函数将二进制转换为点分十进制:
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr in); //该函数内存用一个静态变量存储转化结构,且不可重入
当然,下面这对更新函数也能完成和前面3个函数同样的功能,并同时适用IPv4和IPv6。
#include <arpa/inet.h>
int inet_pton(int af, const char* src, void* dst); //成功返回1,失败返回0并设置errno
const char* inet_ntop(int af, const void* src, char* dst, socklen_t cnt); //成功返回指向目标存储单元的地址,失败则返回NULL并设置errno
cnt参数执行目标存储单元的大小,下面这个宏帮助我们指定大小:
#include <netinet/in.h> #define INET_ADDRSTRLEN 16 //ipv4 #define INET6_ADDRSTRLEN 46 //ipv6
参考文献
[1]游双.Linux高性能服务器编程.机械工业出版社,2043.5.