目录
我们将从如下3个方面讨论Linux网络API:
- socket地址API。socket最开始的含义是一个IP地址和端口对(ip,port)。它唯一地表示了使用TCP通信的一端。本书称为socket地址。
- socket基础API。socket的主要API都定义在sys/socket.h头文件中,包括创建socket、命名sicket、监听socket、接受连接、发起连接、读写数据、获取地址信息、检测带外标记,以及读取和设置socket选项。
- 网络信息API。Linux提供了一套网络信息API,以实现主机名和IP地址之间的转换,以及服务名称和端口号之间的转换。这些API都定义在netdb.h头文件中,我们将讨论其中几个主要的函数。
socket地址API
主机字节序和网络字节序
现代CPU的累加器一次都能装载(至少)4字节(32位机),即一个整数。那么这4个字节在内存中排列的顺序将影响它被累加器装载成的整数的值。这就是字节序问题。字节序分为大端字节序(big endian)和小端字节序(little endian)。大端字节序是指一个整数的高位字节(23-31bit)存储在内存的低地址处,低位字节(0-7bit)存储在内存的高地址处。小端字节序则是指整数的高位字节序存储在内存的高地址处,而低位字节则存储在内存的低地址处。
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。
当格式化的数据(比如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);
在这4个函数中,长整型函数通常用来转换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_UNIX AF_UNIX UNIX本地域协议族 PF_INET AF_INET TCP/IPv4协议族 PF_INET6 AF_INET6 TCP/IPv6协议族 宏PF_*和AF_*都定义在bits/socket.h头文件中,且后者与前者有完全相同的值,所以二者通常混用。
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<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地址和端口号就需要执行烦琐的位操作。所以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)类型的变量在实际使用时都需要转化为通用sock地址类型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是不可重入的。代码的不可重入性体现:
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 address1: 10.194.71.60
下面这对更新的函数也能完成和前面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。
创建socket
UNIX/Linux的一个哲学是:所有东西都是文件。socket也不例外,它就是可读、可写、可控制、可关闭的文件描述符。下面的socket系统调用可创建一个socket:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain , int type , int protocol);
domain参数告诉系统使用哪个底层协议族。对于TCP/IP协议族而言,该参数应该设置为PF_INET(Protocol Family of Internet,用于IPv4)或PF_INET6(用于IPv6);对于UNIX本地域协议族而言,该参数应该设置为PF_UNIX。
type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_DGRAM(数据报)服务。对于TCP/IP协议族而言,其取值SOCK_STREAM表示传输层使用TCP协议,而SOCK_DGRAM表示传输层使用UDP协议。
值得指出的是,自Linux内核版本2.6.17起,type参数可以接受上述服务类型与下面两个重要的标志相与的值:SOCK_NONBLOCK和SOCK_CLOEXEC。它们分别表示将新创建的socket设为非阻塞的以及用fork调用创建子进程时在子进程中关闭该socket。在内核版本2.6.17之前的Linux中,文件描述符的两个属性都需要使用额外的系统调用(比如fcntl)来设置。
protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常都是唯一的(前两个参数已经完全决定了它的值)。几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。
socket系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。
命名socket
创建socket时,我们给它指定了地址族,但是并未指定使用该地址族中的哪个具体socket地址。将一个socket与socket地址绑定称为给socket命名。在服务器程序中,我们通常要命名socket,因为只有命名后客户端才能知道该如何连接它。客户端则通常不需要命名socket,而是采用匿名方式,即使用操作系统自动分配的socket地址。命名socket的系统调用是bind,其定义如下:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd , const struct sockaddr* my_addr , socklen_t addrlen);
bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符,addrlen参数指出该socket地址长度。
bind成功时返回0,失败则返回-1并设置errno。其中两种常见的errno是EACCES错误和EADDRINUSE,它们的含义分别是:
- EACCES,被绑定的地址是受保护的地址,仅超级用户能够访问。比如普通用户将socket绑定到知名服务端口(端口号为0-1023)上时,bind将返回EACCES错误。
- EADDRINUSE,被绑定的地址正在使用中。比如将socket绑定到一个处于TIME_WAIT状态的socket地址。
如果bind绑定的是INADDR_ANY,即表示所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都由这个服务端进程进行处理。
监听socket
socket被命名之后,还不能马上接受客户连接,我们需要使用如下系统调用来创建一个监听队列以存放待处理的客户连接:
#include<sys/socket.h>
int listen(int sockfd , int backlog);
sockfd参数指定被监听的socket。backlog参数提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不再受理新的客户连接,客户端也将收到ECONNREFUSED错误信息。在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。但自内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5.
listen成功时返回0,失败则返回-1并设置errno。
接受连接
下面的系统调用从listen监听队列中接受一个连接:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd , struct sockaddr* addr , socklen_t* addrlen);
sockfd参数是执行过listen系统调用的监听socket。addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出。accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应客户端通信。accept失败则返回-1并设置errno。
accept只是从监听队列中取出连接,而不论连接处于何种状态(如上面的ESTABLISHED状态和CLOSE_WAIT状态),更不关心任何网络状态的变化。
发起连接
如果说服务器通过listen调用来被动接受连接,那么客户端需要通过如下系统调用来主动与服务器建立连接:
#include<sys/types.h>
#include<sys/socket.h>
int connect(int sockfd , const struct sockaddr* serv_addr , socklen_t addrlen);
sockfd参数由socket系统调用返回一个socket。serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度。
connect成功时返回0.一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno。其中两种常见的errno时ECONNREFUSED和ETIMEDOUT,它们的含义如下:
- ECONNREFUSED,目标端口不存在,连接被拒绝。
- ETIMEDOUT,连接超时。
关闭连接
关闭一个连接实际上就是关闭该连接对应的socket,这可以通过如下关闭普通文件描述符的系统调用来完成:
#include<unistd.h>
int close(int fd);
fd参数是待关闭的socket。不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减一。只有当fd的引用计数为0时,才真正关闭连接。多进程程序中,一次fork系统调用默认将使父进程中打开的socket的引用计数加1,因此我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。
如果无论如何都要立即终止连接,可以使用如下shutdown系统调用(相对于close来说,它是专门为网络编程设计的):
#include<sys/socket.h>
int shutdown(int sockfd , int howto);
sockfd参数是待关闭的socket。howto参数决定了shutdown的行为。
可选值 含义 SHUT_RD
关闭sockfd上读的这一半。应用程序不能针对socket文件描述符执行读操作,并且该socket接收缓冲区中的数据都被丢弃。 SHUT_WR 关闭sockfd上写的这一半。sockfd的发送缓冲区中的数据会在真正关闭连接之前全部发送出去,应用程序不可再对该socket文件描述符执行写操作。在这种情况下,连接处于半关闭状态 SHUT_RDWD 同时关闭sockfd上的读和写 由此可见,shutdown能够分别关闭socket上的读和写,或者都关闭。而close在关闭连接时只能将socket上的读和写同时关闭。
shutdown成功时返回0,失败则返回-1并设置errno。
数据读写
TCP数据读写
对于文件的读写操作read和write同样适用于socket,但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。其中用于TCP流数据读写的系统调用是:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recv(int sockfd , void *buf , size_t len , int flags);
ssize_t send(int sockfd , const void *buf , size_t len , int flags);
recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数的含义见后文,通常设置为0即可,recv成功时返回实际读取到的数据的长度,它可能小于我们期望的长度len。因此我们可能要多次调用recv,才能读取到完整的数据,recv可能返回0,这意味着通信双方已经关闭连接了,recv出错时返回-1并设置errno。
send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小,send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。
flags参数为数据收发提供了额外的控制,它可以取表中一个或多个的逻辑或:
选项名 含义 send recv MSG_CONFIRM 指示数据链路层协议持续监听对方的回应,直到得到答复。它仅能用于SOCK_DGRAM和SOCK_RAW类型的socket Y N MSG_DONTROUTE 不查看路由表,直接将数据发送给本地局域网内的主机,这表示发送者确切地知道目标主机就在本地网络上 Y N MSG_DONTWAIT 对socket的此次操作将是非阻塞的 Y Y MSG_MORE 告诉内核应用程序还有更多数据要发送,内核将超时等待新数据写入TCP发送缓冲区后一并发送。这样可防止TCP发送过多小的报文段,从而提高传输效率 Y N MSG_WAITALL 读操作仅在读取到指定数量的字节后才返回 N Y MSG_PEEK 窥探读缓存中的数据,此次读操作不会导致这些数据被清除 N Y MSG_OOB 发送或接收紧急数据 Y Y MSG_NOSIGNAL 在读端关闭的管道或者socket连接中写数据时不引发SIGPIPE信号 Y N MSG_OOB选项给应用程序提供了发送和接收带外数据的方法。
发送带外数据abc时,仅有c被服务器当成真正的带外数据接收,前面的被当作正常信号
*
flags参数只对send和recv的当前调用生效,后面我们将看到如何通过setsockopt系统调用永久性地修改socket的某些属性
UDP数据读写
UDP很明显是可以不通过连接就能够达到目的的。
对于UDP使用connect连接会有什么好处呢。
与默认的未连接的UDP套接字相比,发生了三个变化:
1、我们再也不能给输出操作制定目的IP地址和端口号。也就是说,我们不使用sendto而改用write或send。写到已连接UDP套接字上的任何内容都自动发送到connect指定的协议地址(例如IP地址和端口号)。
2、我们不必使用recvfrom以获悉数据报的发送者,而改用read,recv或recvmsg.在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报。
3、由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而为连接UDP不接收任何异步错误。
socket编程接口中用于UDP数据报读写的系统调用是:
#include<sys/types.h>
#include<sys/socket.h>
ssize_t recvfrom(int sockfd , void *buf , size_t len , int flags , struct sockaddr * src_addr , socklen_t * addrlen);
ssize_t sendto(int sockfd , const void *buf , size_t len , int flags , const struct sockaddr *dest_addr , socklen_t addrlen);
recvfrom读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置大小。因为UDP通信没有连接的概念。所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数指定地址的长度。
sendto往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小。dest_addr参数指定接收端的socket地址,addrlen参数则指定该地址的长度。
这两个系统调用的flags参数以及返回值的含义均与send/recv系统调用的flags参数及返回值相同。
值得一提的是,recvfrom/sendto系统调用也可以用于面向连接(STREAM)的socket的数据读写,只需要把最后两个参数都设置为NULL以忽略发送端/接收端的socket地址(因为我们
已经和对方建立了连接,所以已经知道其socket地址了)
通用数据读写函数
socket编程接口还提供了一对通用的数据读写系统调用。它们不仅能用于TCP流数据,也能用于UDP数据报:
#include<sys/socket.h>
ssize_t recvmsg(int sockfd , struct msghdr *msg , int flags);
ssize_t sendmsg(int sockfd , struct msghdr *msg,int flags);
sockfd参数指定被操作的目标socket。msg参数是msghdr结构体类型的指针,msghdr结构体的定义如下:
struct msghdr { void *msg_name; /*socket地址*/ socklen_t msg_namelen; /*socket地址的长度*/ struct iovec *msg_iov; /*分散的内存块,见后文*/ int msg_iovlen; /*分散内存块的数量*/ void *msg_control; /*指向辅助数据的起始位置*/ socklen_t msg_controllen; /*辅助数据的大小*/ int msg_flags; /*复制函数中的flags参数,并在调用过程中更新*/ };
msg_name成员指向一个socket地址结构变量。它指定通信双方的socket地址。对于面向连接的TCP协议,该成员没有意义,必须被设置为NULL。这是因为对数据流socket而言,对方的地址已经知道。msg_namelen成员则指定了msg_name所指socket地址的长度。
msg_iov成员是iovec结构体类型指针,iovec结构体的定义如下:
struct iovec { void *iov_base; /*内存起始地址*/ size_t iov_len; /*这块内存的长度*/ };
由上可见,iovec结构体封装了一块内存的起始地址和长度。msg_iovlen指定了这样的iovec结构对象有多少个。对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度则由msg_iov指向的数组指定,这称为分散读(scatter read);对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写(gather write)。
msg_control和msg_controllen成员用于辅助数据的传送。我们不详细讨论它们,仅在第13章介绍如何使用它们来实现在进程间传递文件描述符。
msg_flags成员无须设定,他会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。recvmsg还会在调用结束前,将某些更新后的标志设置到msg_flags中。
recvmsg/sendmsg的flags参数以及返回值的含义均与send/recv的flags参数及返回值相同。
带外数据
实际应用中,我们通常无法预期带外数据何时到来,好在Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。内核通知应用程序带外数据到达的两种常见方式是:I/O复用产生的异常事件和SIGURG信号。但是,即使应用程序得到了有带外数据需要接收的通知,还需要知道带外数据在数据流中的具体位置,才能准确接收带外数据。这一点可通过如下系统调用实现:
#include<sys/socket.h>
int sockatmark(int sockfd);
sockatmark判断sockfd是否处于带外标记,即下一个被读取到的数据是否是带外数据。如果是,sockatmark返回1,此时我们就可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0.
地址信息函数
在某些情况下,我们想知道一个连接socket的本端socket地址,以及远端socket地址。下面这两个函数正是用于解决这个问题:
#include<sys/socket.h>
int getsockname(int sockfd , struct sockaddr *address , socklen_t *address_len);
int getpeername(int sockfd , struct sockaddr *address , socklen_t *address_len);
getsockname获取sockfd对应的本端socket地址,并将其存储于address参数指定的内存中,该socket地址的长度则存储于address_len参数指向的变量中。如果实际socket地址的长度大于address所指内存区的大小,那么该socket地址将被截断。getsockname成功时返回0,失败返回-1并设置errno。
getpeername获取sockfd对应的远端socket地址,其参数及返回值的含义与getsockname的参数及1返回值相同。
socket选项
如果说fcntl系统调用是控制文件描述符属性的通用POSIX方法,那么下面两个系统调用则是专门用来读取和设置socket文件描述符属性的方法:
#include<sys/socket.h>
int getsockopt(int sockfd , int level , int option_name , void *option_value , socklen_t *restrict option_len);
int setsockopt(int sockfd , int level , int option_name , const void *option_value , socklen_t option_len);
sockfd参数指定被操作的目标socket。level参数指定要操作哪个协议的选项(即属性),比如IPv4、IPv6、TCP等。option_name参数则指定选项的名字。我们在表中列举了socket通信中几个比较常用的socket选项。option_
value和option_len参数分别是被操作选项的值和长度。不同的选项具有不同类型的值》如表中“数据类型”一列所示。
level option name 数据类型 说明 SOL_SOCKET(通用socket选项,与协议无关) SO_DEBUG int 打开调试信息 SO_REUSEADDR int 重用本地地址 SO_TYPE int 获取socket类型 SO_ERROR int 获取并清除socket错误状态 SO_DONTROUTE int 不查看路由表,直接将数据发送给本地局域网内的主机。含义和send系统调用的MSG_DONTROUTE标志类似 SO_RCVBUF int TCP接收缓冲区大小 SO_SNDBUF int TCP发送缓冲区大小 SO_KIIPALIVE int 发送周期性保活报文以维持连接 SO_OOBINLINE int 接收到的带外数据将存留在普通数据的输入队列中(在线存留),此时我们不能使用带MSG_OOB标志的读操作来读取带外数据(而应该像读普通数据那样读取带外数据) SO_LINGER linger 若有数据待发送,则延迟关闭 SO_RCVLOWAT int TCP接收缓冲区低水位标记 SO_SNDLOWAT int TCP发送缓冲区低水位标记 SO_RCVTIMEO timeval 接收数据超时 SO_SNDTIMEO timeval 发送数据超时 IPPROTO_IP
(IPv4选项)
IP_TOS int 服务类型 IP_TTL
int 存活时间 IPPROTO_IPV6
(IPv6选项)
IPv6_NEXTHOP sockaddr_in6 下一跳IP地址 IPv6_RECVPKTINFO int 接收分组信息 IPv6_DONTFRAG int 禁止分片 IPv6_RECVTCLASS int 接收通信类型 IPPROTO_TCP
(TCP选项)
TCP_MAXSEG int TCP最大报文段大小 TCP_NODELAY int 禁止Nagle算法 getsockopt和setsockopt这两个函数成功时返回0,失败时返回-1并设置errno。
*
值得指出的是,对于服务器而言,有部分socket选项只能在调用listen系统调用前针对监听socket设置才有效。这是因为连接socket只能由accept调用返回,而accept从listen监听队列中接受的连接至少已经完成了TCP三次握手的前两个步骤(因为listen监听队列的连接至少已进入SYN_RCVD状态),这说明服务器已经被接受连接上发送出了TCP同步报文段。但有的socket选项却应该在TCP同步报文段中设置,比如TCP最大报文选项。对这种情况,Linux给开发者人员提供的解决方案是:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项。这些socket选项包括:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。而对客户端而言,这些socket选项则应该在调用connect函数之前设置,因为connect调用成功返回之后TCP三次握手已完成。
SO_REUSEADDR选项
TCP连接TIME_WAIT状态,并提到服务器程序可以通过设置socket选项SO_REUSEADDR来强制使用被处于TIME_WAIT状态的连接占用的socket地址。
经过setsocket的设置之后,即使sock处于TIME_WAIT状态,与之绑定的socket地址也可以立即被重用。此外,我们也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle来快速回收被关闭的socket,从而使得TCP连接根本不进入TIME_WAIT状态,进而允许应用程序立即重用本地的socket地址。
SO_RCVBUF和SO_SNDBUF选项
SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小。不过,当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不小于某个最小值。TCP接收缓冲区的最小值是256字节,而发送缓冲区的最小值是2048字节(不过,不同的系统可能有不同的默认最小值)。系统这样做的目的,主要是确保一个TCP连接拥有足够的空间缓冲区来处理拥堵(比如快速重传算法就期望TCP接收缓冲区能至少容纳4个大小的SMSS的TCP报文段)。此外,我们可以直接修改内核参数/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制TCP接收缓冲区和发送缓冲区没有最小值限制。
SO_RCVLOWAT和SO_SNDLOWAT选项
SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记。它们一般被I/O复用系统调用用来判断socket是否可读或可写。当TCP接收缓冲区中可读数据的总数大于其低水位标记时,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据;当TCP发送缓冲区中的空闲空间(可以写入数据的空间)大于其最低水位标记时,I/O复用系统调用将通知应用程序可以往对应的socket上写入数据。
默认情况下,TCP接收缓冲区的低水位标记和TCP发送缓冲区的低水位标记均为1字节。
SO_LINGER选项
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为。默认情况下,当我们使用close系统调用来关闭一个socket时,close将立即返回,TCP模块负责把该socket对应的TCP发送缓冲区中残留的数据发送给对方。
设置(获取)SO_LINGER选项的值时,我们需要给setsockopt(getsockopt)系统调用传递一个linger类型的结构体,定义如下:
#include<sys/socket.h>
struct linger
{
int l_onoff; /*开启(非0)还是关闭(0)该选项*/
int l_linger; /*滞留时间*/
};
此时close的行为取决于两个条件:一是被关闭的还是非阻塞的。对于阻塞的socket,close将等待一段长为l_linger的时间,直到TCP模块发送完所有残留数据并得到对方的确认。如果这段时间内TCP模块没有发送完残留数据并得到对方的确认,那么close系统调用将返回-1并设置errno为EWOULDBLOCK。如果socket是非阻塞的,close将立即返回,此时我们需要根据其返回值和errno来判断残留数据是否已经发送完毕。
网络信息API
gethostbyname和gethostbyaddr
gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr函数根据IP地址获取主机的完整信息。gethostbyname函数通常先在本地的/etc/hosts配置文件中查找主机,如果没有找到,再去访问DNS服务器。定义如下:
#include<netdb.h>
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr , size_t len , int type);
name参数指定目标主机的主机名,addr参数指定目标主机的IP地址,len参数指定addr所指IP地址的长度,type参数指定addr所指IP地址的类型,其合法取值包括AF_INET(用于IPv4地址)和AP_INET6(用于IPv6地址)
这两个函数返回的都是hostent结构体类型的指针,hostent结构体的定义如下:
#include<netdb.h> struct hostent { char *h_name; /*主机名*/ char **h_aliases; /*主机别名列表,可能有多个*/ int h_addrtype; /*地址类型(地址族)*/ int h_length; /*地址长度*/ char **h_addr_list; /*按网络字节序列出的主机IP地址列表*/ }
getservbyname和getservbyport
getservbyname函数根据名称获取某个服务的完整信息,getservbyport函数根据端口号获取某个服务的完整信息。它们实际上都是通过读取/etc/services文件来获取服务的信息的。两个函数的定义:
#include<netdb.h>
struct servent *getservbyname(const char *name , const char *proto);
struct servent *getservbyport(int port , const char *proto);
name参数指定目标服务的名字,port参数指定目标服务对应的端口号。proto参数指定服务类型,给它传递“tcp”表示获取流服务,给它传递udp表示获取数据报服务,给它传递NULL则表示获取所有类型的服务。
这两个函数返回的都是servent结构体类型的指针,结构体servent的定义如下:
#include<netdb.h> struct servent { char *s_name; /*服务名称*/ char **s_aliases; /*服务的别名列表,可能有多个*/ int s_port; /*端口号*/ char *s_proto; /*服务类型,通常是tcp或者udp*/ };
上面讨论的4个函数都是不可重入的,即非线程安全的。不过netdb.h头文件给出了它们的可重入版本。正如Linux下所有其他函数的可重入版本的命名规则那样,这些函数的函数名是在原函数名尾部加上_r(re_entrant)。
getaddrinfo
getaddrinfo函数既能通过主机名获得IP地址(内部使用的是gethostbyname函数),也能通过服务名获得端口号(内部使用的是getservbyname函数)。它是否可重入取决于内部调用的gethostbyname和getservbyname函数是否是它们的可重入版本。该函数的定义如下:
#include<netdb.h>
int getaddrinfo(const char *hostname , const char *service , const struct addrinfo *hints , struct addrinfo **result);
hostname参数可以接收主机名,也可以接收字符串表示的IP地址(IPv4采用点分十进制字符串,IPv6则采用十六进制字符串)。同样,service参数可以接收服务名,也可以接收字符串表示的十进制端口号。hints参数是应用程序给getaddrinfo的一个提示,以对getaddrinfo的输出进行更精确的控制。hints参数可以被设置为NULL,表示允许getaddrinfo反馈任何可用的结果。result参数指向一个链表,该链表用于存储getaddrinfo反馈的结果。
getaddrinfo反馈的每一条结果都是addringo结构体类型的对象,该结构体addrinfo的定义如下:
struct addrinfo { int ai_flags; /*见后文*/ int ai_family; /*地址族*/ int ai_socktype; /*服务类型,SOCK_STREAM或SOCK_DGRAM*/ int ai_protocol; /*见后文*/ socklen_t ai_addrlen; /*socket地址ai_addr的长度*/ char *ai_canoname; /*主机的别名*/ struct sockaddr *ai_addr; /*指向socket地址*/ struct addrinfo *ai_next; /*指向下一个sockinfo结构的对象*/ };
该结构体中,ai_protocol成员是指具体的网络协议,其含义和socket系统调用的第三个参数相同,它通常被设置为0.ai_flags成员可以取表中的标志的按位或。
选项 含义 AI_PASSIVE 在hints参数中设置,表示调用者是否会将取得的socket地址用于被动打开。服务器通常需要设置它,表示接收任何本地socket地址上的服务请求。客户端程序不能设置它。 AI_CANONNAME 在hints参数中设置,告诉getaddrinfo函数返回主机的别名 AI_NUMERICHOST 在hints参数中设置,表示hostname必须是用字符串表示的IP地址,从而避免了DNS查询 AI_NUMERICSERV 在hints参数中设置,强制service参数使用十进制端口号的字符串形式,而不能是服务名 AI_V4MAPPED 在hints参数中设置,如果ai_family被设置为AI_INET6,那么当没有满足条件的IPv6地址被找到时,将IPv4地址映射为IPv6地址 AI_ALL 必须和AI_V4MAPPED同时使用,否则将被忽略,表示同时返回符合条件的IPv6地址以及由IPv4地址映射得到的IPv6地址 AI_ADDRCONFIG 仅当至少配置有一个IPv4地址(除了回路地址)时,才返回IPv4地址信息;同样,仅当至少配置有一个IPv6地址(除了回路地址)时,才返回IPv6地址信息。他和AI_V4MAPPED是互斥的 当我们使用hints参数的时候,可以设置其ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段必须被设置为NULL。
getnameinfo
getnameinfo函数能通过socket地址同时获得字符串表示的主机名(内部使用的是gethostbyaddr函数)和服务名(内部使用的是getservbyport函数)。它是否可重入取决于其内部调用的gethostbyaddr和getservbyport函数是否是它们的可重入版本。该函数的定义如下:
#include<netdb.h>
int getnameinfo(const struct sockaddr *sockaddr , socklen_t addrlen , char *host , socklen_t hostlen , char *serv , socklen_t servlen , int flags);
getnameinfo将返回的主机名存储在host参数指向的缓存中,将服务名存储在serv参数指向的缓存中,hostlen和servlen参数分别指定这两块缓存的长度。flags参数控制getnameinfo的行为,它可以接收表中选项。
选项 含义 NI_NAMEREQD 如果通过socket地址不能获得主机名,则返回一个错误 NI_DGRAM 返回数据报服务,大部分同时支持流和数据报的服务使用相同的端口号来提供这两种服务,但端口512-514是例外。比如TCP的514端口提供的是shell登录服务,而UDP的514端口提供的是syslog服务 NI_NUMERICHOST 返回字符串表示的IP地址,而不是主机名 NI_NUMERICSERV 返回字符串表示的十进制端口号,而不是服务名 NI_NOFQDN 仅返回主机域名的第一部分,比如对主机名nebula.testing.com,getnameinfo只将nebula写入host缓存 getaddrinfo和getnameinfo函数成功时返回0,失败则返回错误码,可能的错误码如表:
选项 含义 EAI_AGAIN 调用临时失败,提示应用程序过后再试 EAI_BADFLAGS 非法的ai_flags值 EAI_FAIL 名称解析失败 EAI_FAMILY 不支持的ai_family参数 EAI_MEMORY 内存分配失败 EAI_NONAME 非法的主机名或服务名 EAI_OVERFLOW 用户提供的缓冲区溢出。仅发生在getnameinfo调用中 EAI_SERVICE 没有支持的服务,比如用数据报服务类型来查找ssh服务。因为ssh服务只能使用流服务
EAI_SOCKTYPE 不支持的服务类型。如果hints.ai_socktype和hints.ai_protocol不一致,比如前者指定SOCK_DGRAM,而后者使用的是IPROTO_TCP,则会触发这类错误 EAI_SYSTEM 系统错误,错误值存储在errno中 Linux下strerror函数能将数值错误码errno转换成易读的字符串形式。同样,下面的函数可将表中的错误码转换成字符串形式:
#include<netdb.h>
const char *gai_strerror(int error);