从头开始理解TinyWebServer

理解一个项目最好的方法便是从main函数出发!

#include "config.h"

// argc:命令行参数的数量 argv:命令行参数的值
int main(int argc, char *argv[])
{
    //需要修改的数据库信息,登录名,密码,库名
    string user = "root";
    string passwd = "root";
    string databasename = "qgydb";

    //命令行解析
    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;
}

先看前三行,就是配置我们的数据库信息,也就是WebServer接收到游览器端发送的数据后,进行数据对比的地方。这三行根据自己的数据库填写即可。


四五两行主要是用来解析命令行参数,对应作者说的“个性化运行”

.......

进入config.h,主要是一些关于个性化运行的参数变量,构造函数,析构函数以及命令行解析的

parse_arg()函数。接下来我们在config.cpp中看parse_arg()函数是如何解析命令行的

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;
        }
    }
}

首先,函数的参数 'argc' 表示命令行参数的数量,'argv'是指向字符数组的指针数组,也就是命令行参数。其次,str字符串是用来定义期望的命令行选项,每个字母对应一个单独的选项,字母后面的冒号表示该选项需要一个参数值,如'p'选项需要一个参数,可以这样表达:'-p 1234'。while循环用'getopt'函数来逐个解析命令行参数,'getopt'函数会返回下一个选项的字母,如果没有则返回-1,代表解析完成。利用switch对当前解析的选项执行相应的操作,其中,atoi()函数(该函数是标准C库自带的)是将参数值转换为相应的整数,然后赋值给相应的变量(这里的变量也就是config.h中的那些变量)。


第六行是创建一个WebServer对象,这是整个项目的核心。七八九行便是对WebServer对象的初始化(变量的赋值)。初始化的参数,恰好就是我们刚才在config.cpp中解析的命令行参数。


第十行进行服务器的日志写入工作。进入WebServer查看log_write()函数。

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);
    }
}

if(0 == m_close_log),检查成员变量'm_clost_log'是否为0。如果不为0,表示不关闭日志记录,继续执行后续的日志初始化操作。然后根据m_log_write的值启用不同的日志写入方式。

'Log::get_instance()->init();',get_instance()是Log类的成员函数,用于获取日志记录的实例,init()函数是指定日志文件的相关参数。注意:这里获取实例的方式,是单例模式+懒汉模式(简单来说,单例模式就是确保一个类只有一个实例,并提供了一个全局访问点去访问该实例,懒汉模式是只有被需要时才会创建实例,而不是程序一运行就创建实例。这其实很好解释!如果一个服务器又多个日志记录器,这不仅浪费资源,还会导致诸多混乱!!)


第十一行初始化数据库连接池。进入WebServer查看sql_pool()函数。

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);
}

与日志初始化差不多,先通过成员函数获取数据库连接池的实例,然后初始化数据库连接池。

'users -> initmysql_result(m_connPool);'是http_conn类的一个公有成员函数,用于初始化数据库连接相关的信息。


'http_conn'类表示HTTP连接和请求的处理逻辑,用于处理客户端的HTTP请求和生成HTTP响应。


第十二行初始化线程池

函数具体代码:

创建一个线程池对象,参数m_actormodel是命令行解析后得到的参数,m_connPool是上一行创建的数据库连接池对象,m_thread_num是命令行解析后得到的参数。

threadpool类的构造函数:


第十三行是设置触发模式

函数具体代码:

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;
    }
}

主要是根据命令行解析的m_TRIGMode来设置事件的触发方式(LT 模式表示事件在就绪时会一直触发,ET 模式表示事件只在状态变化时触发一次)


第十四行是开启服务器监听,也就是让服务器处于待连接状态,等待客户端的连接。

函数具体代码:

void WebServer::eventListen()
{
    //网络编程基础步骤
    // m_listenfd 存储了监听套接字的文件描述符
    m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    // assert断言来检查套接字创建是否成功,若创建失败,则程序终止
    assert(m_listenfd >= 0);

    //优雅关闭连接
    if (0 == m_OPT_LINGER)
    {
        // 不启用优雅关闭连接,设置so_linger套接字选项,立即关闭连接
        struct linger tmp = {0, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }
    else if (1 == m_OPT_LINGER)
    {
        // 启用优雅关闭连接,设置so_linger套接字选项,等待未发送的数据发送完毕后再关闭连接
        struct linger tmp = {1, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }

    int ret = 0;
    // 创建并配置服务器地址结构体address
    struct sockaddr_in address;
    bzero(&address, sizeof(address)); 
    address.sin_family = AF_INET; // 设置地址族为AF_INET,表示使用IPv4地址
    address.sin_addr.s_addr = htonl(INADDR_ANY);  // 设置IP地址为INADDR_ANY,表示绑定到所有可用的网络接口
    address.sin_port = htons(m_port);

    int flag = 1;
    // 启用SO_REUSEADDR套接字选项,以便在服务器重启时能够重新使用绑定的端口
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag)); 
    // 绑定套接字到指定的地址结构体,这将服务器的监听套接字与指定的端口绑定在一起
    ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    assert(ret >= 0);
    // 调用listen函数开始监听连接请求,设置最大等待连接队列的长度为5
    ret = listen(m_listenfd, 5);
    assert(ret >= 0);

    // 初始化实例utils
    utils.init(TIMESLOT);

    //epoll创建内核事件表
    // 声明了一个存储 epoll 事件的数组 events,数组的大小是 MAX_EVENT_NUMBER
    epoll_event events[MAX_EVENT_NUMBER];
    // 创建了一个 epoll 内核事件表,并将其文件描述符存储在 m_epollfd 成员变量中
    m_epollfd = epoll_create(5);
    assert(m_epollfd != -1);

    // 调用utils对象的addfd函数,将监听套接字m_listenfd添加到epoll事件表中,指定事件监听方式为 m_LISTENTrigmode
    utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
    // 设置http_conn类的成员变量
    http_conn::m_epollfd = m_epollfd;

    // 创建了一个UNIX域的全双工套接字对,其中m_pipefd数组中存储了两个套接字描述符,用于进程间通信。
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
    assert(ret != -1);
    // 将m_pipefd[1]设置为非阻塞模式,以提高管道写入的效率。
    utils.setnonblocking(m_pipefd[1]);
    // 将m_pipefd[0]添加到epoll事件表中,用于监听管道读事件。
    utils.addfd(m_epollfd, m_pipefd[0], false, 0);

    // 设置了忽略SIGPIPE信号,这是为了防止在向已关闭的套接字写入数据时触发SIGPIPE信号导致程序退出
    utils.addsig(SIGPIPE, SIG_IGN);
    // 设置了SIGALRM信号的处理函数为utils::sig_handler,并指定false参数表示不重新启用信号处理。
    utils.addsig(SIGALRM, utils.sig_handler, false);
    // 设置了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;
}

其中,Utils是webserver的成员对象,这个类是处理非阻塞I/O、信号处理、定时等任务的工具类。

初始化实例utils的具体函数:

void Utils::init(int timeslot)
{
    m_TIMESLOT = timeslot;
}

m_TIMESLOT表示一个时间间隔,用于配置定时任务的触发间隔。


最后一行是运行服务器。

函数具体代码:

// 该事件循环不断地等待各种事件的发生,然后根据事件类型进行相应的处理,包括处理新的客户端连接、处理信号、处理读写事件等
// 在定时器超时时,也会触发定时器事件的处理。循环会一直运行,直到stop_server变为true,通常由外部机制出发的,表示服务器要
// 停止运行
void WebServer::eventLoop()
{
    // 是否发生了定时器超时
    bool timeout = false;
    // 是否要停止服务器
    bool stop_server = false;

    while (!stop_server)
    {
        // epoll_wait函数等待事件的发生,m_epollfd是一个epoll文件描述符,events是用于存储发生事件的数组,MAX_EVENT_NUMBER是最大
        // 事件数量,-1表示一直等待直到有事件发生。number保存了发生的事件数量
        int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);

        // 如果返回值小于0且错误码不是EINTR(表示被信号中断),则打印错误信息,并退出循环
        if (number < 0 && errno != EINTR)
        {
            LOG_ERROR("%s", "epoll failure");
            break;
        }

        // 遍历发生的事件
        for (int i = 0; i < number; i++)
        {
            // 获取事件的文件描述符
            int sockfd = events[i].data.fd;

            //处理新到的客户连接
            // 如果事件是由服务器监听套接字m_listenfd触发的,表示有新的客户端连接请求,进入处理新客户端连接的分支
            if (sockfd == m_listenfd)
            {
                //这个函数的目的是处理新的客户端连接数据,并返回一个布尔值flag,表示处理结果。
                bool flag = dealclinetdata();
                //如果处理结果flag为false,则跳过当前循环迭代,继续处理下一个事件。
                if (false == flag)
                    continue;
            }
            // 套接字关闭或发生错误的情况,移除对应的定时器
            else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
            {
                //服务器端关闭连接,移除对应的定时器
                util_timer *timer = users_timer[sockfd].timer;
                deal_timer(timer, sockfd);
            }
          
            // 处理从管道读取的信号事件,用于处理信号
            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);
            }
        }
        // timeout为true,表示发生了定时器超时,会调用utils.timer_handler()处理定时器事件,并打印相关信息
        if (timeout)
        {
            utils.timer_handler();

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

            timeout = false;
        }
    }
}

至此,项目所涉及的模块与函数的功能我们已经大致了解,后续我们将具体地理解每一个模块函数,以及该模块所涉及到的模式(包括模式的重要性与功能)。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值