一、套接字超时的三种设置方法
  • (1) 调用alarm,它在指定超时期满时产生SIGALRM信号。这个方法涉及信号处理,而信号 处理在不同的实现上存在差异,而且可能干扰进程中现有的alarm调用。
  • (2) 在select中阻塞等待I/O(select有内置的时间限制),以此代替直接阻塞在read或 write调用上。
  • (3) 使用较新的SO_RCVTIMEO和SO_SNDTIMEO套接字选项。这个方法的问题在于并非所有实 现都支持这两个套接字选项

三种方法的异同

  • 上述三个技术都适用于输入和输出操作(例如read、write及其诸如recvfrom、sendto 之类的变体),不过我们依然期待可用于connect的技术,因为TCP内置的connect超时相当长 (典型值为75秒钟)。select可用来在connect上设置超时的先决条件是相应套接字处于非阻塞模式,而那两个套接字选项对connect并不适用
  • 我们还指出,前两个技术适用 于任何描述符,而第三个技术仅仅使用于套接字描述符
二、使用SIGALRM信号为connect设置超时
  • 下面给出了我们的connect_timeo函数,它以由调用者指定的超时上限调用connect。 它的前3个参数用于调用connect,第四个参数是等待的秒数
  • 尽管本例子相当简单,但在多线程化程序中正确使用信号却非常困难。因此我们建议只是在未线程化或单线程化的程序中使用本技术
static void	connect_alarm(int);

int connect_timeo(int sockfd, const SA *saptr, socklen_t salen, int nsec)
{
	Sigfunc	*sigfunc;
	int		n;

	sigfunc = Signal(SIGALRM, connect_alarm);
	if (alarm(nsec) != 0)
		err_msg("connect_timeo: alarm was already set");

	if ( (n = connect(sockfd, saptr, salen)) < 0) {
		close(sockfd);
		if (errno == EINTR)
			errno = ETIMEDOUT;
	}
	alarm(0);					/* turn off the alarm */
	Signal(SIGALRM, sigfunc);	/* restore previous signal handler */

	return(n);
}

static void connect_alarm(int signo)
{
	return;		/* just interrupt the connect() */
}
/* end connect_timeo */

代码解析

  • 设置报警(时钟):把本进程的报警时钟设置成由调用者指定的秒数。如果此前已经给本进程设置过报警 时钟,那么alarm的返回值是这个报警时钟的当前剩余秒数,否则alarm的返回值为0。 若是前一种情况,我们还显示一个警告信息,因为我们推翻了先前设置的报警时钟
  • 调用connect:调用connect,如果本调用被中断(即返回EINTR错误),那就把errno值改设为 ETIMEOUT,同时关闭套接字,以防三路握手继续进行。
  • 关闭alarm并恢复原来的信号处理函数:通过以0为参数值调用alarm关闭本进程的报警时钟,同时恢复原来的信号处理函数 (如果有的话)。
  • 处理SIGALRM:信号处理函数只是简单地返回。我们设想本return语句将中断进程主控制流中那个未 决的connect调用,使得它返回一个EINTR错误。回顾我们的signal函数(图5-6), 当被捕获的信号为SIGALRM时,signal函数不设置SA_RESTART标志。

注意事项

  • ①就本例子我们指出两点,第一点是使用本技术总能减少connect的超时期限,但是无法延长内核现有的超时。源自Berkeley的内核中connect的超时通常为75s。在调用我们的函数时, 可以指定一个比75小的值(如10),但是如果指定一个比75大的值(如80),那么connect仍将 在75s后发生超时
  • ②另一点是我们使用了系统调用(connect)的可中断能力,使得它们能够在内核超时发生 之前返回。这一点不成问题的前提是:我们执行的是系统调用,并且能够直接处理由它们返回 的EINTR错误。我们将在29.7节碰到一个也执行系统调用的库函数,不过系统调用返回EINTR时 这个库函数重新执行同一个系统调用。在这种情形下我们仍能使用SIGALRM,不过将在图29-10中看到,我们还不得不使用sigsetjmp和siglongjmp以绕过函数库对于EINTR的忽略。
三、使用SIGALRM信号为recvfrom设置超时
  • 下面代码改写自自定义dg_cli函数,新的dg_cli函数通过调用alarm使得一旦在5秒钟内 收不到任何应答就中断recvfrom
static void	sig_alrm(int);

void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int	n;
	char	sendline[MAXLINE], recvline[MAXLINE + 1];

	Signal(SIGALRM, sig_alrm);

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		alarm(5);
		if ( (n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL)) < 0) {
			if (errno == EINTR)
				fprintf(stderr, "socket timeout\n");
			else
				err_sys("recvfrom error");
		} else {
			alarm(0);
			recvline[n] = 0;	/* null terminate */
			Fputs(recvline, stdout);
		}
	}
}

static void sig_alrm(int signo)
{
	return;			/* just interrupt the recvfrom() */
}

代码解析

  • 处理来自recvfrom的超时:为SIGALRM建立一个信号处理函数,并在每次调用recvfrom前通过调用alarm设置一 个5秒钟的超时。如果recvfrom被我们的信号处理函数中断了,那就输出一个信息并继 续执行。如果读到一行来自服务器的文本,那就关掉报警时钟并输出服务器的应答。
  • SIGALRM信号处理函数:信号处理函数只是简单地返回,以中断被阻塞的recvfrom
  • 本例子工作正常,因为每次调用alarm设置报警时钟后,期待读取的只是单个应答。我们将在广播的dg_cli函数中使用同样的技术,然而由于每个报警时钟对应读取多个应答,我们还得处理存在于 其中的竞争条件
四、使用select为recvfrom设置超时
  • 下面示例了设置超时的第二个技术(使用select)。这个名为readable_timeo的函数等 待一个描述符最多在指定的秒数内变为可读
int readable_timeo(int fd, int sec)
{
	fd_set			rset;
	struct timeval	tv;

	FD_ZERO(&rset);
	FD_SET(fd, &rset);

	tv.tv_sec = sec;
	tv.tv_usec = 0;

	return(select(fd+1, &rset, NULL, NULL, &tv));
		/* 4> 0 if descriptor is readable */
}

代码解析

  • 准备select的参数:在读描述符集中打开与调用者给定描述符对应的位。把调用者给定的等待秒数设置在 一个timeval结构中
  • 阻塞在select上:select等待该描述符变为可读,或者发生超时。本函数的返回值就是select的返回值: 出错时为-1,超时发生时为0,否则返回的正值给出已就绪描述符的数目
  • 本函数不执行读操作,它只是等待给定描述符变为可读。因此本函数适用于任何类型的套 接字,既可以是TCP也可以是UDP。

改版的dg_cli函数

  • 我们可以轻而易举地创建等待描述符变为可写的名为writeable_timeo的类似函数
  • 我们在下面使用这个函数,它改写自dg_cli函数。这个新版本只是在readable_timeo返回一个正值时才调用recvfrom
  • 直到readable_timeo告知所关注的描述符已变为可读后我们才调用recvfrom,这一点保 证recvfrom不会阻塞
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int	n;
	char	sendline[MAXLINE], recvline[MAXLINE + 1];

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		if (Readable_timeo(sockfd, 5) == 0) {
			fprintf(stderr, "socket timeout\n");
		} else {
			n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
			recvline[n] = 0;	/* null terminate */
			Fputs(recvline, stdout);
		}
	}
}
五、使用SO_RCVTIMEO套接字选项为recvfrom设置超时
  • 最后一个例子展示SO_RCVTIMEO套接字选项如何设置超时。本选项一旦设置到某个描述符 (包括指定超时值),其超时设置将应用于该描述符上的所有读操作。本方法的优势就体现在一 次性设置选项上,而前两个方法总是要求我们在欲设置时间限制的每个操作发生之前做些工作。 本套接字选项仅仅应用于读操作,类似的SO_SNDTIMEO选项则仅仅应用于写操作,两者都不能 用于为connect设置超时。
  • 下面是使用SO_RCVTIMEO套接字选项的另一个版本的dg_cli函数
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	int				n;
	char			sendline[MAXLINE], recvline[MAXLINE + 1];
	struct timeval	tv;

	tv.tv_sec = 5;
	tv.tv_usec = 0;
	Setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

	while (Fgets(sendline, MAXLINE, fp) != NULL) {

		Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);

		n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
		if (n < 0) {
			if (errno == EWOULDBLOCK) {
				fprintf(stderr, "socket timeout\n");
				continue;
			} else
				err_sys("recvfrom error");
		}

		recvline[n] = 0;	/* null terminate */
		Fputs(recvline, stdout);
	}
}

代码解析

  • 设置套接字选项:setsockopt的第四个参数是指向某个timeval结构的一个指针,其中填入了期望的超 时值
  • 测试超时:如果I/O操作超时,其函数(这里是recvfrom)将返回一个EWOULDBLOCK错误