UNIX网络编程——I/O复用(select、poll)

本文详细介绍了UNIX网络编程中的I/O复用模型,包括select和poll函数的使用。通过分析阻塞和非阻塞I/O,阐述了I/O复用的优势,特别是在网络应用中的适用场景。同时,文章通过示例揭示了使用select和poll时可能遇到的问题,如混合使用stdio缓冲区和select可能导致的错误,并提供了相应的优化策略。
摘要由CSDN通过智能技术生成

1、五种I/O模型:

     (1)阻塞式I/O;(2)非阻塞式I/O;(3)I/O复用(select、poll);(4)信号驱动式I/O(SIGIO);(5)异步I/O(aio_系列函数);

      一个输入操作分为两个阶段:A. 等待数据准备好;B. 从内核向进程复制数据。

      一个套接字上的输入也类似,A. 等待数据从网络中到达(所等待分组到达时,会被复制到内核中的某个缓冲区);B. 把数据从内核缓冲区复制到应用进程缓冲区。


2、I/O复用

(1)I/O复用模型:阻塞在select或者poll上,而不是阻塞在真正的I/O系统调用(recvfrom)上

      


(2)I/O复用典型网络应用场合:


3、select函数

#include  <sys/select.h>
#include  <sys/time.h>

int  select(int maxfd1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

/*
maxfd1:指定待测试的描述符的个数,它的值是待测试的最大描述符+1(描述符是从0开始的)。比如打开的描述符集合是:{1, 4, 5},那么maxfd1的值就是6。
*/

/*
readset、writeset、exceptset指定我们要让内核测试读、写、异常条件的描述符。
如果对某一个条件不感兴趣,可以把它设为空指针。
如果三个参数均为空,则变成了一个比sleep函数(以秒为最小单位)更为精确的定时器。
*/

/*
timeout:告知内核等待所指定描述符中的任何一个就绪可花多长时间。
(1)永远等待下去:设为空指针;
(2)等待固定一段时间:
timeval结构体:
struct timeval{
	long tv_sec;     // 秒数
	long tv_usec;    // 微妙数
}
(3)根本不等待:指向一个timeval结构,其中的定时器值必须为0;
*/


4、使用select函数:

(要想读懂以下优化历程,需要去读UNIX网络编程的第五章)

(1)不使用select函数会出现什么异常?

//处理客户输入,从标注输入读入一行文本,写到服务器上,读回服务器对该行的回射,并把回射数据写到标准输出上  
void  str_cli(FILE *fp, int sockfd)  
{  
    char    sendline[MAXLINE], recvline[MAXLINE];  
  
    while (Fgets(sendline, MAXLINE, fp) != NULL) {    //读入  
  
        Writen(sockfd, sendline, strlen(sendline));      //发给服务器  
  
        if (Readline(sockfd, recvline, MAXLINE) == 0)    //读回反射数据  
            err_quit("str_cli: server terminated prematurely");  
  
        Fputs(recvline, stdout);   //写到标准输出  
    }  
}  
          我们先启动服务器、客户,以验证正常。接着我们关掉服务器,这导致向客户发送一个FIN,TCP客户响应一个ACK,完成了TCP终止序列的前半部分。但是客户上并没有发生任何特殊之事,也就是说客户阻塞在fgets调用上,仍等待从终端接收一行文本。

       服务器发送FIN只是表示服务器进程关闭了服务端,不再往该连接上发送任何数据。但是TCP客户FIN的接收并没有告知客户该TCP服务器进程已经终止。所以客户继续输入文本,str_cli调用write,把数据发送给服务器。

       服务器收到客户的数据,因为先前打开的套接字进程已经关闭,所以响应一个RST。

       但是此时客户看不到这个RST,因为客户在调用write后立即调用了read,又因为上面客户收到了FIN,所以readline返回0,所以打印出错信息。

       问题发生在:客户应对两个描述符——套接字和用户输入。但是客户不应该阻塞在这两个源中的某个特定源的输入上(例子中客户阻塞于用户输入)。也就是说,当套接字上发生某些事件,而客户正阻塞于fgets调用时,客户得不到服务器终止的通知。


(2)使用select优化:

正如我们上面说的I/O复用,我们将阻塞改为阻塞于select上——或是等待标准输入,或是等待套接字可读。

#include	"unp.h"

//使用select优化
void  str_cli(FILE *fp, int sockfd)
{
	int		maxfdp1;
	fd_set		rset;
	char		sendline[MAXLINE], recvline[MAXLINE];

	FD_ZERO(&rset);
	for ( ; ; ) {

		FD_SET(fileno(fp), &rset);    // 对应于标准I/O的fp指针
		FD_SET(sockfd, &rset);        // 对应于套接字scokfd
		maxfdp1 = max(fileno(fp), sockfd) + 1;    //计算上面两个描述符的最大值
		Select(maxfdp1, &rset, NULL, NULL, NULL);     //阻塞于select,timeout为NULL,阻塞到某个描述符就绪为止

		if (FD_ISSET(sockfd, &rset)) {	/* socket is readable */   //如果套接字可读
			if (Readline(sockfd, recvline, MAXLINE) == 0)         //直接读回回射文本
				err_quit("str_cli: server terminated prematurely");
			Fputs(recvline, stdout);
		}

		if (FD_ISSET(fileno(fp), &rset)) {  /* input is readable */    //如果标准输入可读
			if (Fgets(sendline, MAXLINE, fp) == NULL)                 //就先用fgets读入一行文本
				return;		/* all done */
			Writen(sockfd, sendline, strlen(sendline));
		}
	}
}

我们可以看到优化后的版本是由select驱动的,而第一个版本是由fgets驱动的


       继续来看,如果我们把标准输入和标准输出重定向到文件来运行客户,然后以批量方式运行客户。这时,我们会发现输出文件总是小于输入文件。


        假设输入文件有9行,最后一行在时刻8发出,写完这个请求后,我们虽然数据写入完了,但是并不能关闭该连接,因为管道中还有其他的请求和应答。原因:在最开始的标准输入中,EOF的键入同时意味着完成了从套接字的读入。但是在批量方式下,EOF并不意味着我们同时完成了从套接字的读入——可能仍有请求去往服务器,或者仍有可能应答在返回客户的路上。

        另外一点,在上述代码中我们使用了缓冲机制,这也容易犯错!批量方式中,我们有多个文本输入行,而fgets读取输入,会使可用的文本行被读入到stdio所用的缓冲区中。但是fgets只返回其中一行,其余行仍留在stdio的缓冲区中。下一次,select再次被调用以等待新的工作,没有管stdio缓冲区中的待处理文本行数据。因为select根本不知道stdio使用了缓冲区,select只是从read系统调用的角度支出是否有数据可读,而不是从fgets调用角度考虑。readline的调用存在同样的问题。

       所以,混合使用stdio缓冲区和select是极容易犯错误的!


5、poll函数

poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。

poll函数定义:

#include <poll.h>

int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);


/*
fdarray:纸箱一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构。
struct pollfd{
	int fd;
	short events;   //指定测试条件(输入、输出、异常)
	short recents;  
}
*/

/*
nfds:结构数组中元素的个数
*/

/*
timeout:制定poll函数返回前等待多长时间。
(1)INFTIM:永远等待;
(2)0:立即返回,不阻塞进程;
(3)>0:等待指定的毫秒数;
*/


6、使用poll函数:

使用poll函数的TCP回射服务器程序:

/* include fig01 */
#include	"unp.h"
#include	<limits.h>		/* for OPEN_MAX */

int
main(int argc, char **argv)
{
	int			i, maxi, listenfd, connfd, sockfd;
	int			nready;
	ssize_t			n;
	char			buf[MAXLINE];
	socklen_t		clilen;
	struct pollfd		client[OPEN_MAX];           //pollfd结构的数组,维护客户信息
	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, (SA *) &servaddr, sizeof(servaddr));

	Listen(listenfd, LISTENQ);

	client[0].fd = listenfd;                                       //把client数组的第一项固定用于监听套接字
	client[0].events = POLLRDNORM;                  //设置POLLRDNORM,当有新的连接准备好被接受时poll将通知我们
	for (i = 1; i < OPEN_MAX; i++)
		client[i].fd = -1;		        /* -1 indicates available entry */   //值为-1表示所在项未用
	maxi = 0;					/* max index into client[] array */

	for ( ; ; ) {
		nready = Poll(client, maxi+1, INFTIM);    //调用poll,等待新的连接或者现有连接上有数据可读

		if (client[0].revents & POLLRDNORM) {	/* new client connection */
			clilen = sizeof(cliaddr);
			connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
#ifdef	NOTDEF
			printf("new client: %s\n", Sock_ntop((SA *) &cliaddr, clilen));
#endif

			for (i = 1; i < OPEN_MAX; i++)     //从下标为1开始搜索
				if (client[i].fd < 0) {               //值为-1表示所在项未用
					client[i].fd = connfd;	/* save descriptor */    //保存新连接的描述符
					break;
				}
			if (i == OPEN_MAX)
				err_quit("too many clients");

			client[i].events = POLLRDNORM;     //设置POLLRDNORM
			if (i > maxi)
				maxi = i; /* max index in client[] array */

			if (--nready <= 0)
				continue; /* no more readable descriptors */
		}

		for (i = 1; i <= maxi; i++) {	/* check all clients for data */
			if ( (sockfd = client[i].fd) < 0)
				continue;
			if (client[i].revents & (POLLRDNORM | POLLERR)) {     //检查POLLRDNORM和POLLERR这两个返回事件
				if ( (n = read(sockfd, buf, MAXLINE)) < 0) {
					if (errno == ECONNRESET) {
						/*4connection reset by client */            //一个现有连接被客户重置
#ifdef	NOTDEF
						printf("client[%d] aborted connection\n", i);
#endif
						Close(sockfd);
						client[i].fd = -1;        //fd置为-1
					} else
						err_sys("read error");
				} 
                                else if (n == 0) {
					/*4connection closed by client */               //一个现有连接被客户重置
#ifdef	NOTDEF
					printf("client[%d] closed connection\n", i);
#endif
					Close(sockfd);
					client[i].fd = -1;        //fd置为-1
				} 
                                else
					Writen(sockfd, buf, n);

				if (--nready <= 0)
					break;	/* no more readable descriptors */
			}
		}
	}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值