目录
注意:本文内容源于《Linux高性能服务器编程》 作者:游双
主机字节序和网络字节序
字节在内存中的排列顺序即为字节序,字节序分为大端字节序(big endian)和小端字节序(little endian)。大端字节是指一个整数的高位字节(23~31位)存储在内存的低地址,而低位字节(0~7位)存储在内存的高地址。小端字节序与大端字节序相反。下方函数可用于检测本机的字节序
#include <iostream>
using namespace std;
void bytesOrder();
int main() {
bytesOrder();
return 0;
}
void bytesOrder(){
union{
short value;
char union_byte[sizeof(short)];
}test;
test.value = 0x0102;
if(test.union_byte[0] == 0x01 && test.union_byte[1] == 0x02){
cout<<"big endian\n";
}
else if(test.union_byte[0] == 0x02 && test.union_byte[1] == 0x01){
cout<<"little endian\n";
}
else{
cout<<"unknown endian\n";
}
}
现代PC多采用小端字节,因此,小端字节又被称为主机字节。当两台主机彼此通信时,发送端总会把数据转换成大端字节再发送。因此接收端应当将收到的数据转换成本地的字节序,才能收到正确的数据。【大端字节序也被称为网络字节序】
注意:即使是同一台主机的两个进程之间进行通信,也要考虑字节序的问题。
Linux提供了如下4个函数来实现主机字节序和网络字节序之间的转换:
#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);
转换IP地址时常用长整型函数,转换端口时常用短整型函数。
通用socket地址
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
注意:sa可能是socket address的缩写
sa_family_t是地址族类型的变量,地址族类型通常与协议类型对应,常见的协议族和对应的地址族见下表。
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地协议族 |
PF_INET | AF_INET | TCP/IPV4协议族 |
PF_INET6 | AF_INET6 | TCP/IPV6协议族 |
宏PF_*和AF_*都定义在 bit/socket.h中,且后者与前者的值完全一样,所以二者通常混用。
sockaddr.sa_data用于存放socket地址值,但是,不同协议族的地址值具有不同的含义和长度,如下表:
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件路径名【长度可达到108字节】 |
PF_INET | 16bit端口号和32bit IPv4地址,共6字节 |
PF_INET6 | 16bit端口号,32bit流标识,128bit IPv6地址,32bit范围ID,共26字节 |
由上表可见,14字节的sa_data根本无法容纳多数协议族的地址值,因此,Linux定义了下面的新的通用socket地址结构体
#include <bit/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__ss_align)];
};
此通用socket地址结构体不仅提供了足够大的空间存放地址值,而且是内存对齐的。【__ss_align的对齐作用】
专用socket地址
上述的两个通用socket地址结构体难以使用,操作繁琐,因此,Linux为各个协议族提供了专用的socket地址结构体。
注意:sin可能为socket internet的缩写
UNIX本地域协议族使用如下socket结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; /*地址族 AF_UNIX*/
char sun_path[108]; /*文件路径名*/
};
TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于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地址,应以网络字节序表示*/
};
所有专用socket地址类型的变量在使用时都必须转化为通用socket地址类型sockaddr(强制转换即可)。因为所有socket编程接口使用的地址参数的类型都是sockaddr。
IP地址转换函数
人们习惯用可读性好的字符串表示IP地址,如使用点分十进制字符串表示IPV4地址,或者用16进制字符串表示IPV6地址。但编程时,我们需要先把它们转换为整数(二进制数)才能使用。而记录日志时正好相反,我们需要把整数表示的IP地址转换为可读的字符串。下列3个函数可用于点分10进制字符串表示的IPV4地址和用网络字节序整数表示的IPV4地址间的转换。
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char *cp, struct in_addr* inp);
char* inet_ntoa(struct in_addr in);
inet_addr和inet_aton均可以将点分十进制字符表示的IPV4地址转换为网络字节序的整数表示的IPV4地址。
inet_ntoa将以网络字节序表示的IPV4地址转换为点分十进制字符串表示的IPV4地址,成功返回1,失败返回0。注意,此函数在内部用一个静态变量存储转换结果,因此,此函数是不可重入的。当前转换的结果会覆盖上一次转换的结果。
#include <arpa/inet.h>
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函数用于将字符串表示的IP地址src(用点分十进制字符串表示的IPV4地址或用16进制字符串表示的IPV6地址)转换为用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。其中,af参数指定地址族,可以是AF_INET或AF_INET6,inet_pton成功时返回1,失败时返回0,并设置errno。
inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton相同,最后一个参数cnt指定目标存储单元的大小,下面的两个宏定义可以帮助我们指定这个大小【分别用于IPV4, IPV6】
注意:也许pton是指point to network
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop成功时返回目标存储单元的地址,失败则返回NULL并设置errno。