栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

网络io之select 、poll、epoll小试

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

网络io之select 、poll、epoll小试

此次讨论的是阻塞的fd

先写一个简单的server端

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

#define MAXLEN 4096
int main(){    
    int listenfd,connfd,n;
    struct sockaddr_in servaddr;
    char buf[MAXLEN];

    if((listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1)//创建fd
    {
        printf("create socket error:%s",strerror(errno));
        return -1;
    }


    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(8888);
    
    if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) == -1)//绑定本机地址
    {
        printf("bind socket err:%sn",strerror(errno));
        return -1;
    }

    if(listen(listenfd,10) == -1)//监听 
    {
        printf("listen socket err:%sn",strerror(errno));
        return -1;
    }
    struct sockaddr_in client;
    socklen_t len = sizeof(client);

    printf("----waiting for client requst----n");
    
    if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) //等待客户端连接
    {
        printf("accept socket err:%sn",strerror(errno));
        return -1;
    }   

    while(1)
    {
        

        n = recv(connfd,buf,MAXLEN,0);
        if(n > 0)
        {
            printf("----waiting---n");
            buf[n] = '';
            printf("recv msg form client:%sn",buf);

            send(connfd,buf,n,0);
        }
        else if(n == 0)
        {
            printf("----close--n");
            close(connfd);
        }
    }
    close(listenfd);
    return 0;
}

 使用客户端去连接的时候发现,这样只能进行一对一的服务,当有第二个客户端连接时sock在队列里,没有accpet去取出。

针对这种情况,进行一下改进,每有一个客户端请求,创建一个线程来accpet,来进行服务

//C10K
void *client_pth(void* cfd)
{
    char buf[MAXLEN];
    int connfd = *(int*)cfd;

    while(1)
    {
        int n = recv(connfd,buf,MAXLEN,0);
        if(n > 0)
        {
            buf[n] = '';
            printf("recv client connfd %d:%sn",connfd,buf);
            send(connfd,buf,n,0);
        }
        else if(n  == 0)
        {

            printf("confd:%dn",connfd);
            close(connfd);
            break;
        }
        
    }
    return NULL;
}



    printf("----waiting for client request----n");

    while(1)
    {
        printf("--creat--n ");
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
        {
            printf("accept socket err:%sn",strerror(errno));
            return -1;            
        }
        printf("creat pthreadn");
        pthread_t pid;
        pthread_create(&pid,NULL,(void*)client_pth,(void*)&connfd);

    }

但是问题来了,每来一个新的连接,就创建一个新的线程,当并发数较大时,数据拷贝频繁(缓存I/O、内核将数据拷贝到用户进程空间、阻塞), 进程/线程上下文切换消耗大,很有可能导致操作系统崩溃,这就是我们所说的C10K问题的本质,所以这种方式只适用于并发量较小的局域网或者内部使用。

select

由这可以引入一个新方法,那就是select。select就是把所有的fd全部一个函数中去管理,select会阻塞等待某一个fd或多个fd的写事件、读事件或者异常事件触发,从而实现相应事件的操作。

这里我们举个例子,如一栋公寓,里面有很多户人家,他们要收发快递,多线程的方式是这么做的,每户人家都雇一个人专门为他收发快递,如果住户很多,会造成很大的人员开销,于是这些住户商量着,把收发快递的活全部交给一个人统一去管理,这就是select的处理方式

以下只针对读事件和写事件做处理

printf("----waiting for client request----n");
    fd_set rfd,rset,wfd,wset;
    FD_ZERO(&rfd);
    FD_ZERO(&wfd);
    FD_SET(listenfd,&rfd);
    int max_fd = listenfd;
    while(1)
    {

        printf("start n");   
        wset = wfd;
        rset = rfd;
        int num = select(max_fd+1,&rset,&wset,NULL,NULL);//监听每个fd的写事件和读事件
        printf("num:%d,max_fd:%dn",num,max_fd);
        if(FD_ISSET(listenfd,&rset))//当我们监听的fd有写事件过来时,代表有新的客户端请求
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
            {
                printf("accept socket err:%sn",strerror(errno));
                return -1;            
            }
            FD_SET(connfd,&rfd);
            if(connfd > max_fd) max_fd = connfd;
            printf("max_fd:%dn",max_fd);
            if(--num == 0) continue;
        }
        for(int i = listenfd+1;i <= max_fd;i++)
        {
            printf("fd:%dn",i);
            if(FD_ISSET(i,&rset))
            {
                n = recv(i,buf,MAXLEN,0);
                if(n > 0)
                {
                    buf[n] = '';
                    printf("recv buf:%sn",buf);
                    FD_SET(i,&wfd);           
                    //send(i,buf,n,0);
                }
                else if(n == 0) //n=0代表连接断开
                {
                    FD_CLR(i,&rfd); //读集合里除去这个fd
                    printf("close socket fd:%dn",i);
                    close(i);
                }
                //if(--num == 0) break;
            }
            else if(FD_ISSET(i,&wset))
            {
                printf("send fd:%dn",i);
                //printf("send buf:%sn",buf);
                send(i,buf,n,0);
                memset(buf,'',sizeof(buf));
                FD_SET(i,&rfd);
                FD_CLR(i,&wfd);//一定要清除写集合
            }
            
        }
    }

这里有一个小坑,当写事件被触发后,如果不及时清除,会一直触发写事件,这是我不了解写事件的触发条件造成的,当套接口发送缓冲区有空间容纳数据(大多数时候缓冲区是不满的,会不断产生可写事件)所以每次写完之后要及时清除写集合。

但是,select的容纳个数也是有限的,在内核里FD_SET的最大数默认为1024,改的话需要重新编译内核,这不是一个好办法,或者是多创建几个select来扩容,这里先不谈。还有一种方法则是

poll

        我们知道select是通过一个集合去管理的fd,而集合都是有固定大小的,但poll是通过链表的形式管理,所以没有最大fd的限制。

下面实现poll:

#define POLL_SIZE 1024

    printf("----waiting for client request----n");

    struct pollfd fds[POLL_SIZE] = {0};
    fds[0].fd = listenfd;
    fds[0].events = POLLIN;

    int max_fd = listenfd;
    for(int i = 1; i < POLL_SIZE; i++)
    {
        fds[i].fd = -1;
    }

    while(1)
    {
        int num = poll(fds,max_fd+1,-1);
        
        if(fds[0].revents & POLLIN)
        {
            struct sockaddr_in client;
            socklen_t len;
            if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
            {
                printf("accept socket err:%sn",strerror(errno));
                return -1; 
            }
            printf("accpet fd:%dn",connfd);
            fds[connfd].fd = connfd;
            fds[connfd].events = POLLIN;

            if(connfd > max_fd) max_fd = connfd;
            if(--num == 0) continue;

        }
        for(int i = listenfd+1; i <= max_fd; i++)
        {
            printf("fd:%dn",i);
            if(fds[i].revents & POLLIN)
            {
                n = recv(i,buf,MAXLEN,0);
                if(n > 0)
                {
                    buf[n] = '';
                    printf("recv buf:%sn",buf);
                    fds[i].events = POLLOUT;
                }
                else if(n == 0)
                {
                    fds[i].fd = -1;
                    printf("close socket fd:%dn",i);
                    close(i);
                }
                if(--num == 0) break;
            }
            else if(fds[i].revents & POLLOUT)
            {
                printf("send fd:%dn",i);
                send(i,buf,n,0);
                //fds[i].events = 0;
                fds[i].events = POLLIN;
                if(--num == 0) break;
            }
            
            
            
        }
        
    }

由代码可以看出select和poll的实现方式相差不大,都是通过阻塞等待某一个fd或多个fd的事件触发后,轮询查找相应的fd进行操作。由于每次事件触发,都会去遍历一遍fd,从而导致多次的无用遍历,造成额外的cpu开销。

 接着上面收发快递的例子,select和poll的处理方式是由一个快递员统一管理收发快递,但没有告诉快递员某个快递应该送到哪一户,于是快递员只能一家一家的询问,很耗费快递员的精力,如果我们在收发快递的时候告诉快递员该往哪家送,那快递员的活就会轻松不少,这就是下面要实现的

epoll

        epoll把select、poll的主动轮询为被动通知,当有事件发生时,被动接收通知。每个事件都有一个关联的fd,这样每次的事件操作都是有意义的

printf("----waiting for client request----n");

    int epfd = epoll_create(1);
    struct epoll_event events[POLL_SIZE] = {0};
    struct epoll_event ev;
    
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;

    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);

    while(1)
    {
        int num = epoll_wait(epfd,events,POLL_SIZE,-1);
        if (num == -1) continue;
        for(int i = 0; i < num; i++)
        {
            int clientfd = events[i].data.fd;
            if(clientfd == listenfd)
            {
                struct sockaddr_in client;
                socklen_t len;
                if((connfd = accept(listenfd,(struct sockaddr*)&client,&len)) == -1)
                {
                    printf("accept socket err:%sn",strerror(errno));
                    return -1; 
                }
                printf("accpet fd:%dn",connfd);
                ev.data.fd = connfd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
            }
            else if(events[i].events & EPOLLIN)
            {
                n = recv(clientfd,buf,MAXLEN,0);
                if(n > 0)
                {
                    buf[n] = '';
                    printf("recv buf:%sn",buf);
                    ev.data.fd = clientfd;
                    ev.events = EPOLLOUT;
                    epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&ev);
                }
                else if(n == 0)
                {
                    ev.data.fd = clientfd;
                    ev.events = EPOLLHUP;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,clientfd,&ev);
                    close(clientfd);
                }
            }
            else if(events[i].events & EPOLLOUT)
            {
                printf("send fd:%dn",clientfd);
                send(clientfd,buf,n,0);
                ev.data.fd = clientfd;
                ev.events = EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_MOD,clientfd,&ev);
            }
        }
    }

epoll方法能有效的解决poll和select的一些缺陷,如fd数目限制,cpu开销过大的问题,在海量连接并发的时候起到很大作用。

总结

select、poll、epoll这三种都是io多路复用的用法,实际上在recv与send时还是堵塞的,本质上还是同步io,异步io的用法后续在更新

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/893072.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号