逐步构建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;
};
- nullptr: 永远等待
- 使用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函数需要注意:
- nfds为待测试的最大描述符 + 1(0 - maxfd + 1 -1)
- 三个描述符集都是值->结果参数
-
基本使用:
本节都将以echo服务器举例。
- 与之前相同,我们先使用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);
- 使用描述符集
fd_set allset;
// 初始化描述符集:所有位置0
// FD_ZERO
FD_ZERO(&allset);
// 将listenfd加入描述符集allset
// FD_SET
FD_SET(listenfd, &allset);
- 使用数组存放描述符
int client[FD_SETSIZE]; // select.h: #define __FD_SETSIZE 1024
// 将未使用过的赋为-1
for (int i = 0; i < FD_SETSIZE; i++)
client[i] = -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函数返回前等待多长时间
- INFTIM 永远等待
- 0 立即返回,不阻塞
- >0 等待指定毫秒数
-
基本使用:
- 类似地,与之前相同,我们先使用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);
- 使用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;
- 循环调用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 */
};
可关注事件:
-
EPOLLIN,文件可读;
-
EPOLLOUT,文件可写;
-
EPOLLRDHUP (since Linux 2.6.17),流式套接字,对端关闭了连接,或者半关闭了写操作。该标志,在边沿触发模式下检测对端关闭时,是很有用的;
-
EPOLLPRI,读操作上有紧急数据;
-
EPOLLERR,文件描述符上发生了错误。epoll_wait始终监听该事件,无需再events上设置;
-
EPOLLHUP,文件描述符上发生了Hang up。epoll_wait始终监听该事件,无需再events上设置;
-
EPOLLET,置文件描述符为边沿触发模式,epoll上默认的行为模式是水平触发模式;
-
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。
-
基本使用
- 类似地,与之前相同,我们先使用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);
- 创建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);
- 循环调用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);
}
}
}
}

浙公网安备 33010602011771号