【Linux】select多路转接

目录

  • 前言
  • select的定位与接口分析
  • select的特点
  • select的缺点
  • 基于select的echo服务器代码实现
  • telnet测试

前言

对于服务器来讲,需要一刻不停地监听请求,提供服务
在这里插入图片描述

我们不希望服务器主程序卡住

服务器监听的本质是:while 1 { 等待数据的到来->数据到来后读取数据 }

由于recv是阻塞等待,所以我们想尽一切办法来处理这个阻塞问题

  1. 多线程,把每一个链接交给一个新线程处理,新线程recv阻塞等待对应套接字的时候不会干扰到主线程;这时,阻塞等待+数据读取由线程全权承担
  2. 我们让内核帮助我们检测指定的套接字的状态。一旦套接字有数据就绪,就会把某个数据结构的状态设置为就绪,主程序检测到就绪,就会读取数据。这时,主程序只要不停地去检测一批套接字的状态就行。这时,等待由内核帮我们承担,主程序只要承担数据读取的任务

select的定位与接口分析

select的定位:只负责等待,是就绪事件通知机制,它等待IO就绪;唯一和read,write,recv,send不同的是,select可以等待多个文件描述符

来看select接口:

int select(
	int nfds, 
	fd_set *readfds,
	fd_set *writefds, 
	fd_set *exceptfds,
	struct timeval *timeout
);
  1. timeval结构体:秒+微秒
    nullptr:永久等待,直到某个fd就绪才返回
    3,0:等待3秒,没有就绪就返回
    0,0:没有就绪立马返回
  2. fd_set:比特位的位置代表文件描述符的编号,比特位的内容代表是否关心
    作用:所有关心X事件的文件描述符,都应该添加在这个集合里
    输入:用户告诉内核,你要帮我检测一下在这个集合中的fd的X事件
    输出:内核告诉用户,你关心的fd,有哪些文件描述符已经就绪了不具有保存sock的功能,只具有通知的能力
  3. fd_set:位图
 /* fd_set for select and pselect.  */
 typedef struct         
   {
     /* XPG4.2 requires this member name.  Otherwise avoid the name
        from the global namespace.  */
 #ifdef __USE_XOPEN
     __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
 # define __FDS_BITS(set) ((set)->fds_bits)
 #else
     __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
 # define __FDS_BITS(set) ((set)->__fds_bits)
 #endif
   } fd_set;

fd_set的作用是:比特位的位置代表哪一个文件描述符,比特位0/1代表文件描述符是否就绪

  1. 返回
    读就绪代表该文件描述符的底层数据已经就绪
    写就绪代表文件描述符可以继续写了
    异常就绪代表哪个文件描述符异常了

so

轮询检测有没有关心的fd就绪,覆盖式地写入位图,返回。所以select需要我们花一点点第三方变量把fds保存起来,一般是数组

在我们通知了一轮就绪的sock之后,可能还需要select帮助我们进行监测其他的fd,所以要对fd_set重新设置,故要提前保存

其中,监听套接字只关心读事件

获取新的fd之后我们不读它,因为我们不知道这个新的fd里面有没有读写事件就绪,所以要等下一轮循环

select的特点

  • select只能等待确定数量的文件描述符,有上限,可监控的fd的数量取决于fd_set的大小
  • 需要第三方数组存储fd
  • 每次循环都需要更新nfds的值

select的缺点

  • 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小

基于select的echo服务器代码实现

  1. socket.hpp
#pragma once 
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

namespace ns_sock
{
  static const int BACKLOG = 5;

  enum {
    SOCKET_ERROR = 2,
    BIND_ERROR,
    LISTEN_ERROR,
    ACCEPT_ERROR
  };

  class Sock {
  public:
    static int Socket() {
      int sock = socket(AF_INET, SOCK_STREAM, 0);
      if(sock == -1) {
        std::cerr << "create socket failed" << std::endl;
        exit(SOCKET_ERROR);
      }
      int opt = 1;
      setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
      return sock;
    }
    
    static void Bind(int sock, uint16_t port) {
      sockaddr_in local;
      bzero(&local, sizeof(local));
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
      local.sin_addr.s_addr = INADDR_ANY;
      if(bind(sock, (struct sockaddr*)&local, sizeof(local)) == -1) {
        std::cerr << "bind error" << std::endl;
        exit(BIND_ERROR);
      }
    }

    static void Listen(int sock) {
      if(listen(sock, BACKLOG) == -1) {
        std::cerr << "listen failed" << std::endl;
        exit(LISTEN_ERROR);
      }
    }

    static void Accept(){}

  };
}//ns_sock
  1. select.hpp
#pragma once 
#include "sock.hpp"
#include <sys/select.h>
namespace ns_select_server 
{
  static const uint16_t g_default_port = 8081;
  static const int SELECT_ERROR = 6;
  static const int BUFFER_SIZE = 4096;

  class SelectServer {
    public:
      SelectServer(int port = g_default_port):_listen_sock(-1), _port(port) {
        _listen_sock = ns_sock::Sock::Socket();
        ns_sock::Sock::Bind(_listen_sock, _port);
        ns_sock::Sock::Listen(_listen_sock);
        // 初始化第三方数组
        for(int i = 0; i < FD_SETSIZE; i++) {
          _fd_array[i] = -1;
        }
        // 添加listen套接字为第三方数组的第一个元素
        _fd_array[0] = _listen_sock;
      }

      void Loop() {
        while(true) {
          // 设置读事件的位图
          fd_set rfds;
          FD_ZERO(&rfds); // 初始化
          int max_fd = -1; // 初始化select的第一个参数
          // 告诉内核关心的读事件是什么
          for(int i = 0; i < FD_SETSIZE; i++) {
            if(_fd_array[i] != -1) {
              FD_SET(_fd_array[i], &rfds);
              if(max_fd < _fd_array[i]) {
                max_fd = _fd_array[i];
              }
            }
          }
          // select去进行等待
          int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); // 直到等到某个事件就绪才返回
          // 1. 异常
          if(n < 0) {
            std::cerr << "select error" << std::endl;
            exit(SELECT_ERROR);
          }
          // 2. 没有就绪的读事件
          else if(n == 0) {
            std::cout << "timeout" << std::endl;
          }
          // 3. 有读事件就绪
          else {
            for(int i = 0; i < FD_SETSIZE; i++) {
              if(_fd_array[i] == -1) {
                continue;
              }
              else {
                // 该文件描述符未就绪
                if(!FD_ISSET(_fd_array[i], &rfds)) {
                  continue;
                }
                // 该文件描述符就绪
                else {
                  // listen套接字的读事件就绪,有新的链接到来
                  if(_fd_array[i] == _listen_sock) {
                    sockaddr_in peer;
                    bzero(&peer, sizeof(peer));
                    socklen_t len = sizeof(peer);
                    int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
                    // 不能读数据,因为不确定有没有数据
                    // 添加到第三方数组中等待下一轮循环
                    int index = 0;
                    for(; index < FD_SETSIZE; index++) {
                      if(_fd_array[index] == -1) {
                        _fd_array[index] = sock;
                        break;
                      }
                      else {
                        continue;
                      }
                    }
                    if(index == FD_SETSIZE) {
                      // 第三方数组用完了
                      std::cerr << "select管理的文件描述符超出限制" << std::endl;
                    }
                    else {
                      // 监听套接字的一个读事件处理完
                      std::cout << "sock " << sock << " 建立链接" << std::endl;
                    }
                  }
                  // 普通套接字的读事件就绪,有数据到来
                  else {
                    Handler(_fd_array[i], i);
                  }
                }
              }
            }
          }
        }
      }

    private:
      void Handler(int sock, int i) {
        char buffer[BUFFER_SIZE] = {0};
        ssize_t size = recv(sock, buffer, sizeof(buffer) - 1, 0);
        // 1. recv失败
        if(size < 0) {
          std::cerr << "recv data error" << std::endl;
        }
        // 2. 链接关闭
        else if(size == 0) {
          std::cout << "sock: " << sock << " closed link" << std::endl;
          // 读事件处理完,fd数组中置为-1
          _fd_array[i] = -1;
          // 关闭文件描述符
          close(sock); 
        }
        // 3. 正常读取数据
        else {
          buffer[size] = '\0';
          std::cout << "sock " << sock << "# " << buffer;;
        }
      }

    private:
      int _listen_sock;
      uint16_t _port;
      int _fd_array[FD_SETSIZE];
  };
}
  1. 主程序
#include "select_server.hpp"
int main()
{
  ns_select_server::SelectServer *srv = new ns_select_server::SelectServer;
  srv->Loop();
  return 0;
}

telnet测试

我们使用3台telnet客户端连接select服务器
在这里插入图片描述

github仓库地址

评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DanteIoVeYou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值