select 实现多路复用

以下内容均为本人学习笔记,若有错误,欢迎指出

select的使用

在前面讲了五种基本IO模型:初始化五种IO模型

其实我们用到的最多的就是多路IO,今天来学习了解select()实现多路IO

先看接口如何使用

       #include <sys/time.h>
       #include <sys/types.h>
       #include <unistd.h>

       int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);
  • 第一个参数 nfds 为你所要等待的文件描述符的最大值的值再 +1 ,因为这里的fd_set 文件描述符集其实是一个位图,当每次进行设置修改时都要遍历,这样给定最大范围,可以提高效率
  • 第二个参数为要等待的可读的文件描述符集
  • 第三个参数为要等待的可写的文件描述符集
  • 第四个参数为要等待的异常文件描述符集
  • 第五个参数为一个表示时间的结构体,用来设置select()的等待时间

除了第一个参数,其他参数都是既为输入型参数又为输出型参数
rdset、wrset、exset结构是一个整数数组,理解为一个位图:
*作为输入型参数,用户告诉操作系统,你要等待哪些文件描述符的就绪状态
*作为输出行参数,操作系统告诉用户哪些文件描述符状态已经就绪
timeout结构:
若等待时间没有超过timeout,则返回为剩余时间

其实这里存在一个问题,我们每次非阻塞形式读取文件并不是只读一次,都是循环等待文件描述符状态,读取数据,那么上面的fd_set结构在每次返回的时候,都会更新为就绪的文件描述符集,再次等待的时候,就不能直接使用了,所以这里需要一个第三方变量保存作为输入的文件描述集。需要将输入输出参数关联性分离,其实这也是select的一个短板。

下面的接口用来操作fd_set:

       void FD_CLR(int fd, fd_set *set);
       int  FD_ISSET(int fd, fd_set *set);
       void FD_SET(int fd, fd_set *set);
       void FD_ZERO(fd_set *set);

什么是都就绪状态

  • socket内核中,接收缓冲区的中字节数大于等于低水位标记SO_RCVLOWAT(socket recevice low water mark),此时可以进行无阻塞的对该文件进行读取,并且返回值大于0。
  • socketTCP通信中对端关闭连接,此时对该文件进行读取,则返回0。
  • socket使用非阻塞connect连接成功或者失败之后。
  • socket有未读取的错误时。

什么是写就绪状态

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

使用select只检测标准输入是否就绪

#include <stdio.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>

//使用select 检测标准输入
int main()
{
//1.准备好文件描述符集
  fd_set read_set;
  int fd = 0;//stdin
  FD_ZERO(&read_set);
  FD_SET(fd,&read_set);

  while(1)
  {
    int ret = select(fd + 1,&read_set,NULL,NULL,NULL);
    //此处只检测读就绪,写和异常不用理会,并设置为阻塞式检测
    if(ret < 0)
    {
      perror("select");
      continue;
    }
    printf("ret : %d\n",ret);
    if(FD_ISSET(fd,&read_set) == 0)
    {
      //说明出错
      printf("error!\n");
      continue;
    }
    else
    {
      char buf[1024] = {0};
      if(read(fd,buf,sizeof(buf)-1) > 0)
      {
        printf("%s\n",buf);
      }
    }
    //一次之后,重新调整read_set
    FD_ZERO(&read_set);
    FD_SET(fd,&read_set);
  }
  return 0;
}

之前我们实现的TCP服务器都是采用多进程或者多线程的方式,这种方式其实现实中根本用不到。
用select 实现一个回显服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/select.h>
#include <unistd.h>

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>


//TCP服务器使用select实现多路复用



//定义一个结构体来保存文件描述符集和当前关注的最大文件描述
typedef struct Fd_set
{
  fd_set fds;
  int max_fd;
}Fd_set;

//初始化文件描述符集
void Init_set(Fd_set * Fds)
{
  if(Fds == NULL)
    return;
  Fds->max_fd = -1;
  FD_ZERO(&Fds->fds);
}
//对文件描述符集进行设置新的文件描述符
void Set_fds(int fd,Fd_set * Fds)
{
  if(Fds == NULL)
    return;
  //更新 max_fd
  if(Fds->max_fd < fd)
  {
    Fds->max_fd = fd;
  }
  //设置fds
  FD_SET(fd,&Fds->fds);
}
//从文件描述符集中删除一个文件描述符
void Delete_fds(int fd,Fd_set * Fds)
{
  if(Fds == NULL)
    return;
  if(fd > Fds->max_fd)
    return;
  if(Fds->max_fd > fd)
  {//如果最大的文件描述符大于要删除的文件描述符
    //可以直接进行删除
    FD_CLR(fd,&Fds->fds);
  }
  else
  {//如果最大的文件描述符等于要删除的文件描述符
    //此时就要重新设最大文件描述符
    int i = 0;
    int max_fd = -1;
    FD_CLR(fd,&Fds->fds);
    //此处采用从最大的开始找的方法,虽然时间复杂度都为O(N)
    for(i = Fds->max_fd;i >= 0; --i)
    {
        if(FD_ISSET(i,&Fds->fds))
        {
          max_fd = i ;
        }
    }
    Fds->max_fd = max_fd;
  }
}

//获取listen_socket
int server_start(char * IP,short Port)
{
  //创建socket
  int listen_socket = socket(AF_INET,SOCK_STREAM,0);
  if(listen_socket < 0)
  {
    perror("socket");
    return -1;
  }
  //定义sockaddr_in 结构
  sockaddr_in addr;
  socklen_t addr_len = sizeof(addr);
  addr.sin_family = AF_INET;//IPV4
  addr.sin_addr.s_addr = inet_addr(IP);
  addr.sin_port = htons(Port);

  //绑定IP和端口号
  int ret = bind(listen_socket,(sockaddr *)&addr,addr_len);
  if(ret < 0)
  {
    perror("bind");
    return -1;
  }
  //监听socket使其处于可以被建立连接状态
  ret = listen(listen_socket,5);
  if(ret < 0)
  {
    return -1;
  }
  //将listen_socket返回
  return listen_socket;
}

int process_connection(int new_socket,sockaddr_in *peer)
{
  //这里处理连接就只读一次,因为我们将所有的等到都交给了select 
  //这里只进行读操作和响应
  char buf[1024] = {0};
  ssize_t read_size = read(new_socket,buf,sizeof(buf) - 1);
  if(read_size < 0)
  {
    perror("read");
    return -1;
  }
  if(read_size == 0)
  {
    printf("read done\n");
    return 0;
  }
  buf[read_size] = '\0';
  printf("perr [%s:%d]  say : %s\n",inet_ntoa(peer->sin_addr),ntohs(peer->sin_port),buf);

  //回显服务,收到什么内容,就回复什么内容
  write(new_socket,buf,strlen(buf));
  return 1;
}

int main(int argc ,char * argv[])
{
  //1.判断命令行参数正确性
  if(argc != 3)
  {
    printf("Usage [./server] [IP]  [Port]\n");
    return 1;
  }

  int listen_socket =server_start( argv[1],atoi(argv[2]));
  if(listen_socket < 0)
  {
    printf("start faile\n");
    return -1; 
  }
  printf("start ok\n");

  //定义文件描述符集并进行初始化和设置
  Fd_set in_put;
  Init_set(&in_put);
  Set_fds(listen_socket,&in_put);

  //因为这里第二个参数既是输入行参数,又是输出型参数
  //为了防止将第二次需要再次等待的文件描述符集破坏掉,需要定义一个输出参数用来保存输出文件描述符集
  Fd_set out_put = in_put;

  //进行事件循环
  while(1)
  {
    sockaddr_in peer;
    socklen_t peer_len = sizeof(peer);
    in_put = out_put;
    int ret = select(in_put.max_fd + 1,&in_put.fds,NULL,NULL,NULL);
    if(ret < 0)
    {
      printf("select filed\n");
      continue;
    }
    if(ret == 0)
    {
      printf("time put\n");
      continue;
    }
    //判断listen_sock是否就绪,若就绪,就可以进行accpet()
    if(FD_ISSET(listen_socket,&in_put.fds))
    {
      int new_socket = accept(listen_socket,(sockaddr *)&peer,&peer_len);
      if(new_socket < 0)
      {
        perror("accept");
        continue;
      }
      else
      {//此处建立连接成功后,应该将 new_socket设置在out_put中保存下来中
        Set_fds(new_socket,&out_put);
        printf("client %d connect!\n",new_socket);
      }
    }
    else
    {//处理已经就绪的new_sock
      int i = 0;
      for(i = 0; i < in_put.max_fd + 1; ++i)
      {
        if(FD_ISSET(i , &in_put.fds))
        {//如果这个文件描述符已经就绪,就进行处理本次连接
          //将这一位文件描述符进行设置
          Set_fds(i , &out_put);
          int ret = process_connection(i,&peer);
          if(ret <= 0)
          {//说明本次连接已经结束
            //需要将out_put里面的这个文件描述清理掉
            Delete_fds(i,&out_put);
            close(i);
            printf("%d closed \n",i);
          }
        }
        else
        {
          //如果还没有就绪
          //就检测其他的文件描述符
          continue;
        }
      }
    }
  }
}

//其实这里还存在一个问题,上面我们处理listen_socket和new_socket的处理是is-else结构的
//但是listen_socket和new_socket是很有可能同时就绪的
//但是这里也影响,因为这次没有处理,循环会继续执行,会在下一次循环处理
//我们将这种处理方式称为水平触发

select 的缺点:

- 可监控的文件描述符集大小有限制,sizeof(fd_set)根据机器不同,这边的数值为128字节,说明可监控文件描述符为1024
- 每次调用select,都需要自己手动设置fd_set,从接口使用来说不方便,并且输入输出参数为一个值,还需要自己维护第三个变量
- 每次调用select 都需要将fd集从用户态拷贝到内核态,这边为128*3字节,虽然感觉上不大,但是拷贝次数太频繁
- 每次不管是内核还是我们自己在使用的时候,都要遍历fd,开销也挺大的

猜你喜欢

转载自blog.csdn.net/misszhoudandan/article/details/81103744