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;   //没有可读的描述符了
            }
        }
    }
}
原文地址:https://www.cnblogs.com/dzy521/p/9394746.html