Linux/C++ I/O多路复用——select模型实现服务端Socket通信

目录

文件描述符简介

select模型理解

select缺点

select函数

函数原型

参数说明

 fd_set 相关

程序实例


文件描述符简介

        在进程中,每打开一个文件,操作系统就会创建相应的数据结构来描述这个文件,这就是描述文件的文件结构体,同时,在进程的PCB中,会有一个struct files_struct *files的指针,它实际上是指向一个指针数组,而这个指针数组中就存放了每个打开的文件的指针,而所谓的文件描述符,就是这个指针数组的下标,根据文件描述符就能找到对应的文件的指针,就能对该文件进行操作了。在文件描述符进行分配时,会找到当前没有被使用的最小的下标,作为新的文件描述符。其中,0,1,2分别对应标准输出文件描述符,标准输出文件描述符和标准错误文件描述符。因此当打开其他文件时,分配的文件描述符实际上是从3开始的。

select模型理解

         在前面提到的多进程和多线程来实现服务端与客户端通信中,不管是多进程还是多线程,一般来说,每一个连接成功的文件描述符(socket)都需要一个进程/线程来进行监控,然后就需要不断的对每个进程/线程中的文件描述符进行轮询,例如accept、read等函数,如果没有连接请求、缓冲区中没有数据,它们就会一直阻塞住。这样一来一回就在反复切换进程/线程,系统的开销是非常大的。这两种并发模型都需要进程或者线程自己去等待。

        还有一种就是I/O多路复用模型,select就是其中一种。之前多进程多线程是让进程和线程去阻塞等待,而现在则是直接让内核去等待,只需要主进程去直接询问内核哪些描述符准备好了即可。select模型的关键就是事先将感兴趣的文件描述符放到一个集合中,当用户进程调用了select,那么整个进程会被阻塞,而同时,内核就会“监视”所有select感兴趣的文件描述符,当任何一个文件描述符中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。

select缺点

1、内核中对select模型可监视的fd数量限制为1024,如果要修改这个数字就必须对内核重新编译;

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

select函数

函数原型

int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

参数说明

maxfdp:被监听的文件描述符的最大值大1,因为文件描述符是从0开始计数的;

readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。

timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间

timeval结构体定义如下:

struct timeval
{      
    long tv_sec;   /*秒 */
    long tv_usec;  /*微秒 */   
};

返回值:超时返回0;失败返回-1;成功返回大于0的整数,这个整数表示就绪描述符的数目。

 fd_set 相关

int FD_ZERO(int fd, fd_set *fdset);   //一个 fd_set类型变量的所有位都设为 0 (可理解为清空集合中的所有感兴趣描述符)
int FD_CLR(int fd, fd_set *fdset);  //清除某个位时可以使用 (可理解为清空集合中的某一个描述符)
int FD_SET(int fd, fd_set *fd_set);   //设置变量的某个位置位  (可理解为向集合中添加一个描述符)
int FD_ISSET(int fd, fd_set *fdset); //测试某个位是否被置位   (可理解为判断一个描述符是否在集合中)

程序流程

1.绑定、监听.....

2.创建集合,由于调用select函数时,传入的集合参数在函数返回后可能会改变,因此创建一个集合来保存所有感兴趣的描述符allset,再创建一个集合rset用来作为select的调用参数;再创建一个数组client用来存放所有有效的描述符,并初始化各项为-1;

3.将监听描述符lfd加入allset,此时最大描述符maxfd = lfd;

4.创建while循环,将allset赋值给rset,将rset作为读集合参数,调用select函数开始阻塞等待;

5.select函数返回后,先判断监听描述符lfd是否还存在于rset中,判断方式为if(FD_SET(lfd,&rset))。

6.如果判断为真,说明有新连接,则调用accept函数新连接的文件描述符并存在变量connfd中,然后再将connfd加入allset中和client数组中;

7.然后处理除监听描述符以外的描述符。遍历client数组,查看有效描述符是否发生了读事件,判断方式为if(FD_SET(client[i],&rset));如果判断为真,说明有数据传来,就进行read和write操作;

8.继续下一次循环....

程序实例

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>

using namespace std;

#define SERV_IP "127.1.2.3"
#define SERV_PORT 8888
#define MAX_CONN 1024

int main()
{
    sockaddr_in servaddr,clitaddr;
    sockaddr_in clit_info[MAX_CONN];  //存放成功连接的客户端地址信息
    int client[1024];   //存放成功连接的文件描述符
    char buf[1024];  //读写缓冲区
    int lfd;      //用于监听
    int connfd;   //连接描述符
    int readyfd;  //保存select返回值
    int maxfd = 0;  //保存最大文件描述符
    int maxi = 0;  //maxi反映了client中最后一个成功连接的文件描述符的索引
    socklen_t addr_len = sizeof(clitaddr);;

    fd_set allset;  //存放所有可以被监控的文件描述符
    fd_set rset;

    FD_ZERO(&allset);
    FD_ZERO(&rset);

    if((lfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
    {
        cout<<"creat socket fault : "<<strerror(errno)<<endl;
        return 0;
    }

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERV_IP);

    if(bind(lfd,(sockaddr *)&servaddr,sizeof(servaddr)) == -1)
    {
        cout<<"bind fault : "<<strerror(errno)<<endl;
        return 0;
    }

    if(listen(lfd,128) == -1)
    {
        cout<<"listen fault : "<<strerror(errno)<<endl;
        return 0;
    }

    maxfd = lfd;  //此时只用监控lfd,因此lfd就是最大文件描述符

    //初始化client数组
    for(int i=0;i<MAX_CONN;i++)client[i] = -1;

    FD_SET(lfd,&allset);

    cout<<"Init Success ! "<<endl;
    cout<<"host ip : "<<inet_ntoa(servaddr.sin_addr)<<"  port : "<<ntohs(servaddr.sin_port)<<endl;

    cout<<"Waiting for connections ... "<<endl;

    while(1)
    {
        rset = allset ;  //rset作为select参数时,表示需要监控的所有文件描述符集合,select返回时,rset中存放的是成功监控的文件描述符。因此在select前后rset是可能改变的,所以在调用select前将rset置为所有需要被监控的文件描述符的集合,也就是allset
        readyfd = select(maxfd+1,&rset,NULL,NULL,NULL); //服务端只考虑读的情况
        //执行到这里,说明select返回,返回值保存在readyfd中,表示有多少个文件描述符被监控成功
        if(readyfd == -1)
        {
            cout<<"select fault : "<<strerror(errno)<<endl;
            return 0;
        }

        if(FD_ISSET(lfd,&rset))  //监听描述符监控成功,说明有连接请求
        {
            int i=0;
            connfd = accept(lfd,(sockaddr *)&clitaddr,&addr_len);  //处理新连接,此时accept直接可以返回而不用一直阻塞
            if(connfd == -1)
            {
                cout<<"accept fault : "<<strerror(errno)<<endl;
                continue ;
            }
            cout<<inet_ntoa(clitaddr.sin_addr)<<":"<<ntohs(clitaddr.sin_port)<<" connected ...  "<<endl;
            //成功连接后,就将connfd加入监控描述符表中
            FD_SET(connfd,&allset);

            for(;i<MAX_CONN;i++)
            {
                if(client[i] == -1)
                {
                    client[i] = connfd;
                    clit_info[i] = clitaddr;
                    break;
                }
            }

            if(connfd>maxfd)maxfd = connfd;  //更新最大文件描述符
            if(i>maxi)maxi = i;

            readyfd --;
            if(readyfd == 0)continue;  //如果只有lfd被监控成功,那么就重新select
        }
        //处理lfd之外监控成功的文件描述符,进行轮询
        for(int i=0;i<=maxi;i++)
        {
            if(client[i] == -1)continue; //等于-1说明这个描述符已经无效
            if(FD_ISSET(client[i],&rset))   //在client数组中寻找是否有被监控成功的文件描述符
            {
                //此时说明client[i]对于的文件描述符监控成功,有消息发来,直接读取即可
                int readcount = read(client[i],buf,sizeof(buf));
                if(readcount == 0)  //对方客户端关闭
                {
                    close(client[i]);  //关闭描述符
                    FD_CLR(client[i],&allset);   //将该描述符从描述符集合中去除
                    client[i] = -1;  //相应位置置为-1,表示失效

                    cout<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<" exit ... "<<endl;
                }
                else if(readcount == -1)
                {
                    cout<<"read fault : "<<strerror(errno)<<endl;
                    continue;
                }
                else
                {
                    cout<<"(From "<<inet_ntoa(clit_info[i].sin_addr)<<":"<<ntohs(clit_info[i].sin_port)<<")";
		    for(int j=0;j<readcount;j++)cout<<buf[j];
		    cout<<endl;
                    for(int j=0;j<readcount;j++)buf[j] = toupper(buf[j]);
                    write(client[i],buf,readcount);
                }
                readyfd--;
                if(readyfd == 0)break;
            }
        }
    }
    close(lfd);
    return 0;
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章