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

打印 上一主题 下一主题

主题 942|帖子 942|积分 2826

目录

多路转接之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调用
  1. int select(int nfds, fd_set *readfds,
  2. 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范例的结构:
  1. struct timeval {  
  2.     long tv_sec;   // 秒数  
  3.     long tv_usec;  // 微秒数  
  4. };
复制代码


  • 若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
  1. void FD_ZERO(fd_set *set);
复制代码


  • 功能:把set文件描述符集中的全部比特位清零 
清零文件描述符集中指定fd的值:FD_CLR
  1. void FD_CLR(int fd, fd_set *set);
复制代码


  • 功能:把set集中fd对应的比特位清零 
判定文件描述符集中指定fd的值:FD_ISSET
  1. int  FD_ISSET(int fd, fd_set *set);
复制代码


  • 功能:判定set集中fd对应的比特位是否为1
  • 返回值:就是set集中fd对应比特位的值。(若在即为1,若不在即为0)
新增(置为1)文件描述符集中指定fd:FD_SET
  1. 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发生错误。
  1.     void Loop() // 修改I/O为select
  2.     {
  3.         while(true)//需要不断获取连接,打上死循环即可
  4.         {
  5.             //定义fd_set
  6.             fd_set rfds;
  7.             //填充fd_set
  8.             FD_ZERO(&rfds);
  9.             FD_SET(_listensock,&rfds);
  10.             //调用select
  11.             int n = select(_listensock + 1 , &rfds,nullptr,nullptr,nullptr);//select只关心读事件,并且阻塞等待
  12.             if(n > 0)
  13.             {
  14.                 //表示已经有文件描述符条件就绪了,此时rfds中比特位为1的位置就是已经就绪的文件描述符
  15.                 handlerEvent(rfds);
  16.             }
  17.             else if(n == 0)
  18.             {
  19.                 //阻塞等待不会走到这
  20.                 std::cout << "timeout..." << std::endl;
  21.             }
  22.             else
  23.             {
  24.                 std::cerr << "Select Error!" << std::endl;
  25.                 exit(2);
  26.             }
  27.         }
复制代码

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,此时没有就绪的文件描述符就被修改了
 



  • rfds被select修改了,但我们之后还需要关心1号和5号文件描述符的读事件! 
为相识决这个问题,所以调用select时一样寻常会搭配上一个辅助数据结构来实现!(以数组为例)
辅助数组中一样寻常会存储调用select之前的rfds,方便内核修改了rfds后,用户进行重新设置

在服务器构造的时候,我们可以直接构造好辅助数组,此外,辅助数组我们可以直接设置为成员变量
  1. class TCPServer
  2. {
  3. public:
  4.     const static int N = sizeof(fd_set) * 8; //辅助数组的大小
  5.     const static int defaultfd = -1; //辅助数组元素的初始值
  6.     TCPServer(uint16_t port)
  7.         : _port(port), _isrunning(false)
  8.     {
  9.         //初始化辅助数组
  10.         for(int i = 0 ; i < N ; ++i)
  11.         {
  12.             fd_array[i] = defaultfd;
  13.         }
  14.         //listen套接字是必须要的,直接添加进辅助数组
  15.     }
  16. protected:
  17.     uint16_t _port;
  18.     int _listensock;
  19.     bool _isrunning;
  20.     int fd_array[N]; //辅助数组,元素的含义是需要内核关心的套接字编号
  21. };
复制代码
注意:该服务器的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套接字还是平凡套接字
  1.     void handlerEvent(fd_set &rfds)
  2.     {
  3.         for(int i = 0 ; i < N ; ++i)
  4.         {
  5.             //若该fd不在辅助数组中,说明该文件描述符一定不会就绪
  6.             if(fd_array[i] == defaultfd) continue;
  7.             //走到这,说明该文件描述符被关心,经过rfds判断是否就绪
  8.             if(!FD_ISSET(fd_array[i],&rfds)) continue;
  9.             //区分处理
  10.             if(fd_array[i] == _listensock)
  11.             {
  12.                 //该套接字是listen套接字,完成的工作是处理连接
  13.                 AcceptClient();
  14.             }
  15.             else
  16.             {
  17.                 //该套接字是普通套接字,完成的工作是提供I/O服务
  18.                 Service(fd_array[i]);
  19.             }
  20.         }
  21.     }
复制代码

   处理毗连:AcceptClient 
   到这一步,阐明listen套接字的读事件已经就绪
需要进行两步操作:


  • 获取新毗连,并获取新的套接字
  • 把套接字交给select进行监管
如何交给select进行监管?把这个新的套接字设置进辅助数组即可
  1.     void AcceptClient()
  2.     {
  3.         //1.获取新连接,获取新的套接字
  4.         int sockfd;
  5.         Accept(sockfd);
  6.         //2.把新连接设置进辅助数组
  7.         int pos = 1;
  8.         for(; pos < N ; ++pos)
  9.         {
  10.             //2.1选择一个没被用过的位置
  11.             if(fd_array[pos] == defaultfd) break;
  12.         }
  13.         if(pos < N)
  14.         {
  15.             //2.2找到了一个没有被用的空位置
  16.             fd_array[pos] = sockfd;
  17.         }
  18.         else
  19.         {
  20.             //pos == N
  21.             //2.3说明fd_array已经被使用满了
  22.             close(sockfd);
  23.             std::cerr << "Server is full!" << std::endl;
  24.         }
  25.     }
复制代码

    提供服务:Service
  Service注意不要死循环即可! 
  1.     void Service(int sockfd)
  2.     {
  3.             //获取客户端的输入
  4.             char buffer[1024];
  5.             int n = recv(sockfd, buffer, sizeof(buffer), MSG_WAITALL);
  6.             if (n > 0)
  7.             {
  8.                 // 接收成功
  9.                 std::cout << "Client say# " << buffer << std::endl;
  10.                 // 服务器把buffer中的数据重新发送给客户端,完成服务
  11.                 //发送客户端的输入
  12.                 send(sockfd, buffer, sizeof(buffer), 0);
  13.             }
  14.             else if (n == 0)
  15.             {
  16.                 // 客户端套接字关闭
  17.                 std::cout << "client socket close!" << std::endl;
  18.                 close(sockfd);
  19.             }
  20.             else
  21.             {
  22.                 // recv error
  23.                 std::cerr << "recv error!" << std::endl;
  24.                 close(sockfd);
  25.                 exit(5);
  26.             }
  27.     }
复制代码

   修改Loop函数
  Loop函数的修改重要有几点 


  • 先把listen套接字放入辅助数组
  • 每一次循环都得遍历一遍辅助数组,构建fd_set输入型参数,用于转达select参数
  • 遍历过程中找出最大的文件描述符,用于转达select参数
  1.     void Loop() // 修改I/O为select
  2.     {
  3.         fd_array[0] = _listensock;
  4.         while(true)//需要不断获取连接,打上死循环即可
  5.         {
  6.             //定义fd_set
  7.             fd_set rfds;
  8.             //填充fd_set
  9.             FD_ZERO(&rfds);
  10.             //max_fd主要用于select参数填写
  11.             int max_fd = defaultfd;
  12.             for(size_t i = 0 ; i < N ; ++i)
  13.             {
  14.                 if(fd_array[i] == defaultfd) continue;
  15.                 FD_SET(fd_array[i],&rfds);
  16.                 if(fd_array[i] > max_fd) max_fd = fd_array[i];
  17.             }
  18.             //调用select
  19.             int n = select(max_fd + 1 , &rfds,nullptr,nullptr,nullptr);//select只关心读事件,并且阻塞等待
  20.             if(n > 0)
  21.             {
  22.                 //表示已经有文件描述符条件就绪了,此时rfds中比特位为1的位置就是已经就绪的文件描述符
  23.                 handlerEvent(rfds);
  24.             }
  25.             else if(n == 0)
  26.             {
  27.                 //阻塞等待不会走到这
  28.                 std::cout << "timeout..." << std::endl;
  29.                 continue;
  30.             }
  31.             else
  32.             {
  33.                 std::cerr << "Select Error!" << std::endl;
  34.                 continue;
  35.             }
  36.         }
  37.     }
复制代码

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接口 
  1.        #include <poll.h>
  2.        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结构:
  1.            struct pollfd {
  2.                int   fd;         /* file descriptor */
  3.                short events;     /* requested events */
  4.                short revents;    /* returned events */
  5.            };
复制代码


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



我们可以看出,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企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

渣渣兔

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表