WebServer代码解读(3)【最小堆定时器与队列】

1 - 处理事件

因为epoll_wait函数已经返回了需要处理的事件数目events_num,并且已经将events_num个数的事件排到了数组的最前面,因此使用一个for循环取出前events_num个事件的data

处理事件的逻辑为:

  • 遍历events_num
    • 若当前事件是新连接,接收新连接
    • 否则,判断该事件合法以后,将该事件的请求request加入线程池(使用工作线程处理该事件)

1-1 接收新连接

调用acceptConnection函数接收server socket监听到的连接

acceptConnection函数内部逻辑如下:

  1. 申请一块内存,接收新的client socket连接(调用accept函数),将其放到刚刚申请的内存上
  2. 设置client socket为非阻塞
  3. 为client socket申请request内存,设置事件集合,并在内核事件表中注册
  4. 使用最小堆实现的定时器管理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结构体中,主要的逻辑为:

  1. 从产生request的fd中读取数据
  2. 针对该request的类型(URI、报头、报体、其他)进行对应类型的解析
  3. 若没有完成该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;
    }
}

Guess you like

Origin blog.csdn.net/weixin_44484715/article/details/117322778