当前位置:   article > 正文

I/O多路复用机制(一)_timeout.tv_sec = 5; timeout.tv_usec = 0;

timeout.tv_sec = 5; timeout.tv_usec = 0;

      在实际的开发中,我们经常会遇到这样的场景,我们需要接受多个端口的数据、多个终端的数据抑或是多个文件描述符对应的数据。那么,遇到这样的问题,你在程序中该怎么做呢?通常的做法,在程序中对数据交互的描述符进行轮询。那么问题来了,轮询的时间设置为多少呢?设置的太短,可以保证处理性能和速度,但是CPU的使用率太高,一旦处理的描述符数量多了起来,CPU可能就扛不住了。设置的时间太长,描述符处理的时间片太短,处于空闲的时间较长,性能和速度达不到要求。如果是服务器的话,面对多个用户的连接,处理速度和CPU使用性能是必须考虑的,而且最好要兼顾。这里就需要使用到I/O多路复用机制,这就是博主即将要和小伙伴们探讨的内容。


select简介

#include <sys/select.h>

#include <sys/time.h>

 

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

maxfdp1:待监控的最大描述符数值加1。

readset、writeset和exceptset:指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。

struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

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

         void FD_SET(int fd, fd_set*fdset);   //将一个给定的文件描述符加入集合之中

         void FD_CLR(int fd, fd_set *fdset);  //将一个给定的文件描述符从集合中删除

         int FD_ISSET(int fd, fd_set *fdset);  // 检查集合中指定的文件描述符是否就绪

fd_set定义如下:

typedefstructfd_set {

   u_int fd_count;           //fd_set中监听的文件描述符个数

   intfd_array[FD_SETSIZE];  //存放了要监听的文件描述符

} fd_set;

timeout:告知内核等待轮询的时间。其timeval结构用于指定这段时间的秒数和微秒数。

        struct timeval{

                  long tv_sec;   //seconds

                  long tv_usec;  //microseconds

       };

这个参数有三种可能:

(1)永远等待下去:直到有至少一个描述字准备就绪才返回。将timeout设置为NULL。

(2)等待一段固定时间:在超时前,有一个描述字准备就绪就返回。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须大于0。

(3)不等待:检查描述符后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

返回值:就绪描述符的数目,超时返回0,出错返回-1

 

原理图

       select的工作模式:每次调用select,都需要把fd集合从用户态拷贝到内核态,在内核中完成轮询的工作,待轮询结束再将fd集合从内核态拷贝到用户态。注意,每次调用select前都要重新设置文件描述符和时间,因为事件发生后,文件描述符和时间都被内核做了修改。我们可以将需要监控的描述符(包括文件、终端、套接字等等)添加到fd_set集合中,由select来监控,这样可以将多处阻塞转移到一处。例如,既要接收终端数据录入,又要接收socket(阻塞socket)传递过来的数据,那么在socket数据接收(read)和终端I/O(fgets)处均会阻塞。使用select后就不一样了,我们可以将这些描述符加入fd_set集合中,阻塞的地方只有一处,就是select调用处。

程序实例

       下面我们通过select实现一个简单的服务器回射实例,服务端监听客户端的连接请求,将已连接客户端描述符添加到监控描述符集合中,select对监听描述符和已连接客户端描述符进行监控。当监听描述符就绪,表示有新客户端连接服务端,调用处理连接逻辑;当有客户端描述符就绪,标识符已连接客户端有数据发送给服务器,服务端调用数据接收逻辑,将客户端发送过来的数据原样发送给客户端。好了,废话不多说了,直接看代码吧!

服务端代码

echo_svr.h

#include <stdio.h>

#include <arpa/inet.h>

#include <sys/select.h>

#include <time.h>

#include <unistd.h>

#include <sys/socket.h>

#include <error.h>

#include <sys/types.h>

#include <stdlib.h>

#include <string.h>

 

const int MAXFD = FD_SETSIZE;//FD_SETSIZE 1024 可以监控的最大描述符数量

const char *SERVERIP = "127.0.0.1";//服务端IP

const unsigned short PORT = 6666;//服务端端口号

 

typedef struct server_st

{

    int     cli_num;        //已连接客户端数量

    int     cli_fd[MAXFD-1];//存放已连接客户端描述符

    int    maxfd;          //存放描述符最大值

    int     index;          //保存最大的索引号(已连接客户端数据下标)

    fd_set  allset;         //监控的文件描述符集合(select参数)

    fd_set  set;            //监控的文件描述符集合(中间值)

    int     ready;          //已就绪描述符数量(select返回值)

}server_st_t;

 

//打印出错信息并推出程序

#define handle_error(msg)\

    do{perror(msg); exit(EXIT_FAILURE);}while(0)

 

//定义类

class selectsocket

{

    public:

        //构造函数

       selectsocket(const char *server_ip = SERVERIP, unsigned short port =PORT);

        //析构函数

        virtual~selectsocket();

 

        //客户处理函数(对外接口)

        inthandle_cli_proc();

 

 

    private:

        intserver_init();//初始化server_st_t结构体

        intserver_uninit();//释放server_st_t结构体

        inthandle_create_proc();//创建服务端监听套接字

        int handle_accept_proc();//处理客户端连接

        inthandle_recv_proc();//处理客户端数据发送

 

       selectsocket(const selectsocket &ref);

       selectsocket& operator=(const selectsocket &ref);

    private:

       server_st_t     *m_server_st;

       char            m_server_IP[16];

       unsigned short  m_port;

       int             m_server_fd;

};

 

selectsocket::selectsocket(const char *server_ip /*=SERVERIP*/, unsigned short port /*= PORT*/)

{

   bzero(m_server_IP, sizeof(m_server_IP));//将类成员地址空间清零

   memcpy(m_server_IP, server_ip, strlen(server_ip));//给类成员负值

    m_port =port;

    m_server_st= NULL;

 

   server_init();//初始化

   handle_create_proc();//创建监听套接字

}

 

selectsocket::~selectsocket()

{

    int cli_fd= -1;

    for (int i= 0; i < MAXFD; ++i)

    {

        cli_fd= m_server_st->cli_fd[i];

 

        if (-1!= cli_fd)

        {

           close(cli_fd);

        }

    }

 

   server_uninit();

   close(m_server_fd);

}

 

int selectsocket::server_init()

{

    m_server_st= (server_st_t*)malloc(sizeof(server_st_t));

 

    if (NULL ==m_server_st) handle_error("malloc");

 

   bzero(m_server_st, sizeof(server_st_t));

   //memset(m_server_st->cli_fd, 0xFF, sizeof(m_server_st->cli_fd));

   

    //初始化描述符数组

    for (inti=0; i<MAXFD; ++i)

    {

       m_server_st->cli_fd[i] = -1;

    }

 

   m_server_st->index = -1;//初始索引号

 

   FD_ZERO(&m_server_st->allset);//情况集合

   //FD_ZERO(&m_server_st->set);

 

    return 0;

}

 

int selectsocket::server_uninit()

{

    if (NULL !=m_server_st)

    {

       free(m_server_st);

    }

 

    m_server_st= NULL;

}

 

int selectsocket::handle_create_proc()

{

    //创建TCP套接字

    //参数1:协议族

    //参数2:套接字类型

    //参数3:使用的协议(0:使用套接字类型对应的默认协议)

    if (-1 ==(m_server_fd = socket(AF_INET, SOCK_STREAM, 0)))handle_error("socket");

 

    //地址结构体

    structsockaddr_in serveraddr;

    //将结构体变量空间清零

   bzero(&serveraddr, sizeof(serveraddr));

   serveraddr.sin_family = AF_INET;//协议族

    //注意,设置端口和IP时,要将主机字节序转换为网络字节序

   serveraddr.sin_port = htons(m_port);

   //serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

   inet_pton(AF_INET, m_server_IP, &serveraddr.sin_addr);

   

    int op =true;

    ///*一个端口释放后会等待两分钟之后才能再被使用(TIME_WAIT状态),SO_REUSEADDR是让端口释放后立即就可以被再次使用*/

    if (-1 ==setsockopt(m_server_fd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op)))handle_error("setsockopt");

 

    //命名套接字

    if (-1 ==bind(m_server_fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)))handle_error("serveraddr");

 

    //将套接字由主动变为被动(接受客户端连接状态)

    if (-1 ==listen(m_server_fd, SOMAXCONN)) handle_error("listen");

 

    return 0;

}

 

int selectsocket::handle_accept_proc()

{

    //地址结构体

    structsockaddr_in cli_addr;

    socklen_tlen = (socklen_t)sizeof(cli_addr);

    //将结构体变量空间清零

   bzero(&cli_addr, sizeof(cli_addr));

 

    //接受客户端的连接请求

    int cli_fd= accept(m_server_fd, (struct sockaddr*)&cli_addr, &len);

   

    if (-1 ==cli_fd) handle_error("accept");

 

    fprintf(stdout,"#%d  %s:%d  connected server!\n",m_server_st->cli_num, inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port));

 

    int i = 0;

    //将新链接的客户存入客户端数组中

    for (i = 0;i < MAXFD; ++i)

    {

        if (-1== m_server_st->cli_fd[i])

        {

            //将已连接套接字描述符存入数组

           m_server_st->cli_fd[i] = cli_fd;

 

           //m_server_st->maxfd保存值最大的已连接套接字描述符

           m_server_st->maxfd = m_server_st->maxfd > cli_fd ?m_server_st->maxfd : cli_fd;

 

           //m_server_st->index保存客户数组中已连接套接字描述符的最大索引

           m_server_st->index = m_server_st->index > i ?m_server_st->index : i;

 

            //将套接字描述符关联到集合中

           //FD_SET(cli_fd, &m_server_st->allset);

 

           break;

        }

    }

 

    //如果已连接套接字描述符超过了FD_SETSIZE报错

    if (i ==FD_SETSIZE) handle_error("too many connect");

 

   ++m_server_st->cli_num;//已连接客户数加1

   //m_server_st->set = m_server_st->allset;

}

 

int selectsocket::handle_recv_proc()

{

    //如果数组中不至当前一个用户,遍历数组

    for (int i= 0; i <= m_server_st->index; ++i)

    {

        //如果为空位置,continue

        if (-1== m_server_st->cli_fd[i])

        {

           continue;

        }

 

        //如果非空位置,cli_fd保存当前描述符

        intcli_fd = m_server_st->cli_fd[i];

 

        //如果当前描述符就绪,执行以下代码

        if(FD_ISSET(cli_fd, &m_server_st->allset))

        {

           char buf[256] = {0};

 

            //从网络中读取数据

            intr = read(cli_fd, buf, sizeof(buf));

 

            //如果读取出错

            if(r <= 0)

            {

               //在数组中将当前位置设为空位置

               m_server_st->cli_fd[i] = -1;

 

               //将当前描述符从集合中清除

               FD_CLR(cli_fd, &m_server_st->allset);

 

               //关闭当前描述符

               close(cli_fd);

 

               --m_server_st->cli_num;//已连接客户数减1

            }

 

            //如果接受成功,将数据写回网络

           write(cli_fd, buf, sizeof(buf));

 

            //清空缓存区

           memset(buf, 0x00, sizeof(buf));

        }

 

        //每处理一个描述符,read减1

        //如果read为0(所有就绪描述符都处理完毕),就不用继续向后扫描

        //if(--m_server_st->ready <= 0) break;

    }

 

    return 0;

}

 

int selectsocket::handle_cli_proc()

{

    while (1)

    {

        /*每次调用select前都要重新设置文件描述符集合和超时时间,因为事件发生后,文件描述符和时间都被内核修改啦*/

       FD_ZERO(&m_server_st->allset);

        //将监听套接字描述符装入描述符集合

       FD_SET(m_server_fd, &m_server_st->allset);

 

        //保存最大的描述符

       m_server_st->maxfd = m_server_fd;

       

        //设置超时时间为5秒

        structtimeval timeout;

       timeout.tv_sec = 5;

       timeout.tv_usec = 0;

       

        //更新集合

       //m_server_st->set = m_server_st->allset;

 

        //将已连接客户端描述符添加到集合中

        for(int i = 0; i<=m_server_st->index; ++i)

        {

            intcli_fd = m_server_st->cli_fd[i];

            if(-1 != cli_fd)

            {

                FD_SET(cli_fd,&m_server_st->allset);

            }

 

           //m_server_st->maxfd保存值最大的已连接套接字描述符

           m_server_st->maxfd = m_server_st->maxfd > cli_fd ?m_server_st->maxfd : cli_fd;

 

        }

 

       //select阻塞等待描述符集合中是否有就绪的描述符

        //参数1:描述符集合中最大描述符值加1

        //参数2:读就绪集合

        //参数3:写就绪集合

        //参数4:异常就绪集合

        //参数5:最长等待时间(NULL:无限等待,直到有描述符就绪)

        //一旦有描述符就绪则返回,返回值为以就绪描述符的个数

       m_server_st->ready = select(m_server_st->maxfd+1,&m_server_st->allset, NULL, NULL, &timeout);

 

       //select函数返回异常

        if (-1== m_server_st->ready) break;

 

       //select函数等待超时

        if (0== m_server_st->ready)

        {

           fprintf(stdout, "select timeout\n");

           continue;

        }

 

        //如果有客户断连接服务器,监听套接字就绪,执行以下代码

        if(FD_ISSET(m_server_fd, &m_server_st->allset))

        {

            //处理客户端的连接

           handle_accept_proc();

            //已经处理过来sfd描述符,read减1

            //如果此时read<=0,表示所有就绪描述符已处理完毕,返回select处继续等待就绪描述符

            if (--m_server_st->ready <= 0)

            {

               continue;

           } 

        }

        else

        {

            //处理客户端发送过来的数据

           handle_recv_proc();

        }

 

    }

 

    return 0;

}

 

 

main.cpp

#include "echo_svr.h"

 

int main()

{

   selectsocket selectsock;

   selectsock.handle_cli_proc();

}

 

客户端代码

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/socket.h>

#include <string.h>

 

int main()

{

    //创建IPV4 TCP套接字

    int sfd =socket(AF_INET, SOCK_STREAM, 0);

 

    if (-1 ==sfd) perror("socket"), exit(EXIT_FAILURE);

 

    //地址结构体

    structsockaddr_in addr;

   addr.sin_family = AF_INET;//协议族

   addr.sin_port = htons(6666);//端口

 

    //将地址串转换为网络字节序,存储到addr.sin_addr中

   inet_aton("127.0.0.1", &addr.sin_addr);//连接的服务端IP地址

 

    //连接服务器

    if (-1 ==connect(sfd, (struct sockaddr *)&addr, sizeof(addr)))

    {

       perror("connect");

       exit(EXIT_FAILURE);

    }

 

    charbuf[256] = {};

 

    //从标准输入读取数据

    while (NULL!= fgets(buf, sizeof(buf), stdin))

    {

        //将数据发送给服务器

       write(sfd, buf, strlen(buf));

       

        //清空缓存区

       memset(buf, 0x00, sizeof(buf));

 

        //读取服务器放送过来的数据

        int r =read(sfd, buf, sizeof(buf));

 

        //接受失败

        if (r<= 0)

        {

           break;

        }

 

        //输出

       fprintf(stdout, buf, r);

 

        //清空缓存区

       memset(buf, 0x00, sizeof(buf));

    }

 

    //关闭套接字描述符

    close(sfd);

 

    return 0;

}

 

程序运行截图:

 

select缺陷

       说到这里,我们来谈一谈select的缺陷吧,主要有一下三点。

①每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

②同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

③select支持的文件描述符数量太少了,默认是1024

 

     关于select、poll和epoll的对比,请观看博主的另外一篇博文poll epoll select,在那里博主对他们之间的区别、性能和消息传递方式进行了总结,希望能对你有一点帮助。最后附上一张select的实现结构图,如果想了解跟多关于select的实现原理和源码剖析,请参考文末连接。

参考:select实现分析



声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/225158
推荐阅读
相关标签
  

闽ICP备14008679号