UNP第六章 I/O复用
一 I/O模型
1.1 阻塞式I/O模型
1.2 非阻塞式I/O模型
1.3 I/O复用模型
二 select函数
#include<sys/select.h> #include<sys/time.h> int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
参数说明:
1.timeout:
struct timeval{ long tv_sec; //seconds long tv_usec; //microseconds };
(1)设为NULL时,永远等待;
(2)等待一段固定时间;
(3)设为(struct timeval)0,不等待,检查描述符后立即返回。
Ps:const 限定词表示不会被在函数返回时被修改
2.readset,writeset和exceptset指定要让内核测试读、写和异常条件的描述符使用描述符集:通常是一个整数数组,其中每个整数中的每一位对应一个描述符,假设使用32位整数,那么该数组第一个元素对应于描述符0-31
实现细节隐藏在fd_set数据结构和四个宏中:
void FD_ZERO(fd_set *fdset); //clear all bits in fdset void FD_SET(int fd, fd_set *fdset); //turn on the bit for fd in fdset void FD_CLR(int fd, fd_set *fdset); //turn off the bit for fd in fdset int FD_ISSET(int fd, fd_set *fdset); //is the bit for fd on in fdset?
Ps:如果对某一个条件不感兴趣,可把它设为NULL
3. maxfdp1 : 指定待测试的描述符个数,它的值是待测试的最大描述符加1,描述符1,2,3...maxfdp-1均被测试,头文件<sys/select.h>中定义的FD_SETSIZE常值是fd_set中的描述符总数,其值通常是1024。
4. 返回值说明:
该函数返回值指示哪些描述符已就绪,可通过FD_ISSET宏来测试,描述符集内任何未与就绪描述符对应的位返回时均清成0。所以,每次重新调用select函数,需要把所有描述符集内关心的位置1。
该函数返回值表示跨所有描述符集的已就绪的总位数,如果在任何描述符就绪之前定时器到时,则返回0,返回-1表示出错。
5.描述符就绪条件:
接收低水位标记和发送低水位标记的目的:
允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可写。
6.例子:
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); //fileno函数把标准I/O文件指针转换为对应的描述符 FD_SET(sockfd,&rset); maxfdp1 = max(fileno(fp),sockfd) + 1; Select(maxfdp1, &rest, NULL, NULL, NULL); if(FD_ISSET(sockfd,&rset)) { if(Readline(sockfd,recvline,MAXLINE) == 0) err_quit("str_cli:server terminated prematurely"); Fputs(recvline,stdout); } if(FD_ISSET(fileno(fp),&rset)) { if(Fgets(sendline,MAXLINE,fp) == NULL) { return; } Writen(sockfd,sendline,strlen(sendline)); } } }
三 针对二中例子存在两个问题并进行解决
在批量方式下,客户能够以网络可以接受的最快速度持续发送请求,服务器以相同速度处理它们并发回应答,导致时刻7时管道充满。
假设发出第一个请求后立即发出下一个,客户能够以网络可以接受的最快速度持续发送请求,并且能够以网络可提供的最快速度处理应答。
则存在两个问题:
问题一:在批量方式下,标准输入的EOF并不代表我们同时完成socket的读入,可能仍有请求在发给服务器,或仍有应答在返回给客户,我们希望给服务器发送一个FIN,告知已经完成数据发送,但仍保持socket描述符打开以便读取。
问题二:在上述例子中,用fgets读取输入,这使得已可用的文本输入行被读入到stdio所用的缓冲区,但fgets只返回其中第一行。所以只消费了第一行,stdio缓冲区仍有遗留。同样道理适用于readline函数。
So, how to deal with it?
采用shutdown函数!!
shutdown函数:
(1)close将描述符的引用计数减1,仅在该计数变为0时才关闭socket,使用shutdown可以不管引用计数就激发TCP的正常连接终止序列;
(2)告知对端我们已完成数据传送。
#include<sys/socket.h> int shutdown(int sockfd, int howto);
howto有三个参数:
SHUT_RD: socket中不再有数据可接受,且接受缓冲区中现有数据被丢弃;
SHUT_WR: 半关闭:当前留在socket发送缓冲区中的数据将被发送,后跟TCP正常连接终止序列,进程不可以对该socket调用任何写函数。
SHUT_RDWR: 先调用SHUT_RD,再调用SHUT_WR。
void str_cli(FILE *fp, int sockfd) { int maxfdp1,stdineof; fd_set rset; char buf[MAXLINE]; int n; stdineof = 0; FD_ZERO(&rset); for(;;) { if(stdineof == 0) FD_SET(fileno(fp),&rset); //fileno函数把标准I/O文件指针转换为对应的描述符 FD_SET(sockfd,&rset); maxfdp1 = max(fileno(fp),sockfd) + 1; Select(maxfdp1, &rest, NULL, NULL, NULL); if(FD_ISSET(sockfd,&rset)) { if((n = Read(sockfd,buf,MAXLINE)) == 0) //改用Read,解决了readline的问题 { if(stdineof == 1) return; //这是连接已正常终止,就不读了,读结束 else err_quit("str_cli:server terminated prematurely"); } Write(fileno(stdout),buf,n); } if(FD_ISSET(fileno(fp),&rset)) { if((n = Read(fileno(fp),buf,MAXLINE))== 0) //读到EOF { stdineof = 1; //置位标志位 Shutdown(sockfd,SHUT_WR); //发送FIN,正常终止,但注意此时只是写关闭,仍可以通过socket读 FD_CLR(fileno(fp),&rset); continue; } Writen(sockfd,buf,n); } } }
四 多客户单服务器程序
int main(int argc, char **argv) { int i,maxi,maxfd,listenfd,connfd,sockfd; int nready,client[FD_SETSIZE]; ssize_t n; fd_set rset,allset; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in cliaddr,servaddr; listenfd = Socket(AF_INET,SOCK_STREAM,0); bzero(&servaddr,sizeof(sercaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htos(SERV_PORT); Bind(listenfd,(SA*)&servaddr, sizeof(servaddr)); Listen(listenfd,LISTENQ); maxfd = listenfd; maxi = -1; for(i=0;i<FD_SETSIZE;i++) { client[i] = -1; } FD_ZERO(&allset); FD_SET(listenfd,&allset); for(;;) { rest = allset; nready = Select(maxfd+1, &rset, NULL, NULL, NULL); if(FD_ISSET(listenfd,&rset)) //当有新的客户连接时 { clilen = sizeof(cliaddr); connfd = Accept(listenfd,(SA*)&cliaddr,&client); for(i=0;i<FD_SETSIZE;i++) { if(client[i]<0) //找到第一个为-1的位置,放入已连接描述符索引 { client[i] = connfd; break; } } if(i == FD_SETSIZE) err_quit("too many clients"); FD_SET(connfd,&allset); //将新的描述符添加到allset中 if(connfd > maxfd) maxfd = connfd; //for select if(i > maxi) maxi = i; //max index in client[] array if(--nready <= 0) continue; //没有可读的描述符了 } for(i=0;i<=maxi;i++) { if((sockfd = client[i]) < 0) continue; if(FD_ISSET(sockfd,&rest)) { if((n = Read(sockfd, buf, MAXLINE)) == 0) //该sockfd对应的客户想要关闭连接 { Close(sockfd); FD_CLR(sockfd,&allset); client[i] = -1; }else Writen(sockfd,buf,n); if(--nready <= 0) breal; //没有可读的描述符了 } } } }
五 poll函数
#include <poll.h> int poll(struct pollfd *fdarray, unsigned long nfds, int timeout); struct pollfd{ int fd; //descriptor to check short events; //events of interest on fd short revents; //events that occurred on fd };
要测试的条件由events成员指定,函数在相应的revents成员中返回该描述符的状态。
对之前例子的修改:
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]; struct sockaddr_in cliaddr,servaddr; listenfd = Socket(AF_INET,SOCK_STREAM,0); bzero(&servaddr,sizeof(sercaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htos(SERV_PORT); Bind(listenfd,(SA*)&servaddr, sizeof(servaddr)); Listen(listenfd,LISTENQ); client[0].fd = listenfd; client[0].events = POLLRDNORM; for(i=1;i<OPEN_MAX;i++) client[i].fd = -1; //初始化,代表无效 maxi = 0; //max index into client[] array for(;;) { nready = Poll(client,maxi+1,INFTIM); if(client[0].revents & POLLRDNORM) //当有新的客户连接时 { clilen = sizeof(cliaddr); connfd = Accept(listenfd,(SA*)&cliaddr,&client); for(i=1;i<OPEN_MAX;i++) { if(client[i]<0) //找到第一个为-1的位置,放入已连接描述符索引 { client[i].fd = connfd; break; } } if(i == OPEN_MAX) err_quit("too many clients"); client[i].events = POLLRDNORM; if(i > maxi) maxi = i; //max index in client[] array if(--nready <= 0) continue; //没有可读的描述符了 } for(i=1;i<=maxi;i++) { if((sockfd = client[i].fd) < 0) continue; if(client[i].revents & (POLLRDNORM | POLLERR)) { if((n = read(sockfd, buf, MAXLINE)) < 0) { if(errno == ECONNRESET) { Close(sockfd); client[i].fd = -1; } else err_sys("read error"); }else if(n == 0) //该sockfd对应的客户想要关闭连接 { Close(sockfd); client[i].fd = -1; } else Writen(sockfd,buf,n); if(--nready <= 0) breal; //没有可读的描述符了 } } } }