渣渣兔 发表于 2025-3-7 20:23:12

【Linux网络编程】高效I/O--select/poll服务器

目录

多路转接之select
select服务器实现
获取毗连
handlerEvent
select服务器代码链接 
select的优缺点 
多路转接之poll 
poll服务器实现(select服务器改写) 
poll的优缺点

多路转接之select

   select的作用
I/O的本质 = 等 + 拷贝
多路转接就是通过同时等候多个文件描述符的方式实现效率的提升,这些在五种I/O模子中都详细说过!
select是实现多路复用I/O的一种方式
一个多路复用I/O可以分为两步:


[*]同时等候多个文件描述符
[*]I/O条件就绪,直接进行拷贝
select完成的工作就是I/O等候这一步,即对多个文件描述符进行等候,若有一个或多个文件描述符读写条件就绪,select就会返回!
select返回后,即进行多路复用的第二步拷贝。拷贝是由read/write...这些拷贝函数实现的!
   select调用
int select(int nfds, fd_set *readfds,
fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);


[*]nfds:表示你需要监管的全部的文件描述符中,最大的文件描述符+1
[*]readfds:指向fd_set的指针,包罗需要监督的可读文件描述符
[*]writefds:指向fd_set的指针,包罗需要监督的可写文件描述符(可选)。
[*]exceptfds:指向fd_set的指针,包罗需要监督的非常文件描述符(可选)。
[*]timeout:指定select进行等候时的等候规则
[*]返回值:若大于0,则表示实际I/O条件就绪的文件描述符的个数。若便是0,则表示超时。若小于0,则表示select发生错误,错误码被设置
[*]头文件:sys/select.h
以下是对select参数的详细先容 
   timeout参数 
 struct timeval范例的结构:
struct timeval {
    long tv_sec;   // 秒数
    long tv_usec;// 微秒数
};

[*]若tv_sec设置为0,且tv_usec设置为0,则表示的是select进行非阻塞等候。即便没有任何一个文件描述符就绪,select仍旧会返回0
[*]若timeout参数设置为NULL,则表示select进行阻塞等候,在没有任何外部干扰(如信号中断)的环境下,若没有任何一个文件描述符就绪,则select永不返回,直到有一个或多个文件描述符就绪为止
[*]若timeout中的tv_sec或tv_usec设置为>0的数,此时的timeout是一个输入和输出型参数。比方timeout设置为5秒0微秒之后传入select,过了两秒以后有一个文件描述符就绪了,那么select返回,timeout被修改为3秒0微秒
   fd_set范例 
读写条件就绪:


[*]若一个文件描述符可以进行读了,即该文件的内核接收缓冲区有数据了,我们称该文件描述符的读条件就绪
[*]若一个文件描述符可以进行写了,即该文件的内核发送缓冲区未满,我们称该文件描述符的写条件就绪
fd_set内部是一个对应文件描述符的位图结构
select参数中的fd_set范例参数寄义:


[*]readfds:该文件描述符集中比特位的值为1的文件描述符,select只关心该文件描述符的读条件是否就绪
[*]writefds:该文件描述符集中比特位的值为1的文件描述符,select只关心该文件描述符的写条件是否就绪
[*]exceptfds:该文件描述符集中比特位的值为1的文件描述符,select只关心该文件描述符的非常条件是否就绪
[*]若需要select同时关心一个文件描述符的读写条件,即把readfds和writefds对应位置的值同时设置为1
[*]若无需select关心非常时间,我们把exceptfds设置为空即可!
select参数中的三个文件描述符集都是便是输入型参数,又是输出型参数


[*]以readfds为例
[*]当readfds作为输入型参数的时候,表示的寄义是:用户告诉内核需要关心readfds中全部的fd的读事件
[*]当readfds作为输出型参数的时候,表示的寄义是:内核告诉用户需要关心的文件描述符中,有哪些已经就绪了
fd_set输入输出时位图的寄义:


[*]当作为输入型参数的时候,比特位的位置表示的是第几号fd,比特位的值表示是否需要内核关心的文件描述符的xx事件。若比特位的值为0表示无需内核关心该文件描述符的xx事件,若比特位的值为1表示需要内核关心该文件描述符的xx事件
[*]当作为输出型参数的时候,比特位的位置表示的是第几号fd,比特位的值表示需要内核关心的文件描述符中,哪些已经就绪了。若比特位的值为0表示未关心或未就绪,若比特位的值为1表示已就绪
   文件描述符集的操作
对fd_set的操作只允许使用体系调用,不允许自己去改,哪怕你已经知道了它是个位图
文件描述符集的头文件是:sys/select.h
清空(全部比特位清零)文件描述符集:FD_ZERO
void FD_ZERO(fd_set *set);


[*]功能:把set文件描述符集中的全部比特位清零 
清零文件描述符集中指定fd的值:FD_CLR
void FD_CLR(int fd, fd_set *set);


[*]功能:把set集中fd对应的比特位清零 
判定文件描述符集中指定fd的值:FD_ISSET
intFD_ISSET(int fd, fd_set *set);

[*]功能:判定set集中fd对应的比特位是否为1
[*]返回值:就是set集中fd对应比特位的值。(若在即为1,若不在即为0)
新增(置为1)文件描述符集中指定fd:FD_SET
void FD_SET(int fd, fd_set *set);


[*]功能:把set集中fd对应的比特位置为1
select服务器实现

select服务器和一个简单的TCP服务器的区别重要体如今I/O上,所以我们基于我们之前写过的TCP服务器进行修改即可!TCP服务器
select服务器 vs 之前的TCP服务器


[*]相同点:都是要创建套接字并基于套接字进行通讯,乃至构建套接字的过程都一摸一样
[*]不同点:select的I/O方案接纳的是多路转接,而之前的TCP服务器是接纳阻塞I/O的方式
基于上述原因,所以对于select服务器来说,只需要在TCP服务器的基础上,修改Loop函数即可
获取毗连

accept的本质其实也是I/O,I/O = 等 + 拷贝,accept也是等+拷贝。
所以新毗连到来时就等价于有了读事件。
读事件可以由select进行监管,即可以把listen套接字添加到readfds中
把listen套接字交由select进行监管要完成的3个步骤:


[*]定义fd_set
[*]填充fd_set
[*]体系调用select 
 select获取到哪些fd条件就绪,进行返回时,我们要根据select的返回值来分环境处理


[*]若select的返回值大于0,表示已经有多少个文件描述符的条件就绪。上层已经可以根据readfds进行处理了
[*]若select的返回值便是0,则表示超时。若select是阻塞监管,那么select不会返回0。
[*]若select的返回值小于0,则表示select发生错误。
    void Loop() // 修改I/O为select
    {
      while(true)//需要不断获取连接,打上死循环即可
      {
            //定义fd_set
            fd_set rfds;
            //填充fd_set
            FD_ZERO(&rfds);
            FD_SET(_listensock,&rfds);
            //调用select
            int n = select(_listensock + 1 , &rfds,nullptr,nullptr,nullptr);//select只关心读事件,并且阻塞等待
            if(n > 0)
            {
                //表示已经有文件描述符条件就绪了,此时rfds中比特位为1的位置就是已经就绪的文件描述符
                handlerEvent(rfds);
            }
            else if(n == 0)
            {
                //阻塞等待不会走到这
                std::cout << "timeout..." << std::endl;
            }
            else
            {
                std::cerr << "Select Error!" << std::endl;
                exit(2);
            }
      } handlerEvent

上述,我们已经将listen套接字交给select进行监管了。那么若listen套接字读条件就绪,即有客户端向发送毗连时,如何处理呢?
这就由handlerEvent进行处理
由于select监管的文件描述符可能是listen套接字,也可能是平凡套接字,所以我们需要对这两种套接字进行分类讨论
对于listen套接字来说:


[*]listen套接字的读事件就绪,也就意味着有新的客户端毗连到来,我们首先要accept新毗连
[*]accept过后会获取一个新的平凡套接字,我们需要把平凡套接字交给select进行监管
对于平凡套接字来说:


[*]平凡套接字的读事件就绪,也就意味着客户端发来了一条新的I/O数据
[*]此时服务器提供服务即可! 
   细节问题:辅助数组
当用户调用select传入rfds时,此时的rfds是输入型参数
当select返回时,此时的rfds是输出型参数
rfds作为输入型参数和输出型参数时,寄义是完全不同的!
举个例子:若我们调用select时设置了1号3号5号文件描述符的读事件。假设select在监管时只有3号文件描述符就绪了,那么select会直接修改rfds,此时没有就绪的文件描述符就被修改了
 https://i-blog.csdnimg.cn/direct/f4557009795d43b59dc29f3ff4d8721c.png


[*]rfds被select修改了,但我们之后还需要关心1号和5号文件描述符的读事件! 
为相识决这个问题,所以调用select时一样寻常会搭配上一个辅助数据结构来实现!(以数组为例)
辅助数组中一样寻常会存储调用select之前的rfds,方便内核修改了rfds后,用户进行重新设置
https://i-blog.csdnimg.cn/direct/ddd9c01b2ef44600b82d1f056f8f025c.png
在服务器构造的时候,我们可以直接构造好辅助数组,此外,辅助数组我们可以直接设置为成员变量
class TCPServer
{
public:
    const static int N = sizeof(fd_set) * 8; //辅助数组的大小
    const static int defaultfd = -1; //辅助数组元素的初始值
    TCPServer(uint16_t port)
      : _port(port), _isrunning(false)
    {
      //初始化辅助数组
      for(int i = 0 ; i < N ; ++i)
      {
            fd_array = defaultfd;
      }
      //listen套接字是必须要的,直接添加进辅助数组
    }
protected:
    uint16_t _port;
    int _listensock;
    bool _isrunning;
    int fd_array; //辅助数组,元素的含义是需要内核关心的套接字编号
}; 注意:该服务器的listensock是在初始化套接字时创建的,在socket创建listensock的后面再把listen套接字传入到辅助数组中  
   handlerEvent的实现 
通过之前先容select调用,我们会发现若我们不通知内核需要关心某个fd,那么select返回时这个fd一定不会在rfds中。


[*]若我们告诉内核需要关心第n号文件描述符,那么select返回时该文件描述符不一定会就绪
[*]若我们不告诉内核需要关心第n号文件描述符,那么select返回时该文件描述符一定不会就绪
除此之外,select返回时,可能不止一个文件描述符就绪了
所以第一步我们需要遍历整个辅助数组


[*]若遍历过程中,fd_array的值是默认值,阐明该位置还没被使用,直接跳过即可
[*]若遍历过程中,fd_array的值不是默认值,意味着该位置存储的一定是需要内核关心的文件描述符的编号,此时需要看这个文件描述符是否在输出型参数rfds中,若在,则该文件描述符就绪。否则,该文件描述符没就绪
[*]判定一个文件描述符是否在fd_set中,我们使用的调用是FD_SET 
第二步就是分类讨论,区分该套接字是listen套接字还是平凡套接字
    void handlerEvent(fd_set &rfds)
    {
      for(int i = 0 ; i < N ; ++i)
      {
            //若该fd不在辅助数组中,说明该文件描述符一定不会就绪
            if(fd_array == defaultfd) continue;
            //走到这,说明该文件描述符被关心,经过rfds判断是否就绪
            if(!FD_ISSET(fd_array,&rfds)) continue;
            //区分处理
            if(fd_array == _listensock)
            {
                //该套接字是listen套接字,完成的工作是处理连接
                AcceptClient();
            }
            else
            {
                //该套接字是普通套接字,完成的工作是提供I/O服务
                Service(fd_array);
            }
      }
    }    处理毗连:AcceptClient 
 到这一步,阐明listen套接字的读事件已经就绪
需要进行两步操作:


[*]获取新毗连,并获取新的套接字
[*]把套接字交给select进行监管
如何交给select进行监管?把这个新的套接字设置进辅助数组即可
    void AcceptClient()
    {
      //1.获取新连接,获取新的套接字
      int sockfd;
      Accept(sockfd);
      //2.把新连接设置进辅助数组
      int pos = 1;
      for(; pos < N ; ++pos)
      {
            //2.1选择一个没被用过的位置
            if(fd_array == defaultfd) break;
      }
      if(pos < N)
      {
            //2.2找到了一个没有被用的空位置
            fd_array = sockfd;
      }
      else
      {
            //pos == N
            //2.3说明fd_array已经被使用满了
            close(sockfd);
            std::cerr << "Server is full!" << std::endl;
      }
    }     提供服务:Service
Service注意不要死循环即可! 
    void Service(int sockfd)
    {
            //获取客户端的输入
            char buffer;
            int n = recv(sockfd, buffer, sizeof(buffer), MSG_WAITALL);
            if (n > 0)
            {
                // 接收成功
                std::cout << "Client say# " << buffer << std::endl;
                // 服务器把buffer中的数据重新发送给客户端,完成服务

                //发送客户端的输入
                send(sockfd, buffer, sizeof(buffer), 0);
            }
            else if (n == 0)
            {
                // 客户端套接字关闭
                std::cout << "client socket close!" << std::endl;
                close(sockfd);
            }
            else
            {
                // recv error
                std::cerr << "recv error!" << std::endl;
                close(sockfd);
                exit(5);
            }
    }    修改Loop函数
Loop函数的修改重要有几点 


[*]先把listen套接字放入辅助数组
[*]每一次循环都得遍历一遍辅助数组,构建fd_set输入型参数,用于转达select参数
[*]遍历过程中找出最大的文件描述符,用于转达select参数
    void Loop() // 修改I/O为select
    {
      fd_array = _listensock;
      while(true)//需要不断获取连接,打上死循环即可
      {
            //定义fd_set
            fd_set rfds;
            //填充fd_set
            FD_ZERO(&rfds);
            //max_fd主要用于select参数填写
            int max_fd = defaultfd;
            for(size_t i = 0 ; i < N ; ++i)
            {
                if(fd_array == defaultfd) continue;
                FD_SET(fd_array,&rfds);

                if(fd_array > max_fd) max_fd = fd_array;
            }
            //调用select
            int n = select(max_fd + 1 , &rfds,nullptr,nullptr,nullptr);//select只关心读事件,并且阻塞等待
            if(n > 0)
            {

                //表示已经有文件描述符条件就绪了,此时rfds中比特位为1的位置就是已经就绪的文件描述符
                handlerEvent(rfds);
            }
            else if(n == 0)
            {
                //阻塞等待不会走到这
                std::cout << "timeout..." << std::endl;
                continue;
            }
            else
            {
                std::cerr << "Select Error!" << std::endl;
                continue;
            }
      }
    } select服务器代码链接 

select服务器实现
select的优缺点 

优点:


[*]与其他I/O模子相比,select可以同时等候多个fd。对I/O性能有着显著提升
[*]select是最早出现的多路转接的方案,在许多较旧的服务器或体系上,select仍然会被使用 
缺点:


[*]同时等候的fd的个数,有上限。这一缺点是由于fd_set范例被设置为固定大小的位图。通常为1024个比特位
[*]select输入输出参数混合。每次都要进行重新设定。通过服务器的编写我们会发现,使用select往往会造成大范围的遍历,而遍历对性能的损耗其实也是蛮大的
[*]内核和用户之间的数据拷贝,这个缺点全部的多路转接方案都会存在
[*]select底层监督多个fd的时候,OS会在内部进行遍历检测全部的fd,至少有一个就绪就返回。如果没有,就按照计谋!
由于select的缺点比较多,全部后来提出的多路转接方案都是基于这些缺点进行的改良。
其中,我们接下来先容的poll就对select的某些缺点进行了改良 
多路转接之poll 

poll对select进行一定程度的改良


[*]poll解决了select同时等候fd有上限的问题
[*]poll解决了select输入输出混合,导致代码中出现多次遍历的问题
   poll接口 
       #include <poll.h>

       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

[*]timeout:poll的timeout是输入型参数,poll等候时按照timeout的计谋进行等候,可以类比select的timeout参数,poll的timeout的单位是ms(毫秒),若poll在等候了timeout毫秒后还没有任何fd就绪,那么poll会超时返回。特殊的:若timeout被设置为0则表示非阻塞等候。若timeout被设置为-1则表示阻塞等候
[*]fds和nfds:这两个合起来我们可以看成是一个数组,其中nfds表示的是数组的元素个数,而fds表示数组的首元素地址。数组的元素范例是struct pollfd。
   poll如何解决select输入输出参数混合的问题的? 
 poll的fds实际上也是一个输入且输出型参数!
struct pollfd结构:
         struct pollfd {
               int   fd;         /* file descriptor */
               short events;   /* requested events */
               short revents;    /* returned events */
         };

[*]fd:表示需要关心的文件描述符编号
[*]events:哀求事件,这是用于用户告诉内核需要关心fd的哪些事件,若这个参数用户设置为POLLIN,则表示需要关心fd的读事件。若这个参数用户设置为POLLOUT,则表示需要关心fd的写事件。若用户既想关心读,又想关心写,则 POLLOUT | POLLIN即可,也就是说参数可以进行组合
[*]revents:返回事件,这是内核告诉用户,指定fd的哪些事件好了!若用户想看看该fd的读事件是否就绪,则revents & POLLIN即可!
poll的事件除了读写以外另有哪些:(man手册有详细先容) 


[*]https://i-blog.csdnimg.cn/direct/5253acea102144458c86cf7b3bd5f565.png
我们可以看出,poll的fds参数虽然还是输入输出型的参数,但用户通知内核和内核通知用户所用的变量不再是同一个,这就是poll解决输入输出混合问题的方案 
   poll如何解决select同时等候的fd有上限的问题? 
从参数上,我们已经可以猜到它的解决方案了
poll的fds参数和nfds参数是共同使用的,fds相称于数据首元素地址,nfds是由用户确定的,换句话来说,只要用户想且内存允许,你的fd想要多少就有多少!不会受到poll接口的影响
poll服务器实现(select服务器改写) 

经过select服务器的铺垫,poll服务器改写起来是相对容易的:poll服务器的改写代码
poll的优缺点

优点:


[*]poll在select的基础上,补充了select的一些缺点。而且poll也是多路转接方案的一种,与其他四种I/O模子相比,poll的优点是它可以同时接收多个文件描述符
[*]poll在select的基础上,补充了select同时等候的fd有上限的问题,poll则是完全由用户自主决定fd的大小。
[*]poll在select的基础上,补充了select输入输出参数混合的问题,每次调用poll时,都不再需要重新遍历设定参数!
缺点:


[*]poll与我们后续所学习的epoll相比,它的缺点是底层poll在查抄fd是否就绪时,仍然接纳的是遍历整个fd数组的方式,效率较低
[*]poll虽然比起select来说较优,但由于其出现的时间点和技术发展的趋势,大多数旧的装备/服务器当中,都已经接纳了select。而对于新的装备/服务器来说,poll被epoll所替代。所以poll所处的地位较为尴尬
[*]内核和用户之间的数据拷贝,这个缺点全部的多路转接方案都会存在



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux网络编程】高效I/O--select/poll服务器