第7章 套接字选项

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);

其中 sockfd 必须指向一个打开的套接字描述符,level 指定系统中解释选项的代码或为通用套接字代码,或为某个特定于协议的代码。setsockopt 从 *optval 中取得选项待设置的新值,getsockopt 则把已获取的选项当前值存放到 *optval 中。*optval 大小由最后一个参数指定,它对于 setsockopt 是一个值参数,对于 getsockopt 是一个值-结果参数。以下汇总了这些获取和设置套接字的选项:

返回值:成功返回 0,出错返回 -1。标志列中有 “●” 表示该选项是一个标志选项,当给这些标志选项调用 getsockopt 函数时,*optval 是一个整数:0 -- 表示相应项被禁止,不为 0 -- 表示相应项被启用,同理使用 setsockopt 来设置。不含有 “●” ,那么相应项用于用户进程与系统之间传递所指定数据类型的值。


检查选项是否受支持并获取默认值

/* include checkopts1 */
/* *INDENT-OFF* */
#include	"unp.h"
#include	<netinet/tcp.h>		/* for TCP_xxx defines */

union val {
  int				i_val;
  long				l_val;
  struct linger		linger_val;
  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	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 },
#ifdef	IPV6_DONTFRAG
	{ "IPV6_DONTFRAG",		IPPROTO_IPV6,IPV6_DONTFRAG,	sock_str_flag },
#else
	{ "IPV6_DONTFRAG",		0,			0,				NULL },
#endif
#ifdef	IPV6_UNICAST_HOPS
	{ "IPV6_UNICAST_HOPS",	IPPROTO_IPV6,IPV6_UNICAST_HOPS,sock_str_int },
#else
	{ "IPV6_UNICAST_HOPS",		0,			0,				NULL },
#endif
#ifdef	IPV6_V6ONLY
	{ "IPV6_V6ONLY",		IPPROTO_IPV6,IPV6_V6ONLY,	sock_str_flag },
#else
	{ "IPV6_V6ONLY",		0,			0,				NULL },
#endif
	{ "TCP_MAXSEG",			IPPROTO_TCP,TCP_MAXSEG,		sock_str_int },
	{ "TCP_NODELAY",		IPPROTO_TCP,TCP_NODELAY,	sock_str_flag },
#ifdef	SCTP_AUTOCLOSE
	{ "SCTP_AUTOCLOSE",		IPPROTO_SCTP,SCTP_AUTOCLOSE,sock_str_int },
#else
	{ "SCTP_AUTOCLOSE",		0,			0,				NULL },
#endif
#ifdef	SCTP_MAXBURST
	{ "SCTP_MAXBURST",		IPPROTO_SCTP,SCTP_MAXBURST,	sock_str_int },
#else
	{ "SCTP_MAXBURST",		0,			0,				NULL },
#endif
#ifdef	SCTP_MAXSEG
	{ "SCTP_MAXSEG",		IPPROTO_SCTP,SCTP_MAXSEG,	sock_str_int },
#else
	{ "SCTP_MAXSEG",		0,			0,				NULL },
#endif
#ifdef	SCTP_NODELAY
	{ "SCTP_NODELAY",		IPPROTO_SCTP,SCTP_NODELAY,	sock_str_flag },
#else
	{ "SCTP_NODELAY",		0,			0,				NULL },
#endif
	{ NULL,				0,			0,				NULL }
};
/* *INDENT-ON* */
/* end checkopts1 */

/* include checkopts2 */
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:
				fd = Socket(AF_INET, SOCK_STREAM, 0);
				break;
#ifdef	IPV6
			case IPPROTO_IPV6:
				fd = Socket(AF_INET6, SOCK_STREAM, 0);
				break;
#endif
#ifdef	IPPROTO_SCTP
			case IPPROTO_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);
			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);
}
/* end checkopts2 */

/* include checkopts3 */
static char	strres[128];

static char* sock_str_flag(union val *ptr, int len)
{
/* *INDENT-OFF* */
	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);
/* *INDENT-ON* */
}
/* end checkopts3 */

static char* sock_str_int(union val *ptr, int len)
{
	if (len != sizeof(int))
		snprintf(strres, sizeof(strres), "size (%d) not sizeof(int)", len);
	else
		snprintf(strres, sizeof(strres), "%d", ptr->i_val);
	return(strres);
}

static char* sock_str_linger(union val *ptr, int len)
{
	struct linger	*lptr = &ptr->linger_val;

	if (len != sizeof(struct linger))
		snprintf(strres, sizeof(strres),
				 "size (%d) not sizeof(struct linger)", len);
	else
		snprintf(strres, sizeof(strres), "l_onoff = %d, l_linger = %d",
				 lptr->l_onoff, lptr->l_linger);
	return(strres);
}

static char* sock_str_timeval(union val *ptr, int len)
{
	struct timeval	*tvptr = &ptr->timeval_val;

	if (len != sizeof(struct timeval))
		snprintf(strres, sizeof(strres),
				 "size (%d) not sizeof(struct timeval)", len);
	else
		snprintf(strres, sizeof(strres), "%d sec, %d usec",
				 tvptr->tv_sec, tvptr->tv_usec);
	return(strres);
}

输出如下:

SO_BROADCAST: default = off
SO_DEBUG: default = off
SO_DONTROUTE: default = off
SO_ERROR: default = 0
SO_KEEPALIVE: default = off
SO_LINGER: default = l_onoff = 0, l_linger = 0
SO_OOBINLINE: default = off
SO_RCVBUF: default = 87380
SO_SNDBUF: default = 16384
SO_RCVLOWAT: default = 1
SO_SNDLOWAT: default = 1
SO_RCVTIMEO: default = 0 sec, 0 usec
SO_SNDTIMEO: default = 0 sec, 0 usec
SO_REUSEADDR: default = off
SO_REUSEPORT: default = off
SO_TYPE: default = 1
IP_TOS: default = 0
IP_TTL: default = 64
IPV6_DONTFRAG: default = off
IPV6_UNICAST_HOPS: default = 64
IPV6_V6ONLY: default = off
TCP_MAXSEG: default = 536
TCP_NODELAY: default = off
SCTP_AUTOCLOSE: (undefined)
SCTP_MAXBURST: (undefined)
SCTP_MAXSEG: (undefined)
SCTP_NODELAY: (undefined)

套接字状态

下面的套接字选项是由 TCP 已连接套接字从监听套接字继承来的:

SO_DEBUG,SO_DONTROUNTE,SO_KEEPALIVE,SO_LINGER,SO_OOBINLINE,SO_RCVBUF,SO_RCVLOWAT,SO_SNDBUF,SO_SNDLOWAT,TCP_MAXSEG,TCP_NODELAY。

这对 TCP 很重要,因为 accept 一直要到 TCP 层完成三路握手后才会给服务器返回已连接套接字。如果想在三路握手完成时确保这些套接字选项中的某一个是给已连接套接字设置的,那么我们必须先给监听套接字设置该选项。


通用套接字选项

这些选项是协议无关的,不过其中有些只能应用到某些特定类型的套接字中。

(1) SO_BROADCAST:

本选项开启或禁止进程发送广播消息的能力;只有数据报套接字支持广播,并且还必须是在支持广播消息的网络上(如以太网、令牌环网等)。我们不可能在点对点链路上进行广播,也不可能在基于连接的传输协议(TCP、SCTP)之上进行广播。

(2) SO_DEBUG:

本选项仅由TCP支持。当给一个TCP套接字开启本选项,内核将为TCP在该套接字和接收的所有分组保留详细跟踪信息。这些信息保存在内核的某个环形缓冲区中,并可以使用 trpt 程序进行检查。

(3) SO_DONTROUTE:

本选项规定外出的分组将绕过底层协议的正常路由机制,强制将分组从特定接口送出。比喻 IPv4 协议会将分组定向到适当的本地接口(由其目的地址确定),如果目的地址不可达,将返回 ENETUNREACH 错误。路由守护进程经常使用本选项来绕过路由表(路由表不正确的情况下),强制将分组从特定接口送出。

(4) SO_ERROR:

当一个套接字上发生错误时,源自 Berkeley 的内核中的协议模块将该套接字的名为 so_error 的变量设为标准的 Unix Exxx 值中的一个,我们称它为该套接字的待处理错误。内核能够以下面的两种方式之一立即通知进程这个错误;

  • 如果进程阻塞在对该套接字的 select 调用上,那么无论是检查可读条件还是可写条件,select 均返回并设置其中一个或所有两个条件;
  • 如果进程使用信号驱动式 I/O 模型,那就给进程或者进程产生一个 SIGIO 信号;

进程然后可以通过访问 SO_ERROR 套接字选项获取 so_error 值。由 getsockopt 返回的整数值就是该套接字的待处理错误。so_error 随后由内核复位为 0。

当进程调用 read 且没有数据可读,如果 so_error 为非 0 值,那么 read 返回 -1 且 errno 被置为 so_error 的值,so_error 随后被复位为 0。如果该套接字上有数据在排队等待读取,那么 read 返回那些数据而不返回错误条件。如果进程在调用 write 时 so_error 为非 0 值,那么 write 返回 -1 且 errno 被设为 so_error 的值。so_error 随后被复位为 0。

这是一个我们可以获取但不能设置的套接字选项。

(5) SO_KEEPALIVE:

给一个 TCP 套接字设置保持存活选项后,如果 2 小时内在该套接字的任一方向上没有数据交换,TCP 就自动给对端发送一个保持存活探测分节。这是一个对端必须响应的 TCP 分节,它会导致以下三种情况之一:

  • 对端以期望的 ACK 响应。应用进程将得不到通知(因为正常连接着)。在又经过无动静的 2 小时之后,TCP 将发出另外一个探测分节。
  • 对端以 RST 响应,它告知本端 TCP,对端已崩溃且重新启动。该套接字的待处理错误被置 ECONNRESET,套接字本身则被关闭。
  • 对端对保持存活探测分节没有任何响应。源自 Berkeley 的 TCP 将另外发送 8 个探测分节,两两相隔 75 秒,试图得到一个响应。如果从第一个探测分节后 11 分 15 秒(75×9 秒)没有响应,该套接字的处理错误被置为 ETIMEOUT。而这 8 个其中的任何一个分节在这 11 分 15 秒内得到回应则表示仍在连接中。

本选项一般由服务器使用,不过客户也可使用。因为服务进程一般会阻塞在等待客户请求上,如果客户主机连接掉线、系统崩溃或电源掉电,服务端将永远不会知道,保持存活选项将检测出这些连接并终止它们。但是,请注意,如果仅仅是程序崩溃而系统正常,不管是服务端还是客户端,它的 TCP 将跨连接发送一个 FIN,对端可以通过调用 select 很容易地检测到,此时对端的 read 会返回 0。也就是说仅仅是程序崩溃对端能很快检测出来,可以关闭套接字。

(6) SO_LINGER:

本选项指定 close 函数对面向连接的协议(如 TCP、SCTP,而不是 UDP)如何操作。默认操作是 close 立即返回,但是如果有数据残留在套接字发送缓冲区中,系统将试着把这些数据发送给对端。SO_LINGER 套接字选项使得我们可以改变这个默认设置。本选项要求在用户与内核间传递如下结构,它在头文件 <bits/socket.h> 中定义:

/* Structure used to manipulate the SO_LINGER option.  */
struct linger
  {
    int l_onoff;		/* Nonzero to linger on close.  */
    int l_linger;		/* Time to linger.  */
  };

监测各种 TCP 条件的方法

情形 对端进程崩溃 对端主机崩溃 对端主机不可达
本端 TCP 正动发数据 对端 TCP 发送 FIN,通过可读返回 0可监测到。如果本端对其写,会收到 RST ,再写收到 SIGPIPE 信号 本端 TCP 超时,套接字待处理错误设置为 ETIMEDOUT 本端 TCP 超时,套接字待处理错误设置为 EHOSTUNREACH
本端 TCP 正主动接收数据 对端发送 FIN,我们把它作为 EOF 读入 我们将停止接收数据,只是接收不到数据--没有其他症状 我们将停止接收数据,只是接收不到数据--没有其他症状
连接空闲,保持存活选项设置 FIN,可读,读的时候返回 0 2 小时后,发送 9 个存活探测,然后待处理错误设为 ETIMEDOUT 2 小时后,发送 9 个存活探测,然后待处理错误设为 EHOSTUNREACH
连接空闲,未设置保持存活 FIN,可读,读的时候返回 0

对 setsockopt 的调用将根据其中两个结构成员的值形成下列 3 种情形之一。

(1)如果 l_onoff 为 0,那么关闭本选项。l_linger 的值被忽略,默认设置生效。

(2)如果 l_onoff 为非 0 值且 l_linger 为 0,那么当 close 某个连接时 TCP 将中止该连接。TCP 将丢弃保留在套接字发送缓冲区中的任何数据,并发送一个 RST 给对端,而没有通常的四分组连接终止序列。但是由于避免了 TIME_WAIT 状态而存在潜在问题,参考 TCP状态之TIME_WAIT状态 的说明。

(3)如果 l_onoff 为非 0 值且 l_linger 也为非 0 值,那么当套接字关闭时内核将拖延一段时间。这就是说如果在套接字发送缓冲区中仍残留有数据,那么进程将被投入睡眠,直到:(a) 所有数据都已发送完且均被对方确认;或 (b) 延滞时间到。注意:如果套接字为非阻塞型,那么它将不等待 close 完成,即使延滞时间非 0 也是如此。如果延滞时间到而套接字数据没有发送完并被确认,close 将返回 EWOULDBLOCK 错误,且缓冲区中的任何残留数据都被丢弃。

设置 SO_LINGER 的好处就是,close 的成功返回告诉了我们先前发送的数据和 FIN 分节已由对端 TCP 确认,而不能告诉我们对端应用进程是否已读取数据。如果不设置该选项,那么我们连对端 TCP 是否确认了数据都不知道。当然,知道对端 TCP 是否确认了数据的方法还有:调用 shutdown,应用 ACK等方法。

(7)SO_OOBINLINE:

本选项开启时,带外数据将被留在正常的输入队列中(即在线留存)。这种情况下接收函数的 MSG_OOB 标志不能用来读带外数据。

(8)SO_RCVBUF、SO_SNDBUF

每个套接字都有一个发送缓冲区和一个接收缓冲区。套接字接收缓冲区可用空间的大小限定了 TCP 通告对端的窗口大小,TCP 套接字接收缓冲区不可能溢出,因为不允许对端发出超过本端所通告对端的窗口大小。这就是 TCP 流量控制。如果对端无视窗口大小而发出了超过该窗口大小的数据,本端 TCP 将丢弃它们。UDP 是没有流量控制的,发送端的较快发送,不仅会导致接收端丢弃数据报,甚至会导致本机丢弃数据报。

这两个选项允许我们更改这两个缓冲区的默认大小。当设置 TCP 套接字接收缓冲区的大小时,函数调用顺序很重要。这是因为 TCP 的窗口规模选项是在建立连接时用 SYN 分节与对端互换得到的。对于客户,这意味着 SO_RCVBUF 选项必须在调用 connect 之前设置,因为 connect 会发送连接分节(SYN)告知对端本地窗口大小,所以必须在 connect 之前设置。对于服务器,这意味着该选项必须在调用 listen 之前给监听套接字设置,而不是 accept 因为 accept 直到 TCP 的三路握手完成才会创建并返回已连接的套接字。对于已经建立连接的套接字设置 SO_RCVBUF、SO_SNDBUF 没有任何影响。

TCP 套接字缓冲区的大小至少是相应连接的 MSS 值的四倍。这一点是依据 TCP 快速恢复算法的工作机制。为避免潜在的缓冲区空间浪费,TCP 套接字缓冲区大小还必须是相应连接的 MSS 值的偶数倍。

带宽-延迟积:顾名思义,该值 = 带宽(bit/s) × RTT(秒),再把结果转换为字节。如 RTT 为 60ms 的 T1 链路(1536000 bit/s)的带宽延迟积为 115200 字节。ping 程序即可获得 RTT。如果套接字的缓冲区大小小于该值,管道(两个端点之间的容量)将不饱满。

(9)SO_RCVLOWAT、SO_SNDLOWAT

参看:描述符就绪条件

(10)SO_RCVTIMEO、SO_SNDTIMEO

这两个选项允许我们给套接字的接收和发送设置一个超时值。我们通过设置其值为 0s 和 0us 来禁止超时。默认情况下这两个超时都是禁止的。接收超时影响 5 个输入函数:read、readv、recv、recvfrom、recvmsg。发送超时影响 5 个输出函数:write、writev、send、sendto、sendmsg。

(11)SO_REUSEADDR、SO_REUSEPORT

该选项能起到以下四个功用:

(1)它允许启动一个监听服务器并捆绑其众所周知的端口,即使以前建立的以该端口作为本地端口的连接仍存在。这个条件通常这样碰到:

  • 启动一个监听服务器
  • 连接请求到达,派生一个子进程来处理这个客户
  • 监听服务器终止,但子进程继续为现有连接上的客户提供服务
  • 重启监听服务器

默认情况下,当监听服务器调用 socket、bind、listen 重新启动时,可能由于它试图捆绑一个现有连接上的端口,从而 bind 调用会失败(返回 bind error: Address already in use 错误)。但是如果在 socket 和 bind 两个调用之间设置了 SO_REUSEADDR 套接字选项,那么 bind 将成功。所有 TCP 服务器都应该指定本项。

(2)SO_REUSEADDR 允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地 IP 地址即可。

对于 TCP,我们不能启动捆绑相同 IP 和 相同端口的服务器,这是完全重复的捆绑。为了安全起见,执行通配地址(INADDR_ANY)捆绑的服务进程应该最后一个启动。

(3)SO_REUSEADDR 允许单个进程捆绑同一端口到多个套接字上,只要每次捆绑指定不同的本地 IP 地址即可。

(4)SO_REUSEADDR 允许完全重复的捆绑(捆绑相同 IP 和 相同端口),一般来说本特性仅支持 UDP 套接字。

注意:SO_REUSEADDR 有一个潜在的安全问题,就是如果我们已经有了一个绑定了通配地址和端口 xxx 的套接字指定该项,那么另有一个以实地址 IP 绑定该端口的套接字建立连接,那么此后目的地为端口 xxx 的数据报都会被递送到新的套接字上。

(12)SO_TYPE

本选项返回套接字类型,返回的整数值是一个诸如 SOCK_STREAM 或 SOCK_DGRAM 之类的值。

(13)SO_USELOOPBACK

本选项仅用于路由域的套接字。本选项默认开启,开启时相应套接字将接收在其上发送的任何数据报的一个副本。

 

 

猜你喜欢

转载自blog.csdn.net/lc250123/article/details/81352216