I/O多路复用三种实现

 I/O多路复用三种实现_第1张图片

一.select 实现

(1)select流程

基本流程是:

1. 先构造一张有关文件描述符的表;                     fd_set readfds

2. 清空表                                                              FD_ZERO()

3. 将你关心的文件描述符加入到这个表中;           FD_SET()

4. 调用select函数。                                              selset()

5. 判断是哪一个或哪些文件描述符产生了事件(IO操作);   FD_ISSET()

6. 做对应的逻辑处理;       

(2)selset函数

头文件: #include   #include   

             #include   #include

声明: int select(int nfds, fd_set *readfds, fd_set *writefds,\

                                    fd_set *exceptfds, struct timeval *timeout);

功能:监测是哪些文件描述符产生事件,阻塞等待产生.

参数:nfds:    监测的最大文件描述个数(文件描述符从0开始,这里是个数,记得+1)

          readfds:  读事件集合; // 键盘鼠标的输入,客户端连接都是读事件

          writefds: 写事件集合;  //NULL表示不关心

          exceptfds:异常事件集合;  //NULL 表示不关心

          timeout:   设为NULL,等待直到某个文件描述符发生变化;

                              设为大于0的值,有描述符变化或超时时间到才返回。

        超时时间检测:如果规定时间内未完成函数功能,返回一个超时的信息,我们可以根据该信息设定相应需求;

返回值:  <0 出错            >0 表示有事件产生;

                如果设置了超时检测时间:&tv

                <0 出错            >0 表示有事件产生;      ==0 表示超时时间已到;        

结构体如下:                     

            struct timeval {

               long    tv_sec;         以秒为单位,指定等待时间

               long    tv_usec;        以毫秒为单位,指定等待时间

           };

void FD_CLR(int fd, fd_set *set);  //将set集合中的fd清除掉 

int  FD_ISSET(int fd, fd_set *set); //判断fd是否在set集合中产生了事件

void FD_SET(int fd, fd_set *set);  //将fd加入到集合中

void FD_ZERO(fd_set *set);          //清空集合

(3)Select特点:

Select特点:

1. 一个进程最多只能监听1024个文件描述符 (32位)   [64位为 2048]

2. select被唤醒之后要重新轮询(0-1023)一遍驱动,效率低(消耗CPU资源)

3. select每次会清空未响应的文件描述符,每次都需要拷贝用户空间的表到内核空间,效率低,开销较大

   (0~3G是用户态,3G~4G是内核态,两个状态来回切换  拷贝是非常耗时,耗资源的)

 (4)select机制: 

1. 头文件检测1024个文件描述符  0-1023

2. 在select中0~2存储标准输入、标准输出、标准出错    

3. 监测的最大文件描述个数为fd+1(如果fd = 3,则最大为 4) :  //因为从0开始的    

4. select只对置1的文件描述符感兴趣 ,假如事件产生,select检测时 , 产生的文件描述符会保持1,未产生事件的会置0; 

5. select每次轮询都会清空表(置零的清空)   //需要在select前备份临时表

I/O多路复用三种实现_第2张图片

练习1:

如何通过select实现 响应鼠标事件同时响应键盘事件?

代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, char const *argv[])
{
    int fd = open("/dev/input/mouse0", O_RDONLY);
    if (fd < 0)
    {
        perror("open is err:");
        return -1;
    }
    //1.创建表
    fd_set readfds;
    //2/清空表
    FD_ZERO(&readfds);
    //3.设置表
    FD_SET(0, &readfds);
    FD_SET(fd, &readfds);
    fd_set readfdcp = readfds;
    int maxfd = fd;
    char buf[128] = {0};
    while (1)
    {
        //4.检测是否有相应
        select(maxfd + 1, &readfds, NULL, NULL, NULL);
        //5.检测哪一个文件描述符
        if (FD_ISSET(0, &readfds))
        {

            fgets(buf, sizeof(buf), stdin);
            if (buf[strlen(buf) - 1] == '\n')
                buf[strlen(buf) - 1] = '\0';
            printf("key: %s\n", buf);
        }
        if (FD_ISSET(fd, &readfds))
        {

            int ret = read(fd, buf, sizeof(buf));
            buf[ret] = '\0';
            printf("mouse: %s\n", buf);
        }
        readfds = readfdcp;
    }
    return 0;
}

 练习2:

select是文件描述符和下标一一对应,0只能对应0号文件描述符。因此只有最大的文件描述符关闭时,才--len。注意增加删除时是针对实际表,不是临时表。

使用select实现server的全双工

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int acceptfp;
int main(int argc, char const *argv[])
{

    char buf[128] = {0};
    //1.创建套接字,返回建立链接的文件描述符
    int sockfp = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfp == -1)
    {
        perror("socket is err");
        exit(0);
    }
    printf("%d\n", sockfp);

    //2.绑定ip和端口号
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    socklen_t len = sizeof(struct sockaddr_in);

    if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind is err");
        exit(0);
    }

    //3.listen监听
    if (listen(sockfp, 5))
    {
        perror("liste err");
        exit(0);
    }
    printf("listen ok\n");

    //1.创建表
    fd_set readfds;
    //2/清空表
    FD_ZERO(&readfds);
    //3.设置表
    FD_SET(0, &readfds);
    FD_SET(sockfp, &readfds);
    fd_set readfdcp = readfds;
    int maxfd = sockfp;
    struct timeval st;
    while (1)
    {
        readfds = readfdcp;
        //4.检测是否有响应
        st.tv_sec = 5;
        st.tv_usec = 0;
        int ret = select(maxfd + 1, &readfds, NULL, NULL, &st);
        if (ret < 0)
        {
            perror("select err");
            return -1;
        }
        else if (ret == 0)
        {
            printf("无响应\n");
        }
        //0响应,证明服务器要发送消息
        if (FD_ISSET(0, &readfds))
        {
            fgets(buf, sizeof(buf), stdin);
            if (buf[strlen(buf) - 1] == '\n')
                buf[strlen(buf) - 1] = '\0';
            for (int i = 4; i <= maxfd; ++i)
            {
                send(i, buf, sizeof(buf), 0);
            }
        }
        //sockfp,监听套接字响应证明,有客户端要链接
        if (FD_ISSET(sockfp, &readfds))
        {
            acceptfp = accept(sockfp, (struct sockaddr *)&caddr, &len);
            if (acceptfp < 0)
            {
                perror("acceptfp");
                exit(0);
            }
            printf("port:%d   ip:  %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
            FD_SET(acceptfp, &readfdcp);
            if (acceptfp > maxfd)
                maxfd = acceptfp;
        }
        //检测客户端,检查是哪一个客户端发送的消息
        for (int i = 4; i <= maxfd; ++i)
        {
            if (FD_ISSET(i, &readfds))
            {
                int recvbyte = recv(i, buf, sizeof(buf), 0);
                if (recvbyte < 0)
                {
                    perror("recv err");
                    return -1;
                }
                else if (recvbyte == 0)
                {
                    printf("%d client is exit\n", i);
                    close(i);
                    FD_CLR(i, &readfdcp);
                    if (i == maxfd)
                        --maxfd;
                }
                else
                {
                    printf("%d : %s\n", i, buf);
                }
            }
        }
    }
    return 0;
}

练习3:

使用select实现client的全双工

#include 
#include           /* See NOTES */
#include 
#include 
#include  /* superset of previous */
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, const char *argv[])
{
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
	if(sockfd < 0)
	{
	   perror("socker is err:");
	   return -1;
	}

    struct sockaddr_in saddr;
	saddr.sin_family = AF_INET;
	saddr.sin_port = htons(atoi(argv[1]));
	saddr.sin_addr.s_addr = inet_addr(argv[2]);

    if(connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) < 0)
    {
        perror("connect is err:");
        return -1;
    }
   //1.创建表
   fd_set readfds,tempfds;
   //2.清空表
   FD_ZERO(&readfds);
   FD_ZERO(&tempfds);
   //3.添加文件描述符
   FD_SET(0,&readfds);
   FD_SET(sockfd,&readfds);
   
   int maxfd = sockfd;
   int ret;
   char buf[128];
   while(1)
   {
       tempfds = readfds;
      //4.调select检测
       ret = select(maxfd+1,&tempfds,NULL,NULL,NULL);
       if(ret < 0)
       {
           perror("select is err:");
           return -1;
       }
       if(FD_ISSET(0,&tempfds))
       {
           fgets(buf,sizeof(buf),stdin);
              if(buf[strlen(buf)-1] == '\n')
                 buf[strlen(buf)-1] = '\0';
        
          send(sockfd,buf,sizeof(buf),0);
       }
       if(FD_ISSET(sockfd,&tempfds))
       {
           int recvbyte = recv(sockfd,buf,sizeof(buf),0);
           if(recvbyte < 0)
           {
               perror("recv is err:");
               return -1;
           }
           printf("%s\n",buf);
       }
   }
   close(sockfd);
 return 0;
}

(5)select的超时时间检测:

超时检测的必要性:

1. 避免进程在没有数据时无限制的阻塞;

2. 规定时间未完成语句应有的功能,则会执行相关功能;

结构体如下:                     

            struct timeval {

               long    tv_sec;         以秒为单位,指定等待时间

               long    tv_usec;        以毫秒为单位,指定等待时间

           };

二.poll实现

 (1)poll流程

使用:  1.先创建结构体数组                                           struct pollfd fds[100];

          2.添加结构体成员的文件描述符以及触发方式   fds[0].fd = ?;fds[0].events = POLLIN 

          3.保存数组内最后一个有效元素的下标       

          4. 调用函数poll                                                  ret = poll(fds,nfds+1,-1);

          5.判断结构体内文件描述符是否触发事件          fds[i].revents == POLLIN

          6.根据不同的文件描述符触发不同事件 

(2)poll函数

声明:int poll(struct pollfd *fds, nfds_t nfds, int timeout);

头文件: #include

功能: 监视并等待多个文件描述符的属性变化

参数:

  1.struct pollfd *fds:   关心的文件描述符数组,大小自己定义

   若想检测的文件描述符较多,则建 立结构体数组struct pollfd fds[N]; 

           struct pollfd

           {

                  int fd;        //文件描述符

             short events;  //等待的事件触发条件----POLLIN读时间触发(大多数)

             short revents; //实际发生的事件(未产生事件: 0 ))

            }

    2.   nfds:        最大文件描述符个数

    3.  timeout: 超时检测 (毫秒级):1000 == 1s      

                          如果-1,阻塞          如果0,不阻塞

返回值:  <0 出错              >0 表示有事件产生;

              如果设置了超时检测时间:&tv

                <0 出错                >0 表示有事件产生;            ==0 表示超时时间已到;

(3)poll特点

1. 优化文件描述符个数的限制;

(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组容量为1,如果想监听100个,那么这个结构体数组的容量就为100,多少文件描述符由程序员自己来决定)

2. poll被唤醒之后需要重新轮询一遍驱动,效率比较低(消耗CPU)

3. poll不需重新构造文件描述符表(也不需清空表),只需要从用户空间向内核空间拷贝一次数据(效率相对比较高)

练习: 

使用poll实现server的全双工

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int acceptfp;
int main(int argc, char const *argv[])
{

    char buf[128] = {0};
    //1.创建套接字,返回建立链接的文件描述符
    int sockfp = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfp == -1)
    {
        perror("socket is err");
        exit(0);
    }
    printf("%d\n", sockfp);

    //2.绑定ip和端口号
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    socklen_t len = sizeof(struct sockaddr_in);

    if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind is err");
        exit(0);
    }

    //3.listen监听
    if (listen(sockfp, 5))
    {
        perror("liste err");
        exit(0);
    }
    printf("listen ok\n");
    //1.创建结构体数组
    struct pollfd fds[100];
    //2.添加文件描述符和触发方式
    fds[0].fd = 0;
    fds[0].events = POLLIN;

    fds[1].fd = sockfp;
    fds[1].events = POLLIN;
    int nfds = 1;
    int ret;
    while (1)
    {
        //3.poll轮循检测
        ret = poll(fds, nfds + 1, 2000);
        if (ret < 0)
        {
            perror("poll is err");
            return -1;
        }
        else if (ret == 0)
        {
            printf("qeqweqe\n");
            continue;
        }
        //4. 判断哪一个文件描述符产生响应,并发布任务
        for (int i = 0; i <= nfds; ++i)
        {
            if (fds[i].revents == POLLIN)
            {
                if (fds[i].fd == 0)
                {
                    fgets(buf, sizeof(buf), stdin);
                    if (buf[strlen(buf) - 1] == '\n')
                        buf[strlen(buf) - 1] = '\0';
                    //printf("发送信息:\n");
                    for (int j = 2; j <= nfds; ++j)
                    {
                        send(fds[j].fd, buf, sizeof(buf), 0);
                    }
                }
                else if (fds[i].fd == sockfp)
                {
                    acceptfp = accept(sockfp, (struct sockaddr *)&caddr, &len);
                    if (acceptfp < 0)
                    {
                        perror("acceptfp");
                        exit(0);
                    }
                    printf("port:%d   ip:  %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
                    fds[++nfds].fd = acceptfp;
                    fds[nfds].events = POLLIN;
                }
                else
                {
                    int recvbyte = recv(fds[i].fd, buf, sizeof(buf), 0);
                    if (recvbyte < 0)
                    {
                        perror("recv err");
                        return -1;
                    }
                    else if (recvbyte == 0)
                    {
                        printf("%d client is exit\n", i);
                        close(fds[i].fd);
                        //覆盖
                        fds[i] = fds[nfds];
                        //--i,--nfds后,最后一个循环不到
                        --nfds, --i;
                    }
                    else
                    {
                        printf("%d : %s\n", i, buf);
                    }
                }
            }
        }
    }
    return 0;
}

(4)poll超时时间检测

 timeout: 超时检测 (毫秒级):1000 == 1s      

                  如果-1,阻塞          如果0,不阻塞

三.epoll实现

(1)epoll流程:

Epoll的使用:

1.创建红黑树 和 就绪链表                                      int epfd = epoll_create(1);

2.添加文件描述符和事件信息到树上

    event.events = EPOLLIN|EPOLLET;

    event.data.fd = 0;

    epoll_ctl(epfd,EPOLL_CTL_ADD,0,&event

3.阻塞等待事件的产生,一旦产生事件,则进行处理 

     int ret = epoll_wait(epfd,events,32,-1);

4.根据链中准备处理的文件描述符 进行处理

(2)epoll函数族 

epoll 要使用一组函数:       epoll_create 创建红黑树 和 就序链表

                                          epoll_ctl   添加文件描述符和事件到树上 / 从树上删除

                                          epoll_wait  等待事件产生

epoll_create 

创建红黑树以及链表

头文件:#include 

声明:int epoll_create(int size);

功能:创建红黑树根节点(创建epoll实例) , 同时也会创建就绪链表

返回值:成功时返回一个实例(二叉树句柄),失败时返回-1。

epoll_ctl

控制epoll属性

声明: int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能:控制epoll属性,比如给红黑树添加节点

参数:  1. epfd:   epoll_create函数的返回句柄。//一个标识符

          2. op:表示动作类型,有三个宏:         

                EPOLL_CTL_ADD:注册新的fd到epfd中

                EPOLL_CTL_MOD:修改已注册fd的监听事件

                EPOLL_CTL_DEL:从epfd中删除一个fd

3. 要操作的文件描述符

4. 结构体信息:

        typedef union epoll_data {

                int fd; //要添加的文件描述符,只用这个

                uint32_t u32; typedef unsigned int

                uint64_t u64; typedef unsigned long int

        } epoll_data_t;

        struct epoll_event {

                uint32_t events; 事件

                epoll_data_t data; //共用体(看上面)

        };

           关于events事件:

            EPOLLIN:  表示对应文件描述符可读

            EPOLLOUT: 可写

            EPOLLPRI:有紧急数据可读;

            EPOLLERR:错误;

            EPOLLHUP:被挂断;

            EPOLLET:触发方式,边缘触发;(默认使用边缘触发)

            ET模式:表示状态的变化;

            NULL: 删除一个文件描述符使用,无事件

返回值:成功:0, 失败:-1

epoll_wait

等待事件产生

声明: int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

功能:等待事件产生

   内核会查找红黑树中有事件响应的文件描述符, 并将这些文件描述符放入就绪链表

    就绪链表中的内容, 执行epoll_wait会同时复制到第二个参数events

参数:   epfd:句柄;

           events:用来保存从就绪链表中响应事件的集合;(传出参数,定义结构体数组)

           maxevents:  表示每次在链表中拿取响应事件的个数;

           timeout:超时时间,毫秒,0立即返回  ,-1阻塞

返回值: 成功: 实际从链表中拿出的数目     失败时返回-1

(4)epoll特点

1.监听的最大的文件描述符没有个数限制(取决与你自己的系统 1GB - 10万个左右)

2.异步I/O,epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高

3.epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.

(5)epoll机制

select,poll都属于 同步IO机制(轮询)

epoll属于异步IO机制(不轮询): 

Epoll处理高并发,百万级

  1. 红黑树: 是特殊的二叉树(每个节点带有属性),Epoll怎样能监听很多个呢?首先创建树的根节点,每个节点都是一个fd以结构体的形式存储(节点里面包含了一些属性,callback函数)
  2. 就绪链表: 当某一个文件描述符产生事件后,会自动调用callback函数,通过回调callback函数来找到链表对应的事件(读时间还是写事件)。

 I/O多路复用三种实现_第3张图片

 练习:

epoll实现server

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int acceptfp;
int main(int argc, char const *argv[])
{

    char buf[128] = {0};
    //1.创建套接字,返回建立链接的文件描述符
    int sockfp = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfp == -1)
    {
        perror("socket is err");
        exit(0);
    }
    printf("%d\n", sockfp);

    //2.绑定ip和端口号
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    socklen_t len = sizeof(struct sockaddr_in);

    if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind is err");
        exit(0);
    }

    //3.listen监听
    if (listen(sockfp, 5))
    {
        perror("liste err");
        exit(0);
    }
    printf("listen ok\n");
    //1.创建红黑树以及链表
    //树的跟节点/树的句柄
    int epfd = epoll_create(1);
    //2.上树
    struct epoll_event event;
    struct epoll_event events[32] ;
    event.events = EPOLLET | EPOLLIN;

    event.data.fd = 0;
    epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);

    event.data.fd = sockfp;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfp, &event);

    while (1)
    {
        //3.阻塞等待文件描述符产生事件
        int ret = epoll_wait(epfd, events, 32, -1);
        printf("asdsdfgdsf\n");
        if (ret < 0)
        {
            perror("epoll err");
            return -1;
        }
        //4.根据文件描述符号,进行处理
        for (int i = 0; i < ret; ++i)
        {
            if (events[i].data.fd == 0)
            {
                fgets(buf, sizeof(buf), stdin);
                if (buf[strlen(buf) - 1] == '\n')
                    buf[strlen(buf) - 1] = '\0';
                printf("发送信息:\n");
                //send(fds[j].fd, buf, sizeof(buf), 0);
            }
            else if (events[i].data.fd == sockfp)
            {
                acceptfp = accept(sockfp, (struct sockaddr *)&caddr, &len);
                if (acceptfp < 0)
                {
                    perror("acceptfp");
                    exit(0);
                }
                printf("port:%d   ip:  %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
                //上树
                event.data.fd = acceptfp;
                epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfp, &event);
            }
            else
            {
                int recvbyte = recv(events[i].data.fd, buf, sizeof(buf), 0);
                if (recvbyte < 0)
                {
                    perror("recv err");
                    return -1;
                }
                else if (recvbyte == 0)
                {
                    printf("%d client is exit\n", events[i].data.fd);
                    close(events[i].data.fd);
                    //下树
                    epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                }
                else
                {
                    printf("%d : %s\n", events[i].data.fd, buf);
                }
            }
        }
    }

    return 0;
}

对比 

I/O多路复用三种实现_第4张图片

你可能感兴趣的:(linux,服务器,网络)