IO复用之select函数介绍

文章目录

    • 函数说明
          • 函数原型:
          • 函数功能:
          • 参数说明:
          • 函数原理:
    • 编程实现
      • server.c
      • client.c
    • 总结
    • 描述符就绪条件(拓展)


函数说明

函数原型:
/* According to POSIX.1-2001, POSIX.1-2008 */
#include 
/* According to earlier standards */
#include 
#include 
#include 

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
函数功能:

该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指 的时间后才唤醒它然后返回。

作为一个例子,我们可以调用select,告知内核仅在下列情况发生时才返回:

  • 集合{1,4,5}中的任何描述符准备好读;
  • 集合{2,7}中的任何描述符准备好写;
  • 集合{1, 4}中的任何描述符有异常条件待处理;
  • 设置等待时间为10.2秒。

也就是说,我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等
待多长时间,当感兴趣的事件到来时或经历10.2秒之后,则返回。

注意:我们感兴趣的描述符不局限于套接字,任何描述符都可以使用select来测试。

参数说明:
nfds: 
指定待测试的描述符个数,它的值是待测试的最大描述符 +1(因为描述符是从0开始的),表示描述符012...maxfds-1均将被测试。
注意:该值最大不能超过1024,如果超过1024则改用poll或epoll代替select
	  
readfds、writefds、exceptfds: 
中间的三个参数都是传入传出参数,指定我们要让内核测试读、写和异常条件的描述符。
如果对某个条件不感兴趣,就可以把它设为NULLstruct fd_set可以理解为一个集合,
这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
  void FD_ZERO(fd_set *fdset);           //清空集合,类似于string::clear()

  void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中,类似于insert()

  void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除,类似于earse()

  int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写,类似于find()

timeout: 
告知内核等待所指定描述字中的任何一个就绪可花多少时间。
其timeval结构用于指定这段时间的秒数和微秒数。
         
  struct timeval{
         long tv_sec;   //seconds
         long tv_usec;  //microseconds
  };
     
这个参数有三种可能:
(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。
(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须全为0。
注:前两种情形的等待通常会被进程在等待期间捕获的信号中断,并从信号处理函数返回。

返回值:
表示跨所有描述符集的已就绪的总位数。如果在任何描述符就绪之前定时器到时,那么返回0。
返回-1表示出错(这是可能发生的,譬如本函数被一个所捕获的信号中断)

头文件中定义的FD_ SETSIZE常值是数据类型fd_ set中的描述符总数,其值通常是1024,不过很少有程序用到那么多的描述符。nfds参数迫使我们计算出所关心的最大描述符并告知内核该值。以前面给出的打开描述符1、4和5的代码为例,其nfds值就是6而不是5的原因在于:我们指定的是描述符的个数而非最大值,而描述符是从0开始的。

函数原理:

select函数通过修改由指针readset、writeset和exceptset所指向的的描述符集,来修改对应额值,因而这三个参数都是值一结果参数。调用该函数时,我们指定所关心的描述符的值设为1,该函数返回时,结果将指示哪些描述符已就绪。该函数返回后,我们使用FD_ ISSET宏来测试fd_ set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0,为此,每次重新调用select函数时,我们都得再次把所有描述符集内所关心的位均置为1

使用select时最常见的两个编程错误是:

  1. 忘了对最大描述符加1;
  2. 忘了描述符集是值_结果参数,没有手动修改值对应的结果。

第二个错误导致调用select时,描述符集内我们认为是1的位却被置为0。

编程实现

server.c

/* server.c */
#include 
#include 
#include 
#include 
#include 
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
{
	int i, maxi, maxfd, listenfd, connfd, sockfd;
	int nready, client[FD_SETSIZE]; 	/* FD_SETSIZE 默认为 1024 */
	ssize_t n;
	fd_set rset, allset;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN]; 			/* #define INET_ADDRSTRLEN 16 */
	socklen_t cliaddr_len;
	struct sockaddr_in cliaddr, servaddr;

	listenfd = Socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

Listen(listenfd, 20); 		/* 默认最大128 */

maxfd = listenfd; 			/* 初始化 */
maxi = -1;					/* client[]的下标 */

for (i = 0; i < FD_SETSIZE; i++)
	client[i] = -1; 		/* 用-1初始化client[] */

FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */

for ( ; ; ) {
	rset = allset; 			/* 每次循环时都从新设置select监控信号集 */
	nready = select(maxfd+1, &rset, NULL, NULL, NULL);

	if (nready < 0)
		perr_exit("select error");
	if (FD_ISSET(listenfd, &rset)) { /* new client connection */
		cliaddr_len = sizeof(cliaddr);
		connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
		printf("received from %s at PORT %d\n",
				inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
				ntohs(cliaddr.sin_port));
		for (i = 0; i < FD_SETSIZE; i++) {
			if (client[i] < 0) {
				client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
				break;
			}
		}
		/* 达到select能监控的文件个数上限 1024 */
		if (i == FD_SETSIZE) {
			fputs("too many clients\n", stderr);
			exit(1);
		}

		FD_SET(connfd, &allset); 	/* 添加一个新的文件描述符到监控信号集里 */
		if (connfd > maxfd)
			maxfd = connfd; 		/* select第一个参数需要 */
		if (i > maxi)
			maxi = i; 				/* 更新client[]最大下标值 */

		if (--nready == 0)
			continue; 				/* 如果没有更多的就绪文件描述符继续回到上面select阻塞监听,
										负责处理未处理完的就绪文件描述符 */
		}
		for (i = 0; i <= maxi; i++) { 	/* 检测哪个clients 有数据就绪 */
			if ( (sockfd = client[i]) < 0)
				continue;
			if (FD_ISSET(sockfd, &rset)) {
				if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
					Close(sockfd);		/* 当client关闭链接时,服务器端也关闭对应链接 */
					FD_CLR(sockfd, &allset); /* 解除select监控此文件描述符 */
					client[i] = -1;
				} else {
					int j;
					for (j = 0; j < n; j++)
						buf[j] = toupper(buf[j]);
					Write(sockfd, buf, n);
				}
				if (--nready == 0)
					break;
			}
		}
	}
	close(listenfd);
	return 0;
}

client.c

/* client.c */
#include 
#include 
#include 
#include 
#include "wrap.h"

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;

	sockfd = Socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);

	Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	while (fgets(buf, MAXLINE, stdin) != NULL) {
		Write(sockfd, buf, strlen(buf));
		n = Read(sockfd, buf, MAXLINE);
		if (n == 0)
			printf("the other side has been closed.\n");
		else
			Write(STDOUT_FILENO, buf, n);
	}
	Close(sockfd);
	return 0;
}

总结

解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率,此时可考虑用pollepoll代替select

描述符就绪条件(拓展)

(1) 满足下列四个条件中的任何一个时,一个套接字准备好读。

  1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1。
  2. 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0 (也就是返回EOF)。
  3. 该套接字是一个监听套接字且已完成的连接数不为0。对这样的套接字的accept通常不会阻塞。
  4. 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1 ( 也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误( pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。

(2)下列四个条件中的任何一个满足时,一个套接字准备好写。

  1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值(例如由传输层接受的字节数)。我们可以使用SO_ SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值通常为2048。
  2. 该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。
  3. 使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
  4. 其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回-1 ( 也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ ERROR套接字选项调用getsockopt获取并清除。

(3)如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。

图6-7汇总了.上述导致select返回某个套接字就绪的条件。
IO复用之select函数介绍_第1张图片
上篇:Unix下可用的5种I/O模型总结
下篇:IO复用之poll函数介绍

你可能感兴趣的:(网络编程)