UNIX网络编程卷一 学习笔记 第七章 套接字选项

有很多方法可以获取和设置套接字选项:
1.getsockopt和setsockopt函数。
2.fcntl函数。
3.ioctl函数。

fcntl函数是将套接字设为非阻塞式IO型、设为信号驱动式IO型、设置套接字属主的POSIX方法。

这两个函数只用于套接字:
在这里插入图片描述
以上函数用于获取或设置套接字选项。参数sockfd必须指向一个打开的套接字描述符。setsockopt函数从参数optval指向的内存中取得要设置的新值;getsockopt把要获取的选项当前值存放到参数optval指向的内存中。参数optval指向的内存的大小由最后一个参数指定,它对于函数setsockopt是一个值参数,对于函数getsockopt是一个值-结果参数。

一对level参数和optname参数组成一个我们可由getsockopt函数获取或由setsockopt函数设置的选项,这两个参数的可选值如下,数据类型列是对应的optval参数必须指向的数据类型。套接字选项分为两大基本类型,一是启动或禁止某个特性的二元选项(称为标志选项,下图中标志列中有·的即是,此时*optval是一个整数,0表示相应选项被禁止,非0表示被启用);二是我们可以设置或获取值的选项(称为值选项):
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
检查上图中的选项是否得到支持,如果支持,则输出它们的默认值:

#include "unp.h"
#include <netinet/tcp.h>    /* for TCP_xxx defines */

// 对于getsockopt函数的每个可能的返回值,union类型中都有一个成员
union val {
    int i_val;
    long l_val;
    struct linger linger_opt_val_strval;
    struct timeval timeval_val;
} val;

// 用于输出给定类型的套接字选项的值
static char *sock_str_flag(union val *, int);
static char *sock_str_int(union val *, int);
static char *sock_str_linger(union val *, int);
static char *sock_str_timeval(union val *, int);

struct sock_opts {
    const char *opt_str;
    int opt_level;
    int opt_name;
    char *(*opt_val_str)(union val *, int);
} sock_opts[] = {
    {"SO_BROADCAST", SOL_SOCKET, SO_BROADCAST, sock_str_flag},
    {"SO_DEBUG", SOL_SOCKET, SO_DEBUG, sock_str_flag},
    {"SO_DONTROUTE", SOL_SOCKET, SO_DONTROUTE, sock_str_flag},
    {"SO_ERROR", SOL_SOCKET, SO_ERROR, sock_str_int},
    {"SO_KEEPALIVE", SOL_SOCKET, SO_KEEPALIVE, sock_str_flag},
    {"SO_LINGER", SOL_SOCKET, SO_LINGER, sock_str_linger},
    {"SO_OOBINLINE", SOL_SOCKET, SO_OOBINLINE, sock_str_flag},
    {"SO_RCVBUF", SOL_SOCKET, SO_RCVBUF, sock_str_int},
    {"SO_SNDBUF", SOL_SOCKET, SO_SNDBUF, sock_str_int},
    {"SO_RCVLOWAT", SOL_SOCKET, SO_RCVLOWAT, sock_str_int},
    {"SO_SNDLOWAT", SOL_SOCKET, SO_SNDLOWAT, sock_str_int},
    {"SO_RCVTIMEO", SOL_SOCKET, SO_RCVTIMEO, sock_str_timeval},
    {"SO_SNDTIMEO", SOL_SOCKET, SO_SNDTIMEO, sock_str_timeval},
    {"SO_REUSEADDR", SOL_SOCKET, SO_REUSEADDR, sock_str_flag},
// 我们应该对每个套接字选项都用ifdef测试一下,因为并非所有实现都支持所有套接字选项,为使测试代码简短,只有此处进行了测试
#ifdef SO_REUSEPORT
    {"SO_REUSEPORT", SOL_SOCKET, SO_REUSEPORT, sock_str_flag},
#else
    {"SO_REUSEPORT", 0, 0, NULL},
#endif
    {"SO_TYPE", SOL_SOCKET, SO_TYPE, sock_str_int},
    {"SO_USELOOPBACK", SOL_SOCKET, SO_USELOOPBACK, sock_str_flag},
    {"IP_TOS", IPPROTO_IP, IP_TOS, sock_str_int},
    {"IP_TTL", IPPROTO_IP, IP_TTL, sock_str_int},
    {"IPV6_DONTFRAG", IPPROTO_IPV6, IPV6_DONTFRAG, sock_str_flag},
    {"IPV6_UNICAST_HOPS", IPPROTO_IPV6, IPV6_UNICAST_HOPS, sock_str_int},
    {"IPV6_V6ONLY", IPPROTO_IPV6, IPV6_V6ONLY, sock_str_flag},
    {"TCP_MAXSEG", IPPROTO_TCP, TCP_MAXSEG, sock_str_int},
    {"TCP_NODELAY", IPPROTO_TCP, TCP_NODELAY, sock_str_flag},
    {"SCTP_AUTOCLOSE", IPPROTO_SCTP, SCTP_AUTOCLOSE, sock_str_int},
    {"SCTP_MAXBURST", IPPROTO_SCTP, SCTP_MAXBURST, sock_str_int},
    {"SCTP_MAXSEG", IPPROTO_SCTP, SCTP_MAXSEG, sock_str_int},
    {"SCTP_NODELAY", IPPROTO_SCTP, SCTP_NODELAY, sock_str_flag},
    {NULL, 0, 0, NULL}
};

int main(int argc, char **argv) {
    int fd;
    socklen_t len;
    struct sock_opts *ptr;

    for (ptr = sock_opts; ptr->opt_str != NULL; ++ptr) {
        printf("%s: ", ptr->opt_str);
		if (ptr->opt_val_str == NULL) {
		    printf("(undefined)\n");
		} else {
		    switch (ptr->opt_level) {
		    case SOL_SOCKET:
		    case IPPROTO_IP:
	            case IPPROTO_TCP:
	            // IPv4的TCP套接字
		        fd = Socket(AF_INET, SOCK_STREAM, 0);
				break;
#ifdef IPV6
            case IPPROTO_IPV6:
                // IPv6的TCP套接字
		        fd = Socket(AF_INET6, SOCK_STREAM, 0);
				break;
#endif
#ifdef IPPROTO_SCTP
            case IPPROTO_SCTP:
                // IPv4的SCTP套接字
		        fd = Socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);
				break;
#endif
            default:
	        	err_quit("Can't create fd for level %d\n", ptr->opt_level);
		    }
	
		    len = sizeof(val);
		    // getsockopt出错时不终止进程,许多实现会定义一些不支持的套接字选项,这些不支持的套接字选项会引发ENOPROTOOPT错误
		    if (getsockopt(fd, ptr->opt_level, ptr->opt_name, &val, &len) == -1) {
		        err_ret("getsockopt error");
		    } else {
				printf("default = %s\n", (*ptr->opt_val_str)(&val, len));
		    }
		    close(fd);
		}
    }
    exit(0);
}

static char strres[128];
// 每个类型的选项值都有一个输出函数,sock_str_flag函数输出标志类型选项的值,我们只给出了其中1个函数,其他3个函数与之类似
// 本函数返回的字符串为off或on,取决于标志是否打开
static char *sock_str_flag(union val *ptr, int len) {
    // 由于getsockopt函数的最后一个参数是值-结果参数,因此我们所做的第一个检查是看getsockopt函数返回值的大小是否是期望的大小
    if (len != sizeof(int)) {
        snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);
    } else {
        snprintf(strres, sizeof(strres), "%s", (ptr->i_val == 0) ? "off" : "on");
    }
    return strres;
}

在安装了KAME SCTP补丁的FreeBSD 4.8上运行以上程序的输出:
在这里插入图片描述
在这里插入图片描述
以上输出结果中,SO_TYPE选项(套接字类型)的值为1,对应于该实现的SOCK_STREAM。

以下套接字选项会在创建已连接套接字时从监听套接字继承过来:SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SNDBUF、SO_SNDLOWAT、TCP_MAXSEG、TCP_NODELAY。

以下是与协议无关的通用套接字选项,但其中某些选项只能用到特定类型的套接字中,如尽管我们称SO_BROADCAST套接字是通用的,但它只能应用于数据报套接字:
1.SO_BROADCAST:本选项开启或禁止进程发送广播消息的能力。只有数据报套接字支持广播,并且还必须在支持广播的网络上(如以太网、令牌环网等)。我们不能在点对点链路上进行广播,也不能在基于连接的传输协议(如TCP和SCTP)上进行广播。应用在发送广播数据报前必须设置本套接字选项,因此它能防止一个进程在其应用程序没有设计成可广播时发送广播数据报,例如,一个UDP应用可能以命令行参数的形式获取IP地址,但不期望用户键入一个广播地址,这样不会让应用来确定一个给定地址是否是广播地址,而是在内核中进行测试,如果要发送的数据报的目的地址是一个广播地址且本套接字选项没有设置,则返回EACCES错误。
2.SO_DEBUG:仅TCP支持,当给一个TCP套接字选项开启本选项时,内核将为TCP在该套接字上发送和接收的所有分组保留详细跟踪信息。这些信息保存在内核的某个环形缓冲区中,并可用trpt程序进行检查。
3.SO_DONTROUTE:开启时,外出的分组将绕过底层协议的正常路由机制。正常情况下,IPv4的外出分组将根据其目的地址的网络和子网被定向到适当的本地接口,如果这样的本地接口无法确定(如目的地主机不在点对点链路的另一端,也不在一个共享网络上),会返回ENETUNREACH。以MSG_DONTROUTE参数调用send、sendto、sendmsg也能在单个数据报上取得与本选项相同的效果。路由守护进程(routed、gated)常用本选项来绕过路由表(在路由表不正确时),以强制将分组从特定接口送出。
4.SO_ERROR:当一个套接字上发生错误时,源自Berkeley的内核中的协议模块将该套接字的名为so_error的变量设为标准Unix Exxx值中的一个,称其为该套接字的待处理错误。内核能通过以下方式立即通知进程这个错误:
(1)如果进程阻塞在对该套接字的select函数上,那么select函数会返回可读和可写条件准备好(如果检查读或写条件)。
(2)如果进程使用信号驱动式IO模型,给进程或进程组产生一个SIGIO信号。
进程之后可通过访问套接字选项SO_ERROR获取so_error的值,so_error随后被复位为0。如果so_error为非零值,当进程调用read且没有数据返回时,则read函数返回-1且errno被置为so_error的值,so_error随后被复位为0;如果so_error为非0值,调用read时套接字上有数据在排队等待读取,则read函数不返回错误条件。如果so_error为非0值,则调用write时会返回-1,且errno被设为so_error的值,随后so_error被复位为0。SO_ERROR只能获取不能设置。
5.SO_KEEPALIVE:给一个TCP套接字设置保持存活(keep-alive)选项后,如果2小时内该套接字的任一方向上都没有数据交换,TCP会自动给对端发送一个保持存活探测分节,对端必须对其响应,它会导致一下情况之一:
(1)对端以期望的ACK响应,一切正常,应用进程感知不到。又经过无动静的2小时后,TCP将发出另一个探测分节。
(2)对端以RST响应,它告知本端TCP:对端已崩溃且已重新启动,该套接字的待处理错误被置为ECONNRESET,套接字本身被关闭。
(3)对端对保持存活探测分节没有任何响应,源自Berkeley的实现将发送另外8个探测分节,两两相隔75秒,试图得到一个响应。TCP若在发出第一个探测分节后11分15秒内没有得到任何响应,则放弃。HP-UX以处理数据的方式来处理保持存活探测分节,即在重传超时后发送第二个探测分节,并把超时值加倍,这样一直重传,直到预配置的最大时间间隔为止,默认最大间隔时间为10分钟。如果没有对TCP的探测分节的响应,该套接字待处理错误被置为ETIMEOUT,套接字本身被关闭。即使任何保持存活探测分节均无响应,也不能肯定对端主机已经崩溃,因此TCP可能会终止一个有效连接。如某中间路由器崩溃了15分钟,而这段时间完全与主机的11分15秒的保持存活探测周期完全重叠。
如果套接字收到一个ICMP错误作为某个探测分节的响应,则返回相应的错误,套接字本身被关闭,这种情形下一个常见的ICMP错误是host unreachable(主机不可达),说明对端主机可能并没有崩溃,只是不可达,此时待处理错误被置为EHOSTUNREACH,发生这种情况的原因可能是发生网络故障,可能是对端主机已经崩溃,而最后一条的路由器也已经检测到它的崩溃。对于本选项,如果想把2小时的无活动周期改变,可修改内核中的对应参数,但对大多内核,该参数是基于整个内核,而非每个套接字的,因此如果把无活动周期改变,将影响到该主机上所有开启了本选项的套接字。本选项的作用是检测对端主机是否崩溃或变得不可达(如拨号调制解调器连接掉线,电源发生故障等)。如果对端进程崩溃,它的TCP将跨连接发送一个FIN,这可通过select函数检测到。本选项一般由服务器使用,但客户也能使用,服务器使用本选项是由于它们花大量时间阻塞在等待客户请求上,然而如果客户主机连接掉线、电源掉电、系统崩溃,服务器进程永远不会知道,并继续等待永远不会到达的输入,我们称这种情况为半开连接,保持存活选项能检测出半开连接并终止它们。有些服务器(如FTP服务器)提供一个分钟量级的应用层超时,这是应用本身完成的,一般用于读客户请求的read函数,这个超时值与本选项无关,这通常是清理通向不可达客户的半开连接的较好办法,因为超时是由应用自己实现的,应用进程具备完全的控制能力。SCTP有与TCP保持存活机制类似的心博机制,心博机制通过SCTP_SET_PEER_ADDR_PARAMS套接字选项控制,对SCTP套接字设置本套接字会被忽略,它不影响SCTP的心博机制。以下是TCP连接的另一端发生某些事件时我们能采用的各种检测方法如下:
在这里插入图片描述
6.SO_LINGER:该选项指定close函数对面向连接的协议(如TCP、SCTP)如何操作,默认close函数立即返回,如果有数据残留在套接字发送缓冲区中,系统会试着把这些数据发送到对端,SO_LINGER套接字选项使得我们可以改变这个默认设置。本选项要求在用户进程与内核间传递如下结构,它定义在头文件sys/socket.h中:
在这里插入图片描述
该结构的两个成员根据取值不同形成以下情况:
(1)如果成员l_onoff为0,则关闭本选项,成员l_linger的值被忽略,close函数立即返回;
(2)如果成员l_onoff为非0值且成员l_linger为0,则当调用close时将终止该连接,即TCP丢弃保留在套接字发送缓冲区中的数据,并发送一个RST给对端,而没有通常的四分组连接终止序列,这么一来避免了TCP的TIME_WAIT状态,但存在了以下可能性:在2MSL(最大报文段寿命)秒内创建该连接的另一个化身,导致来自刚被终止的连接上的旧的重复分节被不正确地递送到新的化身上。这种情形下SCTP也发送一个ABORT块给对端而中止性地关闭连接。有些人提倡使用本特性,目的是避免TIME_WAIT状态,从而可以重启监听服务器,但这么做可能导致数据被破坏,我们可以用其他方式实现该效果,即在服务器调用bind前开启SO_REUSEADDR选项,TIME_WAIT状态让旧的重复分节在网络中超时消失。个别环境下使用本特性执行中止性关闭是合理的,一个例子是在使用RS-232接口的服务器上,可能在尝试传送数据到一个卡住的端口,从而永远停留在CLOSE_WAIT状态,如果服务器之后调用close时,如果发送一个RST,则会重置卡住的端口且丢弃待发送的数据;
(3)如果l_onoff非0,且l_linger也非0,则调用close关闭套接字时,内核将拖延一段时间,即如果套接字发送缓冲区中仍有数据,那么进程将睡眠,直到:(1)所有数据都已发完且均被对方确认,或(2)延滞时间到。使用SO_LINGER套接字选项的该特性时,应检查close函数的返回值,如果在数据发送完并被确认前延滞时间到的话,close函数将返回-1,errno会被置为EWOULDBLOCK,且套接字缓冲区中任何残留数据都被丢弃。当套接字被设置为非阻塞型,那么它将不等待close函数完成,即使延滞时间非0时也如此。
在这里插入图片描述
假设在客户数据到达时,服务器暂时处于忙状态,那么这些数据由TCP加入到服务器的套接字接收缓冲区中,下一个FIN分节也加入到套接字接收缓冲区中,默认,客户的close函数立即返回。如上图,客户的close调用可能在服务器读接收缓冲区中的剩余数据前就返回,并且服务器可能在读这些剩余数据之前就崩溃,且客户端不会知道。
在这里插入图片描述
如上图,此时,问题仍然存在:在服务器进程读取接收缓冲区中剩余数据之前,服务器可能崩溃,且客户不会知道。并且当l_linger为较小的正值时,可能客户的发送缓冲区还有数据就超时了:
在这里插入图片描述
设置了SO_LINGER套接字选项后,close函数的成功返回只是告诉我们先前发送的数据和FIN已由对端TCP确认,而不能告诉我们对端应用进程是否已读取数据。
让客户知道服务器进程已读取数据的一个方法是调用shutdown,并将其第二个参数设置为SHUT_WR,并等待对端close调用:
在这里插入图片描述
根据调用close还是shutdown以及是否设置了SO_LINGER套接字选项,可在以下三个时机返回:
1.close调用后立即返回,不等待。(图7-7)
2.close调用后一直拖延到接收了客户端对于FIN的ACK后才返回。(图7-8)
3.shutdown调用后跟一个read函数,一直等待到接收了对端的FIN后read函数再返回。(图7-10)
获知对端应用是否已读取我们的数据的另一个方法是使用应用级确认,简称应用ACK,如客户在向服务器发送数据后调用read来读取一个字节的数据:
在这里插入图片描述
在这里插入图片描述
当客户的read函数返回时,可以保证服务器进程已读完了我们发送的数据(假设服务器端知道我们要发多少数据,或者定义了某个记录结束标志)。上例的应用级ACK是值为0的1个字节。
在这里插入图片描述
在这里插入图片描述
7.SO_OOBINLINE:开启时,带外数据被留在正常的输入队列中,此时recv函数的MSG_OOB标志不能用来读带外数据。
8.SO_REVBUFSO_SNDBUF:每个套接字都有一个发送缓冲区和一个接收缓冲区。接收缓冲区被TCP、UDP、SCTP保存接收到的数据,直到应用进程来读取,对TCP来说,套接字接收缓冲区可用空间大小限定了TCP通告对端的窗口大小。TCP套接字的接收缓冲区不可能溢出,因为不允许对端发出超过本端所通告窗口大小的数据,这就是TCP的流量控制,如果对端无视窗口大小发出了超过该窗口大小的数据,本端TCP将丢弃它们。对UDP来说,当接收到的数据报装不进套接字接收缓冲区时,该数据报就被丢弃,而UDP是没有流量控制的,较快的发送端很容易淹没较慢的接收端,导致接收端UDP丢弃数据报,较快的发送端甚至可以淹没本机的网络接口,导致数据报被本机丢弃。这两个套接字选项允许我们改变这两个缓冲区的默认大小,对不同实现,默认值的大小不同,较早期的源自Berkeley的实现将TCP发送和接收缓冲区的大小均默认为4096字节,而较新的系统使用较大的值,可以是8192~61440字节间的任何值。如果主机支持NFS,那么UDP发送缓冲区的大小经常默认为9000字节左右的一个值,而UDP接收缓冲区的大小常默认为40000字节左右的一个值。设置TCP套接字接收缓冲区大小时,函数调用顺序很重要,因为TCP的窗口大小选项是在建立连接时用SYN分节与对端互换得到的,对于客户,SO_RECVBUF选项必须在调用connect前设置,对于服务器,SO_RECVBUF选项必须在调用listen前给监听套接字设置(会继承给已连接套接字)。给已连接套接字设置该选项对于可能支持的窗口扩大选项没有任何影响,因为accept函数直到TCP的三路握手完成才会创建并返回已连接套接字,而窗口扩大选项只能出现在SYN报文中,这也是为什么需要给监听套接字设置该选项。TCP套接字缓冲区的大小至少应该是相应连接的MSS的四倍,对于单向的数据传输(如单个方向的文件传送),我们说套接字缓冲区大小时,我们指的是发送端主机上的套接字发送缓冲区大小和接收端主机上的套接字接收缓冲区大小,对于双向数据传输,在发送端指的是收发两个套接字缓冲区大小,在接收端也是值收发两个套接字缓冲区大小。典型的缓冲区大小默认值是8192字节,典型的MSS值为512或1460字节,此要求一般都能满足。TCP套接字缓冲区大小至少为MSS 4倍的依据是TCP快速恢复算法的工作机制,TCP发送端使用是否收到3个重复的确认来检测某个分节是否丢失,发现某目标分节没有到时,接收端给新收到的每个非目标分节发送一个重复的确认,如果窗口大小不足以存放4个这样的非目标分节,就不能连发三个重复的确认,从而无法激活快速恢复算法。为避免潜在的缓冲区空间浪费,TCP套接字缓冲区大小最好是相应连接的MSS值的偶数倍,有些实现替应用进程处理这个细节问题,在连接建立时将套接字缓冲区大小向上舍入,这是在建立连接前设置这两个套接字的另一个原因,例如,使用默认套接字缓冲区大小为8192字节的4.4 BSD,假设以太网的MSS为1460字节,在连接建立时收发套接字缓冲区的大小都将被向上舍入成8760(6*1460)字节,但这个要求并非必需,只不过套接字缓冲区中MSS整数倍大小以外的空间不会被使用。
在这里插入图片描述
上图中,上方有4个数据分节,下方有4个ACK,客户端需要至少8个分节容量的发送缓冲区,因为客户TCP必须为每个分节保留一个副本,直到收到来自服务器的相应ACK,上图中有8个ACK客户还未收到。上图忽略了一些细节,首先,TCP的慢启动算法限制了在一个空闲连接上最初发送分节的速度,其次,TCP通常每两个分节确认一次,而非上图中每个分节确认一次。管道的容量称为带宽-时延积,它是带宽(bit/s)和RTT(秒)相乘,再将结果由位转换成字节计算得到,RTT可以由ping程序测得。带宽取决于两个端点之间最慢的链路。在RTT为60ms的一条T1链路(1536000bit/s)的带宽-延迟积为11520字节,如果套接字缓冲区大小小于该值,管道将不会处于满状态,性能也将低于期望值,当带宽变大(如45Mbit/s的T3链路)或RTT变大(如RTT约为500ms的卫星链路)时,套接字缓冲区也需要增长。当带宽-延迟积超过TCP的最大正常窗口大小(65535字节)时,两端需要设置TCP长胖管道相关选项(窗口扩大选项、时间戳选项)。大多实现对套接字发送缓冲区和接收缓冲区的大小都设有上限,有时这个上限可以由管理员修改,较早期的源自Berkeley的实现有一个约52000字节的硬上限,而较新实现将默认值增加为256000字节甚至更大,且通常可由管理员继续增加,对应用来说,没有简单的方法来确定这个极限,POSIX定义了函数fpathconf(大多实现都支持),用_PC_SOCK_MAXBUF常值作为第二个参数调用fpathconf,就能获取套接字缓冲区的最大大小,应用也可以先尝试把套接字缓冲区设置成预期的大小,若失败则减半继续尝试直到成功。应用在设置套接字缓冲区大小时,应确保缓冲区不会变小,因此最好一开始就调用getsockopt获取系统的默认值并判断是否已经够大。
9.SO_REVLOWATSO_SND_LOWAT:每个套接字都有一个接收低水位标记和一个发送低水位标记,它们由select函数使用,这两个套接字选项允许我们修改这两个低水位标记。接收低水位标记是让select函数返回可读时套接字接收缓冲区中所需的数据量,对于TCP、UDP、SCTP套接字,其默认值为1;发送低水位标记是让select函数返回可写时套接字发送缓冲区中所需的可用空间,对于TCP,其默认值通常为2048字节,UDP也使用发送低水位标记,但UDP套接字的发送缓冲区中可用空间字节数不改变(因为UDP不为数据报保留副本),只要一个UDP套接字的发送缓冲区大小大于该套接字的低水位标记,该UDP套接字就总是可写。UDP没有发送缓冲区,只有发送缓冲区大小这个属性。
10.SO_RCVTIMEOSO_SNDTIMEO:这两个选项允许我们给套接字的接收和发送设置一个超时值,访问它们的getsockopt和setsockopt函数的参数是指向timeval结构的指针,与select函数所用参数相同,这可让我们用秒数和微秒数来规定超时,我们通过设置其值为0s和0μs来禁止超时,默认这两个超时都是禁止的。接收超时影响的5个函数:read、readv、recv、recvfrom、recvmsg;发送超时影响的5个函数:write、writev、send、sendto、sendmsg。这两个套接字选项和套接字接收超时和发送超时的概念是在4.3 BSD Reno中增加的。源自Berkeley的实现中,这两个值实现为inactivity timer而非absolute timer。

11.SO_REUSEADDRSO_REUSEPORT:SO_REUSEADDR有以下作用:
(1)允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将该端口用作它们的本地端口的连接仍存在,这个条件通常是这样碰到的:
(a)启动一个监听服务器。
(b)连接请求到达,派生一个子进程处理此客户。
(c)监听服务器终止,但子进程继续为现有连接上的客户提供服务。
(d)重启监听服务器。

默认,当监听服务器在步骤d通过socket、bind、listen函数重新启动时,由于它试图绑定一个现有连接(由早先派生的子进程处理着的连接)上的端口,从而bind函数会失败,如果设置了SO_REUSEADDR选项,则bind函数会成功。所有TCP服务器都应指定此套接字选项,以允许这种情况下服务器重新启动。

(2)SO_REUSEADDR允许同一端口上启动同一服务器的多个实例,每个实例只要捆绑一个不同的本地IP即可。这对于使用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服务器以本地地址198.69.10.129和本地端口号80调用bind,这次调用bind成功的先决条件也是预先设置SO_REUSEADDR选项。假设SO_REUSEADDR均已设置,3个服务器都将启动成功,此时,目的地址为198.69.10.128、目的端口号为80的外来TCP连接请求被递送到第2个服务器,目的地址为198.69.10.129、目的端口号为80的外来TCP连接请求将被递送给第3个服务器,目的端口号为80的所有其他TCP连接请求都将递送给第1个服务器。第一个服务器配置的是通配IP地址,通配的含义是没有更好的(更明确的)匹配的任何地址。

上述一台主机上多个设置了SO_REUSEADDR选项的相同服务的连接请求的处理是自动完成的,这种情况下建议设置该选项。对于TCP,我们不能同时启动捆绑了同一IP和同一端口号的多个服务器,即使设置了SO_REUSEADDR选项。为安全起见,有些操作系统不允许对已经绑定了通配地址的端口再捆绑任何更明确的地址,在这样的系统上,执行通配地址捆绑的服务器进程必须最后一个启动,这么做是为了防止把恶意的服务器捆绑到某个系统服务正在使用的IP和端口上,造成合法请求被截取,这一点对于NFS更成问题,因为NFS通常不使用特权端口。

(3)SO_REUSEADDR允许单个进程捆绑同一端口到多个套接字上,只要每次捆绑指定不同的本地IP即可。在不支持IP_RECVDSTADDR套接字选项的系统上,这对于要求知道客户请求的目的IP地址的UDP服务器来说非常普遍,TCP服务器通常不使用这种方法,因为TCP服务器在建立连接后总能通过调用getsockname来确定客户请求的目的IP地址,然而对于希望在一个多目的主机的若干个(而非全部)本地IP地址上服务的TCP服务器进程来说,仍需采用这种方式。

(4)SO_REUSEADDR允许完全重复的捆绑,只要传输协议支持,当一个IP地址和端口已绑定到某个套接字上时,同样的IP地址和端口还可以捆绑到另一套接字上,一般本特性仅支持UDP套接字。本特性用于多播时,允许在同一主机上同时运行同一应用程序的多个副本,当一个UDP数据报需要被这些重复绑定的套接字中的每一个接收时,如果该数据报的目的地址是一个广播地址或多播地址,则给每个匹配的套接字递送一个该数据报的副本,但如果该数据报的目的地址是一个单播地址,则它只递送给单个套接字,由哪个套接字接收它取决于实现。

4.4 BSD随多播支持的添加引入了SO_REUSEPORT套接字选项,它的语义为:
(1)本选项允许完全重复的捆绑,只要想要捆绑同一IP地址和端口的每个套接字都指定了SO_REUSEPORT才行。
(2)如果被捆绑的IP地址是一个多播地址,则SO_REUSEADDR和SO_REUSEPORT是等效的。
SO_REUSEPORT的问题在于并非所有系统都支持它,在不支持此选项但支持多播的系统上,我们使用SO_REUSEADDR以允许合理的完全重复的捆绑(即同一时刻、同一主机上可运行多次某个期待接收相同多播或广播的UDP服务器)。

对SO_REUSERADDR有以下建议:
(1)所有TCP服务器在调用bind前设置SO_REUSEADDR套接字选项。
(2)当编写一个可在同一时刻、同一主机上运行多次的多播应用时,设置SO_REUSEADDR套接字选项,并将其所参加的多播组的地址作为本地IP地址捆绑。

SO_REUSEADDR有一个潜在安全问题,假设存在一个绑定了通配地址和端口5555的套接字,如果指定SO_REUSEADDR,我们就可以把相同端口捆绑到不同IP地址上,如所在主机的主IP地址,此后目的地为5555及新绑定IP地址的数据报将被递送到新的套接字,而不是绑定了通配地址的已有套接字,这些IP数据报可以是TCP的SYN分节、SCTP的INIT块、UDP数据报,对于大多众所周知的服务,如HTTP、FTP、Telnet来说这不成问题,因为这些服务器绑定的是保留端口,后来的试图捆绑这些端口的更为明确的实例(即盗用这些端口)的进程需要超级用户特权,但对于NFS可能是一个问题,因为它的通常端口2049不是保留端口。
套接字API的一个问题是,套接字对的设置由两个函数调用bind和connect而不是一个来完成的,[Torek 1994]为解决本问题提议了如下单个函数:
在这里插入图片描述
其中laddr参数指定本地IP地址和本地端口号,faddr参数指定外地IP地址和外地端口号,listen函数指定是客户(值传0)还是服务器(值传非0,与listen函数的backlog参数相同)。bind函数相当于将空指针传给faddr参数、将0传给faddrlen参数来调用bind_connect_listen;connect函数相当于将空指针传给laddr参数,将0传给laddrlen参数来调用bind_connect_listen。有些应用需要同时指定连接的本地地址对和外地地址对,它们就可以直接调用bind_connect_listen。有了此函数就不需要SO_REUSEADDR了,除非是明确要求允许完全重复地捆绑相同IP地址和端口的多播UDP服务器。使用此函数时,对于TCP服务器,可以限定自己仅为来自特定IP地址和端口的连接请求提供服务,这是RFC 793规定的,但对于现有套接字API来说是无法实现的。

12.SO_TYPE:返回套接字的类型,返回值是一个诸如SOCK_STREAM或SOCK_DGRAM之类的值,通常由启动时继承了套接字的进程使用。
13.SO_USELOOPBACK:仅用于路由域AF_ROUTE的套接字,对于这些套接字,该选项的默认设置为打开(这是唯一一个默认值为打开的SO_xxx二元套接字选项),本选项打开时,相应套接字将接收在其上发送的任何数据报的一个副本。禁止这些环回副本的另一个方法是调用shutdown,并将其第二个参数设为SHUT_RD。

以下套接字选项由IPv4处理,它们的级别(即getsockopt和setsockopt函数的第二个参数)为IPPROTO_IP:
1.IP_HDRINCL:如果本选项设置给一个原始IP套接字,那么我们必须为所有在该原始套接字上发送的数据报构造自己的IP首部,一般情况下,在原始套接字上发送的IP数据报其首部是由内核构造的,但有些程序(如traceroute)需要构造自己的IP首部。开启本选项时,IP首部中以下字段内核会处理:
(1)IP总是计算并存储IP首部校验和。
(2)如果我们将IP标志字段置为0,内核将设置该字段(该字段唯一地标识主机发送的每一份数据报,通常每发送一份报文它的值就加1)。
(3)如果源IP地址是INADDR_ANY,IP将把它设为外出接口的主IP地址。
(4)如果设置IP选项取决于实现,有些实现取出我们预先使用IP_OPTIONS套接字选项设置的IP选项,把它们添加到我们构造的首部中,而其他实现要求我们亲自在首部指定任何期望的IP选项。
(5)IP首部中有些字段必须以主机字节序填写,有些必须由网络字节序填写,具体取决于实现,这使得代码不便于移植。
2.IP_OPTIONS:设置此选项后,会将设置的IP选项设置到IPv4首部中。
3.IP_RECVDSTADDR:设置本选项后,所收到的UDP数据报的目的IP地址由recvmsg函数作为辅助数据返回。
4.IP_RECVIF:设置本选项后,所收到的UDP数据报的接收接口的索引由recvmsg函数作为辅助数据返回。
5.IP_TOS:本选项允许我们为TCP、UDP、SCTP套接字设置IP首部中的服务类型字段,如下图:
在这里插入图片描述
历史上的TOS(Type Of Service,服务类型)字段(首部长度字段后,总长度字段前的部分)已被替换为两个字段:6位区分服务码点(Differentiated Services Code Point,DSCP)和2位显式拥塞通知(Explicit Congestion Notification,ECN)。我们用IP_TOS套接字选项设置该值时,内核可能为了实施Diffserv策略或实现ECN而覆盖设置的值。
如果我们用IP_TOS套接字选项调用getsockopt,会获得用于放入外出IP数据报首部的DSCP和ECN字段中的TOS当前值(默认为0),我们没有办法从接收到的IP数据报中取得该值。
应用可把DSCP设置成用户和网络业务供应商预先协商好的某个值,以便接受预定的服务,如对IP电话的低延迟服务、对海量数据传送的高吞吐量服务。由RFC 2474定义的区分服务(diffserv)体系结构只是有限向后兼容历史上的TOS字段定义,把IP_TOS设置成头文件netinet/ip.h中定义的某个常值(如IPTOS_LOWDELAY、IPTOS_THROUGHPUT)的应用应改为使用由用户指定的某个DSCP值。区分服务还保留的TOS值只有优先级6(internetwork control,网间控制)和7(network control,网内控制),这意味着把IP_TOS设置成IPTOS_PREC_NETCONTROL或IPTOS_PREC_INTERNETCONTROL的应用在区分服务网络中可以继续工作。
RFC 3168中有ECN字段的定义,应用通常应该把ECN字段的设置留给内核,即把由IP_TOS设置的值中的低两位设为0。
6.IP_TTL:可用本选项设置或获取某个特定套接字发送的单播分组上的默认TTL。多播TTL值使用IP_MULTICAST_TTL套接字选项设置。4.4 BSD对TCP和UDP套接字使用的默认值都是64(由“IANA的IP Option Numbers注册处”规定),对原始套接字使用的默认值是255。与TOS字段一样,调用getsockopt返回的是系统将用于外出数据报的字段的默认值,我们没有办法从接收到的IP数据报中取得该值。traceroute程序中会设置该选项。

ICMPv6套接字选项只有一个,它的级别为IPPROTO_ICMPV6:
1.ICMP6_FILTER:允许我们获取或设置icmp6_filter结构,该结构指出256个ICMPv6消息类型中哪些将由某个原始套接字传递给所在进程。

以下选项由IPv6处理,它们的级别是IPPROTO_IPV6:
1.IPV6_CHECKSUN:本选项指定用户数据中校验和所处位置的字节偏移。如果该值非负,那么内核将:
(1)给所有外出分组计算并存储校验和;
(2)验证外来分组的校验和,丢弃所有校验和无效的分组。
本选项影响除ICMPv6原始套接字外的所有IPv6原始套接字,内核总是给ICMPv6原始套接字计算并存储校验和。如果指定本选项值为-1(默认值),则内核不会在相应的原始套接字上计算并存储外出分组的校验和,也不会验证外来分组的校验和。
所有使用IPv6的协议在它们各自的协议首部都应该有一个校验和,这些校验和计算时也包含伪首部(如UDP伪首部,它是一个虚拟的数据结构,为计算校验和存在,其中的信息来自IPv4首部,如源IP和目的IP),伪首部中会包含作为校验和计算的一部分的源IPv6地址(这一点不同于通常使用IPv4原始套接字实现的其他协议),这样不必让使用原始套接字的应用进行源地址选择,而是由内核这么做,并由内核计算并存储包含标准IPv6伪首部的检验和。
2.IPV6_DONTFRAG:开启本选项会禁止为UDP套接字或原始套接字自动插入分片首部,外出分组中大小超过发送接口MTU的那些分组将被丢弃。发送分组的系统调用不会为此返回错误,因为已发送出去仍在途中的分组也可能因为超过路径MTU而被丢弃。应用应开启IPV6_RECVPATHMTU选项以获悉路径MTU的变动。
3.IPV6_NEXTHOP:本选项将外出数据报的下一跳地址指定为一个套接字地址结构,这是一个特权操作。
4.IPV6_PATHMTU:本选项只能获取,不能设置,返回值为路径MTU发现功能确定的当前MTU。
5.IPV6_RECVDSTOPTS:开启本选项时,任何接收到的IPv6目的地选项都由recvmsg函数作为辅助数据返回,本选项默认关闭。
6.IPV6_RECVHOPLIMIT:开启本选项时,任何接收到的TTL字段都将由recvmsg函数作为辅助数据返回,本选项默认关闭。对IPv4而言,没有办法获取到接收到的TTL字段。
7.IPV6_RECVHOPOPTS:开启本选项时,接收到的IPv6 hop-by-hop选项将由recvmsg函数作为辅助函数返回,本选项默认关闭。
8.IPV6_RECVPATHMTU:开启本选项时,某条路径的路径MTU在变化时将由recvmsg函数作为辅助数据返回(不伴随任何数据)。
9.IPV6_RECVPKTINFO:开启本选项时,接收到的IPv6数据报的IPv6地址和到达接口索引将由recvmsg函数作为辅助数据返回。
10.IPV6_RECVRTHDR:开启本选项时,接收到的IPv6路由首部将作为recvmsg函数的辅助数据返回,本选项默认关闭。
11.IPV6_RECVTCLASS:开启本选项时,接收到的流通类别(包含DSCP和ECN字段)字段(traffic class,相当于IPv4的TOS字段)将由recvmsg函数作为辅助数据返回。
12.IPV6_UNICAST_HOPS:类似于IPv4的IP_TTL选项,设置本选项会给在相应套接字上发送的外出数据报指定默认跳限,获取本选项会返回内核用于相应套接字的跳限值。traceroute程序会设置该选项。
13.IPV6_USE_MIN_MTU:把本选项设为1表示,路径MTU发现功能不必执行,为避免分片,分组就使用IPv6的最小MTU发送。把本选项设为0表示,路径发现功能对于所有目的地都要执行。把本选项设为-1表示,路径MTU发现功能仅对单播目的地执行,对于多播目的地使用最小MTU。本选项默认值为-1。
14.IPV6_V6ONLY:在一个AF_INET6套接字上开启本选项将限制它只执行IPv6通信。本选项默认关闭,但有些系统上存在默认开启此选项的手段。
15.IPV6_XXX:修改IPv6的大部分首部选项时假定,UDP套接字使用recvmsg和sendmsg函数的辅助数据在内核和应用间传递信息;TCP套接字使用getsockopt和setsockopt函数获取和存储这些值。套接字选项(TCP)的类型与辅助数据(UDP)的类型相同,且套接字选项(TCP)缓冲区中包含的信息也与辅助数据(UDP)缓冲区中的相同。

TCP有两个套接字选项,它们的级别为IPPROTO_TCP:
1.TCP_MAXSEG:本选项可获取或设置TCP连接的最大分节大小(MSS)。返回值是TCP可以一次发送给对端的最大数据量,它通常是由对端使用SYN分节通告的MSS,除非我们的TCP实现选择使用一个比对端通告的MSS小些的值。如果该值在相应套接字的连接建立前获取,则返回值是未从对端收到MSS选项的情况下的默认值。另外,可能连接使用的MSS值比返回值小,譬如使用了时间戳选项时,因为这个选项在每个TCP段的TCP选项字段中存放12字节数据(时间戳选项实际占10字节,再补2字节使TCP首部长度为4字节的倍数)。如果TCP支持路径MTU发现功能,那么它发送的每个分节的最大数据量可能在连接存在期内改变,如果到对端的路径发生变动,该值可能就有调整。本套接字也可由应用设置,但并非所有系统都支持,毕竟本选项原本是只读的。4.4 BSD限制应用只能减少其值,这是明智的,一旦连接建立,本选项的值就是对端通告的MSS选项值,TCP不能发送超过该值的分节,而TCP总是可以发送数据量少于对端通告的MSS的分节。
2.TCP_NODELAY:开启本选项将禁用TCP的Nagle算法,默认该算法是启动的。Nagle算法目的是为了减少广域网(WAN)上小分组的数目,该算法指出:如果某个给定连接上有待确认数据,那么用户写的小分组将不会发出,直到现有数据被确认,小分组的定义是小于MSS的分组。TCP总是尽可能发送最大大小的分组,Nagle算法目的是防止一个连接上在某一时刻有多个小分组待确认。Rlogin和Telnet的客户端是常见的产生小分组的进程,它们通常把每次击键作为单个分组发送,在快速的局域网(LAN)上,我们通常不会见到Nagle算法对这些客户进程的影响,因为收到确认的时间一般就几毫秒,远小于我们相继键入两个字符的间隔时间,但在广域网上,收到确认的时间可能长达1秒,我们就会注意到字符回显的延迟,且该延迟往往被Nagle算法进一步放大,如以下例子:我们在Rlogin或Telnet客户端键入6个字符hello!,每个字符间的间隔正好是250ms,到服务器端的RTT是600ms,且服务器立即发回每个字符的回显,我们假设对客户端字符的ACK和字符回显是一同发回给客户端的,且忽略客户发送的对服务器端回显的ACK,此时如果Nagle算法是禁止的,我们会得到下图12个分组:
在这里插入图片描述
上图中每个字符都在各自的分组中发送,数据分节从左到右,ACK从右到左。如果Nagle算法是开启的(默认情形),我们会得到下图8个分组:
在这里插入图片描述
上图中,第一个字符独自作为一个分组发送,但下两个字符没有没有立即发送,因为该连接上有一个待确认的小分组,在时刻600ms处收到对第一个分组的ACK后(该ACK由第一个字符的回显捎带),这两个字符才被发送,在第2个分组在时刻1200ms处被确认前,不会发送其他小分组。Nagle算法常与另一个TCP算法联合使用:ACK延滞算法,该算法使得TCP接收到数据后不立即发送ACK,而是等待一段时间(典型值为50~200ms),然后才发送ACK,TCP期待在等待时间内自身有数据发送回对端,被延滞的ACK就可由这些数据捎带,从而省掉一个TCP分节,这对于Rlogin和Telnet客户来说可行,因为它们的服务器一般都回显客户发送来的每个字符,这样对客户字符的ACK完全可以在服务器对该字符的回显中捎带返回。但对于服务器不产生数据以便携带ACK的客户来说,ACK延滞算法有问题,这些客户可能觉察到明显的延迟,因为客户TCP要等到服务器的ACK延滞定时器超时并收到ACK后才继续给服务器发送数据,这些客户可通过TCP_NODELAY选项禁止Nagle算法。另一类不适合使用Nagle算法和TCP延滞算法的客户时以若干小片数据向服务器发送单个逻辑请求的客户,如某客户向它的服务器发送一个400字节的请求,该请求由一个4字节的请求类型和后跟396字节的请求数据构成,如果客户先执行一个4字节write调用,在执行一个396字节的write调用,那么第2个写操作的数据将一直等到服务器的TCP确认了第一个写操作的4字节数据后才由客户TCP发送出去,而且,由于服务器进程难以在收到其余396字节前对先收到的4字节数据进行操作,服务器的TCP将延滞这4字节的ACK直到延滞定时器超时,有3种方式修改这类客户:
(1)使用writev函数而不是两次调用write,单个writev调用最终导致调用一次TCP输出功能,结果是产生一个TCP分节,这是首选办法。
(2)把前4字节的数据和后396字节的数据复制到单个缓冲区中,然后对该缓冲区调用一次write。
(3)设置TCP_NODELAY套接字选项,继续调用write两次,这是最不可取的方法,且有损于网络,通常不应该考虑。

SCTP套接字数目较多,反映出SCTP为应用开发人员提供了较细粒度的控制能力,它们的级别为IPPROTO_SCTP。

若干用于获取SCTP相关信息的选项要求把一些数据(如关联ID、对端地址)传递进内核,尽管getsockopt函数的一些实现支持进程与内核间的双向数据传递,但并非所有实现都能做到,SCTP的API为此定义了sctp_opt_info函数来隐藏这个差异。在getsockopt函数支持双向数据传递的系统上,sctp_opt_info函数只是getsockopt函数的一个简单外包,其他系统上,它执行所需的操作,其中可能用到定制的ioctl函数或某个新的系统调用。当获取SCTP选项时,建议使用sctp_opt_info函数以便移植。图7-2中,这些选项的get列是一个匕首符号。

SCTP套接字选项:
1.SCTP_ADAPTION_LAYER:在关联初始化期间,两端都可能会指定一个适配层指示,它是一个32位无符号整数,可被两端的应用用来协调任何本地应用的适配层。这个选项允许获取或设置将会提供给对端的适配层指示。获取本选项的值时,调用者得到的是本地套接字会提供给对端的值,要获取对端的适配层指示,应用进程必须订阅适配层事件。
2.SCTP_ASSOCINFO:该套接字选项可用于:
(1)获取关于某个现有关联的信息。
(2)改变某个已有关联的参数。
(3)为未来的关联设置默认信息。

在获取关于某个现有关联的信息时,应使用sctp_opt_info函数而非getsockopt函数,本选项对应的参数是sctp_assocparams结构:
在这里插入图片描述
以上结构中各个字段的含义:
(1)sasoc_assoc_id:存放待访问关联的标识(即关联ID),如果调用setsockopt时置0本字段,则该套接字上的sasoc_asocmaxrxt和sasoc_cookie_life字段描述的值会被设为默认值。调用getsockopt时,如果提供此关联ID,返回的是特定于该关联的信息;如果置0此字段,返回的是默认的端点设置信息。
(2)sasoc_asocmaxrxt:存放某个关联在已发送数据没有得到确认的情况下尝试重传的最大次数,达到这个次数后SCTP放弃重传,报告用户对端不可用,然后关闭该关联。
(3)sasoc_number_peer_destinations:存放对端目的地址数,它不能设置,只能获取。
(4)sasoc_peer_rwnd:存放对端的当前接收窗口,该值表示还能发送给对端的数据字节总数,本字段是动态的,本地端点发送数据时其值减小,外地应用进程读取已经收到的数据时其值增大,它不能设置,只能获取。
(5)sasoc_local_rwnd:存放本地SCTP协议栈当前通告对端的接收窗口,本字段也是动态的,并受SO_SNDBUF套接字选项的影响,它不能设置,只能获取。
(6)sasoc_cookie_life:存放送给对端的状态cookie以毫秒为单位的有效期。为了防止重放攻击,每个INIT-ACK块送给对端的状态cookie都关联有一个生命期。原本为60000毫秒的生命期默认值可以通过置sasoc_assoc_id为0并设置本选项加以修改。该值可以降低,从而更好地防护重放攻击,但如果这样做,在关联发起期间会降低网络延迟的稳定性。
3.SCTP_AUTOCLOSE:本选项允许我们获取或设置一个SCTP端点的自动关闭时间,自动关闭时间是一个SCTP关联在空闲时保持打开的秒数,SCTP协议把空闲定义为一个关联的两个端点都没有在发送或接收用户数据的状态。自动关闭功能默认是未启用的。

自动关闭选项只能用于一到多式SCTP接口,设置本选项时,传递给它的整数值为某空闲关联被自动关闭前的持续秒数,值为0表示不会自动关闭。本选项仅影响由相应本地端点将来创建的关联,已有关联保持现行设置不变。

自动关闭功能可由服务器用来强制关闭空闲关联,服务器无需为此维护额外的状态,使用本特性的服务器进程应估算其关联最长空闲时间。
4.SCTP_DEFAULT_SEND_PARAM:SCTP有许多可选的发送参数,它们通常作为辅助数据传递,或由sctp_sendmsg函数调用发送(sctp_sendmsg函数通常实现为库函数,可替用户传递辅助数据)。希望发送大量消息且所有消息具有相同发送参数的应用可使用本选项设置默认参数,从而避免使用辅助数据或调用sctp_sendmsg。本选项对应的参数类型为sctp_sndrcvinfo结构:
在这里插入图片描述
sctp_sndrcvinfo结构中的字段含义:
(1)sinfo_stream:指定新的默认流,所有外出消息将被发送到该流中。
(2)sinfo_ssn:设置默认发送参数时该字段被忽略;使用recvmsg或sctp_recvmsg函数接收消息时,本字段将存放由对端置于SCTP DATA块流序列号(Stream Sequence Number,SSN)字段中的值。
(3)sinfo_flags:指定新的默认标志,它将应用于所有消息发送,以下是该字段允许的SCTP标志值:
在这里插入图片描述
(4)sinfo_ppid:外出消息使用的是什么协议(即对方收到此值就知道该如何解释外出消息,该值由用户定义,可传任意用户想传的值),可被对方接收并理解。
(5)sinfo_context:该字段中存放一个默认值,该默认值在检索到无法发送到对端的消息时作为一个标记。当发送无法被满足时,通常用户程序可以用这个值来索引应用的某些特定数据结构。
(6)sinfo_timetolive:指定新的默认生命期,它将应用于所有消息发送,SCTP协议栈使用本字段判定何时丢弃尚未执行首次传送就因过度拖延而失效的外出消息。如果同一关联的两个端点都支持部分可靠性(partial reliablility)选项,那么本生命期也用于指定完成首次传送后的消息的有效期。
(7)sinfo_tsn:如果是在设置默认发送参数,则此字段被忽略。当使用recvmsg或sctp_recvmsg函数接收消息时,本字段存放由对端置于SCTP DATA块的传输序号(Transport Sequence Number,TSN)字段中的值。
(8)sinfo_cumtsn:如果是在设置默认发送参数,则此字段被忽略。当使用recvmsg或sctp_recvmsg函数接收消息时,本字段存放本地SCTP协议栈与它的对端相关联的当前TSN累积值。
(9)sinfo_assoc_id:指定请求者希望设置的关联标识的默认值。对于一对一式套接字,忽略本字段。

以上所有默认设置只影响没有指定sctp_sndrcvinfo结构的消息发送,指定了该结构的消息发送(如带辅助数据的sctp_sendmsg或sendmsg函数)将覆盖默认设置。也可以本选项为参数通过sctp_opt_info函数获取当前的默认设置。
5.SCTP_DISABLE_FRAGMENTS:SCTP通常把太大而不适合置于单个SCTP分组中的用户消息分割成多个DATA块,开启本选项将在发送端禁止这种行为。被禁止后,SCTP将为此向用户返回EMSGSIZE错误,且不会发送用户消息。SCTP的默认行为是会对用户消息执行分片。

希望自己控制消息大小的应用可使用本选项,以确保每个用户应用消息都适合置于单个IP分组中。开启本选项的应用要准备好处理出错情况(消息过大),消息过大时,这些应用可以提供应用层的消息分片机制或改用较小消息。
6.SCTP_EVENTS:该选项允许调用者获取、开启、禁用SCTP通知。SCTP通知是由SCTP协议栈发送给应用进程的消息,这种消息就像普通消息那样读取,只需把recvmsg函数的msghdr结构参数中的msg_flags字段设置为MSG_NOTIFICATION。不使用recvmsg或sctp_recvmsg函数的应用不应开启事件通知功能。使用本选项可传递一个sctp_event_subscribe结构就可预定8类事件的通知,该结构格式如下,其中各个字段值为0表示不预定,为1表示预定:
在这里插入图片描述
在这里插入图片描述
以下是sctp_event_subscribe结构中字段的含义:
在这里插入图片描述
7.SCTP_GET_PEER_ADDR_INFO:本选项用于获取某个给定对端地址的信息,如拥塞窗口、平滑后的RTT、MTU等。本选项对应的是sctp_paddrinfo结构,调用者在其中的spinfo_address字段填入待查询的对端地址,且为了便于移植,应使用sctp_opt_info函数而非getsockopt函数。sctp_paddrinfo结构如下:
在这里插入图片描述
返回给调用者的sctp_paddrinfo结构中各个字段的含义:
(1)spinfo_assoc_id:存放关联标识,它和communication up(SCTP_COMM_UP)通知中提供的信息一致。几乎所有SCTP操作都能用这个唯一的值作为相应关联的标识。
(2)spinfo_address:由调用者设置,用于告知SCTP套接字想要获取哪一个对端地址的信息,调用返回时该值不变。
(3)spinfo_state:存放SCTP对端地址状态,可选值如下:
在这里插入图片描述
上图中的未证实地址是一个对端已作为有效地址列出,但本地SCTP尚未证实对端确实持有该地址。当送往某个地址的心博或用户数据得到对端确认时,本地SCTP端点就可以证实该地址确实为对端所持有了。未证实的地址没有有效的重传超时值(Retransmission TimeOut,RTO)。活跃地址被认为是可用的地址。
(4)spinfo_cwnd:所指定的对端地址维护的当前拥塞窗口。
(5)spinfo_srtt:所指定的对端地址的平滑后RTT的当前估计值。
(6)spinfo_rto:所指定的对端地址的当前重传超时值。
(7)spinfo_mtu:由路径MTU发现功能发现的通往所指定对端地址的路径MTU当前值。

本选项的一个用途是,把一个IP地址结构转换成一个可用于其它调用的关联标识。另一个用途是,由应用进程跟踪一个多宿对端主机每个地址的性能,并把相关联的主目的地址更新为其中性能最佳的一个。这些值也有利于日志记录和程序调试。
8.SCTP_I_WANT_MAPPED_V4_ADDR:用于为AF_INET6类型套接字开启或禁用IPv4映射地址,默认为开启。开启本选项时,所有IPv4地址在送往应用进程前将被映射为一个IPv6地址;禁止本选项时,SCTP套接字不会对IPv4地址进行映射,而是作为sockaddr_in结构直接传递。
9.SCTP_INITMSG:用于获取或设置某个SCTP套接字在发送INIT消息时所用的默认初始参数,本选项对应的参数是sctp_initmsg结构,定义如下:
在这里插入图片描述
sctp_initmsg结构中字段含义如下:
(1)sinit_num_ostreams:表示应用想要请求的外出SCTP流的数目,该值要等到相应关联完成初始握手后才得到确认,且可能因为对端的限制而向下协调。
(2)sinit_max_instreams:表示应用准备允许的外来SCTP流的最大数目,如果该值大于SCTP协议栈支持的最大允许流数,则它将被改为这个最大值。
(3)sinit_max_attempts:表示SCTP协议栈重传多少次INIT消息才认为对端不可达。
(4)sinit_max_init_timeo:表示用于INIT定时器的最大RTO值,在初始定时器进行指数退避期间,该值将替代RTO.max作为重传RTO极限,该值以毫秒为单位。

当设置以上字段时,SCTP将忽略其中的0值。一对多式套接字的用户在关联隐性建立期间也可能在辅助数据中传递一个sctp_initmsg结构。
10.SCTP_MAXBURST:用于获取或设置用于分组发送的最大猝发大小(maximum burst size),当SCTP向对端发送数据时,一次不能发送多于这个数目的分组,以免网络被分组淹没。具体SCTP实现有两种方式:
(1)把拥塞窗口(cwnd,congestion window,发送端任何时刻发送的数据的序号不能超过当前已确认的最大数据序号和cwnd之和)缩减为当前飞行大小(flight size,已发送但还未收到ack的数据大小)加上最大猝发大小与路径MTU的乘积。
(2)把该值作为一个独立的控制量,在任意一个发送机会最多只发送这个数目的分组。
11.SCTP_MAXSEG:用于获取或设置SCTP分片的最大片段大小,与TCP的TCP_MAXSEG选项类似。当SCTP发送端从其应用进程收到一个大于这个大小的消息时,它将把该消息分割成多个块,以便分别传送到对端。 SCTP可能以比本选项所请求的值更小的值来分割消息,当通往对端的某条路径的PMTU(路径MTU,每条路径对应一个对端地址)变得比本选项所请求的值还要小时,这种更小的分割就会发生。此选项关联的数据类型是int,该选项的默认值为0,含义为用户不限制分片,只有PMTU会影响到SCTP的分片大小选择。
12:SCTP_NODELAY:开启本选项将禁止SCTP的Nagle算法,本选项默认关闭。SCTP的Nagle算法与TCP的Nagle算法工作原理相同,开启后意味着数据包会尽可能快地发送,不会引入不必要的延迟,代价是网络中的数据包会变多。
13:SCTP_PEER_ADDR_PARAMS:用于获取或设置关于某个关联的对端的各种参数,本选项关联的数据结构是sctp_paddrparams:
在这里插入图片描述
sctp_paddrparams结构中的字段含义:
(1)spp_assoc_id:关联标识,如果该值为0,则访问的是对对端端点的默认参数,而非特定于关联的参数。
(2)spp_address:对端IP地址,如果spp_assoc_id字段为0,则忽略本字段。
(3)spp_hbinterval:心博间隔时间,将该值设为SCTP_NO_HB将禁止心博,设为SCTP_ISSUR_HB将强制发送一次心博,设为其他值则将该值以毫秒为单位设为新的心博间隔。设置对对端端点的默认参数时,不能使用SCTP_ISSUE_HB。
(4)spp_pathmaxrxt:在声明对端地址为不活跃前,尝试的重传次数。当主目的地址被声明为不活跃时,另一个对端地址将被选为主地址。
14:SCTP_PRIMARY_ADDR:用于获取或设置主目的地址,主目的地址是本端发送给对端的所有消息的默认目的地址,与本选项关联的结构为sctp_setprim:
在这里插入图片描述
sctp_setprim结构中字段含义:
(1)ssp_assoc_id:关联标识,对于一到一式套接字,本字段被忽略。
(2)ssp_addr:要指定的主目的地址(必须是一个属于对端的地址),使用setsockopt函数设置此选项时,本地端为请求者填入的要求设置的主目的地址的新值,使用getsockopt函数获取本选项时,本字段返回当前所用主目的地址的值。

在只有一个本地地址与之关联的一到一式套接字上获取本选项的值跟直接调用getsockname(获取对端ip和端口)是一样的。
15:SCTP_RTOINFO:用于获取或设置各种RTO信息,它们既可以是关于某个给定关联的设置,也可以是用于本地端点的默认设置,为便于移植,应使用sctp_opt_info函数而非getsockopt函数,本选项关联的数据结构是sctp_rtoinfo:
在这里插入图片描述
sctp_rtoinfo结构中字段的含义:
(1)srto_assoc_id:关联标识或0,若值为0,当前函数调用针对的是系统默认参数。
(2)srto_initial:存放初始RTO值,该值以毫秒为单位,默认值为3000。
(3)srto_max:存放最大RTO值,该值以毫秒为单位,默认值为60000。
(4)srto_min:存放最小RTO值,该值以毫秒为单位,默认值为1000。

如果设置该选项时,以上后三个字段中传的是0值,则表示不改变此值。
16.SCTP_SET_PEER_PRIMARY_ADDR:设置本套接字将发送一个消息,请求对端更改主目的地址为所提供的地址,该选项关联的数据结构是sctp_setpeerprim,该结构中所提供的的本地地址必须已经绑定在本地端点:
在这里插入图片描述
sctp_setpeerprim结构中字段含义:
(1)sspp_assoc_id:关联标识,对于一到一式套接字,本字段被忽略。
(2)sspp_ddr:存放想要对端设置为主目的地址的本地地址。

本选项是可选的,需要两端均支持才能运作。如果本地端点不支持本特性,会给调用者返回EOPNOTSUPP错误,如果远程端点不支持本特性,会返回给调用者EINVAL错误。该套接字选项只能设置,不能获取。
17.SCTP_STATUS:用于获取某个SCTP关联的状态,为了便于移植,调用者应该使用sctp_opt_info函数而非getsockopt函数。本选项关联的数据结构是sctp_status,调用者必须填写关联标识,关于这个关联的信息将在返回时填写到该结构的其他字段中。sctp_status结构如下:
在这里插入图片描述
sctp_status结构的字段含义:
(1)sstat_assoc_id:存放关联标识。
(2)sstat_state:存放下图中的常值之一,指出关联的状态:
在这里插入图片描述
(3)sstat_rwnd:存放本地端点对于对端接收窗口的当前估计。
(4)sstat_unackdata:存放未确认的DATA块数目。
(5)sstat_penddata:存放本地端点暂存并等待应用读取的未读DATA块数目。
(6)sstat_instrms:存放对端用于向本端发送数据的流的数目。
(7)sstat_outstrms:存放本端可用于向对端发送数据的流的数目。
(8)sstat_fragmentation_point:本地SCTP端点将其用作用户消息分割点,该值通常是所有目的地址的最小MTU,或是由本地应用进程使用SCTP_MAXSEG套接字选项设置的更小值。
(9)sstat_primary:存放当前主目的地址。

以上字段可用于诊断或确定会话的特征,如sctp_get_no_strms函数将用sstat_outstrms成员确定有多少外出流可用;偏低的sstat_rwnd字段值或偏高的stat_unackdata字段值可用于判断对端的接收套接字缓冲区正变满,这一点是让应用尽可能降低发送速率的信号;有些应用使用stat_fragmentation_point字段值减少SCTP的分片数量,方法是发送较小的应用消息。

fcntl(file control,文件控制)函数可执行各种描述符控制操作,以下是fcntl、ioctl、路由套接字所能做的操作总结:
在这里插入图片描述
上图中前六个操作可由任何进程用于套接字,后两个操作由诸如ifconfig或route之类的管理程序执行。

执行前四个操作的方法不止一种,最后一列指出POSIX规定fcntl函数是首选的。最后一列还指出,POSIX提供sockatmark函数作为测试是否处于带外标志的首选方法。最后一列中的空白表示该操作没有被POSIX标准化。

对于前两个操作,历史上使用fcntl函数的FNDELAY和FASYNC命令执行,但POSIX定义的是使用FSETFL命令的O_xxx常值来执行。

fcntl函数提供与网络编程相关的以下特性:
1.非阻塞式IO:通过使用F_SETFL命令设置O_NONBLOCK文件状态标志,把一个套接字设置为非阻塞型。
2.信号驱动式IO:通过使用F_SETFL命令设置O_ASYNC文件状态标志,把一个套接字设置为一旦其状态发生变化,内核就产生一个SIGIO信号。
3.F_SETOWN命令允许我们指定用于接收SIGIO和SIGURG信号的套接字属主(进程ID或进程组ID),其中SIGIO信号是套接字被设置为信号驱动式IO型后产生的;SIGURG信号是在新的带外数据到达套接字时产生的。F_GETOWN命令返回套接字的当前属主。术语套接字属主由POSIX定义,历史上源自Berkeley的实现称之为套接字的进程组ID,因为存放该ID的变量是socket结构的so_pgid成员。
在这里插入图片描述
每种描述符(如套接字描述符)都有一组由F_GETFL命令获取或由F_SETFL命令设置的文件标志,其中影响套接字描述符的两个标志是O_NONBLOCK(非阻塞式IO)和O_ASYNC(信号驱动式IO)。

使用fcntl函数开启非阻塞式IO的典型代码:

int flags;

/* Set a socket as nonblocking */
if ((flags = fcntl(fd, F_GETFL, 0)) < 0) {
    err_sys("F_GETFL error");
}
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0) {
    err_sys("F_SETFL error");
}

以下是错误地设置所期望标志的代码:

/* Wrong way to set a socket as nonblocking */
if (fcntl(fd, F_SETFL, O_NOBLOCK) < 0) {
    err_sys("F_SETFL error");
}

以上代码在设置非阻塞标志的同时也清除了所有其他文件状态标志,正确方式应该是先取得当前标志,与新标志逻辑或后再设置标志。

以下代码关闭非阻塞标志,其中flags是由上面所示的fcntl调用设置的:

flags &= ~O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) < 0) {
    err_sys("F_SETFL error");
}

信号SIGIO和SIGURG与其他信号的不同之处在于,这两个信号仅在已使用F_SETOWN命令给相关套接字指派了属主后才产生。F_SETOWN命令的整数类型arg参数既可以是一个正整数,用来指出接收信号的进程ID,也可以是一个负整数,其绝对值指出接收信号的进程组ID。F_GETOWN命令把套接字属主作为fcntl函数的返回值返回,它既可以是一个进程ID(一个正的返回值),也可以是进程组ID(除-1以外的负值)。指定接收信号的套接字属主为一个进程时,仅单个进程接收信号;指定套接字属主为一个进程组时,整个进程组中所有进程都接收到信号。

使用socket函数创建的套接字没有属主,如果一个新套接字是从一个监听套接字创建而来,则已连接套接字属主会从监听套接字继承而来。

每个TCP和SCTP套接字都有一个发送缓冲区和一个接收缓冲区,每个UDP套接字都有一个接收缓冲区。

在连接建立前后显示套接字接收缓冲区大小和MSS值:

#include "unp.h"
#include <netinet/tcp.h>    /* for TCP_MAXSEG */

int main(int argc, char **argv) {
    int sockfd, rcvbuf, mss;
    socklen_t len;
    struct sockaddr_in servaddr;

    if (argc != 2) {
        err_quit("usage: rcvbuf <IPaddress>");
    }

    sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    len = sizeof(rcvbuf);
    Getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);
    len = sizeof(mss);
    Getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len);
    printf("defaults: SO_RCVBUF = %d, MSS = %d\n", rcvbuf, mss);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);  
    Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

    Connect(sockfd, (SA *)&servaddr, sizeof(servaddr));
	
    len = sizeof(rcvbuf);
    Getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &len);
    len = sizeof(mss);
    Getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len);
    printf("after connect: SO_RCVBUF = %d, MSS = %d\n", rcvbuf, mss); 

    exit(0);
}

以上代码的输出结果随系统而变化,有些系统(如Solaris 2.5.1及更早版本)的套接字缓冲区大小总是返回0,使得我们无法查看套接字缓冲区大小在连接前后有什么变化。

对于MSS,在调用connect前显示的是实现的默认值(通常是536或512),调用connect后显示的值取决于可能有的来自对端的MSS,本地以太网上调用connect后的值可能是1460,然而调用connect连接某远程网络的一台服务器主机后显示的MSS值可能类似默认值,除非你的系统支持路径MTU发现功能。还可通过tcpdump程序查看来自对端的SYN分节中真正的MSS选线值。

对于套接字缓冲区大小,许多实现在建立连接后把它向上舍入成MSS的倍数。还可通过tcpdump程序观察TCP的通告窗口来查看连接建立后的套接字缓冲区大小。

设置SO_LINGER套接字选项:

str_cli(stdin, sockfd);

struct linger ling;
ling.l_onoff = 1;
ling.l_linger = 0;
Setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));

exit(0);

以上代码会发送一个RST而非正常的TCP连接终止序列,此时服务器的读操作会返回ECONNRESET错误,对应消息为Connection reset by peer

如果有两个TCP客户同时启动,都设置了SO_REUSEADDR套接字选项,且以相同的本地IP地址和相同的端口号调用bind,但一个客户连接到198.69.10.2的端口7000,另一个客户连接到198.69.10.2(相同IP地址)的端口8000,会发生什么?第一个客户的函数调用顺序为setsockopt、bind、connect,如果第二个客户在第一个客户调用bind和connect之间调用bind,那第二个客户将返回EADDRINUSE错误,然而一旦第一个客户已连接到对端,第二个客户的bind就正常工作,因为第一个客户的套接字当时已处于连接状态,处理这种竞争状态的唯一办法是让第二个客户在调用bind返回EADDRINUSE错误时再多次尝试调用bind,而非返回该错误后就放弃。

使用sock程序作为TCP服务器(-s选项),分别捆绑通配地址、主机的某个接口地址、环回地址,看是否需要指定SO_REUSEADDR(-A选项),以下是在一个支持多播的主机MacOS X 10.2.6上的运行结果:
在这里插入图片描述
以下是在一个不支持多播的主机Unix Ware 2.1.2上的运行结果:
在这里插入图片描述
继续前面的例子,这次启动UDP服务器(-u选项)的两个实例,捆绑相同的本地IP和端口号,如果实现支持SO_REUSEPORT,试着使用它(-T选项)。我们首先在支持多播,但不支持SO_REUSEPORT选项的主机Solaris 9上尝试:
在这里插入图片描述
在这个系统上,第一个bind调用不必指定SO_REUSEADDR,但第二个起必须指定。

在既支持多播也支持SO_REUSEPORT选项的主机MacOS X 10.2.6上尝试,我们首先启动两个指定了SO_REUSEADDR选项的服务器:
在这里插入图片描述
发现SO_REUSEADDR选项不起作用。接着我们只给第二个服务器使用SO_REUSEPORT选项,而第一个服务器不使用:
在这里插入图片描述
发现SO_REUSEPORT选项也不起作用,这是因为完全重复的捆绑要求共享同一捆绑的所有套接字都使用SO_REUSEPORT选项:
在这里插入图片描述
有些ping程序的版本有一个-d选项用于开启SO_DEBUG套接字选项,它没有任何作用,因为ping使用ICMP套接字,而SO_DEBUG套接字选项只影响TCP套接字。该选项的描述为:这个选项开启相应协议层中的调试,但实现该选项的唯一协议层一直是TCP。

在开启TCP_NODELAY的情况下,如果客户为一个请求执行了两个write调用,第一个写4字节,第二个写396字节,假设服务器的ACK延滞时间为100ms,客户与服务器之间的RTT为100ms,服务器处理客户请求的时间为50ms,以下是时间线图:
在这里插入图片描述
当关闭了TCP_NODELAY时:
在这里插入图片描述
但如果我们不想关闭TCP_NODELAY时,可以用writev函数一次性处理4字节缓冲区和396字节缓冲区,这样可以减少分组个数:
在这里插入图片描述
RFC 1122 [Barden 1989]中建议,ACK的延滞时间必须低于0.5秒,当对端发来的全是满大小的段时,至少每第二个段就应该发一次ACK。Berkeley的实现将ACK的延滞时间设为最多200ms。

如果客户和服务器都设置了SO_KEEPALIVE套接字选项,连接后没有交换任何数据,当保持存活定时器每2小时到期时,会发生什么?会交换2个而非4个TCP分节,因为两个系统的保持存活定时器精确同步的可能性非常低,一端的保持存活定时器会比另一端略早一点超时,首先超时的一端发送保持存活侦探分组,另一端会确认这个分组,然后保持存活侦探分组的接收会导致时钟略慢的主机把保持存活定时器重置成2小时。

几乎所有实现都在头文件sys/socket.h中定义了SO_ACCEPTCON常值,此常值的作用为,在最初的socket API中,没有listen函数,且socket函数拥有第4个参数用来指定套接字选项,SO_ACCEPTCON常值就是用来指定监听套接字的,加了listen函数后,这个选项还是保留着,现在由内核来设置这个选项。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值