1.1 概述
我们可以使用很多方法来获取和设置影响套接字的选项:
- getsockopt 和 setsockopt 函数;
- fcntl 函数;
- ioctl 函数。
我们会从介绍getsockopt 和 setsockopt 函数开始,然后介绍所有的套接字选项。我们按以下分类进行详细介绍:通用、IPv4、IPv6、TCP和SCTP。最后,我们还介绍fcntl函数,因为它是把套接字设置为非阻塞式I/O型或信号驱动式I/O型以及设置套接字属主的POSIX的方法。
1.2 getsockopt 和 setsockopt 函数
这两个函数仅用于套接字。
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
均返回:若成功则为0,若出错则为-1
其中sockfd必须指向一个打开的套接字描述符,level(级别)指定系统中解释选项的代码或为通用套接字代码,或为某个特定于协议的代码(例如IPv4、IPv6、TCP或SCTP)。optval是一个指向某个变量(*optval)的指针,setsockopt从 *optval中取得选项待设置的新值,getsockopt则把已获取的选项当前值存放到*optval中。*optval的大小由最后一个参数指定,它对于setsockopt是一个值参数,对于getsockopt是一个值-结果参数。
套接字选项粗分为两大基本类型:一是启用或禁止某个特性的二元选项(称为标志选项),二是取得并返回我们可以设置或检查的特定值得选项(称为值选项)。
1.3 套接字状态
对于某些套接字选项,针对套接字的状态,什么时候设置或获取选项有时序上的考虑。
下面的套接字 选项是由TCP已连接套接字从监听套接字继承来的SO_DEBUG、SO_DONTROUTE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG和TCP_NODELAY。这对TCP是很重要的因为accept一直要到TCP层完成三路握手后才会给服务器返回已连接套接字。如果想在三路握手完成时确保这些套接字选项中的某一个是给 已 连接套接字设置的,那么我们必须先给监听套接字设置该选项。
1.4 通用套接字选项
我们从通用套接字选项开始讨论、这些选项是协议无关的(也就是说,它们由内核中的协议无关代码处理,而不是由诸如IPv4之类特殊的协议模块处理),不过其中有些选项只能应用到某些特定类型的套接字中。举例来说,尽管SO_BROADCAST套接字选项是“通用”的,它却只能应用于数据报套接字。
1.4.1 SO_BROADCAST 套接字选项
本选项开启或禁止进程发送广播消息的能力。只有数据报套接字支持广播,并且还必须是在支持广播消息的网络上(例如以太网、令牌环网等)。我们不能再点对点链路上进行广播也不可能在基于连接的传输协议(例如TCP和SCTP)之上进行广播。
由于应用程序在发送广播数据报之前必须设置本套接字选项,因此它能够有效地防止一个进程在其应用程序根本没有设计成可广播时就发送广播数据报。
1.4.2 SO_DEBUG 套接字选项
本选项仅由TCP支持。当给一个TCP套接字开启本选项时,内核将为TCP在该套接字发送和接收的所有分组保留详细跟踪信息。这些信息保存在内核的某个环型缓冲区中,并可使用trpt程序进行检查。
1.4.3 SO_DONTROUTE 套接字选项
本选项规定外出的分组将绕过底层协议的正常路由机制。举例来说,在IPv4情况下外出分组将被定向到适当的本地接口,也就是由其目的地址的网络和子网部分确定的本地接口。如果这样的本地接口无法由目的地址确定(譬如说目的地主机不在一个点对点链路的另一端,也不在一个共享的网络上),那么返回ENETUNREACH错误。
路由守护进程(routed和gated)经常使用本选项来绕过路由表(路由表不正确的情况下),以强制将分组从特定接口送出。
1.4.4 SO_ERROR 套接字选项
当一个套接字上发生错误时,内核能够以下面两种方式之一立即通知进程这个错误。
- 如果进程阻塞在对该套接字的select调用上,那么无论是检查可读条件还是可写条件,select均返回并设置其中一个或所有两个条件。
- 如果进程使用信号驱动式I/O模型,那 就给进程或进程组产生一个SIGIO信号。
进程然后可以通过访问SO_ERROR套接字选项获取so_error的值。由getsockopt返回的整数值就是该套接字的待处理错误。so_error随后由内核复位为0。
这是我们遇到的第一个可以获取但不能设置的套接字选项。
1.4.5 SO_KEEPALIVE 套接字选项
给一个TCP套接字设置保持存活选项后,如果2小时内在该套接字的任意方向上都没有数据交换,TCP就自动给对端发生一个保持存活探测分节。这是一个对端必须响应的TCP分节,它会导致以下三种情况之一。
- 对端以期望的ACK响应。应用进程得不到通知(因为一切正常)。在又经过仍无动静的2小时后,TCP将发出另一个探测分节。
- 对端以RST响应,它告知本端TCP:对端已崩溃且已重新启动。该套接字的待处理错误置为ECONNRESRT,套接字本身则被关闭。
- 对端对保持存活探测分节没任何响应。
如果根本没有对TCP的探测分节的响应,该套接字的待处理错误就被置为ETIMEOUT,套接字本身则被关闭。然而如果该套接字收到一个ICMP错误作为某个探测分节的响应,那就返回相应的错误,套接字本身也被关闭。
本选项的功用是检测对端主机是否崩溃或变得不可达(譬如拨号调制解调器连接掉线,电源发生故障等)。
1.4.6 SO_LINGER 套接字选项
本选项指定close函数对面向连接的协议(例如TCP和SCTP,但不是UDP)如何操作。默认操作是close立即返回,但是如果有数据残留在套接字发生缓冲区中,系统将试着把这些数据发送给对端。
SO_LINGER套接字选项使得我们可以改变这个默认设置。本选项要求在用户进程和内核间传递如下结构,它在头文件<sys/socket.h>中定义:
struct linger
{
int l_onoff; /* 0 = off, nonzero = on */
int l_linger; /* linger time, POSIX specifies units as seconds */
};
对setsockopt的调用将根据其中两个结构成员的值形成下列3种情况之一。- 如果l_onoff为0,那么关闭本选项。l_linger的值被忽略,先前讨论的TCP默认设置生效,即close立即返回。
- 如果l_onoff为非0值且l_linger为0,那么当close某个连接时TCP将中止该连接。这就是说TCP将丢弃保留在套接字发生缓冲区中的任何数据,并发送一个RST给对端,而没有通常的四分组连接终止序列。
- 如果l_onoff为非0值且l_linger也为非0值,那么当套接字关闭时内核将拖延一段时间。这就是说如果在套接字发生缓冲区中仍残留有数据,那么进程将被投入睡眠,直到所有数据都已发送完且均被对方确认或延滞时间到。如果套接字被设置为非阻塞型,那么它将不等待close完成,即使延滞时间为非0也是如此。当使用SO_LINGER选项的这个特性是,应用进程检查close的返回值是非常重要的,因为如果在数据发送完并被确认前延滞时间到的话,close将返回EWOULDBLOCK错误,且套接字发送缓冲区的任何残留数据都被丢弃。
这里有个基本原则:设置SO_LINGER套接字选项后,close的成功返回只是告诉我们先前发送的数据(和FIN)已由对端TCP确认,而不能告诉我们对端应用进程是否已读取数据。
1.4.7 SO_OOBINLINE 套接字选项
当本选项开启时,带外数据将被留在正常的输入队列中(即在线留存)。这种情况下接收函数的MSG_OOB标志不能用来读带外数据。
1.4.8 SO_RCVBUF 和 SO_SNDBUF 套接字选项
每个套接字都有一个发送缓冲区和一个接收缓冲区。
接收缓冲区被TCP、UDP和SCTP用来保存接收到的数据,直到由应用进程来读取。对于TCP来说,套接字接收缓冲区中可用空间的大小限定了TCP通告对端的窗口大小。TCP套接字接收缓冲区不可能溢出,因为不允许对端发出超过本端所通告窗口大小的数据。这就是TCP的流量控制,如果对端无视窗口大小而发出了超过该窗口大小的数据,本端TCP将丢弃它们。然而对于UDP来说,当接收到的数据报装不进套接字接收缓冲区时, 该数据就被丢弃。UDP是没有流量控制的,较快的发送端可以很容易地淹没较慢的接收端,导致接收端的UDP丢弃数据报。
这两个套接字选项允许我们改变这两个缓冲区的默认大小。
当设置TCP套接字接收缓冲区的大小时,函数调用的顺序很重要。这是因为TCP的窗口规模选项是在建立连接时用SYN分节与对端互换得到的。对客户,这意味着SO_RCVBUG选项必须在调用connect之前设置;对于服务器,这意味着该选项必须在调用listen之前给监听套接字设置。给已连接套接字设置该选项对于可能存在的窗口规模选项没有任何影响,因为accept直到TCP的三路握手完成才会创建并返回已连接套接字。这就是必须给监听套接字设置本选项的原因。(套接字缓冲区的大小总是由新创建的已连接套接字从监听套接字继承而来)。
TCP套接字缓冲区的大小至少应该是相应连接的MSS值得四倍。对于单向数据传输(譬如单个方向的文件传送),当我们说“套接字缓冲区大小时”,我们指的是发送端主机上的套接字发送缓冲区大小和接收端主机上的套接字接收缓冲区大小。对于双向数据传输,我们在发送端指的是收发两个套接字缓冲区的大小,在接收端也是指收发两个套接字缓冲区的大小。
为了避免潜在的缓冲区空间浪费,TCP套接字缓冲区大小还必须是相应连接的MSS值得偶数倍。
1.4.9 SO_RCVLOWAT 和 SO_SNDLOWAT 套接字选项
每个套接字还有一个接收低水位标记和一个发送低水位标记。它们由select函数使用。
接收低水位标记是让select返回“可读”是套接字接收缓冲区中所需的数据量。对于TCP、UDP和SCTP套接字,其默认值为1.发送低水位标记是让select返回“可写”时套接字发送缓冲区中所需的可用空间。UDP也使用发送低水位标记,然而由于UDP套接字的发送缓冲区中可用空间的字节数从不改变(因为UDP并不为由应用进程传递给它的数据报保留副本),只要一个UDP套接字的发送缓冲区大小大于该套接字的低水位标记,该UDP套接字就总是可写。
1.4.10 SO_RCVTIMEO 和 SO_SNDTIMEO 套接字选项
这两个选项允许我们给套接字的接收和发送设置一个超时值。注意,访问它们的getsockopt进而setsockopt函数的参数是指向timeval结构的指针,与select所用参数相同。这可让我们用秒数和 微秒数来规定超时,我们通过设置其值为0s和0us来禁止超时。默认情况下这两个超时都是禁止的。
接收超时影响5个输入函数:read、readv、recv、recvfrom和recvmsg。发送超时影响5个输出函数:write、writev、send、sendto和sendmsg。
1.4.11 SO_REUSEADDR 和 SO_REUSEPORT 套接字选项
SO_REUSEADDR套接字选项能起到以下4个不同的功能。
- SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将该端口用作它们的本地端口的连接仍存在。这个条件通常是这样碰到的:
- 启动一个监听服务器;
- 连接请求到达,派生一个子进程来处理这个客户;
- 监听服务器终止,但子进程继续为现有连接上的客户提供服务;
- 重启监听服务器。
默认情况下,当监听服务器在步骤4通过调用socket、bind和listen重新启动时,由于它试图捆绑一个现有连接(即正由早先派生的那个子进程处理着的连接)上的端口,从而bind调用会失败。但是如果该服务器在socket和bind两个调用直接设置了SO_REUSEADDR套接字选项,那么bind将成功。所有TCP服务器都应该指定本套接字选项,以允许服务器在这种情况下被重新启动。
- SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只有每个实例捆绑一个不同的本地IP地址即可。这对于使用IP别名技术托管多个HTTP服务器的网点来说是很常见的。举例来说,假设本地主机的主机IP地址为198.69.10.2,不过它有两个别名:198.69.10.128和198.69.10.129.在其上启动三个HTTP服务器。第一个HTTP服务器以本地通配IP地址INADDR_ANY和本地端口号80(HTTP的众所周知端口)调用bind。第二个HTTP服务器以本地IP地址198.69.10.128和本地端口号80调用bind。这次调用bind将失败,除非在调用前设置了SO_REUSEADDR套接字选项。第三个HTTP服务器以本地IP地址198.69.10.129和本地端口号80调用bind。这次调用bind成功的先决条件同样是预先设置SO_REUSEADDR。假设SO_REUSEADDR已设置,从而三个服务器都启动了,目的IP地址为198.69.10.128、目的端口号为80的外来TCP将被递送给第二个服务器,目的IP地址为198.69.10.129、目的端口号为80的外来TCP将被递送给第三个服务器,目的端口号为80的所有其他TCP连接请求将都递送给第一个服务器。
对于TCP,我们绝不可能启动捆绑相同IP地址和相同端口号的多个服务器:这是完全重复的捆绑。
- SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字上,只要每次捆绑指定不同本地IP地址即可。
- SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口已绑定到某个套接字上时,如果传输协议支持,同样的IP地址和端口还可以捆绑到另一个套接字上。一般来说本特性仅支持UDP套接字。
本特性用于多播时,允许在同一个主机上同时运行同一个应用程序的多个副本。当一个UDP数据报需由这些重复捆绑套接字中的一个接收时,所用规则为:如果该数据报的目的地址是个广播地址或多播地址,那就给每个匹配的套接字递送一个该数据的副本;但是如果该数据报的目的地址是一个单播地址,那么它只递送给单个套接字。在单播的情况下,如果有多个套接字匹配该数据报,那么该选择由哪个套接字接收它取决于实现。
后续引入了SO_REUSEPORT这个套接字选项。它并未在SO_REUSEADDR上重载所需多播语义(即允许完全重复的捆绑),而是给SO_REUSEPORT引入了以下语义:
- 本选项允许完全重复的捆绑,不过只有在想要捆绑同一IP地址和端口的每个套接字都指定了本套接字选项才行;
- 如果被捆绑的IP地址是 一个多播地址,那么SO_REUSEADDR和SO_REUSEPORT被认为是等效的。
本套接字选项的问题在于并非所有系统都支持他。
我们以下面的建议来总结对这些套接字选项的讨论:
- 在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接字选项;
- 当编写一个可在同一时刻在同一主机上运行多次的多播应用程序时,设置SO_REUSEADDR套接字选项,并将所参加多播组的地址作为本地IP地址捆绑。
1.4.12 SO_TYPE 套接字选项
本选项返回套接字的类型,返回的整数值是一个诸如SOCK_STREAM或SOCK_DGRAM之类的值。本选项通常由启动时继承了套接字的进程使用。
1.4.13 SO_USELOOPBACK 套接字选项
本选项仅用于路由域(AF_ROUTE)的套接字。对于这些套接字,它的默认设置为打开(这是唯一一个默认值为打开而不是关闭的SO_xxx二元套接字选项)。当本选项开启时,相应套接字将接收在其上发送的任何数据报的一个副本。
1.5 IPv4 套接字选项
这些套接字选项由IPv4处理,它们的级别(即getsockopt和setsockopt函数的第二个参数)为IPPRORO_IP。
1.5.1 IP_HDRINCL 套接字选项
如果本选项是给一个原始IP套接字设置的,那么我们必须为所有在该原始套接字上发送的数据报构造自己的IP首部。一般情况下,在原始套接字上发送的数据报其IP首部是由内核构造的,不过有些应用程序(特别是路由跟踪程序traceroute)需要构造自己的IP首部以取代IP置于该首部中的某些字段。
1.5.2 IP_OPTIONS 套接字选项
本选项的设置允许我们在IPv4首部中设置IP选项。
1.5.3 IP_RECVDSTADDR 套接字选项
本套接字选项导致所收到的UDP数据报的目的IP地址由recvmsg函数作为辅助数据返回。
1.5.4 IP_RECVIF 套接字选项
本套接字选项导致所收到UDP数据报的接收接口索引由recvmsg函数作为辅助数据返回。
1.5.5 IP_TOS 套接字选项
本套接字选项允许我们为TCP、UDP或SCTP套接字设置IP首部中的服务器类型字段(该字段包含DSCP和ECN子字段)。如果我们给本选项调用getsockopt,那么用于放入外出IP数据报首部的DSCP和ECN字段中的TOS当前值(默认为0)将返回。我们没办法从接收到的IP数据报中取得该值。
1.5.6 IP_TTL 套接字选项
我们可以使用本选项设置或获取系统用在从某个给定套接字发送的单播分组上的默认TTL值。,调用getsockopt返回的是系统将用于外出数据报的字段的默认值。我们没有办法从接收到的IP数据报中取得该值。
1.6 ICMPv6 套接字选项
这个唯一的套接字选项由ICMPv6处理,它的级别(即getsockopt和setsockopt函数的第二个参数)为IPPROTO_ICMPV6.
1.6.1 ICMP6_FILTER 套接字选项
本选项允许我们获取或设置一个icmp6_filter结构,该结构指出256个可能的ICMPv6消息类型中哪些将经由某个原始套接字传递给所在进程。
1.7 IPv6 套接字选项
这些套接字选项由IPv6处理,它们的级别(即getsockopt和setsockopt函数的第二个参数)为IPPROTO_IPV6.
1.7.1 IPV6_CHECKSUM 套接字选项
本选项指定用户数据中校验和所处位置的字节偏移。如果该值为非负,那么内核将:(1)给所有外出分组计算并储存校验和;(2)验证外来分组的校验和,丢弃所有校验和无效的分组。本选项影响除ICMPv6原始套接字以外的所有IPv6原始套接字。(内核总是给ICMPv6原始套接字计算并储存校验和)。如果指定本地选项的值为-1(默认值),那么内核不会再相应的原始套接字上计算并储存外出分组的校验和,也不会验证外来分组的校验和。
1.7.2 IPv6_DONTFRAG 套接字选项
开启本选项将禁止为UDP套接字或原始套接字自动插入分片首部,外出分组中大小超过发送接口MTU的那些分组将被丢弃。发送分组的系统调用不会为此返回错误,因为已发送出去仍在途中的分组也可能因为超过路径MTU而被丢弃。
1.7.3 IPV6_NEXTHOP 套接字选项
本选项将外出数据报的下一跳地址指定为一个套接字地址结构。这是一个特权操作。
1.7.4 IPV6_PATHMTU 套接字选项
本选项不能设置,只能获取。获取本选项时,返回值为由路径MTU发现功能确定的当前MTU。
1.7.5 IPV6_RECVDSTOPTS 套接字选项
开启本选项表明,任何接收到的IPv6目的地选项都将由recvmsg作为辅助数据返回。本选项默认关闭。
1.7.6 IPV6_RECVHOPLIMIT 套接字选项
开启本选项表明,任何接收到的I跳限字段都将由recvmsg作为辅助数据返回。本选项默认关闭。
1.7.7 IPV6_RECVHOPOPTS 套接字选项
开启本选项表明,任何接收到的IPv6步跳选项都将由recvmsg作为辅助数据返回。本选项默认关闭。
1.7.8 IPV6_RECVPATHMTU 套接字选项
开启本选项表明,某条路径的路径MTU在发生变化时将由recvmsg作为辅助数据返回。本选项默认关闭。
1.7.9 IPV6_RECVPKTINFO 套接字选项
开启本选项表明,接收到的IPv6数据报的以下两条信息都将由recvmsg作为辅助数据返回:目的IPv6地址和到达接口索引。
1.7.10 IPV6_RECVRTHDR 套接字选项
开启本选项表明,任何接收到的IPv6路由首部都将由recvmsg作为辅助数据返回。本选项默认关闭。
1.7.11 IPV6_RECVTCLASS 套接字选项
开启本选项表明,接收到的流通类别将由recvmsg作为辅助数据返回。本选项默认关闭。
1.7.12 IPV6_UNICAST_HOPS 套接字选项
本IPv6选项类似于IPv4的IP_TTL套接字选项。设置本选项会给在相应套接字上发送的外出数据报指定默认跳限,获取本选项会返回内核用于相应套接字的跳限值。
1.7.13 IPV6_USE_MIN_MTU 套接字选项
把本选项设置为1表明,路径MTU发现功能不必执行,为避免分片,分组就使用IPv6的最小MTU发送。把本选项设置为0表明,路径MTU发现功能对于所有目的地都得执行。把本选项设置为-1表明,路径MTU发现功能仅对单播目的地执行,对于多播目的地就使用最小MTU。
1.7.14 IPV6_V6ONLY 套接字选项
在一个AF_INET6套接字上开启本选项将限制它只执行IPv6通信。本选项默认关闭。
1.7.15 IPV6_XXX 套接字选项
大多数用于修改协议首部的IPv6选项假设:就UDP套接字而言,信息由recvmsg和sendmsg作为辅助数据在内核和应用进程直接传递;就TCP套接字而言,同样的信息改用getsockopt和setsockopt获取和设置。套接字选项和辅助数据的类型一致,并且访问套接字选项的缓冲区所含有的信息和辅助数据中存放的信息也一致。
1.8 TCP套接字选项
TCP有两个套接字选项它们的级别(即getsockopt和setsockopt函数的第二个参数)为IPPROTO_TCP。
1.8.1 TCP_MAXSEG 套接字选项
本选项允许我们获取或设置TCP连接的最大分节(MSS)。返回值是我们的TCP可以发送给对端的最大数据量。TCP不能发送超过该值的分节。当然,TCP总是可以发送数据量少于对端通告的MSS值的分节。
1.8.2 TCP_NODELAY 套接字选项
开启本选项将禁止TCP的Nagle算法。默认情况下该算法是启动的。Nagle算法的目的在于减少广域网上小分组的数目,防止一个连接在任何时刻有多个小分组待确认。
1.9 SCTP 套接字选项
略。
1.10 fcntl 函数
与代表“file control”(文件控制)的名字相符,fcntl函数可执行各种描述符控制操作。下图汇总了由fcntl、ioctl和路由套接字执行的不同操作。
- 非阻塞式I/O。通过使用F_SETFL命令设置O_NONBLOCK文件状态标记,我们可以把一个套接字设置为非阻塞型。
- 信号驱动式I/O。通过使用F_SETFL命令设置O_ASYNC文件状态标志,我们可以把一个套接字设置成一旦状态发生变化,内核就产生一个SIGIO信号。
- F_SETOWN命令允许我们指定用于接收SIGIO和SIGURG信号的套接字属主(进程ID或进程组ID)。其中SIGIO信号是套接字被设置为信号驱动式 I/O后产生的,SIGURG信号是在新的带外数据到达套接字时产生的。F_GETOWN命令返回套接字的当前属主。
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* int arg */);
返回:若成功则取决于cmd,若出错则为-1
每种描述符(包括套接字描述符)都有一组由F_GETFL命令获取或由F_SETFL命令设置的文件标记。其中影响套接字描述符的两个标志是:- O_NONBLOCK——非阻塞式I/O
- O_ASYNC——信号驱动式I/O