linux网络编程---IO多路复用

IO模型:

阻塞等待模型(BIO):

1)优点:不占用CPU时间片,阻塞时,cpu时间片交给别人,缺点:但同时只能处理同一个操作。

2)可以使用多线程/多进程实现并发处理多个客户端请求,每个线程对应一个客户端,线程里进行读写操作,主线程则可以继续运行监听客户端。  (但每个线程里 其实也会存在阻塞问题)

while(1)

{

        accept(lfd,...);  (blocking)

        create thread->read/write; (blocking)

}

但是缺点:a.线程/进程会消耗资源,b.调度线程/进程会消耗CPU资源。

非阻塞IO模型:(忙轮询 NIO)

1)accept read write 非阻塞,没读到继续往下执行,不断轮询。优点:提高了程序执行效率。缺点:需要占用更多的CPU和系统资源。

while(1)

{

        accept(lfd,...);

       读写现有的已accept的fd, 遍历轮询read/write;  ->浪费系统资源。

}

2)使用IO多路复用技术来改进。

IO多路复用

使程序能够同时监听多个文件描述符(监听内核缓冲区),提高程序性能。(可以判断同时有多少客户端来和我通信)。(告诉内核要监听的fd集合,而非自己对所有fd轮询read write).

select/poll

1.select

但是select poll内核不会告诉使用者具体是哪些快递到了,只会告诉总数。底层使用二进制位表示某个fd有无数据,相比read write判断有无数据效率更高。

1. 将要监听的fd添加到fd列表。

2. 调用select(),内核会监听列表中的fd,当有某个/多个fd进行IO操作,函数返回。 (阻塞函数)

3. 返回时,会告诉进程有多少fd要进行IO操作。

select()将我们想监听的读写异常fd交给内核去检测。 (fd集合最大1024个)

对fd集合二进制位的操作

 工作过程

 readfds等是指针,指明要检测的fd的集合,如0010.内核检测后会将其改变为检测到的结果。如第二个有数据可以读,第3个没有,则修改为0100;

缺点:

1)每次调用select,要将fds从用户态拷贝到内核态。fds很大时开销会很大。

2)每次调用select,内核要对fds进行遍历。fds很大时开销会很大。再拷贝回用户态,也开销很大。 用户态又要遍历判断是哪些fd发生了改变。 O(n)

3)select支持的fd最大数量1024,(fd_set 数组大小128字节,1024位)太小(最多支持1024客户端)。

4)fd集合不能重用(被内核修改了),为了下次检测每次都需要重置。

代码实现: 

client.cpp

#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
using namespace std;

int main()
{
    
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd==-1)
    {
        perror("socket:");
        exit(-1);
    }
    sockaddr_in serveraddr;
    inet_pton(AF_INET,"127.0.0.1",&serveraddr.sin_addr.s_addr);
    serveraddr.sin_port=htons(9090);
    serveraddr.sin_family=AF_INET;
    int ret=connect(fd,(sockaddr*)&serveraddr,sizeof(serveraddr));
    if(ret==-1)
    {
        perror("connect: ");
        exit(-1);
    }

    int n=0;
    while(1)
    {
        if(n<9)    
            n++;
        else 
        n=0;
        
        
        char sendBuf[50]="hello,this is ";
        sendBuf[14]=(char)(n+'0');
        
        write(fd,sendBuf,strlen(sendBuf));
        
        char readBuf[80]="client received ";
        
        int len=read(fd,sendBuf,sizeof(sendBuf));
        if(len==0)
        {
            cout<<"server closed\n";
            break;
        }
        else if(len==-1)
        {
            perror("read: ");
            exit(-1);
        }
        strcat(readBuf,sendBuf);
        cout<<readBuf<<endl;
       sleep(2);
    }
    close(fd);

}

select.cpp

#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/select.h>
#include <unistd.h>
#include <string.h>
#include <iostream>

int main()
{
    int lfd=socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port=htons(9090);
    saddr.sin_family=AF_INET;
    saddr.sin_addr.s_addr=INADDR_ANY;

    int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
    if(ret==-1)
    {
        perror("bind");
        exit(-1);
    }
    ret=listen(lfd,8);
    if(ret==-1)
    {
        perror("listen");
        exit(-1);
    }

    fd_set rdset,tmp_set;//要检测的fd集合
    FD_ZERO(&rdset);//初始化
    FD_SET(lfd,&rdset);//添加要检测的fd
    int maxfd=lfd;

    while(1)
    {
        tmp_set=rdset; //使内核修改fd不影响我们要检测的fd
        //让内核检测fd集合 永久阻塞,直到有变化
        int ret=select(maxfd+1,&tmp_set,nullptr,nullptr,nullptr);
        if(ret==-1)
        {
            perror("select");
            exit(-1);
        }
        //检测到有fd对应的缓冲区的数据发生了改变。
        
        //判断监听的lfd 是否有客户端要连接
        if(FD_ISSET(lfd,&tmp_set))
        {
            sockaddr_in clientaddr;
            int len=sizeof(clientaddr);
            int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);

            //将通信fd加入rdset。
            FD_SET(cfd,&rdset);
            maxfd = maxfd>cfd ? maxfd : cfd;           
        }
        //遍历其他的cfd,判断是否有数据读 从监听的lfd+1开始遍历。
        for(int i=lfd+1;i<maxfd+1;i++)
        {
            if(FD_ISSET(i,&tmp_set))
            {
                char buf[1024]={0};
                int len=read(i,buf,sizeof(buf));
                if(len==-1)
                {
                    perror("read");
                    exit(-1);
                }   
                else if(len==0)
                {
                    printf("%d client closed\n",i); //客户端断开,将其从fd集合删去
                    close(i);
                    FD_CLR(i,&rdset);
                }
                else
                {
                    std::cout<<"read buf= "<<buf<<" from "<<i<<std::endl;
                    write(i,buf,strlen(buf)+1);
                }

            }
        }
    }
    close(lfd);

}

2.poll  

通过events和revents,内核修改时会修改revents,使得events可以重用。并且没有1024的限制。

 

 

 缺点:

1)基本和select一致,但是没有1024最大连接数的限制。(基于链表存储)

2)events集合可重用。  

3)水平触发。如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

代码实现

#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/poll.h>
#include <unistd.h>
#include <string.h>
#include <iostream>

int main()
{
    int lfd=socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port=htons(9090);
    saddr.sin_family=AF_INET;
    saddr.sin_addr.s_addr=INADDR_ANY;

    int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
    if(ret==-1)
    {
        perror("bind");
        exit(-1);
    }
    ret=listen(lfd,8);
    if(ret==-1)
    {
        perror("listen");
        exit(-1);
    }

    pollfd fdset[2048]; //要监听的fd集合结构体
    for(int i=0;i<2048;i++)
    {
        fdset[i].fd=-1;
        fdset[i].events=POLLIN;//检测读事件
    }
    fdset[0].fd=lfd; //将监听lfd加入集合。

    int maxfd=0;

    while(1)
    {
       
        //让内核检测fd集合 阻塞,直到有变化
        int ret=poll(fdset,maxfd+1,-1);
        if(ret==-1)
        {
            perror("poll");
            exit(-1);
        }
        //检测到有fd对应的缓冲区的数据发生了改变。
        
        //判断监听的lfd 是否有客户端要连接
        if(fdset[0].revents & POLLIN)
        {
            sockaddr_in clientaddr;
            int len=sizeof(clientaddr);
            int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);

            //将通信fd加入rdset。  遍历是为了重用在连接过程中已使用又断开的fd位置。
            for(int i=1;i<2048;i++)
            {
                if(fdset[i].fd==-1)
                {
                    fdset[i].fd=cfd;
                    fdset[i].events=POLLIN;
                    maxfd = maxfd>i ? maxfd : i;  //更新最大的fd索引
                    break;
                }
            }
                    
        }
        //遍历其他的cfd,判断是否有数据读 从监听的lfd+1开始遍历。
        for(int i=1;i<maxfd+1;i++)
        {
            if(fdset[i].revents&POLLIN)
            {
                char buf[1024]={0};
                int len=read(fdset[i].fd,buf,sizeof(buf));
                if(len==-1)
                {
                    perror("read");
                    exit(-1);
                }   
                else if(len==0)
                {
                    printf("%d client closed\n",fdset[i].fd); //客户端断开,将其从fd集合删去
                    close(fdset[i].fd);
                    fdset[i].fd=-1;
                }
                else
                {
                    std::cout<<"read buf= "<<buf<<" from "<<fdset[i].fd<<std::endl;
                    write(fdset[i].fd,buf,strlen(buf)+1);
                }

            }
        }
    }
    close(lfd);

}

epoll

 用epoll_create直接在内核创建一个epoll实例eventpoll,通过epoll_create返回的fd控制这个实例。减少select poll将fd集合从用户态拷贝到内核态的切换。

 rbr 记录需要检测的fd,  底层红黑树,遍历效率非常快。

 rdlist 就绪fd的链表。可以告诉用户具体改变的fd。

epoll_ctl 管理epoll实例

epoll_data中的fd可以在遍历变化的fd作判断时用到

 

 

 代码实现

#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <iostream>

int main()
{
    int lfd=socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port=htons(9090);
    saddr.sin_family=AF_INET;
    saddr.sin_addr.s_addr=INADDR_ANY;

    int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
    if(ret==-1)
    {
        perror("bind");
        exit(-1);
    }
    ret=listen(lfd,8);
    if(ret==-1)
    {
        perror("listen");
        exit(-1);
    }

    //创建eventpoll实例 参数>0就可以
    int epfd=epoll_create(100);

    //将监听Lfd相关信息加入epfd实例
    epoll_event epev;
    epev.events=EPOLLIN;
    epev.data.fd=lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

    //被监听后,发生改变的fd集合
    epoll_event epevs[1024];

    while(1)
    {
        //让内核检测epev实例 阻塞,直到有变化
        int ret=epoll_wait(epfd,epevs,1024,-1);
        
        if(ret==-1)
        {
            perror("epoll_wait");
            exit(-1);
        }
        //检测到有ret个fd对应的缓冲区的数据发生了改变。
        printf("ret = %d\n",ret);

        
        
        for(int i=0;i<ret;i++)
        {
            //判断监听的lfd 是否有客户端要连接  这里就用到了epoll_data中的fd
            if(epevs[i].data.fd==lfd)
            {
                sockaddr_in clientaddr;
                int len=sizeof(clientaddr);
                int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);
                epev.events=EPOLLIN;
                epev.data.fd=cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);    
            }
            else //cfd 有数据到达,需要通信
            {
              //  if(epev[i].data.fd & EPOLLOUT)
               // {
                //    continue;
                //}
                //有数据可读
                char buf[1024]={0};
                int len=read(epevs[i].data.fd,buf,sizeof(buf));
                if(len==-1)
                {
                    perror("read");
                    exit(-1);
                }   
                else if(len==0)
                {
                    printf("%d client closed\n",epevs[i].data.fd); //客户端断开,将其从fd集合删去
                    close(epevs[i].data.fd);
                    epoll_ctl(epfd,EPOLL_CTL_DEL,epevs[i].data.fd,nullptr);
                }
                else
                {
                    std::cout<<"read buf= "<<buf<<" from "<<epevs[i].data.fd<<std::endl;
                    write(epevs[i].data.fd,buf,strlen(buf)+1);
                }

            }
        }
    }
    close(lfd);
    close(epfd); //关闭epoll实例

}

epoll的工作模式:

1) LT (默认)

水平触发,支持阻塞和非阻塞socket。内核通知用户某fd就绪后,用户未操作或只操作了一部分数据,下次epoll_wait仍会通知用户该fd就绪,直到用户操作完缓冲区数据,fd变为未就绪。

2)ET

边沿触发,只支持非阻塞socket。内核通知用户某fd就绪后,用户未操作或只操作了一部分数据,下次epoll_wait不会通知用户该fd就绪,直到用户操作后使fd变为非就绪,下次fd再次就绪才会通知用户。

所以在用户操作该fd时,需要循环读取完其所有数据,否则数据未读完下次不被通知就会一直处理不到。在读取过程中,为了不断read时不会阻塞,ET只支持非阻塞SOCKET。

优点:减少了epoll事件被重复触发的次数,效率高于LT。

 

#include <iostream>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <cstring>
using namespace std;

int main()
{
    
    int fd=socket(AF_INET,SOCK_STREAM,0);
    if(fd==-1)
    {
        perror("socket:");
        exit(-1);
    }
    sockaddr_in serveraddr;
    inet_pton(AF_INET,"127.0.0.1",&serveraddr.sin_addr.s_addr);
    serveraddr.sin_port=htons(9090);
    serveraddr.sin_family=AF_INET;
    int ret=connect(fd,(sockaddr*)&serveraddr,sizeof(serveraddr));
    if(ret==-1)
    {
        perror("connect: ");
        exit(-1);
    }

    int n=0;
    while(1)
    {
        // if(n<9)    
        //     n++;
        // else 
        // n=0;
        
        
        // char sendBuf[50]="hello,this is ";
        // sendBuf[14]=(char)(n+'0');
        char sendBuf[1024]={0};
        fgets(sendBuf,sizeof(sendBuf),stdin);
        
        write(fd,sendBuf,strlen(sendBuf)+1);
        
        char readBuf[80]="client received ";
        
        int len=read(fd,sendBuf,sizeof(sendBuf));
        if(len==0)
        {
            cout<<"server closed\n";
            break;
        }
        else if(len==-1)
        {
            perror("read: ");
            exit(-1);
        }
        strcat(readBuf,sendBuf);
        cout<<readBuf<<endl;
        
    }
    close(fd);

}
#include <sys/types.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <fcntl.h>
#include <errno.h>
int main()
{
    int lfd=socket(PF_INET,SOCK_STREAM,0);
    struct sockaddr_in saddr;
    saddr.sin_port=htons(9090);
    saddr.sin_family=AF_INET;
    saddr.sin_addr.s_addr=INADDR_ANY;

    int ret=bind(lfd,(sockaddr*)&saddr,sizeof(saddr));
    if(ret==-1)
    {
        perror("bind");
        exit(-1);
    }
    ret=listen(lfd,8);
    if(ret==-1)
    {
        perror("listen");
        exit(-1);
    }

    //创建eventpoll实例 参数>0就可以
    int epfd=epoll_create(100);

    //将监听Lfd相关信息加入epfd实例
    epoll_event epev;
    epev.events=EPOLLIN;
    epev.data.fd=lfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);

    //被监听后,发生改变的fd集合
    epoll_event epevs[1024];

    while(1)
    {
        //让内核检测epev实例 阻塞,直到有变化
        int ret=epoll_wait(epfd,epevs,1024,-1);
        
        if(ret==-1)
        {
            perror("epoll_wait");
            exit(-1);
        }
        //检测到有ret个fd对应的缓冲区的数据发生了改变。
        printf("ret = %d\n",ret);

        
        
        for(int i=0;i<ret;i++)
        {
            //判断监听的lfd 是否有客户端要连接  这里就用到了epoll_data中的fd
            if(epevs[i].data.fd==lfd)
            {
                sockaddr_in clientaddr;
                int len=sizeof(clientaddr);
                int cfd=accept(lfd,(sockaddr*)&clientaddr,(socklen_t*)&len);
                printf("new client\n");
                //设置cfd非阻塞
                int flag=fcntl(cfd,F_GETFL);
                flag|=O_NONBLOCK;
                fcntl(cfd,F_SETFL,flag);
                 //设置ET模式
                epev.events=EPOLLIN|EPOLLET;
                epev.data.fd=cfd;
                epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);    
            }
            else //cfd 有数据到达,需要通信
            {

                char buf[5]={0};
                int len=0;
                //循环读取所有数据
                while((len=read(epevs[i].data.fd,buf,sizeof(buf)))>0)
                {
                    std::cout<<"read buf= "<<buf<<" from "<<epevs[i].data.fd<<std::endl;
                    write(epevs[i].data.fd,buf,strlen(buf)+1);
                }
                if(len==-1)
                {
                    if(errno==EAGAIN) //数据读完了
                        printf("data over\n");
                    else
                    {
                        perror("read");
                        exit(-1);
                    }                 
                }   
                else if(len==0)
                {
                    printf("%d client closed\n",epevs[i].data.fd); //客户端断开,将其从fd集合删去
                    close(epevs[i].data.fd);
                    epoll_ctl(epfd,EPOLL_CTL_DEL,epevs[i].data.fd,nullptr);
                }

            }
        }
    }
    close(lfd);
    close(epfd); //关闭epoll实例

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值