逐步构建HTTP服务器(二)——初识IO多路复用

逐步构建HTTP服务器(二)——初识IO多路复用

上篇构建了一个简单的TCP服务器,基本结构:

socket();   // 获取一个阻塞 'TCP套接字'
bind();     //  'TCP套接字' 绑定地址
listen();   //  'TCP套接字' 监听
whlie(1)
{
    accept();   // 若有连接请求,接受连接获得 '新套接字' ,否则阻塞
    read();     // 若 '新套接字' 有数据可读,读出数据,否则阻塞
    write();    // 若 '新套接字' 有数据可写,发送数据,否则阻塞
    close();    // 阻塞,直至对 '新套接字' 连接关闭完成
}

我们实现了一个能够对发起连接客户端发送一段HTTP报文能力的简单服务器,同时也留下了遗憾:每次客户想连接到服务器必须等上一个客户完成连接建立、发送数据、接受数据和断开连接的过程。

这势必会影响我们服务器的响应性,同时也有可能因为一个客户端的阻塞而导致整个服务器阻塞,而拒绝为其他客户提供服务,即所谓的拒绝服务型攻击。

简单想法:能不能同时管理多个连接,即同时管理多个描述符,我们可以引入IO多路复用技术:IO 多路复用是什么意思?。IO多路复用的实现有:select、poll和epoll。

select函数

先来看下select函数:

#include <sys/select.h>
int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict timeout);

nfds:表示指定待测试描述符个数,等于待测试最大描述符 + 1

readfds, writefds, exceptfds:让内核监视读、写和异常条件的描述符

select使用的是描述符集fd_set,通常是一个1024位的整数数组,其中每个整数中的每一对应一个描述符。典型实现:

typedef struct {
    long fds_bits[1024 / (8 * sizeof(long))];
    // 最多存放1024个描述符
} fd_set;

timeout:告知内核等待所指定的描述符中的任何一个就绪可花多长时间,timeval:

struct timeval {
    long tv_sec;
    long tv_usec;
};
  1. nullptr: 永远等待
  2. 使用timeval:指定最长等待时间

对fd_set的操作:

#include <sys/select.h>
void FD_ZERO(fd_set *fdset);            // 初始化fd_set:所有位置零
void FD_SET(int fd, fd_set *fdset);     // 添加描述符
void FD_CLR(int fd, fd_set *fdset);     // 删去描述符
int FD_ISSET(int fd, fd_set *fdset);    // 查看描述符是否就绪
  • 使用select函数需要注意:

    1. nfds为待测试的最大描述符 + 1(0 - maxfd + 1 -1)
    2. 三个描述符集都是值->结果参数
  • 基本使用:

本节都将以echo服务器举例。

  1. 与之前相同,我们先使用socket()获得一个TCP套接字,用来监听新连接
#define SERV_PORT 8080
#define LISTENQ 1024

int listenfd;
sockaddr_in servaddr;

// socket
listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// bind
bind(listenfd, (sockaddr *) &servaddr, sizeof(servaddr));
// listen
listen(listenfd, LISTENQ);
  1. 使用描述符集
fd_set allset;

// 初始化描述符集:所有位置0
// FD_ZERO
FD_ZERO(&allset);

// 将listenfd加入描述符集allset
// FD_SET
FD_SET(listenfd, &allset);
  1. 使用数组存放描述符
int client[FD_SETSIZE];     // select.h: #define __FD_SETSIZE		1024

// 将未使用过的赋为-1
for (int i = 0; i < FD_SETSIZE; i++)
    client[i] = -1;
  1. 循环调用select
  #define MAXLINE 4096
  char buf[MAXLINE];
  sockaddr_in cliaddr;
  
  int maxi = -1;        // 标记有使用的描述符最大下标,遍历检测描述符时需要
  int maxfd = listenfd; // 标记最大描述符值,select()需要

  while (1) {
    fd_set rest = allset;   // 由于select参数是值->结果式的,我们用一个变量来保存结果

    // 阻塞直至rest中有描述符就绪
    // 正常返回就绪的描述符数量
    // int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, cosnt struct timeval *timeout)
    int nready = select(maxfd + 1, &rest, NULL, NULL, NULL);

    // 检测listenfd,看是否有新连接
    // FD_ISSET
    if (FD_ISSET(listenfd, &rest)) {
      int clilen = sizeof(cliaddr);
      // accept
      connfd = accept(listenfd, (sockaddr *) &cliaddr, &clilen);
      // 寻找第一个空位置(-1)
      for (i = 0; i < FD_SETSIZE; i++) {
        if (client[i] < 0) {
          client[i] = connfd;
          break;
        }
      }
      // 加入allset
      // FD_SET
      FD_SET(connfd, &allset);
      if (connfd > maxfd)
        maxfd = connfd;
      if (i > maxi)
        maxi = i;
      // 没有就绪的描述符了
      if (--nready <= 0)
        continue;
    }

    // 遍历所有client
    for (i = 0; i <= maxi; i++) {
      // skip -1
      if (client[i] < 0)
        continue;
      sockfd = client[i];
      // FD_ISSET
      if (FD_ISSET(sockfd, &rest)) {
        // client close connection
        if ((n = read(sockfd, buf, MAXLINE)) == 0) {
          // close
          close(sockfd);
          // FD_CLR
          FD_CLR(sockfd, &allset);
          client[i] = -1;
        }
          // read available
        else {
          write(sockfd, buf, n);
        }
        // 没有就绪的描述符了
        if (--nready <= 0)
          break;
      }
    }
  }

poll函数

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

poll使用结构体pollfd数组来管理描述符,描述符数量无限制。

fds是指向一个结构体pollfd数组的第一个元素的指针,pollfd:

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */
    short revents;    /* returned events */
};

pollfd事件:

nfds即pollfd数组长度

timeout指定poll函数返回前等待多长时间

  1. INFTIM 永远等待
  2. 0 立即返回,不阻塞
  3. >0 等待指定毫秒数
  • 基本使用:

  1. 类似地,与之前相同,我们先使用socket()获得一个TCP套接字,用来监听新连接
#define SERV_PORT 8080
#define LISTENQ 1024

int listenfd;
sockaddr_in servaddr;

// socket
listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// bind
bind(listenfd, (sockaddr *) &servaddr, sizeof(servaddr));
// listen
listen(listenfd, LISTENQ);
  1. 使用pollfd数组
#define OPENMAX 256

pollfd client[OPENMAX];

// 将listenfd放入pollfd数组,并指定检测普通读事件
client[0].fd = listenfd;
client[0].events = POLLRDNORM;

// 初始化未使用的数组元素,置-1
for (i = 1; i < OPENMAX; i++)
  client[i].fd = -1;
  1. 循环调用poll函数
int maxi = 0;

while (1) {
    // 正常返回就绪的描述符数量
    // int poll(struct pollfd *fdarray, unsigned long nfdf, int timeout)
    int nready = poll(client, maxi + 1, INFTIM);
    // 检测listenfd,对于我们想要的普通可读事件是否就绪
    if (client[0].revents & POLLRDNORM) {
      clilen = sizeof(cliaddr);
      // accept
      connfd = accept(listenfd, (sockaddr *) &cliaddr, &clilen);
      // 新连接放入client[]
      for (i = 1; i < OPENMAX; i++)
        if (client[i].fd < 0) {
          client[i].fd = connfd;
          break;
        }
      if (i == OPENMAX)
        std::cout << "too many clients" << nready << std::endl;
      client[i].events = POLLRDNORM;
      if (i > maxi)
        maxi = i;
      // 没有就绪的描述符了
      if (--nready <= 0)
        continue;
    }

    // 遍历所有client
    for (i = 0; i <= maxi; i++) {
      // skip -1
      if ((sockfd = client[i].fd) < 0)
        continue;
      if (client[i].revents & (POLLRDNORM | POLLERR)) {
        if ((n = read(sockfd, buf, MAXLINE)) < 0) {
        }
        // close
        else if (n == 0) {
          close(sockfd);
          client[i].fd = -1;
        } else {
          write(sockfd, buf, n);
        }
        if (--nready <= 0)
          break;
      }
    }
  }

epoll函数

int epoll_create1(int flags);

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

要使用epoll需要先使用epoll_create来创建一个epoll实例,参数flag可为0或EPOLL_CLOEXEC。

epoll使用epoll_event来管理描述符,epoll_event:

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;    /* Epoll events */
    epoll_data_t data;      /* User data variable */
};

可关注事件:

  1. EPOLLIN,文件可读;

  2. EPOLLOUT,文件可写;

  3. EPOLLRDHUP (since Linux 2.6.17),流式套接字,对端关闭了连接,或者半关闭了写操作。该标志,在边沿触发模式下检测对端关闭时,是很有用的;

  4. EPOLLPRI,读操作上有紧急数据;

  5. EPOLLERR,文件描述符上发生了错误。epoll_wait始终监听该事件,无需再events上设置;

  6. EPOLLHUP,文件描述符上发生了Hang up。epoll_wait始终监听该事件,无需再events上设置;

  7. EPOLLET,置文件描述符为边沿触发模式,epoll上默认的行为模式是水平触发模式;

  8. EPOLLONESHOT (since Linux 2.6.2),置文件描述符为一次性的(one-shot)。这意味着,当该文件上通过epoll_wait触发了一个事件之后,该文件描述符就被内部disable了,在epoll实例上不再报告该文件描述符上的事件了。用户必须用EPOLL_CTL_MOD调用epoll_ctl,以新的事件掩码再次注册该描述符。

epoll_ctl主要有三个操作:EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)和EPOLL_CTL_DEL(删除)。

epoll_wait通过events返回所有就绪的epoll_event。

  • 基本使用

  1. 类似地,与之前相同,我们先使用socket()获得一个TCP套接字,用来监听新连接
// socket
#define SERV_PORT 8001
#define LISTENQ 1024

int listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
// bind
bind(listenfd, (sockaddr *)&servaddr, sizeof(servaddr));
// listen
listen(listenfd, LISTENQ);
  1. 创建epoll实例
// 创建一个epoll实例

int epollfd = epoll_create1(0);

// 使用epoll_event封装关注的事件和描述符
epoll_event listenev;
listenev.events = EPOLLIN;
listenev.data.fd = listenfd;

// 添加epoll_event
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listenev);
  1. 循环调用epoll_wait,处理新连接,处理客户端发送的消息
#define OPENMAX 256

// 被epoll_wait用来存放就绪的epoll_event
epoll_event events[OPENMAX];
while (1)
{
    // 正常返回就绪的描述符数量
    // int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    int nready = epoll_wait(epollfd, events, OPENMAX, -1);

    // 不必遍历所有epoll_event,调用epoll_wait后,所有就绪的epoll_event被放入events里,共nready个
    for (int i = 0; i != nready; i++)
    {
        // new connection
        if (events[i].data.fd == listenfd)
        {
            int connfd = accept(listenfd, (sockaddr *)&cliaddr, &clilen);
            epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = connfd;
            epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev);
        }
        else
        {
            if ((n = read(events[i].data.fd, buf, MAXLINE)) < 0)
            {
                // error
            }
            // close
            else if (n == 0)
            {
                close(events[i].data.fd);
                epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &events[i]);
            }
            else
            {
                write(events[i].data.fd, buf, n);
            }
        }
    }
}

总结

一文搞懂select、poll和epoll区别

posted @ 2021-08-08 21:35  ithepug  阅读(125)  评论(0)    收藏  举报