1 - 处理事件
因为epoll_wait函数已经返回了需要处理的事件数目events_num,并且已经将events_num个数的事件排到了数组的最前面,因此使用一个for循环取出前events_num个事件的data
处理事件的逻辑为:
- 遍历events_num
- 若当前事件是新连接,接收新连接
- 否则,判断该事件合法以后,将该事件的请求request加入线程池(使用工作线程处理该事件)
1-1 接收新连接
调用acceptConnection函数接收server socket监听到的连接
acceptConnection函数内部逻辑如下:
- 申请一块内存,接收新的client socket连接(调用accept函数),将其放到刚刚申请的内存上
- 设置client socket为非阻塞
- 为client socket申请request内存,设置事件集合,并在内核事件表中注册
- 使用最小堆实现的定时器管理client request是否超时,维护client request队列(结合锁实现定时器节点的添加和刹车农户)
void acceptConnection(int listen_fd, int epoll_fd, const string &path)
{
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(struct sockaddr_in));
socklen_t client_addr_len = 0;
int accept_fd = 0;
while((accept_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len)) > 0)
{
/*
// TCP的保活机制默认是关闭的
int optval = 0;
socklen_t len_optval = 4;
getsockopt(accept_fd, SOL_SOCKET, SO_KEEPALIVE, &optval, &len_optval);
cout << "optval ==" << optval << endl;
*/
// 设为非阻塞模式
int ret = setSocketNonBlocking(accept_fd);
if (ret < 0)
{
perror("Set non block failed!");
return;
}
requestData *req_info = new requestData(epoll_fd, accept_fd, path);
// 文件描述符可以读,边缘触发(Edge Triggered)模式,保证一个socket连接在任一时刻只被一个线程处理
__uint32_t _epo_event = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_add(epoll_fd, accept_fd, static_cast<void*>(req_info), _epo_event);
// 新增时间信息
mytimer *mtimer = new mytimer(req_info, TIMER_TIME_OUT);
req_info->addTimer(mtimer);
pthread_mutex_lock(&qlock);
myTimerQueue.push(mtimer);
pthread_mutex_unlock(&qlock);
}
//if(accept_fd == -1)
// perror("accept");
}
1-2 最小堆定时器
设计定时器的一种思路:
将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们在心搏函数tick中处理该定时器。然后,从剩余的定时器中找到超时时间最小的一个,将这段最小时间设置为下一次心搏间隔。重复以上过程既能实现定时功能。
最小堆又叫小根堆,指的是每个节点的值都 ≤ 子节点的值 的完全二叉树
关于最小堆的节点插入与删除操作,可以参考我之前总结的博文 堆排序【二叉堆简介】【二叉堆插入/删除堆顶/构造】【堆排序】【最大堆/最小堆】
定时器是针对请求request来说的,有一个request到来,就为他创建一个定时器。定时器的结构如下:
struct mytimer
{
bool deleted;
size_t expired_time;
requestData *request_data;
mytimer(requestData *_request_data, int timeout);
~mytimer();
void update(int timeout);
bool isvalid();
void clearReq();
void setDeleted();
bool isDeleted() const;
size_t getExpTime() const;
};
使用request初始化定时器的时候,要获取当前时间:
mytimer::mytimer(requestData *_request_data, int timeout): deleted(false), request_data(_request_data)
{
//cout << "mytimer()" << endl;
struct timeval now;
gettimeofday(&now, NULL);
// 以毫秒计
expired_time = ((now.tv_sec * 1000) + (now.tv_usec / 1000)) + timeout;
}
然后将定时器添加到request的信息中,并且将该定时器添加到定时器队列中(注意定时器队列属于临界资源,进队列进出操作都需要加锁)。
使用STL的priority_queue实现定时器队列。在priority_queue中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。因此,需要根据定时器自定义队列优先级排序规则
extern priority_queue<mytimer*, deque<mytimer*>, timerCmp> myTimerQueue;
//...
struct timerCmp
{
bool operator()(const mytimer *a, const mytimer *b) const;
};
//...
bool timerCmp::operator()(const mytimer *a, const mytimer *b) const
{
return a->getExpTime() > b->getExpTime();
}
//...
size_t mytimer::getExpTime() const
{
return expired_time;
}
1-4 将request加入线程池
这里就用到了之前博文中 WebServer代码解读(1)【main函数流程】【忽略SIGPIPE信号:handle_for_sigpipe】【创建EPOLL内核事件表】【线程池组成部分与工作逻辑】 中的 threadpool_add函数
///添加需要执行的任务。第二个参数为对应函数指针,第三个为对应函数参数。flags 未使用。
int threadpool_add(threadpool_t *pool, void (*function)(void *), void *argument, int flags);
//传入参数为
int rc = threadpool_add(tp, myHandler, events[i].data.ptr, 0);
解释一下实参myHandler函数,该函数用于处理request。这是一个函数指针,代码如下:
void myHandler(void *args)
{
requestData *req_data = (requestData*)args;
req_data->handleRequest();
}
1-5 处理request
处理request的函数封装在requestData结构体中,主要的逻辑为:
- 从产生request的fd中读取数据
- 针对该request的类型(URI、报头、报体、其他)进行对应类型的解析
- 若没有完成该request(state没有置为完成状态),那么将该request重新设计定时器(包括将定时器加入定时器队列),重新在内核事件表中注册
void requestData::handleRequest()
{
char buff[MAX_BUFF];
bool isError = false;
while (true)
{
int read_num = readn(fd, buff, MAX_BUFF);
if (read_num < 0)
{
perror("1");
isError = true;
break;
}
else if (read_num == 0)
{
// 有请求出现但是读不到数据,可能是Request Aborted,或者来自网络的数据没有达到等原因
perror("read_num == 0");
if (errno == EAGAIN)
{
if (againTimes > AGAIN_MAX_TIMES)
isError = true;
else
++againTimes;
}
else if (errno != 0)
isError = true;
break;
}
string now_read(buff, buff + read_num);
content += now_read;
if (state == STATE_PARSE_URI)
{
int flag = this->parse_URI();
if (flag == PARSE_URI_AGAIN)
{
break;
}
else if (flag == PARSE_URI_ERROR)
{
perror("2");
isError = true;
break;
}
}
if (state == STATE_PARSE_HEADERS)
{
int flag = this->parse_Headers();
if (flag == PARSE_HEADER_AGAIN)
{
break;
}
else if (flag == PARSE_HEADER_ERROR)
{
perror("3");
isError = true;
break;
}
if(method == METHOD_POST)
{
state = STATE_RECV_BODY;
}
else
{
state = STATE_ANALYSIS;
}
}
if (state == STATE_RECV_BODY)
{
int content_length = -1;
if (headers.find("Content-length") != headers.end())
{
content_length = stoi(headers["Content-length"]);
}
else
{
isError = true;
break;
}
if (content.size() < content_length)
continue;
state = STATE_ANALYSIS;
}
if (state == STATE_ANALYSIS)
{
int flag = this->analysisRequest();
if (flag < 0)
{
isError = true;
break;
}
else if (flag == ANALYSIS_SUCCESS)
{
state = STATE_FINISH;
break;
}
else
{
isError = true;
break;
}
}
}
if (isError)
{
delete this;
return;
}
// 加入epoll继续
if (state == STATE_FINISH)
{
if (keep_alive)
{
printf("ok\n");
this->reset();
}
else
{
delete this;
return;
}
}
// 一定要先加时间信息,否则可能会出现刚加进去,下个in触发来了,然后分离失败后,又加入队列,最后超时被删,然后正在线程中进行的任务出错,double free错误。
// 新增时间信息
pthread_mutex_lock(&qlock);
mytimer *mtimer = new mytimer(this, 500);
timer = mtimer;
myTimerQueue.push(mtimer);
pthread_mutex_unlock(&qlock);
__uint32_t _epo_event = EPOLLIN | EPOLLET | EPOLLONESHOT;
int ret = epoll_mod(epollfd, fd, static_cast<void*>(this), _epo_event);
if (ret < 0)
{
// 返回错误处理
delete this;
return;
}
}