TinyWebSever源码逐行注释(七)_简单代码的整合

前言

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

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

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

main.cpp

#include "config.h"

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.h

#ifndef WEBSERVER_H
#define WEBSERVER_H

// 导入必要的头文件,用于处理网络编程、文件描述符、epoll机制、线程池等功能
#include <sys/socket.h>  // 套接字相关
#include <netinet/in.h>  // IP地址相关
#include <arpa/inet.h>   // IP转换相关
#include <stdio.h>       // 标准输入输出
#include <unistd.h>      // POSIX 操作系统 API,如close等
#include <errno.h>       // 错误处理
#include <fcntl.h>       // 文件控制(非阻塞设置)
#include <stdlib.h>      // 标准库函数,如malloc等
#include <cassert>       // 断言,用于调试
#include <sys/epoll.h>   // epoll相关

// 导入线程池和HTTP连接相关模块
#include "./threadpool/threadpool.h"
#include "./http/http_conn.h"

// 常量定义
const int MAX_FD = 65536;           // 最大文件描述符数量
const int MAX_EVENT_NUMBER = 10000; // epoll 监听的最大事件数
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();
    // 设置监听事件(epoll监听)
    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 dealclientdata();
    // 处理信号(如关闭服务器、超时等)
    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;       // 事件处理模式(Reactor/Proactor)

    // 管道文件描述符(用于处理信号)
    int m_pipefd[2];
    // epoll 文件描述符
    int m_epollfd;
    // 所有客户端的 HTTP 连接数据
    http_conn *users;

    // 数据库相关
    connection_pool *m_connPool;    // 数据库连接池
    string m_user;         // 数据库用户名
    string m_passWord;     // 数据库密码
    string m_databaseName; // 数据库名称
    int m_sql_num;         // 数据库连接数量

    // 线程池相关
    threadpool<http_conn> *m_pool; // 线程池指针
    int m_thread_num;              // 线程数量

    // epoll 事件相关
    epoll_event events[MAX_EVENT_NUMBER]; // 用于存储epoll等待到的事件

    // 套接字相关
    int m_listenfd;        // 监听套接字
    int m_OPT_LINGER;      // 是否使用优雅关闭连接(linger选项)
    int m_TRIGMode;        // 触发模式(边沿或水平触发)
    int m_LISTENTrigmode;  // 监听套接字的触发模式
    int m_CONNTrigmode;    // 连接套接字的触发模式

    // 定时器相关
    client_data *users_timer; // 用户定时器数据
    Utils utils;              // 工具类,管理定时器和信号
};
#endif

Config.cpp

这里面也只有一个解析命令行的代码:

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::parse_arg 函数的实现,它用于解析命令行参数,将传入的命令行参数根据不同的标志(如 -p, -l, -m 等)转换为对应的配置值。

下面是详细的解释:

1. 函数签名

void Config::parse_arg(int argc, char *argv[])
  • 这是一个 Config 类的成员函数,名字是 parse_arg
  • argc 是命令行参数的个数,argv 是存储命令行参数的字符数组。argcargv 通常在 main 函数中作为参数传递。
  • 这个函数的作用是根据命令行输入的参数,解析并设置类中的相关配置项。

2. 定义变量

int opt;
const char *str = "p:l:m:o:s:t:c:a:";
  • opt:用于存储解析到的选项字符(如 -p, -l 等)。
  • str:定义了命令行参数选项的格式。每个字符代表一个参数的标志,后面的冒号(:)表示该选项需要一个参数。例如,'p' 后面有冒号,因此 -p 选项必须带有一个参数。

3. getopt 函数

while ((opt = getopt(argc, argv, str)) != -1)
  • getopt 是一个用于解析命令行参数的标准库函数,它依次解析由 argv 传入的参数,并根据 str 中定义的选项返回对应的标志字符(如 pl 等)。
  • getopt 返回 -1 时,表示已经没有更多的选项可供处理。

4. switch 语句处理选项

  • 根据 getopt 返回的标志字符(存储在 opt 中),switch 语句分别处理不同的命令行选项。
switch (opt)
{
    case 'p':
        PORT = atoi(optarg);
        break;
    // 其他选项处理
}
  • 每个 case 语句处理对应的标志选项(如 -p-l 等)。
  • atoi(optarg)optarggetopt 提供的当前选项的参数值(它是一个字符串)。atoi 函数用于将字符串转换为整数。例如,当用户输入 -p 8080 时,optarg"8080",而 atoi(optarg) 将其转换为整数 8080
  • 这些参数分别赋值给类中的成员变量(如 PORT, LOGWrite, TRIGMode 等)。

5. 解析的命令行选项

  • -p:解析端口号,赋值给 PORT
  • -l:日志写入方式,赋值给 LOGWrite
  • -m:触发模式,赋值给 TRIGMode
  • -o:设置 linger 选项,赋值给 OPT_LINGER
  • -s:数据库连接池的连接数,赋值给 sql_num
  • -t:线程池中的线程数,赋值给 thread_num
  • -c:是否关闭日志,赋值给 close_log
  • -a:选择处理模型,赋值给 actor_model

6. 默认行为

  • 如果传入了未定义的选项(即不在 str 中的标志),default 部分将不做任何处理。

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

lst_timer.h

宏定义部分

#ifndef LST_TIMER
#define LST_TIMER
  • 这部分是 头文件保护,防止该头文件被重复包含。#ifndef#define 保证当头文件已经被包含时,编译器不会再次包含它,避免重复定义。

头文件引用部分

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
// ... 省略部分 include
#include "../log/log.h"
  • 引入了一系列与系统 I/O、网络、信号处理、线程和日志相关的头文件。这些头文件提供了网络编程和多线程编程所需的基础功能。

client_data 结构体

struct client_data
{
    sockaddr_in address;      // 客户端的地址信息(IP 和端口)
    int sockfd;               // 客户端 socket 文件描述符
    util_timer *timer;        // 指向与该客户端关联的定时器
};
  • client_data 结构体保存了与每个客户端连接相关的数据,包括它的 sockaddr_in 地址信息,sockfd(用于通信的 socket),以及关联的 util_timer 定时器。

util_timer

class util_timer
{
public:
    util_timer() : prev(NULL), next(NULL) {}

public:
    time_t expire;            // 定时器到期时间,类型是 time_t(通常为秒)
    void (* cb_func)(client_data *); // 定时器到期后执行的回调函数,传入 client_data 指针
    client_data *user_data;    // 关联的客户端数据
    util_timer *prev;          // 指向前一个定时器
    util_timer *next;          // 指向下一个定时器
};
  • util_timer 是一个定时器类,它代表一个定时器节点。
    • expire 表示该定时器的到期时间。
    • cb_func 是定时器到期时要执行的回调函数,它接受 client_data 类型的参数。
    • user_data 是与该定时器关联的客户端数据。
    • prevnext 指向双向链表中的前一个和后一个定时器节点。

sort_timer_lst

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

    void add_timer(util_timer *timer);
    void adjust_timer(util_timer *timer);
    void del_timer(util_timer *timer);
    void tick();

private:
    void add_timer(util_timer *timer, util_timer *lst_head);

    util_timer *head;
    util_timer *tail;
};
  • sort_timer_lst 是一个管理定时器的类。它使用一个升序排序的双向链表来管理多个定时器,保证定时器按照到期时间顺序排列。
    • add_timer() 用于将新的定时器添加到链表中。
    • adjust_timer() 用于调整定时器的时间(比如延长某个连接的超时时间)。
    • del_timer() 用于删除某个定时器。
    • tick() 会遍历定时器链表,检查每个定时器是否到期并执行回调函数。
    • add_timer() 私有函数是一个辅助函数,用于将定时器插入到链表中正确的位置。

Utils

class Utils
{
public:
    Utils() {}
    ~Utils() {}

    void init(int timeslot);

    // 对文件描述符设置为非阻塞模式
    int setnonblocking(int fd);

    // 注册文件描述符到 epoll 实例中,支持 ET 模式和 EPOLLONESHOT
    void addfd(int epollfd, int fd, bool one_shot, int TRIGMode);

    // 信号处理函数
    static void sig_handler(int sig);

    // 设置某个信号的处理函数
    void addsig(int sig, void(handler)(int), bool restart = true);

    // 定时器处理函数,每次定时触发 SIGALRM 信号
    void timer_handler();

    // 显示错误信息给客户端
    void show_error(int connfd, const char *info);

public:
    static int *u_pipefd;         // 用于信号通信的管道
    sort_timer_lst m_timer_lst;   // 管理定时器的链表
    static int u_epollfd;         // 全局的 epoll 文件描述符
    int m_TIMESLOT;               // 定时器的时间间隔
};
  • Utils 类封装了工具函数,如设置非阻塞文件描述符、处理信号、管理定时器等。
    • setnonblocking():将某个文件描述符设置为非阻塞模式。
    • addfd():将文件描述符添加到 epoll 事件监听列表中,支持 ET 模式和 EPOLLONESHOT(即一次触发模式)。
    • sig_handler():信号处理函数,用于处理收到的信号。
    • addsig():设置信号处理函数,可选 restart 表示收到信号后是否自动重启系统调用。
    • timer_handler():定时器处理函数,用于处理定时任务并重新设置定时器。
    • show_error():向客户端发送错误信息。

回调函数 cb_func

void cb_func(client_data *user_data);
  • 这是一个定义在全局作用域中的回调函数,作用是当定时器到期时处理相应的客户端连接(例如超时关闭连接),它会接收一个指向 client_data 结构体的指针作为参数。具体的实现代码通常会关闭客户端连接并释放资源。

总结:

  • 这个头文件定义了一个基于定时器链表的超时管理系统,通常用于 Web 服务器管理客户端连接超时。
  • 每个客户端有自己的 client_data 结构体,存储其 socket 和定时器。
  • util_timer 类是定时器节点,sort_timer_lst 类是定时器管理器,负责对定时器链表进行排序、添加、删除等操作。
  • Utils 类封装了信号处理、文件描述符管理以及定时器的处理机制。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值