2.1 套接字基础
Linux 网络编程接口的发展分为两个方向。一个是套接字接口(Sockets),有时称为“Berkeley 套接口”,因为它源于 Berkeley Unix。另一个是传输层接口(TLI),是System V的开发人员开发。其中由于套接字既简单又实用,所以得到了广泛的发展,已成为网络编程的事实标准。
套接字是一种网络API(应用程序编程接口),可以使用它开发网络程序。套接字接口提供一种进程间通信的方法,使得在相同或不同的主机上的进程能以相同的规范进行双向信息传送。进程通过调用套接字接口来实现相互之间的通信,而套接字接口又利用下层的网络通信协议功能和系统调用实现实际的通信工作。进程通信与套接字接口的关系如图2-1 所示。
进程之间要进行通信,首先要调用网络编程接口,由套接字负责将进程接收和发送的请求信息通过下层的网络通信协议服务接口( TCP/IP )向上或向下交付。所以套接字接口是应用层到传输层的接口。
2.2 套接字的类型
通信协议
套接字支持各种通信协议,目前 Linux 系统常用的协议有以下两种:
- INET:IP版本4
- INET6:IP版本6
套接字类型
套接字类型是指创建套接字的应用程序要使用的通信服务的类型。Linux 系统支持多种套接字类型,最常用的有以下几种:
- SOCK_STREAM:流式套接字,提供面向连接、可靠的数据传输服务,数据是按字节流、按照顺序收发,保证数据在传输过程中无丢失、无冗余。TCP协议支持该套接字。
- SOCK_DGRAM:数据报套接字,提供面向无连接的服务,数据收发无序,不能保证数据的准确到达。UDP协议支持该套接字。
- SOCK_RAW:原始套接字。允许对低于传输层的协议或物理网络直接访问,例如可以接收和发送ICMP报。常用于检测新的协议。
2.3 套接字地址结构★
在使用套接字函数编写网络程序时,大多数函数都需要一个指向地址结构的参数。每个协议族都定义了自己的套接字地址结构,这些结构的名字均以 sockaddr_ 开始,并以对应每个协议族为后缀。例如, IPv4 的套接字地址结构为 sockaddr_in ,而 IPv6 的套接字地址结构为sockaddr_in6 。
2.3.1 IPv4套接字地址结构
IPv4套接字地址结构通常也称为“网际套接字地址结构”,它的名字为 sockaddr_in ,定义在头文件 <netinet/in.h>中,其结构定义如下:
typedef uint32_t in_addr_t;
typedef uint16_t in_port_t;
typedef unsigned short sa_family_t;
struct in_addr{
in_addr_t s_addr;
};
struct sockaddr_in {
uint8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
在sockaddr_in 结构体包含 5 个成员, 需要注意的是其中 sin_addr 成员的类型是结构体,它存储的是存放 IP 地址的结构体名称 。 各成员的作用如下:
- sin_len是长度成员,存储套接字地址结构的长度,但不是所有系统都支持,有了它可以简化变长套接字地址结构的处理。一般情况下不需要设置它和检查它,除非涉及到路由套接字。
- sin_family是Internet地址族,在IPv4 中是AF_INET。
- sin_port是端口号,以网络字节序存储。
- sin_addr是一个结构,该结构中的成员存储的才是IP地址。
- sin_zero成员暂时没有被使用,但总是将它置为0。所以为了方便,在初始化结构时,将整个结构置为0。
- in_addr结构中的s_addr成员存储的是网络字节序的32位IPv4地址。
通过给出的IPv4 套接字地址结构发现,可以有两种不同的方式来访问 IP 地址。
例如 struct sockaddr_in ser;
则 ser.sin_addr
给出的是一个存放地址的 in_addr 结构,而ser.sin_addr.s_addr
存储的是地址中的内容,也就是 IP 地址的值 。因此在 IP 地址作为参数传
递时,一定要注意,传递结构和传递整数编译器的处理是完全不同的。
2.3.2 IPv6套接字地址结构
略
2.3.3 两种套接字地址结构的比较
前面的小节介绍了IPv4 和IPv6 两种套接字地址结构的构成,如图2-2 所示,用图形的方式画出了两种地址结构的构成,可以看出IPv6 的地址长度要大于IPv4,而且IPv6 多出一个32 位流标签。
2.3.4 通用套接字地址结构
套接字地址结构作为参数传递给任一个套接字函数时,通常通过指针来传递。当套接字函数取得此参数时,参数中可能存放的是来自所支持的任何协议族的地址结构。因此在调用套接字函数时,需要将指向特定于协议的地址结构的指针类型转换成指向通用的地址结构的指针。
通用套接字地址结构sockaddr定义在头文件<sys/socket.h>中。具体的结构定义如下:
struct sockaddr{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
许多套接字函数使用该通用的地址结构作为参数。例如:
struct sockaddr_in ser;
bind(sockfd, (struct sockaddr *)&ser,sizeof(ser));
2.4 套接字基本函数
2.4.1 字节排序函数★
小端字节序和大端字节序
内存中存储字节有两种不同的方法,不同系统采用的存储方式可能不同,为了使不同主机能够相互通信,必须定义一个存储数据的标准。如图2-3 所示字节存储的顺序分为两类:
- 小端字节序:将低序字节存储在起始地址。
- 大端字节序:将高序字节存储在起始地址。
主机字节序和网络字节序
将某给定主机所使用的字节序称为主机字节序(host byte order)。为了使采用不同字节序的主机能够互相通信,TCP/IP 协议规定了网络字节序。所有主机或路由器在发送IP 数据包之前要首先将相应的信息转换成网络字节序。相应的,在接收数据包后,要将网络字节序转换成主机字节序。因此,无论主机系统采用的是何种字节序,它们之间都可以正常进行通信。
主机/网络字节序的相互转换函数
主机字节序和网络字节序之间的相互转换,要用到以下四个函数:
#include <netinet/in.h>
uint16_t htons(uint16_t hosts);
uint32_t htonl(uint32_t hostl);
uint16_t ntohs(uint16_t nets);
uint32_t ntohl(uint32_t netl);
在上述四个函数中,h 代表主机host,n 代表网络network,s 代表16 位短整形short,l 代表32 位长整形long。
- htons:将16 位的短整形数,从主机字节序转换成网络字节序。
- htonl:将32 位的长整形数,从主机字节序转换成网络字节序。
- ntohs:将16 位的短整形数,从网络字节序转换成主机字节序。
- ntohl:将32 位的长整形数,从网络字节序转换成主机字节序。
当使用以上函数时,不必关心主机字节序和网络字节序的真实值。只是调用适当的函数对给定值进行主机字节序与网络字节序之间的转换。如果当前主机字节序和网络字节序的存储字节的顺序相同,这四个函数通常被定义成空宏。
2.4.2 字节操纵函数★
当使用套接字地址结构时,其中的IP 地址字段,包含多个字节的0,但又不是C 字符串,对这样的字段进行操作时需要用到字节操纵函数。下面介绍两组操纵函数,第一组函数名字是以字母b(byte)打头,起源于4.2BSD。第二组函数名字是以mem(memory)打头,起源于ANSI C 标准。
源于4.2BSD的函数
#include <string.h>
void bzero(void *dest, size_t len);
void bcopy(const void *src, void *dest, size_t len);
int bcmp(const void *src, void *dest, size_t len);
- bzero 函数将目标中指定数目的字节置为0,经常用此函数来对套接字地址结构进行初始化。
- bcopy 函数将指定数目的字节从源拷贝到目标。
- bcmp 函数比较源和目标两个字符串,若相同返回值为0,否则返回非0 值。
源于ANSI C的函数
void *memset(void *dest, int x, size_t len);
void *memcpy(void *dest, const void *src, size_t len);
int memcmp(const void *str1, const void *str2, size_t len)
memset 函数将目标指定数目的字节置为值x。
bcopy 与memcpy 函数类似,但交换了源和目标指针参数的位置。当源与目标重迭时,bcopy 函数能正确处理,而memcpy 函数的操作结果则是不可知的。
memcmp 函数比较两个字符串,若相同返回值为0,否则返回非0 值,如果str1 所指字节大于str2 所指字节,则返回值大于0,否则返回值小于0。
2.4.3 IP地址转换函数▽
通常人们比较喜欢用点分十进制来表示IP 地址,而套接字地址结构中却需要用网络字节序的二进制值存储。为方便字符串的 IP 和二进制值的 IP 相互转换, Linux 系统提供了相应的地址转换函数。
#include <arpa/inet.h>
in_addr_t inet_addr(const char *str);
int inet_aton(const char *str, struct in_addr *numstr);
char *inet_ntoa(struct in_addr inaddr);
函数中 a 代表 ASCII 串, n 代表数值( numeric )格式,是存在于套接字地址结构中的二进制值。
inet_addr函数
将字符串形式的 IP 地址转换成 32 位二进制值的 IP 地址。 str 指向字符串形式的 IP 地址。函数调用成功,返回值为 32 位网络字节序的二进制值的 IP 地址。这个函数不对 IP 地址的有效性进行验证,所有 2^32 个 从 0.0.0.0 到 255.255.255.255) 可能的二进制值它都认为是有效的 IP 地址。如果该函数它会返回一个常值 INADDR_NONE (一般为一个 32位均为 1 的值),所以对于点分十进制的 IP 地址 255.255.255.255 不能由此函数处理。现在人们常用 inet_aton 函数代替 inet_addr 函数。
inet_aton函数
进行相同的转换。 str 指向字符串形式的 IP 地址。 numstr 指向转换后的32 位网络字节序的 IP 地址。如果成功返回 1 ,否则返回 0 。
inet_ntoa函数
将一个 32 位 网络字节序的二进制值的 IP 地址转换成相应的点分十进制的 IP 地址。这个函数的参数是一个结构,而不是指向结构的指针。 该函数的返回值所指向的 串留在静态内存中,所以函数是不可重入的。
适用IPv6地址的inet_pton和inet_ntop函数
上述三个地址转换函数都只能处理IPv4 协议,而不能处理 IPv6 地址。下面介绍两个较新的函数,它们对 IPv4 和 IPv 6 地址都能处理。
#include <arpa/inet.h>
int inet_pton(int family, const char *str, void *numstr);
const char *inet_ntop(int family, const void *numstr, char *str, size_t len);
函数中p 代表地址的表达( presentation )格式通常是 ASCII 串, n 代表数值(numeric)格式,是存在于套接字地址结构中的二进制值。这两个函数中的 family 参数,指的是操作地址的地址族, IPv4 是 AF_INET ,而 IPv6 是 AF_INET6 。
inet_pton函数:将指针 str 所指的字符串形式的 IP 地址,转换成网络字节序的二进制值的 IP 地址,并用指针 numstr 存储。如果成 功返回 1 ,如果对于指定的 famil y 输入的字符串不是一个有效的表达格式,则返回值为 0 ,出错返回 1 。
inet_ntop函数:进行相反的操作,将 numstr 所指的二进制值的 IP 地址转换成字符串形式的 IP 地址,并用指针 str 存储。参数 len 是目标的大小,为了避免函数溢出其调用者的缓冲区。如果 len 太小无法容纳表达式的结果,则返回一个空指针,并置 errno 为 ENOSPC 。而对于参数 str 不能是空指针,调用者必须为目标分配内存并指定大小,成功时,此指针就是函数的返回值。
如图 2-4 所示 给出了这五个地址转换函数。
2.4.4 isfdtype 函数▽
很多时候需要测试某个描述符是不是给定的类型,这就需要用到isfdtype 函数。
#include <sys/stat.h>
int isfdtype(int fd, int fdtype);
isfdtype 函数:测试描述符fd 是不是fdtype 参数指定的类型。
为了测试描述符是否是套接字描述符,fdtype 参数应设为S_IFSOCk。例如:
isfdtype(sockfd, S_IFSOCk);
2.5 值-结果参数▽
当把套接字地址结构传递给套接字函数时,总是通过指针来传递的,即传递的是一个指向套接字地址结构的指针,结构的长度也可用参数传递。
从进程到内核传递套接字地址结构的函数通常有三个,分别是bind、connect 和sendto,在这三个函数的参数中都含有两个相似的参数,分别是指向套接字地址结构的指针及该地址结构的大小。例如:
struct sockaddr_in ser;
bind(sockfd, (struct sockaddr *)&ser,sizeof(ser));
这里的bind 函数将套接字地址结构和结构大小都传递给了内核,所以进程到内核拷贝的数据长度是确定的。
而从内核到进程传递套接字地址结构的函数通常有四个,分别是accept、recvfrom、getsockname 和getpeername。这些函数也包含两个参数分别是,指向套接字地址结构的指针和指向套接字地址结构大小的指针,例如:
struct sockaddr_in client;
socklen_t len;
len = sizeof(client);
accept(listenfd, (struct sockaddr *)&client, &len);
由于accept 函数中存放套接字地址结构长度的参数是个指针,那么在函数调用时,结构长度是个值,这个值告诉内核该结构的长度,使内核在写这个结构时不会越界。而当函数返回时,结构大小的值发生了变化,变成了内核在此结构中确切存储的数据长度。进程可以通过这个值得到内核写入多少信息到这个结构中。这种在参数传递的过程,其值发生变化了的参数称为值-结果参数,内核和进程间的两种传递参数的方式如图2-5 所示。
当使用值-结果参数作为套接字地址结构的长度时,如果套接字地址结构的长度是定长的,则从内核返回的值也是定长的,没有发生变化,如IPv4 的套接字地址结构sockaddr_in是16。但如果套接字地址结构不是定长的,这时的返回值可能小于套接字地址结构的最大长度。