《Linux高性能服务器编程》学习记录(二)linux网络编程基础API
Linux 网络API主要有三种:
- socket地址API。socket最开始的含义是一个IP地址和端口对(ip,port)。它唯一地表示了使用TCP通信的一端,称其为socket地址。
- socket基础API。socket的主要API都定义在sys/socket.h头文件中,包括创建socket、命名socket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
- 网络信息API。Linux提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。这些API都定义在netdb.h头文件中。
1. socket地址API
1.1 主机字节序和网络字节序
现代CPU的累加器一次都能装载(至少)4字节(32位机),即一个整数int。那么这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; // 二进制数为00000001 00000010
if((test.union_bytes[0]==1)&&(test.union_bytes[1]==2))
printf("big endian\n");
else if((test.union_bytes[0]==2)&&(test.union_bytes[1]==1))
printf("little endian\n");
else
printf("unknown...\n");
}
int main(){
byteorder();
return 0;
}
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
当格式化的数据(比如32 bit整型数和16 bit短整型数)在两台使用不同字节序的主机之间直接传递时,接收端必然错误的解析数据。
解决问题的方法是:发送端总是把要发送的数据转化成大端字节序数据后再发送,而接收端知道对方传送过来的数据总是采用大端字节序,所以接收端可以根据自身采用的字节序决定是否对接收到的数据进行转换(小端机转换,大端机不转换)。因此大端字节序也称为网络字节序,它给所有接收数据的主机提供了一个正确解释收到的格式化数据的保证。
需要注意的是,即使是同一台机器上的两个进程(比如一个由C语言编写,另一个由JAVA编写)通信,也要考虑字节序的问题(JAVA虚拟机采用大端字节序)。
Linux提供了如下4个函数来完成主机字节序和网络字节序之间的转换:
#include<netinet/in.h>
//htonl表示“host to network long”,即将长整型(32 bit)的主机字节序数据转化为网络字节序数据。
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);
1.2 通用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成员的作用)。
1.3 专用socket地址
上面这两个通用 socket 地址结构体显然很不好用,比如设置与获取IP地址和端口号就需要执行烦琐的位操作。所以Linux为各个协议族提供了专门的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 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转换即可),因为所有 socket 编程接口使用的地址参数的类型都是 sockaddr。
1.4 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是不可重入的。
下面代码揭示了其不可重入性。
char*szValue1=inet_ntoa(“1.2.3.4”);
char*szValue2=inet_ntoa(“10.194.71.60”);
printf(“address 1:%s\n”,szValue1);
printf(“address 2:%s\n”,szValue2);
//运行这段代码,得到的结果是:
address1:10.194.71.60
address2:10.194.71.60
突然有一天假期结束,时来运转,人生才是真正开始了。