多路转接(多路复用)

文章目录

    • 引言
    • 三种多路转接
      • select
        • fd_set使用方式
        • demo
      • epoll
        • 接口
          • 事件结构 struct epoll_event
      • demo
      • epoll 工作模式
        • 水平触发 LT
        • 边缘触发 ET
      • `1. 边沿触发只通知一次的问题:`

引言

在写TCP的基本通信流程时,由于accept()函数的性质,在单执行流的程序里无法实现多人个持续通信,因此引入了多进程和多线程的方法,但这种方法并不利于并发。由此引入了多路转接IO。

三种多路转接

  • select:
    • 优点:
      1. 跨平台。
      2. 超时时间可精却到微秒。
    • 缺点:
      1. select 监控fd时采用轮询的方式,时间复杂度O(n).
      2. 监控fd的个数有上限,取决于宏_FD_SETSIZE; //1024
      3. 在返回就绪的fd时会丢掉未未就绪的fd,需要我们手动添加。
      4. 返回就绪的fd时,返回的时set集合,需要我们使用FD_ISSET()判断。
  • poll
    • 优点:
      1. 提出了事件结构的方式,给poll传参时不用分别添加对应的事件集合中。
      2. 事件结构数组无上限。
      3. 不用就绪后重新添加fd。
    • 缺点:
      1. 不夸平台
      2. 内核对与事件结构数组还是遍历的方式查询O(n)。
  • epoll
    • 优点:
      1. 接口使用更方便,不用每次都设置关注的文件描述符,利用事件结构将每个fd都封装起来了使用起来更简易。。
      2. 数据拷贝轻量:只在调用EPOLL_CTL_ADD时将fd拷贝到内核中的红黑树中。
      3. fd数量无上限。
      4. 事件访问机制:避免了遍历,将就绪的fd结构加入到就绪队列中,epoll_wait()返回值直接访问就绪队列就能知道那个fd就绪。
    • 缺点:
    • 不支持跨平台

select

原理:阻塞监视(等待)多个文件描述符的就绪状态,只要有至少一个文件文件描述符就绪,立马拷贝。

int select(int nfds, fd_set *readfds, fd_set *writefds,\
 		  fd_set *exceptfds, struct timeval *timeout);
nfds:最大文件描述符的值+1;
fd_set *readfds:读事件集合(接收缓冲区就绪了,就是来东西了)
fd_set *writefds:写事件集合(发送缓冲区有空闲了)
fd_set *exceptfds:异常事件集合(套接字描述符异常关闭了)
struct timeval *timeout :
		阻塞:NULL
		非阻塞:0{秒,微秒}
return :返回就绪个数
		失败:-1

struct fd_set

typedef long int __fd_mask;

/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FDELT(d) ((d) / __NFDBITS)
#define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))

/* 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;

/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE   //__FD_SETSIZE等于1024


可以看出 __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];就是一个可以融纳1024个bit位的位图。

fd_set使用方式

/* Access macros for `fd_set'.  */
#define FD_SET(fd, fdsetp)      __FD_SET (fd, fdsetp)//加入
#define FD_CLR(fd, fdsetp)      __FD_CLR (fd, fdsetp)//去除
#define FD_ISSET(fd, fdsetp)    __FD_ISSET (fd, fdsetp)//判断fd是否在此集合中
#define FD_ZERO(fdsetp)         __FD_ZERO (fdsetp)//清空
demo
#include
#include
#include
#include
#include
#include
#include
int main()
{
  //新建fd
  int fd = socket(AF_INET,SOCK_STREAM,0);
  if(fd<0)
  {
    perror("fd\n");
  }

  //绑定
  struct sockaddr_in addr;
  addr.sin_port = htons(9997);
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr=inet_addr("0.0.0.0");
  if(bind(fd,(sockaddr*)&addr,sizeof(addr))<0)
  {
    perror("bind");
  }

  //listen
  if(listen(fd,5)<0)
  {
    perror("listen\n");
  }
  //添加事件结构
   fd_set read_;
   FD_ZERO(&read_);
   FD_SET(fd,&read_);
   int nfds = fd+1;
 
  while(1)
  {
    fd_set tmp = read_;
    int fs =  select(nfds,&tmp,NULL,NULL,0);
    if(fs < 0)
    {
      //错误
      continue;
    }
    if(fs == 0) continue;
    //开始遍历
    for(int i = 0;i < nfds; i++)
    {
      if(FD_ISSET(i,&tmp))//文件描述符 i 有响应
      {
        if(i==fd)//侦听套接字有反应
        {       
          struct sockaddr_in add;
          socklen_t len = sizeof(add);
          int newfd = accept(fd,(sockaddr*)&add,&len);
          if(newfd<0)
          {
            perror("accept");
            continue;
          }
          FD_SET(newfd,&read_);
          std::cout<<"accept\n";
          nfds = newfd+1>nfds?newfd +1:nfds;
          continue;
        }
        /newfd 有情况
     
        char buf[1024]={0};
        ssize_t n =  recv(i,buf,1024,0);
        if(n<0) continue;
        else if(n==0) 
       {
         FD_CLR(i,&read_);
         close(i);
         continue;
       }
         printf("%s\n",buf);
         std::cout<<"re send:";
         memset(buf,0,1024);
         std::cin>>buf;
         send(i,buf,1024,0);
      }   
    }
  }
  close(fd);
  return 0;
}

epoll

原理:
每创建一个epoll,内核都创建一个eventpoll结构体。可以利用epoll_ctl函数对齐操作。
在epoll中,每个事件都有一个epitem结构体。每添加一个事件都会挂载在红黑树中,且每个添加进来的事件都会和网卡建立回调,若有相应会将这个事件放到一个双向链表中,epoll_wait()只需要检查这个双向链表是否为空,不为空就将这些事件赋值給用户态。

struct epitem{ 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 
}
接口
//创建一个epoll
int epoll_create(int size);
size>0 已经弃用
return epoll的操作句柄
//操作epoll
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
opfd:epoll 操作句柄
op:
	EPOLL_CTL_ADD: 添加一个文件描述符到epoll中
	EPOLL_CTL_MOD:修改...
	EPOLL_CTL_DEL:删除..
fd:待处理的操作句柄
event:fd对应的事件结构
//epoll等待
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout)
events:是事件结构的结构体数组,不能为空,内核将数据赋值进去。
maxevents:一次最多获取的事件结构数量。
timeout:>0 超时事件 ;=0 非阻塞;<0 阻塞;
return :就绪的fd个数;=0 超时;<0 错误;
事件结构 struct epoll_event
struct epoll_event{
uint32_t events;
epoll_data_t data;
}
events:关心的事件:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭); 
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

typedef union epoll_data{
void* ptr;
int fd;//方便用户使用
uint32_t u32;
uint64_t u64;
}epoll_data_s;

demo

#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
  //新建fd
  int fd = socket(AF_INET,SOCK_STREAM,0);
  if(fd<0)
  {
    perror("fd\n");
  }

  //绑定
  struct sockaddr_in addr;
  addr.sin_port = htons(9997);
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr=inet_addr("0.0.0.0");
  if(bind(fd,(sockaddr*)&addr,sizeof(addr))<0)
  {
    perror("bind");
  }

  //listen
  if(listen(fd,5)<0)
  {
    perror("listen\n");
  }
  //建立epoll操作句柄
  int epfd = epoll_create(9);
  if(epfd<0)
  {
    printf("create\n");
  }
  //给侦听套接字准备一份事件结够
  epoll_event listen_fd;
  listen_fd.events = EPOLLIN;
  listen_fd.data.fd = fd;
  //把fd添加到epoll
  epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&listen_fd);

  while(1)
  {
    struct epoll_event se[9];
    int nfd = epoll_wait(epfd,se,9,-1);
    if(nfd<0) {perror("wait\n");break;}
    if(nfd == 0) continue;
    for(int i = 0; i < nfd; i++)
    {
      if(se[i].data.fd==fd)//侦听
      {
        int newfd = accept(fd,NULL,NULL);
        if(newfd<0) continue;
        struct epoll_event ee;
        ee.events=EPOLLIN;
        ee.data.fd = newfd;
        epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&ee);
        continue;
      }
      recv
      
      char buf[1024]={0};
      ssize_t n =  recv(se[i].data.fd,buf,1024,0);
      if(n<0) continue;
      else if(n==0) 
      {
        epoll_ctl(epfd,EPOLL_CTL_DEL, se[i].data.fd,NULL);
        close(se[i].data.fd);
        continue;
      }
        printf("%s\n",buf);
        std::cout<<"re send:";
        memset(buf,0,1024);
        std::cin>>buf;
        send(se[i].data.fd,buf,1024,0);
    }
  }
  close(fd);
  close(epfd);
  return 0;
}

epoll 工作模式

水平触发 LT

也叫条件触发,是默认的触发方式,只要缓冲区不空就返回读就绪,只要缓冲区不满就返回写就绪。

边缘触发 ET

读事件

  1. 缓冲区由空变为不空时(不可变可读)
  2. 有新数据到达时
  3. 缓冲区有数据可读且对应的放fd的events修改位EPOLLIN时。

写事件:

  1. 缓冲区由满变为空时,(不可写变可写时)
  2. 有数据被发送时
  3. 缓冲区有空间且对应的放fd的events修改位EPOLLOUT时。

1. 边沿触发只通知一次的问题:

因为阻塞IO不保证一次处理完,如果这次每有处理完,而且后来再没有相应的事件触发,那么就再也无法处理了。
因此:必须使用非阻塞IO:
2. 循环读取无法确定退出条件问题:
对于 recv ,write ,read, send
阻塞:<0 出错; =0 连接关闭; >0 处理的数据量
非阻塞:<0 :erron = EINTR||EWOULDBLOCK||EAGAIN 都是连接正常的
EINTR(操作信号被中断)
EWOULDBLOCK||EAGAIN (缓冲区空/满)

你可能感兴趣的:(算法,linux)