socket最开始的含义是一个IP地址和端口队(ip,port)。它唯一地表示了使用TCP通信的一端。这就是socket地址。
主机字节序和网络字节序
现在CPU的累加器一次都能装载(至少)4字节(这里考虑32位机器,下同),即一个整数。那么这4个字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序的问题。
字节序分为大端字节序(big endian)和小端字节序(little endian)。大端字节序是指一个整数的高位字节(23 ~ 31 bit)存储在内存的地址处,低位字节(0~7 bit)存储在内存的高地址处。小端字节序则指整数的高位字节序存储在内存的高地址处,而低位字节序则存在在内存的低地址处。
下面的代码是检查机器的字节序:
#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 endiann");
}
else if((test.union_bytes[0] == 2) && (test.union_bytes[1] == 1)){
printf("little endiann");
}
else{
printf("unknownn");
}
}
int main(int argc, char const *argv[])
{
byteorder();
return 0;
}
当格式化的数据(比如32bit整型数和16bit短型数)在两台使用不同字节序的主机之间传递时,接收端必然错误地解释之。
解决问题的方法是:发送端总是把要发送的数据转化成大端字节序再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接受数据的主机提供了一个正确解释收到的格式化数据的保证。
需要指出的是,即使是同一台机器上的两个进程(比如一个由C语言,另一个JAVA编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。
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);
它们的含义很明确,比如htonl表示“host to network long",即将长整型(32bit)的主机字节序转换为网络字节序数据。这四个函数中,长整型函数通常用来转换IP地址,短整型函数用来转换端口号。(当然不限于此。任何格式化的数据通过网络传输时,都应该使用这些函数来转化字节序)。
通用socket地址
socket网络编程接口中表示socket地址的是结构体sockaddr,其定义如下:
#include <bits/socket.h>
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
sa_family成员是地址族类型(sa_family_t)的变量。地址族类型通常与协议族类型对应。
常见的协议族(protocol family,也称domain)和对应的地址族如下表:
宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。
sa_data成员用于存放socket地址值。但是不同的协议族的地址值具有不同的含义和长度。如下表所示:
由此可以发现,14字节的sa_data根本无法完全容纳多数协议族的地址值。因此,Linux定义了下面这个新的通用socket地址结构体:
#include <bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128 - sizeof(__ss_align)];
};
这个结构体不仅提供了足够大的空间用于存放地址值,而且是内存对齐的(这是__ss_align成员的作用)。
专用socket地址
上面这两个通用socket地址结构体显然很不好用,比如设置与获取IP地址和端口号就需要执行烦琐的位操作。所以Liunx为各个协议族提供了专门的socket地址结构体。
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_storage)类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(强制转化即可),因为所有socket编程接口使用的地址参数的类型都是sockaddr。
IP地址转换函数
通常,人们习惯用可读性好的字符串来表示IP地址,比如用点分十进制字符串表示IPv4地址,以及用十六进制字符串表示IPv6地址。但编程中我们需要先把它们转化为整数(二进制数)方能使用。而记录日志则相反,我们要把整数表示的IP地址转化为可读的字符串。
下面3个函数可用于用点分十进制字符串表示的字符串表示的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函数将用点分十进制串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。失败返回INADDR_NONE。
inet_aton函数完成和inet_addr同样的功能,但是将转化结果存储与参数inp指向的地址结构中。它成功返回1,失败则返回0。
inet_ntoa函数将用网络字节序整数表示的IPv4地址转换为用点分十进制字符串表示的IPv4地址。但需要注意的是:该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char const *argv[])
{
char ip1[] = "1.2.3.4";
char ip2[] = "10.194.71.60";
struct in_addr inAddr1;
struct in_addr inAddr2;
inet_aton(ip1, &inAddr1);
inet_aton(ip2, &inAddr2);
char *szValue1 = inet_ntoa(inAddr1);
char *szValue2 = inet_ntoa(inAddr2);
printf("address1: %sn", szValue1);
printf("address2: %sn", szValue2);
return 0;
}
不可重入的inet_ntoa函数实验结果
下面这对更新的函数也能完成和前面3和函数一样的功能,并且它们使用适用于IPv4地址和IPv6地址:
#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地址或用十六进制字符串表示的IPv6地址)转换成用网络字节序整数表示的IP地址,并把转换结果存储于dst指向的内存中。其中,af参数指定地址族:
- AF_INET
- AF_INET6
inet_pton成功返回1,失败则返回0并设置errno。
inet_ntop函数进行相反的转换,前三个参数的含义与inet_pton参数相同,最后一个cnt指定目标存储单元的大小。下面两个宏能帮助我们指定这个大小(分别用于IPv4和IPv6):
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
inet_ntop成功时返回目标存储单元的地址,失败返回NULL并设置errno。
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
char *ipv4 = "10.0.0.200";
char *ipv6 = "fe80::4bde:83d8:dbcf:72f3";
in_addr inAddr4;
in6_addr inAddr6;
inet_pton(AF_INET, ipv4, &inAddr4);
inet_pton(AF_INET6, ipv6, &inAddr6);
char addr1[INET_ADDRSTRLEN];
char addr2[INET6_ADDRSTRLEN];
if(addr1 == inet_ntop(AF_INET, (void *)&inAddr4, addr1, INET_ADDRSTRLEN)){
printf("truen");
}
printf("IPv4 addr: %sn", inet_ntop(AF_INET, (void *)&inAddr4, addr1, INET_ADDRSTRLEN));
printf("IPv4 addr: %sn", inet_ntop(AF_INET6, (void*)&inAddr6, addr2, INET6_ADDRSTRLEN));
return 0;
}