io多路复用select、poll、epoll模型

一、select模型
1.函数参数含义
int select(int maxfd,fd_set *rset,fd_set *wset,fd_set *eset,struct timeval *timeout);
将需要检测的fd_set集合(参数二、三、四)拷贝到内核,如果有io就绪清空fd_set集合,重新设置fd_set集合,在拷贝到用户层。

参数一:判断最大fd的值 方便内部循环判断fd是否io就绪
参数二:文件描述符可读
参数三:文件描述符可写
参数四:文件描述符异常
参数五:多久轮询一次 NULL:阻塞 0:立即返回不阻塞进程
返回值:大于0表示有io就绪事件
代码实现:

	int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    seraddr.sin_port = htons(2048);
    if(-1==bind(sockfd,(struct sockaddr*)&seraddr,sizeof(struct sockaddr)))
    {
        perror("bind");
        return -1;
    }
    printf("----bind\n");
    listen(sockfd,10);

    fd_set rfds,rset;// 一个设置 一个读判断
    FD_ZERO(&rfds);
    FD_SET(sockfd,&rfds);
    int maxfd = sockfd;
    while(1)
    {
        rset = rfds;
        int nready = select(maxfd+1,&rset,NULL,NULL,NULL);//第五个参数为空表示一直等待
        if(FD_ISSET(sockfd,&rset))//判断是否有连接
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
            printf("accept\n");
            FD_SET(clientfd,&rfds);
            if (clientfd > maxfd) maxfd = clientfd;
        }
        int i=0;
        for(i=sockfd+1;i<=maxfd;i++)
        {
            if(FD_ISSET(i,&rset))//读事件
            {
                char buffer[128] = {0};
                int count = recv(i,buffer,sizeof(buffer),0);
                if(count == 0){
                	FD_CLR(i,&rfds);
                	close(i);
                }
                if(count > 0)
                    printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
            }
        }
    }

select缺点
1.参数较多 不利于维护理解
2.对io的数量是有限制的 linux1024个,windows64个。
3.每次都需要拷贝待检测的io集合
4.每次需要遍历io集合,而返回就绪集合

二、poll模型
1.函数参数含义
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数一:struct pollfd{
int fd; 文件描述
short events;//带入的参数 POLLIN \ POLLOUT…
short revents;//返回的信息 POLLIN \ POLLOUT…
}
参数二:最大fd的值 方便内部循环判断fd是否io就绪
参数三:多久轮询一次 。NULL:阻塞 0:立即返回不阻塞进程

/*struct pollfd{
	int fd;
	short events;//带入的参数
	short revents;//返回的信息
}*/
	int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    seraddr.sin_port = htons(2048);
    if(-1==bind(sockfd,(struct sockaddr*)&seraddr,sizeof(struct sockaddr)))
    {
        perror("bind");
        return -1;
    }
    printf("----bind\n");
    listen(sockfd,10);
    
    struct pollfd fds[1024] = {0};
    fds[sockfd].fd=sockfd;
    fds[sockfd].events = POLLIN;
    int maxfd = sockfd;//遍历次数少一些 fd按递增的
    while(1)
    {
        int nready = poll(fds,maxfd+1,-1);
        if(fds[sockfd].revents & POLLIN)
        {
            struct sockaddr_in clientaddr;
            socklen_t len = sizeof(clientaddr);
            int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
            printf("accept\n");
            fds[clientfd].fd = clientfd;
            fds[clientfd].events = POLLIN;
            if (clientfd > maxfd) maxfd = clientfd;
        }
        int i = 0;
        for (i = sockfd+1;i <= maxfd;i ++) {
            if (fds[i].revents & POLLIN) {
                char buffer[128] = {0};
                int count = recv(i,buffer,sizeof(buffer),0);
                if (count == 0) {
                    fds[i].fd = -1;
                    fds[i].events = 0;		
                    close(i);
                    break; 
                }
                if(count > 0)
                    printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
            }
        }
    }

poll 与select类似也是采用轮询的方式,poll对文件描述符没有1024个限制,使用链表存储fd

三、epoll模型
1.函数含义
int epoll_create(int size);//参数没有意义 大于0就行
创建一个新的epoll实例并返回一个引用该实例的文件描述符,会创建一个红黑树和io就绪队列。
epoll_crl(int epfd,int op,int fd,struct epoll_event *event);
添加修改删除特定文件描述符到epoll集中(红黑树)。
参数一:epoll_create创建的fd返回值
参数二:EPOLL_CTL_ADD 添加fd到红黑树
EPOLL_CTL_DEL 删除红黑树上fd的值
EPOLL_CTL_MOD 修改红黑树上fd的值
参数三:具体的网络fd,scokfd,clientfd
参数四: typedef union epoll_data {
void ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;//保存触发事件的某个文件描述符相关的数据
struct epoll_event {
__uint32_t events; /
epoll event / EPOLLIN读 EPOLLOUT写 EPOLLLT水平触发 EPOLLET边缘触发
epoll_data_t data; /
User data variable */
};
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待I/O事件,如果当前没有可用的事件,则阻塞调用线程。
参数一:epoll_create创建的fd返回值
参数二:接口的返回参数,epoll把发生的事件的集合从内核复制到 events数组中
参数三:与参数二预分配的数组的大小是相等的
参数四:表示在没有检测到事件发生时最多等待的时间,超时时间(>=0),单位是毫秒ms,-1表示阻塞,0表示不阻塞
返回值:成功返回需要处理的事件数目。失败返回0,表示等待超时。

//面向io处理: 通过listenfd进行accept 通过clientfd进行读写
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <fcntl.h>
#include <pthread.h>

#include <sys/poll.h>
#include <sys/epoll.h>
int main()
{
    int sockfd = socket(AF_INET,SOCK_STREAM,0);
    if(sockfd < 0)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    seraddr.sin_port = htons(2048);
    if(-1==bind(sockfd,(struct sockaddr*)&seraddr,sizeof(struct sockaddr)))
    {
        perror("bind");
        return -1;
    }
    printf("----bind\n");
    listen(sockfd,10);

    int epfd = epoll_create(1); 
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);
    struct epoll_event events[1024] = {0};
    while(1)
    {
        int nready = epoll_wait(epfd,events,1024,-1);
        if (nready < 0) continue;
        int i = 0;
        for(i = 0;i < nready;i++)
        {
            int connfd = events[i].data.fd;
            if(sockfd == connfd)	//处理listenfd 面向io分类
            {
                struct sockaddr_in clientaddr;
                socklen_t len = sizeof(clientaddr);
                int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
                printf("accept\n");
                ev.events = EPOLLIN;
                ev.data.fd = clientfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else if(events[i].events & EPOLLIN) //处理clientfd
            {
                char buffer[128] = {0};
                int count = recv(connfd,buffer,sizeof(buffer),0);
                if(count == 0)
                {
                    epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
                    close(i);
                }
                if(count > 0)
                    printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
                continue;
            }
        }
    }
}

2.epoll水平触发LT(默认触发方式): io缓冲区有数据或有空间写就会一直触发, 不断的通过epoll_wait()返回
测试:服务器每次接受10个字节,客户端每次发送32字节

while(1)
    {
        int nready = epoll_wait(epfd,events,1024,-1);
        printf("epoll_wait\n");
        if (nready < 0) continue;
        int i = 0;
        for(i = 0;i < nready;i++)
        {
            int connfd = events[i].data.fd;
            if(sockfd == connfd)	//处理listenfd 面向io分类
            {
                struct sockaddr_in clientaddr;
                socklen_t len = sizeof(clientaddr);
                int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
                printf("accept\n");
                ev.events = EPOLLIN;
                ev.data.fd = clientfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else if(events[i].events & EPOLLIN) //处理clientfd
            {
                char buffer[10] = {0};//每次接受10个数据
                int count = recv(connfd,buffer,sizeof(buffer),0);
                if(count == 0)
                {
                    epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
                    close(i);
                }
                if(count > 0)
                    printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
                continue;
            }
        }
    }

在这里插入图片描述
3.epoll边缘触发ET: io缓冲区状态变化就会触发,读:来数据就触发一次,写:写缓冲区从满到有空间才会触发一次
测试:服务器每次接受10个字节,客户端每次发送32字节

    while(1)
    {
        int nready = epoll_wait(epfd,events,1024,-1);
        if (nready < 0) continue;
        int i = 0;
        for(i = 0;i < nready;i++)
        {
            int connfd = events[i].data.fd;
            if(sockfd == connfd)
            {
                struct sockaddr_in clientaddr;
                socklen_t len = sizeof(clientaddr);
                int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
                printf("accept\n");
                ev.events = EPOLLIN | EPOLLET;//设置边缘触发
                ev.data.fd = clientfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else if(events[i].events & EPOLLIN)
            {
                char buffer[10] = {0};
                int count = recv(connfd,buffer,sizeof(buffer),0);
                if(count == 0)
                {
                    epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
                    close(i);
                }
                if(count > 0)
                    printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
                continue;
            }
        }
    }

在这里插入图片描述
点击再次发送,继续读出10个字节
在这里插入图片描述
通过循环读取解决读不完整情况

 while((count = recv(connfd,buffer,sizeof(buffer),0)) > 0)
 {
     if(count == 0)
     {
         epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
         close(i);
     }
     else if(count < 0)
     {
          if (errno == EAGAIN) {
              break;
          }
          epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
          close(i);
      }
      else if(count > 0)
      {
      	  printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
          memset(buffer,0,sizeof(buffer));
      }                       
      continue;
 }

在这里插入图片描述

4.LT与ET应用场景
tcp协议有粘包的过程,通常在业务数据包首部加个长度或者加分隔符(redis)
以定义包长为例,读两次,先读长度,在读内容
short length = 0;
recv(fd,&length ,2,0);
length = ntohs(length);
recv(fd,buffer,length,0);

redis协议格式如下:
*3\r\n
$3\r\n //长度
SET\r\n //内容
$6\r\n //长度
mykey\r\n
$5\r\n //长度
12345\r\n
水平触发:适合上述情况
边缘触发:一次性数据读不完的情况 比如发送一个大文件

5.一次发送一个大文件(超过读写缓冲区,读写缓冲区都设置为16384字节)

#写缓冲区大小
$ sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096     16384   4194304
#更改写缓冲区大小
$ sysctl -w net.ipv4.tcp_wmem="4096 16384 8388608"
#读缓冲区大小
sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096     131072  6291456
#更改发送缓冲区大小
$ sysctl -w net.ipv4.tcp_rmem="4096 16384 8388608"

在这里插入图片描述
ET模式下,发送47411字节数据,只调用一次epoll_wait
在这里插入图片描述
LT模式下,发送47411字节数据,会多次调用epoll_wait

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值