多路转接select

  • 理解5种IO模型的基本概念,重点是IO多路转接。
  • 掌握select编程模型,能够实现select版本的TCP服务器。
  • 掌握poll编程模型,能够实现poll版本的TCP服务器。
  • 掌握epoll编程模型,能够实现epoll版本的TCP服务器。
  • 理解epoll的LT模式和ET模式。
  • 理解select和epoll的优缺点对比。

五种IO模型

当我们想要读写数据的时候,缓冲区可能没有数据,所以我们可能需要等待,等数据准备就绪,再执行读写操作。
IO = 等待 + 拷贝。
读IO = 等待读事件就绪 + 将数据从内核拷贝到用户的接收缓冲区。
写IO = 等待写事件就绪 + 将数据从用户空间拷贝到内核的发送缓冲区。

  • 高级IO,就是高效IO。实际上,只有数据拷贝的过程才对IO做贡献,等待的过程对IO是没有贡献的。
  • 高效IO的本质是,尽可能的减少等的比重!!

小栗子:

钓鱼分为两步:等待 + 钓。

  • 张三:张三去钓鱼,他死死的看着鱼漂,直到鱼咬钩。
  • 李四:李四左手拿着鱼竿,右手拿着《effectiveC++》,他看一会书,就看一眼鱼漂是否有鱼上钩,然后再看书。李四循环这个过程。
  • 王五:王五将一个铃铛挂在了鱼竿的顶部,然后就一直看手机。只要铃铛响了,王五才收杆。
  • 赵六:赵六是个小土豪,拉着一卡车鱼竿,大概100个左右。赵六在100个鱼竿左右来回徘徊,只要有一个鱼竿动了,就去钓起。赵六就这样轮询检测鱼竿。
  • 田七:田七是真正的有钱人,他想吃鱼了,直接让他的司机小李去钓鱼。田七就直接走了,等小刘钓到很多鱼,就给田七打电话。

前4种方式,自己等自己钓鱼的方式,叫做同步IO。
田七的钓鱼方式,田七只是IO的发起者,钓鱼的行为是小刘做了,这叫做异步IO。

select,poll,epoll只负责IO当中的等待。

阻塞IO

  • 张三使用的钓鱼方式就是阻塞IO模型。
  • 内核如果没有将数据准备好,那么系统调用就会一直阻塞。所有的套接字,默认都是阻塞方式。

阻塞IO

非阻塞IO

  • 李四采用的就是非阻塞IO模型。
  • 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。
  • 非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询。 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
    非阻塞IO

信号驱动IO

  • 王五使用的就是信号驱动IO。
  • 遇到IO的时候,进程继续执行。当内核数据准备好时,系统通知进程(发送信号SIGIO,29号信号)去拷贝数据。
  • 系统通知的方式一般都是信号。
  • 操作系统收到数据时,会向目标进程发送SIGIO,SIGIO的执行动作默认忽略。所以我们需要建立signal的处理函数。
    信号驱动IO

多路转接

  • 赵六使用的就是多路转接,也叫做多路复用。
  • 我称之为阻塞IO的高级版本(doge)。
  • 多路转接最核心的地方在于IO多路转接能够同时等待多个文件描述符的就绪状态。
  • 多路转接只负责一件事,就是等待!!
    多路转接IO

异步IO

  • 田七使用的就是异步IO。
  • 当内核将数据拷贝完成时,通知进程。(信号驱动是等待完成,就通知进程。)而进程一直在运行。
    异步IO

小结:

  • 任何IO过程中,都包含两个步骤. 第一是等待, 第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。 让IO更高效, 最核心的办法就是让等待的时间尽量少。

高级IO重要概念

同步通信 vs 异步通信

  • 简单一句话,在IO种,自己参与的IO叫做同步IO。
  • 自己不需要参与的就是异步IO。
  • 这里的同步和多线程的同步完全不一样!! 所以查找同步时一定要说清楚背景。

阻塞 vs 非阻塞

阻塞等待就是在调用返回结果前,线程会被挂起;
非阻塞就是调用后立刻返回,不会影响线程执行。

非阻塞IO

文件描述符的默认等待方式都是阻塞等待。但是我们可以手动设置成非阻塞等待。我们使用fcntl函数。

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd,/* arg */ );
  • 第一个参数fd代表你要设置的文件描述符。
  • cmd代表你要对这个fd执行的操作,cmd不同,后面的可变参数就不同。
  • fcntl函数有很多功能,这里介绍修改fd为非阻塞的方法。
/********* 修改标准输入为非阻塞 ********/
  1 #include <stdio.h>                                                                                   
  2 #include <fcntl.h>
  3 #include <unistd.h>
  4 #include <errno.h>
  5 #include <stdlib.h>
  6 void SetNoBlock(int fd){
  7   int fl = fcntl(fd, F_GETFL);
  8   if(fl < 0){
  9     printf("fcntl error\n");
 10     exit(1);
 11   }
 12 
 13   fcntl(fd, F_SETFL, fl | O_NONBLOCK);
 14 
 15 }
 16 
 17 int main(){
 18   SetNoBlock(0);
 19   char ch = '\0';
 20   for(;;){
 21     sleep(1);
 22     ssize_t s = read(0, &ch, sizeof(ch)); //有一个究极BUG!!把ssize_t改成size_t后,会出现错误!我怀疑是因为强转类型转换!
 23 
 24     if(s > 0){
 25       printf("%c\n", ch);
 26     } 
 27      else if(s < 0 && errno == EAGAIN){
 28       printf("read conditon not ok\n");
 29     }
 30 
 31     else{
 32       printf("read error\n");
 33       exit(2);
 34     }
 35 
 36     printf("................\n");
 37   }
 38 }                    

多路转接之select

  • select的作用就是一个字,等。
  • select可以同时等待多个文件描述符,这样就相当于并行,提高了单进程服务器的效率。

函数介绍:
select

  • nfds是你想关系的文件描述符中最大的+1。
  • 第2,3,4个参数对应读事件,写事件和异常事件的集合。
  • 最后一个参数是一个timeval型的参数。

fd_set:

  • fd_set是一张位图。它既是输入参数又是输出参数。
  • 作为输入参数,表示进程告诉操作系统要关系哪些文件描述符。位图的每一位的下标代表文件描述符,而每一位的内容(0或者1)代表该文件描述符你是否关心。
  • 作为输出参数,表示操作系统告诉进程哪些文件描述符已经就绪。位图的每一位的下标表示文件描述符,而每一位的内容表示是否就绪。
  • 输出的位图中被设置的位一定在你关心的那些比特位中。不可能你不关心3号文件描述符,而返回3号就绪!
  • 而因为是位图,所以系统也有一系列接口使用它:

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

timeval:

  • timeval是一个结构体,用来描述一段时间。select在这段时间内进行阻塞等待。
    timeval
  • 注意!!!timeout既是输入参数,也是输出参数!
  • 输入代表你想要select阻塞等待的时间。而timeout在输出的时候会清零!

timeout的取值:

  • 特定的时间值:表示你希望select阻塞等待的时间,超过这个时间后,select超时返回。在这段时间内有文件描述符就绪,select也会返回。
  • NULL:表示你希望select阻塞等待。直到有文件描述符就绪。
  • 0:表示非阻塞等待。仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。

返回值:

  • 大于零的整数数:表示就绪的文件描述符个数。
  • 等于0:超时,没有文件描述符就绪。
  • 小于0:select错误!比如某个文件描述符已经关闭,而你却还在等待。

socket就绪条件

读就绪

  • socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT(并不是接收缓冲区只要一有数据,就立马读,而是设置了一个低水位标记,为了保证读取的效率). 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求,这个请求是以读事件报告给操作系统的;
  • socket上有未处理的错误;

写就绪

  • socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  • socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  • socket使用非阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;

select还要考虑上层协议

  • 由于select是只要读取的字符多余水位线就会返回,那么你在应用层(比如http服务器)读取的时候,可能只读取到一部分http包,那么你就需要在你的代码中自己处理。比如,http包不完整时,先暂存起来,将该文件描述符继续放到select中等待,直到读取一个完整的http报文,再交付给select来等待写事件就绪。

select代码

  • 我们的想法是让接收连接的读等待,和处理数据(recv)时候的读等待全部交给select来处理。这样做即高效,又可以实现单进程并行处理多个请求。
/************ sock.hpp **************/
#pragma once                                                                                         
  2 
  3 #include <iostream>
  4 #include <string>
  5 #include <unistd.h>
  6 #include <sys/types.h>
  7 #include <sys/socket.h>
  8 #include <netinet/in.h>
  9 #include <cstring>
 10 #define LOGBACK 5
 11 using namespace std;
 12 class Sock{  // 这个类用于封装socket
 13 public:
 14   static int Socket(){ //创建套接字
 15       int sockfd = socket(AF_INET, SOCK_STREAM, 0);
 16       if(sockfd < 0){
 17         cerr << "socket error "<< endl;
 18         exit(1);
 19       }
 20       return sockfd;
 21   }
 22   static void Bind(int sockfd, int port){ //绑定
 23       struct sockaddr_in local;
 24       local.sin_family = AF_INET;
 25       local.sin_port = htons(port);
 26       local.sin_addr.s_addr = htonl(INADDR_ANY);
 27       int ret = bind(sockfd, (struct sockaddr*)&local, sizeof(local));
 28       if(ret < 0){
 29         cerr << "bind error" << endl;                                                                
 30         exit(2);
 31       }
 32   }
 33   static void Listen(int sockfd){ //监听
 34     int ret = listen(sockfd, LOGBACK);
 35     if(ret < 0){
 36       cerr << "listen error" << endl;
 37       exit(3);
 38     }
 39   }
 40   static int Accept(int sockfd){ //接收
 41     struct sockaddr_in peer;
 42     socklen_t len = sizeof(peer);
 43     int sock = accept(sockfd, (struct sockaddr*)&peer, &len);
 44     if(sock < 0){
 45       cerr << "accept error" << endl;
 46         return -1;  // accept wrong, 服务器继续运行
 47     }
 48       return sock;
 49   }
 50   static void Setsockopt(int lsock){ //端口复用
 51     int opt = 1;
 52     setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 53   }
 54 };


/*************** SelectServer.hpp *******************/
    1 #pragma once                                                                                       
    2 #include "sock.hpp"
    3 
    4 #define NUM (sizeof(fd_set)*8)
    5 #define EMPTY -1
    6 
    7 class SelectServer{
    8 private:
    9   int port;
   10   int lsock;
   11   int fd_array[NUM]; //维护fd_set中的关心文件描述符
   12 public:
   13   SelectServer(int _port = 8080)
   14     :port(_port),lsock(-1){
   15       for(auto& e : fd_array){
   16         e = EMPTY;
   17       }
   18     }
   19 
   20   void Init(){
   21     //端口复用,基于tcp的服务器挂掉之后,用来立刻重启。
   22     Sock::Setsockopt(lsock);
   23     lsock = Sock::Socket();
   24 
   25     fd_array[0] = lsock; //将监听套接字放入数组中
   26 
   27     Sock::Bind(lsock, port);
   28     Sock::Listen(lsock);
   29   }
   30   void Start(){
   31     int fds = -1;
   32     int maxfd = -1;
   33     fd_set rfds;
   34     struct timeval timeout = {5, 0}; //设置等待时间
   35     for(;;){
   36     FD_ZERO(&rfds); //如果每次不清零,会有bug!!,因为没有清零的文件描述符(比如4)
   37       timeout.tv_sec = 5; //已经被你关闭了,但是你还在告诉系统要使用它!!
   38       timeout.tv_usec = 0;
   39       for(int i = 0; i < NUM; ++i){
   40         if(fd_array[i] != EMPTY){ // fd_array[i]里面有值;
   41           FD_SET(fd_array[i], &rfds);
   42         if(fd_array[i] > maxfd){                                                                   
   43           maxfd = fd_array[i];  // 维护maxfd
   44           } 
   45         }
   46       }
   47       ArrayPrint();
   48       cout << "select begin ..." << endl;
   49        fds = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
   50       if(fds < 0){
   51         cerr << "select error" << endl;
   52         exit(4);
   53       }
   54       else if(fds == 0){
   55         cout << "timeout..." << endl;
   56         continue;
   57       }
   58       else{
   59         HandleEvents(&rfds);  //此处的参数类型也可以是const fd_set&
   60       }
   61     }
   62   }
   63   ~SelectServer(){
   64     if(lsock > 0){
   65       close(lsock);                                                                                
   66     }
   67   }
   68 
   69 private:
   70   void ArrayPrint(){
   71     cout << "fd_array : ";
   72     for(int i = 0; i < NUM; ++i){
   73       if(fd_array[i] != EMPTY){
   74         cout << fd_array[i] << " ";
   75       }
   76     }
   77     cout << endl;
   78   }
   79   void AddFdToArray(int fd){
   80     for(int i = 0; i < NUM; ++i){
   81       if(fd_array[i] == EMPTY){
   82           fd_array[i] = fd;
   83           break;
   84       }
   85     }
   86   }
   87 
   88   void DeleteFdFromArray(int fd){
   89     for(int i = 0; i < NUM; ++i){                                                                  
   90       if(fd_array[i] == fd){
   91         fd_array[i] = EMPTY;
   92         break;
   93       }
   94     }
   95   }
   96   void HandleEvents(fd_set* p_rfds){                                                               
   97     for(int i = 0; i < NUM; ++i){
   98       if(fd_array[i] == EMPTY){
   99         continue;
  100       }
  101       else{ // 该文件描述符在我们等待的范围内;
  102         if(FD_ISSET(fd_array[i], p_rfds)){  // 如果该文件描述符就绪
  103           if(fd_array[i] == lsock){ // 套接字接收到了 连接
  104             int sock = Sock::Accept(lsock);
  105             cout << "get a new link..." << endl;
  106             AddFdToArray(sock);
  107           }
  108           else{   // 套接字接收到了数据
  109             char buf[10240] = {0};
  110             ssize_t ss = recv(fd_array[i], buf, sizeof(buf) - 1, 0);
  111             if(ss > 0){
  112               cout << "client # " << buf << endl; 
  113             }
  114             else if(ss == 0){  //客户端退出
  115               cout << "client quit...." << endl;
  116               close(fd_array[i]);
  117               DeleteFdFromArray(fd_array[i]);
  118             }
  119             else{
  120               cout << "recv error" << endl;
  121               exit(5);
  122             }
  123           }
  124         }
  125         else{
  126           // 文件描述符未就绪,继续select;
  127           // donothing
  128         }
  129       }
  130     }
  131   }
  132 };          

/*************** SelectServer.cc ******************/
    1  #include "SelectServer.hpp"
    2 
    3 void Usage(char* arg){
    4   cout << "Usage: \n\r" << "arg port\n" ;                                                       
    5 }
    6 int main(int argc, char *argv[]){
    7   if(argc != 2){
    8     Usage(argv[0]);
    9     exit(50);
   10   }
   11 
   12   SelectServer* ss = new SelectServer(atoi(argv[1]));
   13   ss->Init();
   14   ss->Start();
   15 
   16   delete ss;
   17 }
      
  • select最精彩的地方就在于利用一个数组去维护我们关心的文件描述符。将对位的操作转移到对数组的操作。
  • 每次select之前,rfds都要重置,否则会影响结果。

select的优缺点

优点:

  • 高效。

缺点:

  • fd_set是有长度上限的,这就意味着select可监听的文件描述符是有数量限制的。
  • 因为要用到大量的f轮询检测,随着文件描述符的增大,效率会下降。
  • 每次都需要将fd_set拷贝到内核,再从内核拷贝到用户,这样很浪费效率。
  • select支持的文件描述符太少。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值