TinyWebSever源码逐行注释(一)_webserver.cpp

前言

项目源码地址
项目详细介绍

项目简介:

Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.

  1. 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
  2. 使用状态机解析HTTP请求报文,支持解析GET和POST请求
  3. 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
  4. 实现同步/异步日志系统,记录服务器运行状态
  5. 经Webbench压力测试可以实现上万的并发连接数据交换

webserver.cpp用于实现web服务器的核心配置和运行,包括但不限于监听套接字、数据库连接池、线程池、信号与管道配置、epoll(ET和LT模式)的配置,原项目地址的注释较少不适合初学者,于是我将每行都加上了注释帮助大家更好的理解:

源码注释

cpp 复制代码
#include "webserver.h"
//构造函数
WebServer::WebServer()
{
    //创建客户端连接数组
    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()
{
    //释放epoll文件描述符
    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;
    //日志写入的方式 同步or异步
    m_log_write = log_write;
    //用于控制连接关闭的变量
    m_OPT_LINGER = opt_linger;
    //控制监听和连接采用ET or LT触发模式
    m_TRIGMode = trigmode;
    //日志的开关 0为开
    m_close_log = close_log;
    //控制事件处理的模型为Reactor还是Proactor
    m_actormodel = actor_model;
}

//通过m_TRIGMode选择监听和连接的触发模式 LT: Level Triggered ET:Edge Triggered
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)
    {
        //m_log_write控制异步还是同步
        if (1 == m_log_write)
            Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);
        else
        //每写一次刷新一次log
            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()
{
    //创建套接字 PF_INET=IPV4 SOCK_STREAM=TCP
    m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
    //检查是否创建成功
    assert(m_listenfd >= 0);

    //优雅关闭连接
    if (0 == m_OPT_LINGER)
    {
        struct linger tmp = {0, 1};
        //setsockopt函数设置套接字的相关属性
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }
    else if (1 == m_OPT_LINGER)
    {
        struct linger tmp = {1, 1};
        setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
    }

    //用于捕获和检查绑定和监听是否成功
    int ret = 0;
    //该结构体定义在头文件 <netinet/in.h> 中 用于存放IP地址和端口号
    struct sockaddr_in address;
    //清除内存中的垃圾,即结构体中的非0随机值
    bzero(&address, sizeof(address));
    //表示使用的是IPV4地址
    address.sin_family = AF_INET;
    //存放IPV4地址具体的值 这里的htonl(INADDR_ANY)绑定了所有本地IP地址
    address.sin_addr.s_addr = htonl(INADDR_ANY);
    //存放端口号 htons(m_port) 即Host TO Network Short 设置监听的端口号的过程中将主机字节序的端口号转换为网络字节序。
    address.sin_port = htons(m_port);
    //1即启用SO_REUSEEADDR允许重用本地地址  使得在重启服务器后快速重新绑定原来的端口
    int flag = 1;
    setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    //绑定套接字到本地IP地址
    ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
    //是否绑定成功
    assert(ret >= 0);
    //设置套接字为监听模式,最多5个连接请求排队
    ret = listen(m_listenfd, 5);
    //检测是否成功
    assert(ret >= 0);
    //初始化定时器 设置最小超时单位
    utils.init(TIMESLOT);

    //存储事件信息结构体的数组
    epoll_event events[MAX_EVENT_NUMBER];
    //epoll创建内核事件表 管理着所有的文件描述符及其事件
    m_epollfd = epoll_create(5);
    //检查是否创建成功
    assert(m_epollfd != -1);

    //添加监听描述符到epoll事件表
    //这里的这个布尔值决定是否启用 EPOLLONESHOT 模式。
    //如果为 true,则在这个文件描述符上触发一个事件后,epoll 不会再次监视这个文件描述符,直到你手动重置它。这在多线程环境中非常有用,避免同一个文件描述符的事件被多个线程处理。
    //如果为 false,则 epoll 在每次该文件描述符上有事件发生时都会通知你。
    utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
    
    //设置客户端连接的epoll
    http_conn::m_epollfd = m_epollfd;

    //创建用于信号处理的套接字(管道) 这两个套接字 sv[0] 和 sv[1] 之间可以进行双向通信。socketpair 通常用于进程间通信
    ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
    assert(ret != -1);

    //将管道的写段设置为非阻塞 避免写操作阻塞进程:如果 m_pipefd[1] 是阻塞的,当你尝试写入数据时,如果管道已满(即 m_pipefd[0] 没有被读取),写操作将阻塞进程,直到有足够的空间可用。这可能导致程序挂起,等待管道缓冲区变得可写。通过将 m_pipefd[1] 设置为非阻塞模式,如果管道已满,写操作将立即返回失败(通常返回 EAGAIN 或 EWOULDBLOCK),程序可以处理这个情况而不被阻塞。
    utils.setnonblocking(m_pipefd[1]);
    //将管道的读端添加到epoll 使其可以通过信号驱动
    utils.addfd(m_epollfd, m_pipefd[0], false, 0);

    //设置对关闭信号的处理(ignore)
    utils.addsig(SIGPIPE, SIG_IGN);

    //设置对定时器触发信号的处理()
    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];
    //设置回调函数,即定时器超时用cb_func来处理
    timer->cb_func = cb_func;
    //获取当前时间
    time_t cur = time(NULL);
    //超时时间为当前时间+3*TIMESLOT
    timer->expire = cur + 3 * TIMESLOT;
    //将定时器与用户数据关联
    users_timer[connfd].timer = timer;
    //将定时器添加到定时器链表中,以便定时器可以参与时间轮机制或其他定时器管理机制。
    utils.m_timer_lst.add_timer(timer);
}

//调整定时器
void WebServer::adjust_timer(util_timer *timer)
{
    //若有数据传输,则将定时器往后延迟3个单位
    time_t cur = time(NULL);
    timer->expire = cur + 3 * TIMESLOT;
    //并对新的定时器在链表上的位置进行调整
    utils.m_timer_lst.adjust_timer(timer);
    //写入log
    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
    LOG_INFO("close fd %d", users_timer[sockfd].sockfd);
}

//根据监听模式处理客户端连接
bool WebServer::dealclientdata()
{
    struct sockaddr_in client_address;
    //知道客户端地址结构体大小,便于使用accept()
    socklen_t client_addrlength = sizeof(client_address);
    //LT触发模式下的accept
    if (0 == m_LISTENTrigmode)
    {
        int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
        //记录出错的log
        if (connfd < 0)
        {
            LOG_ERROR("%s:errno is:%d", "accept error", errno);
            return false;
        }
        //当前排队的客户端连接已到峰值
        if (http_conn::m_user_count >= MAX_FD)
        {
            //向客户端发送busy信息
            utils.show_error(connfd, "Internal server busy");
            LOG_ERROR("%s", "Internal server busy");
            return false;
        }
        //为这个新连接设置一个定时器
        timer(connfd, client_address);
    }
    //ET模式处理监听事件
    else
    {
        //一次性接收所有客户端连接
        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];
    //从管道中读取信号 将其存储在singals数组中
    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)
        {
            //improv为1代表事件处理完成
            if (1 == users[sockfd].improv)
            {
                //timer_fkag代表定时器事件触发 需要deal_timer处理如超时或客户端关闭连接
                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);
            //同Reactor
            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()
{
    //控制超时事件
    bool timeout = false;
    //主循环的开关
    bool stop_server = false;

    while (!stop_server)
    {
        //epoll_wait阻塞(监听)已注册的套接字 (epoll套接字 epoll内核表,最大事件数量,-1代表无限等待)
        int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        //检测epoll_wait是否出错
        {
            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 = dealclientdata();
                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);
               //处理失败时的log
               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;
        }
    }
}
相关推荐
闻缺陷则喜何志丹几秒前
【C++前后缀分解 动态规划】2100. 适合野炊的日子|1702
c++·算法·动态规划·力扣·前后缀分解·日子·适合
Zucker n6 分钟前
猫狗识别大模型——基于python语言
开发语言·python
西岭千秋雪_10 分钟前
谷粒商城のElasticsearch
java·大数据·服务器·spring boot·elasticsearch·搜索引擎
bluebonnet2723 分钟前
【Rust练习】15.match 和 if let
开发语言·后端·rust
大鹅同志25 分钟前
在服务器上开Juypter Lab教程(远程访问)
运维·服务器·pytorch·jupyter·cuda·云服务器
yueqingll38 分钟前
020、二级Java选择题综合知识点(持续更新版)
java·开发语言
冲,干,闯1 小时前
VScode相关问题与解决
c++·ide·vscode
一只小白菜~1 小时前
实现实时Web应用,使用AJAX轮询、WebSocket、还是SSE呢??
前端·javascript·websocket·sse·ajax轮询
驯龙高手_追风1 小时前
Ubuntu下安装最新版本Apache2文件服务器
linux·服务器·ubuntu
计算机学姐1 小时前
基于python+django+vue的在线学习资源推送系统
开发语言·vue.js·python·学习·django·pip·web3.py