Web服务器—TinyWebServer代码详细讲解(main与WebServer)

Web服务器—TinyWebServer代码详细讲解(main与WebServer)编写一个Web服务器-代码模块详细讲解(上)config独立参数模块ET与LT模式config.h代码解读config.cpp代码解读main模块这里的参照的代码是https://github.com/qinguoyi/TinyWebServer对于原代码的不足之处,我会在之后的文章中给出改进代码在笔者fork的这版中,原代码作者对于代码作出了更细化的分类细节问题可以参考《APUE》《Linux高性能服务器编程》或者我之前的博客阅读任何源码一定要先从readme入手,如果没有rea

这里的参照的代码是https://github.com/qinguoyi/TinyWebServer

  • 对于原代码的不足之处,我会在之后的文章中给出改进代码
  • 在笔者fork的这版中,原代码作者对于代码作出了更细化的分类

细节问题可以参考《APUE》《Linux高性能服务器编程》或者我之前的博客

阅读任何源码一定要先从readme入手,如果没有readme,请从main入口入手。

config 独立参数模块

首先映入眼帘的是一个config的头文件,根据标识可以知道这是一个用户自定义头,所以我们先跳进去看看有什么东西。
我在每个每个条目中都给出了注释。

这里体现出整个项目的优点:模式切换
也就是说这是一个复合的ET/LT。作者在Listenfd上和cfd上都建立了两种模式,意味着我们有四种组合方式。

ET与LT模式

lfd的ET代表着一次性接受所有连接,笔者认为这里是考虑到网络接入量巨大,瞬间占到Max_size的情况。LT代表一次取一个,当然这是默认的方式也是最常见的方式。

cfd的两种方式也就是对应了epoll的方式,默认的LT和手动的ET

config.h代码解读

#ifndef CONFIG_H
#define CONFIG_H

#include "webserver.h" //懒得引用一堆头文件了

using namespace std;

class Config
{ 
   
public:
    Config();
    ~Config(){ 
   };

    void parse_arg(int argc, char*argv[]);

    //端口号
    int PORT;

    //日志写入方式
    int LOGWrite;

    //触发组合模式
    int TRIGMode;

    //listenfd触发模式
    int LISTENTrigmode;

    //connfd触发模式
    int CONNTrigmode;

    //优雅关闭链接
    int OPT_LINGER;

    //数据库连接池数量
    int sql_num;

    //线程池内的线程数量
    int thread_num;

    //是否关闭日志
    int close_log;

    //并发模型选择
    int actor_model;
};

#endif

config.cpp代码解读

其实很简单,在构造函数里作出了对于各种初始模式的设定
并且在这版代码中,对于并发模式的处理,作者给出了reactor和preactor两种方式。(后面会详细讲解)
原作者的测试环境为4核8线,所以这里给出了池内线程为8
TRIGMode默认为最低效模式,可以改为1,实现服务器的最高性能,大概实现10wQPS

#include "config.h"

Config::Config(){ 
   
    //端口号,默认9006
    PORT = 9006;

    //日志写入方式,默认同步
    LOGWrite = 0;

    //触发组合模式,默认listenfd LT + connfd LT
    TRIGMode = 0;

    //listenfd触发模式,默认LT
    LISTENTrigmode = 0;

    //connfd触发模式,默认LT
    CONNTrigmode = 0;

    //优雅关闭链接,默认不使用
    OPT_LINGER = 0;

    //数据库连接池数量,默认8
    sql_num = 8;

    //线程池内的线程数量,默认8
    thread_num = 8;

    //关闭日志,默认不关闭
    close_log = 0;

    //并发模型,默认是proactor
    actor_model = 0;
}

void Config::parse_arg(int argc, char*argv[]){ 
   
    int opt;
    const char *str = "p:l:m:o:s:t:c:a:";
    while ((opt = getopt(argc, argv, str)) != -1)
    { 
   
        switch (opt)
        { 
   
        case 'p':
        { 
   
            PORT = atoi(optarg);
            break;
        }
        case 'l':
        { 
   
            LOGWrite = atoi(optarg);
            break;
        }
        case 'm':
        { 
   
            TRIGMode = atoi(optarg);
            break;
        }
        case 'o':
        { 
   
            OPT_LINGER = atoi(optarg);
            break;
        }
        case 's':
        { 
   
            sql_num = atoi(optarg);
            break;
        }
        case 't':
        { 
   
            thread_num = atoi(optarg);
            break;
        }
        case 'c':
        { 
   
            close_log = atoi(optarg);
            break;
        }
        case 'a':
        { 
   
            actor_model = atoi(optarg);
            break;
        }
        default:
            break;
        }
    }
}

总结: 简单的初始化形式的分割,改动参数的时候我只需要改动config.cpp就行了。

main 模块

main模块的主要功能是,驱动Sever。
WebServer被单独作为一个类实现,并且封装好了调度函数。
也就是说,main函数相当于一个开关,我打开了服务器的开关让他进入了listen状态,同时也转身打开了数据库的开关。
当然,这个开关的信息,来自于config

main.cpp代码解读

这里的命令行解析是给数据库的运行方式传参,当然你可以什么都不传。
定义,之后初始化了一个服务器对象。
首先打开线程池,然后设置运行模式,之后就是启动监听和进入工作循环(事务循环)

#include "config.h"

int main(int argc, char *argv[])
{ 
   
    //需要修改的数据库信息,登录名,密码,库名
    string user = "root";
    string passwd = "1215";
    string databasename = "Liweb_db";

    //命令行解析
    Config config;
    config.parse_arg(argc, argv);

    WebServer server;

    //初始化
    server.init(config.PORT, user, passwd, databasename, config.LOGWrite, 
                config.OPT_LINGER, config.TRIGMode,  config.sql_num,  config.thread_num, 
                config.close_log, config.actor_model);
    

    //日志
    server.log_write();

    //数据库
    server.sql_pool();

    //线程池
    server.thread_pool();

    //触发模式
    server.trig_mode();

    //监听
    server.eventListen();

    //运行
    server.eventLoop();

    return 0;
}

总结: 其实这一版的好处就是,给main瘦身。main本质上就是提供了入口,入口不需要太复杂。

WebServer模块

对于这种复杂模块,我会尽量根据调度顺序进行每个函数的分析,对于优点部分,我会重点标出。
线程池是同步部分,数据库是额外部分这两个后面再讲
目前是要搞清楚,怎么弄个反应堆打到我可以拿到事务,处理的问题稍后再说。目前只需要知道,我有个处理业务逻辑的池。

trig_mode函数

不用解释,对应不同功能

void WebServer::trig_mode()
{ 
   
    //LT + LT
    if (0 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 0;
        m_CONNTrigmode = 0;
    }
    //LT + ET
    else if (1 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 0;
        m_CONNTrigmode = 1;
    }
    //ET + LT
    else if (2 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 1;
        m_CONNTrigmode = 0;
    }
    //ET + ET
    else if (3 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 1;
        m_CONNTrigmode = 1;
    }
}

在仔细阅读这种复杂功能的模块之前,一定要理解清除调用逻辑。
首先看main中分别调用了eventListen与eventLoop。根据见名知意的原则,我们可以推测出这是实现了listen部分与事务处理部分。

eventListen函数

如果你是初学者,不需要关注什么叫优雅的关闭连接这一部分,具体可以参考我的文章:Linux网络编程:知识点补充
简单的民工三连调用不需要解释,作者加入了assert,提升了健壮性。
之后就是调用epoll的三连了,将lfd上树,这里的上树封装为了addfd目的是为了可以更改模式。(cfd需要one_shot而lfd不需要)

之后就是创建了管道,这里牵扯到进程间通信的问题。这么做的好处就是统一事件源。因为正常情况下,信号处理与IO处理不走一条路。
这里的信号主要是超时问题
具体的做法是,信号处理函数使用管道将信号传递给主循环,信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出信号值,使用I/O复用系统调用来监听管道读端的可读事件,这样信号事件与其他文件描述符都可以通过epoll来监测,从而实现统一处理。

void WebServer::eventListen()
{ 
   
    //网络编程基础步骤
    m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    //如果它的条件返回错误,则终止程序执行
    assert(m_listenfd >= 0);

    //TCP连接断开的时候调用closesocket函数,有优雅的断开和强制断开两种方式
    
    //优雅关闭连接
    if (0 == m_OPT_LINGER)
    { 
   
        //底层会将未发送完的数据发送完成后再释放资源,也就是优雅的退出
        struct linger tmp = { 
   0, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }
    else if (1 == m_OPT_LINGER)
    { 
   
        //这种方式下,在调用closesocket的时候不会立刻返回,内核会延迟一段时间,这个时间就由l_linger得值来决定。
        //如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,closesocket会返回正确,socket描述符优雅性退出。
        //否则,closesocket会直接返回 错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则closesocket会直接返回值。
        struct linger tmp = { 
   1, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }

    int ret = 0;
    struct sockaddr_in address;
    // bzero() 会将内存块(字符串)的前n个字节清零;
    // s为内存(字符串)指针,n 为需要清零的字节数。
    // 在网络编程中会经常用到。
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    address.sin_port = htons(m_port);

    int flag = 1;
    //允许重用本地地址和端口
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    //传统绑定步骤
    ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    //>=0的设定 因为只有小于0才是错误情况
    assert(ret >= 0);
    //传统监听步骤
    ret = listen(m_listenfd, 5);
    assert(ret >= 0);

    utils.init(TIMESLOT);

    //epoll创建内核事件表
    epoll_event events[MAX_EVENT_NUMBER];
    m_epollfd = epoll_create(6);
    assert(m_epollfd != -1);

    //将lfd上树
    utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
    http_conn::m_epollfd = m_epollfd;

    //创建管道套接字
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
    assert(ret != -1);
    //设置管道写端为非阻塞,为什么写端要非阻塞?
    //send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,
    //这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞。
    utils.setnonblocking(m_pipefd[1]);

    //设置管道读端为ET非阻塞 统一事件源
    utils.addfd(m_epollfd, m_pipefd[0], false, 0);

    utils.addsig(SIGPIPE, SIG_IGN);

    //传递给主循环的信号值,这里只关注SIGALRM和SIGTERM
    utils.addsig(SIGALRM, utils.sig_handler, false);
    utils.addsig(SIGTERM, utils.sig_handler, false);

    //每隔TIMESLOT时间触发SIGALRM信号
    alarm(TIMESLOT);

    //工具类,信号和描述符基础操作
    Utils::u_pipefd = m_pipefd;
    Utils::u_epollfd = m_epollfd;
}

总结: 完成了设置lfd与统一事件源,并且创建了带有一个节点的epoll树,同时完成了超时设定

eventLoop函数

这个函数可以说是始终伴随着程序始终。只要服务器不关,我就一直不退出,因为我退出了,main也退出了。
明显看出,这一函数的逻辑就是不断的处理产生事件的节点(思考一下这里为什么这么叫)。
而在epoll_wait返回后,我们主要处理三种事件:io事件,信号,新的连接
也就是在for循环中的三次判断,并且每次处理完一组后,我们会刷新定时。

void WebServer::eventLoop()
{ 
   
    bool timeout = false;
    bool stop_server = false;

    while (!stop_server)
    { 
   
        //监测发生事件的文件描述符
        int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        if (number < 0 && errno != EINTR)
        { 
   
            LOG_ERROR("%s", "epoll failure");
            break;
        }

        //轮询有事件产生的文件描述符
        for (int i = 0; i < number; i++)
        { 
   
            int sockfd = events[i].data.fd;

            //处理新到的客户连接
            if (sockfd == m_listenfd)
            { 
   
                bool flag = dealclinetdata();
                if (false == flag)
                    continue;
            }
            else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
            { 
   
                //服务器端关闭连接,移除对应的定时器
                util_timer *timer = users_timer[sockfd].timer;
                deal_timer(timer, sockfd);
            }
            //处理信号
            //管道读端对应文件描述符发生读事件
            //因为统一了事件源,信号处理当成读事件来处理
            //怎么统一?就是信号回调函数哪里不立即处理而是写到:pipe的写端
            else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN))
            { 
   
                bool flag = dealwithsignal(timeout, stop_server);
                if (false == flag)
                    LOG_ERROR("%s", "dealclientdata failure");
            }
            //处理客户连接上接收到的数据
            else if (events[i].events & EPOLLIN)
            { 
   
                dealwithread(sockfd);
            }
            else if (events[i].events & EPOLLOUT)
            { 
   
                dealwithwrite(sockfd);
            }
        }
        if (timeout)
        { 
   
            utils.timer_handler();

            LOG_INFO("%s", "timer tick");

            timeout = false;
        }
    }
}

dealclinetdata函数

其实笔者认为不加data更加符合情境,因为当前只是建立连接。
就像刚才的模式介绍一样,lfd的两种模式。其实ET的存在就是应对存在服务器应付不过来连接请求,来提高效率的办法。
建议LT使用。
其实LT的额外循环是在epollwait部分,并且延迟体验是在用户端,用户可能傻傻的觉得自己卡了,问题不大

bool WebServer::dealclinetdata()
{ 
   
    struct sockaddr_in client_address;
    socklen_t client_addrlength = sizeof(client_address);
    //LT
    if (0 == m_LISTENTrigmode)
    { 
   
        int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
        if (connfd < 0)
        { 
   
            LOG_ERROR("%s:errno is:%d", "accept error", errno);
            return false;
        }
        if (http_conn::m_user_count >= MAX_FD)
        { 
   
            utils.show_error(connfd, "Internal server busy");
            LOG_ERROR("%s", "Internal server busy");
            return false;
        }
        timer(connfd, client_address);
    }

    else
    { 
   
        //ET
        while (1)
        { 
   
            int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
            if (connfd < 0)
            { 
   
                LOG_ERROR("%s:errno is:%d", "accept error", errno);
                break;
            }
            if (http_conn::m_user_count >= MAX_FD)
            { 
   
                utils.show_error(connfd, "Internal server busy");
                LOG_ERROR("%s", "Internal server busy");
                break;
            }
            timer(connfd, client_address);
        }
        return false;
    }
    return true;
}

dealwithread函数

按照之前的思想,对于整个并发模式的思路,存在两个模式的切换:reactor与preactor(同步io模拟出)。它们的区别是对于数据的读取者是谁,对于reactor是同步线程来完成,整个读就绪放在请求列表上;而对于preactor则是由主线程,也就是当前的WebServer进行一次调用,读取后将读完成纳入请求队列上。

同样,对于当前的fd我们要对他进行时间片的调整。同样的,当时间到期时,在定时器对象中,会有对应的下树操作。

void WebServer::dealwithread(int sockfd)
{ 
   
    util_timer *timer = users_timer[sockfd].timer;

    //reactor
    if (1 == m_actormodel)
    { 
   
        if (timer)
        { 
   
            adjust_timer(timer);
        }

        //若监测到读事件,将该事件放入请求队列
        m_pool->append(users + sockfd, 0);

        while (true)
        { 
   
            if (1 == users[sockfd].improv)
            { 
   
                if (1 == users[sockfd].timer_flag)
                { 
   
                    deal_timer(timer, sockfd);
                    users[sockfd].timer_flag = 0;
                }
                users[sockfd].improv = 0;
                break;
            }
        }
    }
    else
    { 
   
        //proactor
        if (users[sockfd].read_once())
        { 
   
            LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

            //读完成事件,将该事件放入请求队列
            m_pool->append_p(users + sockfd);

            if (timer)
            { 
   
                adjust_timer(timer);
            }
        }
        else
        { 
   
            deal_timer(timer, sockfd);
        }
    }
}

dealwithwrite函数

逻辑与模式大致相同

void WebServer::dealwithwrite(int sockfd)
{ 
   
    util_timer *timer = users_timer[sockfd].timer;
    //reactor
    if (1 == m_actormodel)
    { 
   
        if (timer)
        { 
   
            adjust_timer(timer);
        }

        m_pool->append(users + sockfd, 1);

        while (true)
        { 
   
            if (1 == users[sockfd].improv)
            { 
   
                if (1 == users[sockfd].timer_flag)
                { 
   
                    deal_timer(timer, sockfd);
                    users[sockfd].timer_flag = 0;
                }
                users[sockfd].improv = 0;
                break;
            }
        }
    }
    else
    { 
   
        //proactor
        if (users[sockfd].write())
        { 
   
            LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

            if (timer)
            { 
   
                adjust_timer(timer);
            }
        }
        else
        { 
   
            deal_timer(timer, sockfd);
        }
    }
}

dealwithsignal函数

与读写不同的是,这里的signal是处理函数,它不需要上队列。这里是通过管道的方式来告知WebServer。管道由epoll监控

bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{ 
   
    int ret = 0;
    int sig;
    char signals[1024];

    //从管道读端读出信号值,成功返回字节数,失败返回-1
    //正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
    ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
    if (ret == -1)
    { 
   
        return false;
    }
    else if (ret == 0)
    { 
   
        return false;
    }
    else
    { 
   
        //处理信号值对应的逻辑
        for (int i = 0; i < ret; ++i)
        { 
   
            //这里面明明是字符
            switch (signals[i])
            { 
   
            //这里是整型
            case SIGALRM:
            { 
   
                timeout = true;
                break;
            }
            case SIGTERM:
            { 
   
                stop_server = true;
                break;
            }
            }
        }
    }
    return true;
}

总结:目前我们只了解append是一个加入请求队列的函数,不去探究具体实现,当然在编程过程中我们应该也是这种思想,按照模块平行编程。而不是我想到什么功能就一定要先实现出来。递归编程容易把自己搞乱

time函数集

同样,我们把与时间片相关的调用,放在WebServer里,但是里面的细节,通过time这个类来实现。

timer函数

首先搞清楚,timer在什么时候调用?答案是在accept得到cfd的时候。这时候通过timer函数不只是初始化了cfd的时间,而且整体初始化。
也就是说,当前服务器已经认可了这一连接,完成了三次握手,并且得到了用户标识,允许传输数据。
这里为了提升性能,给到了3倍阈值的超时。

void WebServer::timer(int connfd, struct sockaddr_in client_address)
{ 
   
    users[connfd].init(connfd, client_address, m_root, m_CONNTrigmode, m_close_log, m_user, m_passWord, m_databaseName);

    //初始化client_data数据
    //创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中
    users_timer[connfd].address = client_address;
    users_timer[connfd].sockfd = connfd;
    util_timer *timer = new util_timer;
    timer->user_data = &users_timer[connfd];
    timer->cb_func = cb_func;
    time_t cur = time(NULL);
    timer->expire = cur + 3 * TIMESLOT;
    users_timer[connfd].timer = timer;
    utils.m_timer_lst.add_timer(timer);
}

adjust_timer与deal_timer

也是区分清楚什么时候调用。
adjust可以看到是在产生事件之后,我为了还能传输数据,再给你刷新一下你的时间或者给你延长;而deal则是对于坏连接的一个处理。
如果你对此不理解,可以跳过,你只需要知道what it is。至于how and why我会在time部分详细解析。

void WebServer::adjust_timer(util_timer *timer)
{ 
   
    time_t cur = time(NULL);
    timer->expire = cur + 3 * TIMESLOT;
    utils.m_timer_lst.adjust_timer(timer);

    LOG_INFO("%s", "adjust timer once");
}
void WebServer::deal_timer(util_timer *timer, int sockfd)
{ 
   
    timer->cb_func(&users_timer[sockfd]);
    if (timer)
    { 
   
        utils.m_timer_lst.del_timer(timer);
    }
    LOG_INFO("close fd %d", users_timer[sockfd].sockfd);
}

总结

WebServer可以说是全功能的一个大集合,也就是说我们构建出了一个领导角色,左手epoll右手线程池。我尽量按照调用顺序来讲解函数,方便读者阅读代码。

WebServer.h 头文件

#ifndef WEBSERVER_H
#define WEBSERVER_H

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <cassert>
#include <sys/epoll.h>

#include "./threadpool/threadpool.h"
#include "./http/http_conn.h"

const int MAX_FD = 65536;           //最大文件描述符
const int MAX_EVENT_NUMBER = 10000; //最大事件数
const int TIMESLOT = 5;             //最小超时单位

class WebServer
{ 
   
public:
    WebServer();
    ~WebServer();

    void init(int port , string user, string passWord, string databaseName,
              int log_write , int opt_linger, int trigmode, int sql_num,
              int thread_num, int close_log, int actor_model);
    //线程池函数
    void thread_pool(); 
    //数据库池函数
    void sql_pool();    
    void log_write(); 
    //更改模式 
    void trig_mode();  
    //创建lfd 
    void eventListen(); 
    //当服务器非关闭状态 用于处理事件
    void eventLoop();   
    
    //定时器的操作
    void timer(int connfd, struct sockaddr_in client_address); 
    void adjust_timer(util_timer *timer);
    void deal_timer(util_timer *timer, int sockfd); 

    //处理用户数据 这里原作者存在拼写错误
    bool dealclinetdata(); 
    //信号
    bool dealwithsignal(bool& timeout, bool& stop_server); 
    //读事件
    void dealwithread(int sockfd);
    //写事件
    void dealwithwrite(int sockfd);

public:
    //基础
    //监听端口
    int m_port;
    char *m_root;
    //日志
    int m_log_write;
    int m_close_log;
    //触发模式
    int m_actormodel;

    //进程通信模块
    int m_pipefd[2];
    //epoll根
    int m_epollfd;
    
    //用于接受用户连接
    http_conn *users;

    //数据库相关
    connection_pool *m_connPool;
    string m_user;         //登陆数据库用户名
    string m_passWord;     //登陆数据库密码
    string m_databaseName; //使用数据库名
    int m_sql_num;

    //http线程池
    threadpool<http_conn> *m_pool;
    int m_thread_num;

    //epoll_event 注册节点事件 
    epoll_event events[MAX_EVENT_NUMBER];

    int m_listenfd; //监听fd 申请一次
    int m_OPT_LINGER;
    int m_TRIGMode; //触发模式 ET+LT LT+LT LT+ET ET+ET 
    int m_LISTENTrigmode; // 监听 ET/LT
    int m_CONNTrigmode;   // 连接 ET/LT

    //定时器相关
    client_data *users_timer;
    Utils utils;
};
#endif

WebServer.cpp 完整代码

#include "webserver.h"

WebServer::WebServer()
{ 
   
    //用来调用指定fd的所需功能模块
    users = new http_conn[MAX_FD];

    //root文件夹路径
    char server_path[200];
    getcwd(server_path, 200);
    char root[6] = "/root";
    m_root = (char *)malloc(strlen(server_path) + strlen(root) + 1);
    strcpy(m_root, server_path);
    strcat(m_root, root);

    //定时器
    users_timer = new client_data[MAX_FD];
}

WebServer::~WebServer()
{ 
   
    close(m_epollfd);
    close(m_listenfd);
    close(m_pipefd[1]);
    close(m_pipefd[0]);
    delete[] users;
    delete[] users_timer;
    delete m_pool;
}

void WebServer::init(int port, string user, string passWord, string databaseName, int log_write, 
                     int opt_linger, int trigmode, int sql_num, int thread_num, int close_log, int actor_model)
{ 
   
    m_port = port;
    m_user = user;
    m_passWord = passWord;
    m_databaseName = databaseName;
    m_sql_num = sql_num;
    m_thread_num = thread_num;
    m_log_write = log_write;
    m_OPT_LINGER = opt_linger;
    m_TRIGMode = trigmode;
    m_close_log = close_log;
    m_actormodel = actor_model;
}

void WebServer::trig_mode()
{ 
   
    //LT + LT
    if (0 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 0;
        m_CONNTrigmode = 0;
    }
    //LT + ET
    else if (1 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 0;
        m_CONNTrigmode = 1;
    }
    //ET + LT
    else if (2 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 1;
        m_CONNTrigmode = 0;
    }
    //ET + ET
    else if (3 == m_TRIGMode)
    { 
   
        m_LISTENTrigmode = 1;
        m_CONNTrigmode = 1;
    }
}

void WebServer::log_write()
{ 
   
    if (0 == m_close_log)
    { 
   
        //初始化日志
        if (1 == m_log_write)
            Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);
        else
            Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
    }
}

void WebServer::sql_pool()
{ 
   
    //初始化数据库连接池
    m_connPool = connection_pool::GetInstance();
    m_connPool->init("localhost", m_user, m_passWord, m_databaseName, 3306, m_sql_num, m_close_log);

    //初始化数据库读取表
    users->initmysql_result(m_connPool);
}

void WebServer::thread_pool()
{ 
   
    //线程池
    m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
}

void WebServer::eventListen()
{ 
   
    //网络编程基础步骤
    m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    //如果它的条件返回错误,则终止程序执行
    assert(m_listenfd >= 0);

    //TCP连接断开的时候调用closesocket函数,有优雅的断开和强制断开两种方式
    
    //优雅关闭连接
    if (0 == m_OPT_LINGER)
    { 
   
        //底层会将未发送完的数据发送完成后再释放资源,也就是优雅的退出
        struct linger tmp = { 
   0, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }
    else if (1 == m_OPT_LINGER)
    { 
   
        //这种方式下,在调用closesocket的时候不会立刻返回,内核会延迟一段时间,这个时间就由l_linger得值来决定。
        //如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,closesocket会返回正确,socket描述符优雅性退出。
        //否则,closesocket会直接返回 错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的时,如果socket描述符被设置为非堵塞型,则closesocket会直接返回值。
        struct linger tmp = { 
   1, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }

    int ret = 0;
    struct sockaddr_in address;
    // bzero() 会将内存块(字符串)的前n个字节清零;
    // s为内存(字符串)指针,n 为需要清零的字节数。
    // 在网络编程中会经常用到。
    bzero(&address, sizeof(address));
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    address.sin_port = htons(m_port);

    int flag = 1;
    //允许重用本地地址和端口
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    //传统绑定步骤
    ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    //>=0的设定 因为只有小于0才是错误情况
    assert(ret >= 0);
    //传统监听步骤
    ret = listen(m_listenfd, 5);
    assert(ret >= 0);

    utils.init(TIMESLOT);

    //epoll创建内核事件表
    epoll_event events[MAX_EVENT_NUMBER];
    m_epollfd = epoll_create(6);
    assert(m_epollfd != -1);

    //将lfd上树
    utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
    http_conn::m_epollfd = m_epollfd;

    //创建管道套接字
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
    assert(ret != -1);
    //设置管道写端为非阻塞,为什么写端要非阻塞?
    //send是将信息发送给套接字缓冲区,如果缓冲区满了,则会阻塞,
    //这时候会进一步增加信号处理函数的执行时间,为此,将其修改为非阻塞。
    utils.setnonblocking(m_pipefd[1]);

    //设置管道读端为ET非阻塞 统一事件源
    utils.addfd(m_epollfd, m_pipefd[0], false, 0);

    utils.addsig(SIGPIPE, SIG_IGN);

    //传递给主循环的信号值,这里只关注SIGALRM和SIGTERM
    utils.addsig(SIGALRM, utils.sig_handler, false);
    utils.addsig(SIGTERM, utils.sig_handler, false);

    //每隔TIMESLOT时间触发SIGALRM信号
    alarm(TIMESLOT);

    //工具类,信号和描述符基础操作
    Utils::u_pipefd = m_pipefd;
    Utils::u_epollfd = m_epollfd;
}

void WebServer::timer(int connfd, struct sockaddr_in client_address)
{ 
   
    users[connfd].init(connfd, client_address, m_root, m_CONNTrigmode, m_close_log, m_user, m_passWord, m_databaseName);

    //初始化client_data数据
    //创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中
    users_timer[connfd].address = client_address;
    users_timer[connfd].sockfd = connfd;
    util_timer *timer = new util_timer;
    timer->user_data = &users_timer[connfd];
    //时间到了踢出树
    timer->cb_func = cb_func;
    time_t cur = time(NULL);
    timer->expire = cur + 3 * TIMESLOT;
    users_timer[connfd].timer = timer;
    utils.m_timer_lst.add_timer(timer);
}

//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
void WebServer::adjust_timer(util_timer *timer)
{ 
   
    time_t cur = time(NULL);
    timer->expire = cur + 3 * TIMESLOT;
    utils.m_timer_lst.adjust_timer(timer);

    LOG_INFO("%s", "adjust timer once");
}

void WebServer::deal_timer(util_timer *timer, int sockfd)
{ 
   
    timer->cb_func(&users_timer[sockfd]);
    if (timer)
    { 
   
        utils.m_timer_lst.del_timer(timer);
    }

    LOG_INFO("close fd %d", users_timer[sockfd].sockfd);
}


bool WebServer::dealclinetdata()
{ 
   
    struct sockaddr_in client_address;
    socklen_t client_addrlength = sizeof(client_address);
    //LT
    if (0 == m_LISTENTrigmode)
    { 
   
        int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
        if (connfd < 0)
        { 
   
            LOG_ERROR("%s:errno is:%d", "accept error", errno);
            return false;
        }
        if (http_conn::m_user_count >= MAX_FD)
        { 
   
            utils.show_error(connfd, "Internal server busy");
            LOG_ERROR("%s", "Internal server busy");
            return false;
        }
        timer(connfd, client_address);
    }

    else
    { 
   
        //ET
        while (1)
        { 
   
            int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
            if (connfd < 0)
            { 
   
                LOG_ERROR("%s:errno is:%d", "accept error", errno);
                break;
            }
            if (http_conn::m_user_count >= MAX_FD)
            { 
   
                utils.show_error(connfd, "Internal server busy");
                LOG_ERROR("%s", "Internal server busy");
                break;
            }
            timer(connfd, client_address);
        }
        return false;
    }
    return true;
}

bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{ 
   
    int ret = 0;
    int sig;
    char signals[1024];

    //从管道读端读出信号值,成功返回字节数,失败返回-1
    //正常情况下,这里的ret返回值总是1,只有14和15两个ASCII码对应的字符
    ret = recv(m_pipefd[0], signals, sizeof(signals), 0);
    if (ret == -1)
    { 
   
        return false;
    }
    else if (ret == 0)
    { 
   
        return false;
    }
    else
    { 
   
        //处理信号值对应的逻辑
        for (int i = 0; i < ret; ++i)
        { 
   
            //这里面明明是字符
            switch (signals[i])
            { 
   
            //这里是整型
            case SIGALRM:
            { 
   
                timeout = true;
                break;
            }
            case SIGTERM:
            { 
   
                stop_server = true;
                break;
            }
            }
        }
    }
    return true;
}

void WebServer::dealwithread(int sockfd)
{ 
   
    util_timer *timer = users_timer[sockfd].timer;

    //reactor
    if (1 == m_actormodel)
    { 
   
        if (timer)
        { 
   
            adjust_timer(timer);
        }

        //若监测到读事件,将该事件放入请求队列
        m_pool->append(users + sockfd, 0);

        while (true)
        { 
   
            if (1 == users[sockfd].improv)
            { 
   
                if (1 == users[sockfd].timer_flag)
                { 
   
                    deal_timer(timer, sockfd);
                    users[sockfd].timer_flag = 0;
                }
                users[sockfd].improv = 0;
                break;
            }
        }
    }
    else
    { 
   
        //proactor
        if (users[sockfd].read_once())
        { 
   
            LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

            //读完成事件,将该事件放入请求队列
            m_pool->append_p(users + sockfd);

            if (timer)
            { 
   
                adjust_timer(timer);
            }
        }
        else
        { 
   
            deal_timer(timer, sockfd);
        }
    }
}

void WebServer::dealwithwrite(int sockfd)
{ 
   
    util_timer *timer = users_timer[sockfd].timer;
    //reactor
    if (1 == m_actormodel)
    { 
   
        if (timer)
        { 
   
            adjust_timer(timer);
        }

        m_pool->append(users + sockfd, 1);

        while (true)
        { 
   
            if (1 == users[sockfd].improv)
            { 
   
                if (1 == users[sockfd].timer_flag)
                { 
   
                    deal_timer(timer, sockfd);
                    users[sockfd].timer_flag = 0;
                }
                users[sockfd].improv = 0;
                break;
            }
        }
    }
    else
    { 
   
        //proactor
        if (users[sockfd].write())
        { 
   
            LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));

            if (timer)
            { 
   
                adjust_timer(timer);
            }
        }
        else
        { 
   
            deal_timer(timer, sockfd);
        }
    }
}

void WebServer::eventLoop()

今天的文章Web服务器—TinyWebServer代码详细讲解(main与WebServer)分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/8723.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注