当前位置: 首页 > news >正文

C++项目详细分析_WebServer

前言

本文适合刚学习完C++基础知识并尝试实现网络编程的同学,包含了许多我在复现该项目时遇到的许多疑问以及代码的详细注释和分析。
项目地址
项目详细介绍

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

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

这是一个非常有意义的项目,它通过实现一个功能完整且高效的Web服务器,帮助初学者更好地理解和实践Linux下的网络编程。在这个项目中,每个设计选择都针对Web服务器的实际需求,确保其能够在真实环境中高效、稳定地运行。让我们逐点来看:

1. 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型

实际运用与意义:
  • 线程池:通过预先创建一组线程,避免了高并发下频繁创建和销毁线程的开销。线程池可以高效地处理多个客户端请求,提升服务器的响应速度和资源利用率。

    现实意义:这样设计的服务器可以在面对大量并发请求时依然保持高效稳定,不会因为频繁的线程操作而降低性能。

  • 非阻塞socket:允许服务器在处理客户端请求时,不会因为某个操作(比如等待数据)而卡住整个程序。非阻塞IO能够让服务器同时处理多个连接,从而提高吞吐量。

    现实意义:这种设计确保了服务器在等待某些事件(如数据到达)时,不会停下来,而是可以继续处理其他请求,使得服务器能够高效处理多个并发连接。

  • epoll(ET和LT):epoll是Linux下非常高效的I/O多路复用技术。ET(边缘触发)和LT(水平触发)是两种事件通知模式。ET模式减少了重复通知的开销,提高了性能;LT模式则更为安全,适合稳定运行。

    现实意义:通过同时实现ET和LT,程序可以灵活应对不同的需求。ET模式可以在高负载下减少系统调用次数,LT模式则确保事件不遗漏,特别是在复杂场景下更为可靠。

  • Reactor和模拟Proactor模型:Reactor模型是服务器模型中常用的一种,主要是在事件到达时触发事件处理器;Proactor模型则是事件处理的另一种方法,通常是事件处理器主动触发操作。模拟Proactor指的是在应用层模拟这种行为。

    现实意义:通过实现这两种模型,程序可以根据不同的场景选择最合适的并发处理方式。Reactor适合处理事件驱动的应用,模拟Proactor则更能体现服务器在处理完成时再执行相关操作的特点。

2. 使用状态机解析HTTP请求报文,支持解析GET和POST请求

实际运用与意义:
  • 状态机解析HTTP请求:状态机是一种将复杂任务分解为不同状态并根据状态转移来处理的模型。通过状态机,服务器可以精准、有效地解析HTTP请求,不管是简单的GET请求还是复杂的POST请求,都能正确解析处理。

    现实意义:使用状态机可以让服务器的请求解析过程更加清晰和高效,减少错误处理,提高响应速度。对于Web服务器来说,这种设计非常重要,因为它能确保服务器在处理不同类型请求时都能准确执行。

3. 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件

实际运用与意义:
  • 数据库交互:服务器通过与数据库交互,实现用户的注册和登录功能。这是Web应用中最常见的功能之一,也是用户与服务器交互的核心。

    现实意义:这不仅展示了服务器的基本功能,还使项目更加实用,能够处理实际Web服务需求,如用户管理和数据存储。能够提供图片和视频文件的请求处理展示了服务器处理静态资源的能力。

4. 实现同步/异步日志系统,记录服务器运行状态

实际运用与意义:
  • 日志系统:日志系统是服务器的重要组成部分,用于记录运行时的各种信息,如错误、请求、状态等。同步日志可以实时记录,而异步日志则可以在不影响主线程的情况下批量处理日志。

    现实意义:通过日志系统,开发者可以监控服务器的运行状态,发现和定位问题,提高调试效率。异步日志在高并发场景下尤为重要,能够确保记录日志的同时,不会阻塞主要的请求处理线程。

5. 经Webbench压力测试可以实现上万的并发连接数据交换

实际运用与意义:
  • Webbench压力测试:Webbench是一种常用的压力测试工具,用于测试Web服务器的性能。通过该工具,项目展示了服务器能够在高并发环境下处理上万连接的能力。

    现实意义:这一点直接证明了该服务器的高效性和可靠性。能够在高负载下依然保持良好的性能,体现了这个项目在实际应用中的潜力。

接下来是正式的源码分析:

源码详细分析

项目路径如下:
在这里插入图片描述

1.webserver.cpp

头文件和构造函数

#include "webserver.h"WebServer::WebServer()
{// http_conn类对象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];
}
  1. #include "webserver.h":包含WebServer类的声明。
  2. 构造函数WebServer::WebServer():初始化WebServer对象。
    • users = new http_conn[MAX_FD];:创建一个http_conn对象数组,用于处理客户端连接。
    • getcwd(server_path, 200);:获取当前工作目录。
    • 设置root文件夹路径:将当前工作目录和"/root"拼接成新的字符串,存储在m_root中。使用strcpy(m_root, server_path)将当前工作目录的路径复制到m_root中。使用strcat(m_root, root)将/root附加到当前工作目录路径的末尾。
    • users_timer = new client_data[MAX_FD];:创建一个client_data对象数组,用于管理客户端定时器。
      这段代码的主要作用是构建服务器的根目录路径,将当前工作目录与/root路径拼接在一起,最终用于指向服务器资源文件的位置(如HTML、GIF、JPG等文件)。

析构函数

WebServer::~WebServer()
{close(m_epollfd);close(m_listenfd);close(m_pipefd[1]);close(m_pipefd[0]);delete[] users;delete[] users_timer;delete m_pool;
}
  • 析构函数WebServer::~WebServer():释放资源。
    • 关闭epoll文件描述符、监听文件描述符、管道文件描述符。
    • 删除动态分配的usersusers_timer数组。
    • 删除线程池对象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;
}
  • 初始化函数WebServer::init:初始化服务器的各项参数。
    • 设置端口号、数据库用户名和密码、数据库名、日志写入方式、关闭连接选项、触发模式、数据库连接池大小、线程池大小、日志关闭选项、事件模型等。

触发模式函数

void WebServer::trig_mode()
{// LT + LTif (0 == m_TRIGMode){m_LISTENTrigmode = 0;m_CONNTrigmode = 0;}// LT + ETelse if (1 == m_TRIGMode){m_LISTENTrigmode = 0;m_CONNTrigmode = 1;}// ET + LTelse if (2 == m_TRIGMode){m_LISTENTrigmode = 1;m_CONNTrigmode = 0;}// ET + ETelse if (3 == m_TRIGMode){m_LISTENTrigmode = 1;m_CONNTrigmode = 1;}
}
  • 触发模式函数WebServer::trig_mode:设置监听和连接的触发模式。
    • LT(水平触发):0
    • ET(边缘触发):1
    • 根据m_TRIGMode的值来设置监听和连接的触发模式。

WebServer::trig_mode函数用于设置服务器的监听(m_LISTENTrigmode)和连接(m_CONNTrigmode)的触发模式。触发模式有两种:LT(水平触发,Level-Triggered)ET(边缘触发,Edge-Triggered),分别用01表示。

根据m_TRIGMode的值,trig_mode函数将决定监听和连接操作的触发模式:

  • m_TRIGMode == 0: 监听和连接均采用LT(水平触发)。
  • m_TRIGMode == 1: 监听采用LT,连接采用ET(边缘触发)。
  • m_TRIGMode == 2: 监听采用ET,连接采用LT。
  • m_TRIGMode == 3: 监听和连接均采用ET。
水平触发(LT)和边缘触发(ET)的区别:

这两种触发模式是针对I/O事件的不同处理方式,通常用于 epoll 或者 select/poll等 I/O 多路复用机制。

  1. LT(Level-Triggered,水平触发)

    • 工作方式:在水平触发模式下,只要文件描述符上还有数据未处理,epoll会反复通知应用程序。因此,只要某个事件没有被处理,下一次调用epoll_wait时,仍会返回该事件。
    • 特点
      • 容易编程,适合大部分场景。
      • 可能导致重复处理同一事件。
    • 场景:适用于要求及时处理事件的场景,编程简单,但效率相对较低。
  2. ET(Edge-Triggered,边缘触发)

    • 工作方式:在边缘触发模式下,epoll只会在文件描述符状态发生变化时通知应用程序,且只通知一次。如果应用程序没有在第一次通知时处理完所有数据,后续epoll_wait不会再通知该事件,除非状态再次发生变化。
    • 特点
      • 更高效,减少了系统调用次数。
      • 编程复杂,需要确保一次性处理所有数据,否则可能会错过事件。
    • 场景:适用于高性能、高并发服务器,需要精确控制I/O操作。

日志写入函数

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);elseLog::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);}
}
  • 日志写入函数WebServer::log_write:初始化日志系统。
    • 根据m_log_write的值来选择不同的日志初始化方式。
            Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 800);
  • 如果m_log_write等于1,调用日志系统的init函数来初始化日志文件。这里使用的是异步日志模式,参数说明如下:
    • "./ServerLog":日志文件的路径,日志将写入到当前目录下的ServerLog文件中。
    • m_close_log:传递日志开关变量,这里是0,表示日志功能开启。
    • 2000:日志队列最大长度,代表日志的最大条目数。
    • 800000:日志文件的最大大小,单位是字节。日志文件达到此大小后,可能会进行滚动或创建新日志文件。
    • 800:表示日志的刷新频率,通常用于异步日志模式下的刷新间隔(即多长时间刷新一次日志到文件)。
        elseLog::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
  • 如果m_log_write不等于1,执行这个else分支,调用init函数初始化日志文件。这时的最后一个参数是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);
}
  • 数据库连接池初始化函数WebServer::sql_pool:初始化数据库连接池并读取数据库表。
    • 获取数据库连接池实例,并进行初始化。
    • 调用http_conn对象的initmysql_result方法,初始化数据库读取表。
      tips:在我看来 连接池这个概念的作用就类似于缓存,可以理解为,连接池和缓存都旨在提高系统的性能和效率,但它们处理的对象和应用场景不同。

线程池初始化

void WebServer::thread_pool()
{// 线程池m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
}
  • 线程池初始化函数WebServer::thread_pool:创建并初始化线程池对象m_pool
    在这段代码中,threadpool<http_conn> 使用了模板类 threadpool,并且将 http_conn 作为模板参数传递给它。
    T 是一个模板参数,可以是任何类型。在这段代码中,T 被替换为 http_conn,表示线程池中的任务将处理 http_conn 类型的对象。

事件监听函数

void WebServer::eventListen()
{// 网络编程基础步骤m_listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(m_listenfd >= 0);// 优雅关闭连接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){struct linger tmp = {1, 1};setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));}int ret = 0;struct sockaddr_in address;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));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(5);assert(m_epollfd != -1);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);utils.setnonblocking(m_pipefd[1]);utils.addfd(m_epollfd, m_pipefd[0], false, 0);utils.addsig(SIGPIPE, SIG_IGN);utils.addsig(SIGALRM, utils.sig_handler, false);utils.addsig(SIGTERM, utils.sig_handler, false);alarm(TIMESLOT);// 工具类,信号和描述符基础操作Utils::u_pipefd = m_pipefd;Utils::u_epollfd = m_epollfd;
}

这段代码是一个Web服务器的事件监听函数 eventListen(),用于设置网络通信的基础环境,创建监听套接字、设置连接选项、初始化 epoll 事件表,并设置必要的信号处理。下面逐行解释这个函数的工作原理:

1. 创建监听套接字

m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(m_listenfd >= 0);
  • socket(PF_INET, SOCK_STREAM, 0);:创建一个TCP套接字。
    • PF_INET 表示使用IPv4协议。
    • SOCK_STREAM 表示使用面向连接的TCP协议。
    • 0 表示协议选择默认的传输协议(TCP)。
  • assert(m_listenfd >= 0);:检查套接字创建是否成功。如果 m_listenfd 小于0,表示创建失败,程序会在这里终止。

2. 设置优雅关闭连接

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)
{struct linger tmp = {1, 1};setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
}
  • 这里的 SO_LINGER 选项用于控制关闭套接字时的行为。
    • m_OPT_LINGER 是一个标志变量,控制 SO_LINGER 的行为。
  • struct linger tmp = {0, 1};:如果 m_OPT_LINGER 为0,SO_LINGER 的延时关闭行为被禁用。
  • struct linger tmp = {1, 1};:如果 m_OPT_LINGER 为1,则套接字关闭时会在 SO_LINGER 时间内尝试发送剩余数据。

3. 绑定地址并监听

int ret = 0;
struct sockaddr_in address;
//在网络编程中,sockaddr_in 结构体用来存储地址信息(例如IP地址和端口)。为了避免结构体中的某些未初始化的成员包含随机值(即内存中的“垃圾”数据),在使用 address 结构体之前,通常先将它的所有字节清零,这样可以确保结构体中的所有字段初始值为0。
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));
assert(ret >= 0);
ret = listen(m_listenfd, 5);
assert(ret >= 0);

sockaddr_in 结构体用于存储IP地址和端口信息。

  • sin_family = AF_INET;:设置地址族为IPv4。
  • sin_addr.s_addr = htonl(INADDR_ANY);:绑定到本地所有IP地址(设置了服务器要监听的IP地址)。

tips:绑定到本地所有IP地址的含义:
在网络编程中,一个服务器通常需要绑定到一个特定的IP地址和端口,以便能够接收来自客户端的连接请求。INADDR_ANY 是一个特殊的常量,用于表示“本地所有IP地址”。

INADDR_ANY: 当服务器的套接字绑定到 INADDR_ANY 时,意味着服务器会监听本地机器上所有的网络接口(如Wi-Fi、以太网、回环地址等)的IP地址。换句话说,不论客户端连接到本地机器的哪个IP地址,服务器都能接受到请求。

  • sin_port = htons(m_port);:设置了服务器要监听的端口号,并在设置的过程中将主机字节序的端口号转换为网络字节序。

这一步的作用是将端口号从主机字节序(通常是小端序)转换为网络字节序(大端序),并将其绑定到套接字,以确保端口号在网络上传输时能够被正确识别。

tips:

sin_port: 这是 sockaddr_in 结构体中的一个字段,用于存储端口号。这个结构体常用于指定服务器绑定的IP地址和端口。

htons(m_port):
htons 是 “Host TO Network Short” 的缩写,它将主机字节序的端口号转换为网络字节序。
不同计算机架构可能使用不同的字节序来表示数据。在网络通信中,数据必须按照统一的字节序进行传输,通常使用大端序(网络字节序)。
通过 htons 函数,确保在不同架构之间传输的端口号能够被正确理解,在网络通信中,统一的字节序是必要的,以确保不同主机之间的互操作性。通过使用 htons,开发者可以确保本地主机上的端口号在网络上传输时能够被远端主机正确解析。

  • setsockopt(SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));:设置 SO_REUSEADDR 选项,允许重用本地地址。

tips:结合场景
场景1:快速重启服务器
你在电脑上运行了一个程序(比如服务器),它占用了一个端口号(比如8080)。当你关闭这个程序时,系统会“保留”这个端口一段时间,防止旧的网络数据混乱。在这段时间里,你无法立即重新启动服务器去占用同样的端口,系统会报错说“端口被占用了”。

但是如果设置了 SO_REUSEADDR,你可以立刻再次使用这个端口,程序可以顺利重启,不用等待系统释放端口。

场景2:多个程序同时监听同一个端口
假设你有两个程序,它们都需要监听同一个端口(比如8080),但分别处理不同的IP地址。一般情况下,系统不允许两个程序同时占用一个端口,但 SO_REUSEADDR 允许这种“共享”情况。

  • bind() 函数将套接字绑定到指定的IP地址和端口。
  • listen() 函数使套接字进入监听状态,准备接收连接。

4. 初始化定时器

utils.init(TIMESLOT);
  • 初始化定时器工具 utils,并设置时间间隔 TIMESLOT,通常用于管理连接的超时事件。

5. 创建 epoll 内核事件表

epoll_event events[MAX_EVENT_NUMBER];
m_epollfd = epoll_create(5);
assert(m_epollfd != -1);
  • epoll_create(5);:创建一个 epoll 事件表,参数 5 是提示内核事件表的大小,但实际上Linux 2.6.8之后的内核忽略了这个参数。
  • assert(m_epollfd != -1);:确保 epoll 创建成功。

6. 添加监听文件描述符到 epoll 事件表

utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
http_conn::m_epollfd = m_epollfd;
  • 将监听套接字 m_listenfd 添加到 epoll 事件表中,以便 epoll 可以监控这个套接字的事件(如新连接到来)。
  • http_conn::m_epollfd = m_epollfd;:将 epoll 文件描述符存储在静态变量中,以便HTTP连接可以使用。

7. 创建用于信号通信的管道

ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
assert(ret != -1);
utils.setnonblocking(m_pipefd[1]);
utils.addfd(m_epollfd, m_pipefd[0], false, 0);
  • socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);:创建一个双向通信的UNIX域套接字对 m_pipefd,用于进程内部通信。
  • 将管道的读端 m_pipefd[0] 添加到 epoll 事件表中,以便可以通过信号驱动 epoll 的事件循环。
    tips:

结合一个场景来理解为什么需要创建双向通信的套接字

假设你有一个餐厅,餐厅里有一个服务员负责处理顾客的点单。这时候有两种情况会让服务员忙起来:

  1. 顾客点餐:这类似于服务器处理网络请求。
  2. 厨房准备好了菜:需要通知服务员去上菜,这就像服务器内部需要处理的事情,比如定时任务或信号。

问题

如果厨房想通知服务员“菜好了”,但服务员正在忙着处理顾客点餐,这时候服务员可能没法立刻去处理厨房的通知。

解决方案:创建一个内部通信通道

为了让服务员在处理顾客点餐的同时也能及时收到厨房的通知,你在餐厅内部装了一个“内部电话”(类似于双向套接字)。当厨房准备好菜后,它就会通过这个内部电话告诉服务员。服务员可以一边处理顾客的点单,一边通过电话听到厨房的通知。

代码中的实现

在你的服务器中,这个“内部电话”就是通过 socketpair 创建的双向套接字对。

  • 当你的服务器处理网络请求时,它还会通过 epoll 监控这个套接字对。
  • 如果有一些内部事件发生,比如需要处理的定时任务,服务器就可以通过写入这个套接字对来通知自己:“嘿,有事情要处理!”。
  • 服务器会通过 epoll 监听这个通知,并做出相应的处理。

创建这个内部通信的套接字对,就像给餐厅的服务员装了一个内部电话,确保他在忙着处理顾客时,也能及时接收到厨房的通知,避免漏掉任何重要的事情。这样,服务器既能处理外部网络请求,也能处理自己的内部任务。

8. 添加信号处理

//忽略SIGPIPE:不会因为向关闭的连接写数据而崩溃。
utils.addsig(SIGPIPE, SIG_IGN);
//处理SIGALRM:可以在定时器触发时执行特定任务,比如检查超时。
utils.addsig(SIGALRM, utils.sig_handler, false);
//处理SIGTERM:在终止程序前可以先完成一些必要的清理工作。
utils.addsig(SIGTERM, utils.sig_handler, false);
  • utils.addsig(SIGPIPE, SIG_IGN);:忽略 SIGPIPE 信号,防止在向一个已关闭的连接写数据时引发进程终止。
  • utils.addsig(SIGALRM, utils.sig_handler, false);:添加定时器信号 SIGALRM 的处理函数 sig_handler
  • utils.addsig(SIGTERM, utils.sig_handler, false);:添加终止信号 SIGTERM 的处理函数 sig_handler

9. 启动定时器

alarm(TIMESLOT);
  • 设置定时器,每隔 TIMESLOT 秒发送一次 SIGALRM 信号,用于处理定时任务,比如检查超时连接。

10. 初始化工具类中的全局变量

Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
  • 将管道文件描述符和 epoll 文件描述符传递给工具类 Utils,使工具类可以访问并处理这些描述符。

eventListen函数总结

这个 eventListen 函数完成了服务器在启动时所需的各项初始化工作,包括创建监听套接字、设置套接字选项、初始化 epoll、设置信号处理和定时器等。最终,服务器准备好监听来自客户端的连接,并可以处理各种事件和信号。

定时器相关函数

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);
}
  • 定时器相关函数WebServer::timer:为新连接初始化定时器。
    • 初始化http_conn对象。
    • 初始化client_data对象,并创建新的util_timer定时器。
    • 将定时器加入定时器链表m_timer_lst

调整定时器

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");
}
  • 调整定时器函数WebServer::adjust_timer:调整定时器的过期时间并重新加入定时器链表。

定时器回调函数

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::deal_timer:处理定时器过期事件,关闭连接并删除定时器。

处理客户端数据

bool WebServer::dealclinetdata()
{struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);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{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;
}
  • 处理客户端数据函数WebServer::dealclinetdata:处理新客户端连接。
    • 接受新的客户端连接。
    • 如果触发模式为LT,则一次接受一个连接;如果为ET,则循环接受所有连接。
    • 如果连接数达到最大值,则显示错误信息。

信号处理函数

bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{int ret = 0;int sig;char signals[1024];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;
}
  • 信号处理函数WebServer::dealwithsignal:处理信号。
    • 接收信号并根据信号类型设置标志位。
    • 处理SIGALRM信号,设置timeout标志位。
    • 处理SIGTERM信号,设置stop_server标志位。

处理读事件

void WebServer::dealwithread(int sockfd)
{util_timer *timer = users_timer[sockfd].timer;// reactorif (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{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);}}
}
  • 处理读事件函数WebServer::dealwithread:处理客户端的读事件。
    • 如果是Reactor模型,则调整定时器并将事件加入线程池处理。
    • 如果是Proactor模型,则直接读取数据,并将事件加入线程池处理。

处理写事件

void WebServer::dealwithwrite(int sockfd)
{util_timer *timer = users_timer[sockfd].timer;// reactorif (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{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);}}
}
  • 处理写事件函数WebServer::dealwithwrite:处理客户端的写事件。
    • 如果是Reactor模型,则调整定时器并将事件加入线程池处理。
    • 如果是Proactor模型,则直接写入数据,并将事件加入线程池处理。

主函数运行

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 ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN)){bool flag = dealwithsignal(timeout, stop_server);if (false == flag)continue;}// 处理客户连接上接收到的数据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;}}
}
  • 主循环函数WebServer::eventLoop:服务器主事件循环。
    • 进入循环,通过epoll_wait等待事件发生。
    • 处理各种事件:
      • 新的客户端连接。
      • 信号事件。
      • 客户端读事件。
      • 客户端写事件。
    • 如果定时器超时,则处理定时器事件。

总结

这个webserver.cpp文件的主要作用是实现一个Web服务器的核心逻辑。具体来说,它负责:

  1. 事件管理和处理:通过epoll管理所有的网络事件,包括客户端连接、读写操作、以及异常和超时事件。

  2. 连接处理:实现新客户端连接的接收和管理,包括创建连接对象,并将其注册到epoll中进行监听。

  3. 读写操作:根据服务器的模式(ReactorProactor),处理客户端的读写请求。在Reactor模式下,主要通过线程池异步处理请求;在Proactor模式下,则在读写完成后直接处理业务逻辑。

  4. 定时器管理:使用定时器对客户端连接进行超时控制,通过调整和删除定时器来管理连接的生命周期,确保资源及时释放。

  5. 信号处理:处理系统信号(如终止信号、定时信号),用于控制服务器的停止和定时任务的执行。

总体来说,这个cpp文件实现了Web服务器运行的主循环和核心功能,确保服务器在高并发情况下高效、稳定地处理网络请求。

1.1 为什么网络编程需要套接字(Socket)

套接字(Socket)是计算机网络编程中的基础概念和工具,它的作用和必要性可以从以下几个方面理解:

1.1.1 通信抽象
  • 统一的接口:套接字提供了一个统一的接口,使程序员能够通过相同的方式进行网络通信,无论底层使用的是哪种协议(例如TCP、UDP)。这就像是一种抽象层,屏蔽了底层实现的复杂性。
  • 跨平台:套接字在不同操作系统上表现一致,提供了跨平台的通信能力,使开发者能够编写具有良好可移植性的网络应用程序。
1.1.2. 网络通信的基础
  • 网络通信的端点:在网络通信中,套接字扮演的是“通信端点”的角色。任何网络通信都是在两个端点(一个客户端和一个服务器端)之间进行的。套接字就是这个端点,它代表了一个IP地址和端口的组合。
  • 支持多种协议:套接字不仅仅支持TCP(面向连接的通信),还支持UDP(无连接的通信)等协议,能够满足不同类型的网络通信需求。
1.1.3. 数据传输的机制
  • 数据收发:套接字提供了发送(send)和接收(recv)数据的机制,通过这些函数,程序可以在网络中传输数据。这是实现网络功能的核心部分。
  • 流控制和连接管理:对于TCP套接字,套接字还提供了连接的管理(例如监听、接受连接)以及流控制等功能,使得数据能够可靠地传输。
1.1.4. 操作系统的支持
  • 操作系统接口:在操作系统中,套接字是与操作系统网络栈交互的接口。通过套接字,应用程序可以与操作系统内核进行通信,进而通过网络适配器与外部世界通信。
  • 资源管理:套接字作为一种系统资源,由操作系统管理,能够确保资源的合理分配和回收。这避免了网络资源的浪费和冲突。
1.1.5 总结

套接字在网络编程中是不可或缺的,因为它提供了网络通信的基础设施和统一的接口,使得复杂的网络操作变得可管理和可操作。通过套接字,开发者能够构建出跨平台、可扩展的网络应用程序。没有套接字,程序将无法直接与网络进行通信,网络编程也就无从谈起。

1.2 epoll是什么

epoll 是 Linux 内核提供的一种高效的 I/O 多路复用机制,用于监控多个文件描述符,以便在这些文件描述符上发生事件时通知应用程序进行相应处理。相比于传统的 selectpollepoll 在处理大量文件描述符时表现更为高效,特别是在高并发场景下。

epoll 的主要特点:

  1. 高效性

    • epoll 使用的是基于事件通知的机制,只有发生事件的文件描述符才会被返回,因此在大量文件描述符中只有少数有事件发生时,epoll 的性能优势显著。
    • epoll 在内核空间维护了一个事件表,避免了每次调用都要传递整个文件描述符集合,减少了内核与用户态之间的数据拷贝。
  2. 水平触发和边缘触发

    • 水平触发(Level-triggered, LT):默认模式,只要某个文件描述符上有事件发生,epoll_wait 就会返回该文件描述符,直到事件被处理。
    • 边缘触发(Edge-triggered, ET):更为高效,但要求更细致的处理。当文件描述符状态从无事件变为有事件时才会通知,适用于减少系统调用频率,提高程序效率。
  3. 对文件描述符数量的支持

    • epoll 能够支持大规模的文件描述符集合,理论上上限是系统的最大文件描述符数,而 selectpoll 通常有较小的文件描述符限制。

epoll 的工作流程:

  1. 创建 epoll 实例

    • 使用 epoll_createepoll_create1 函数创建一个 epoll 实例,返回一个 epoll 文件描述符。
  2. 注册事件

    • 使用 epoll_ctl 函数将需要监控的文件描述符添加到 epoll 实例中,并指定要监听的事件类型(如可读、可写、异常等)。
  3. 等待事件发生

    • 使用 epoll_wait 函数等待事件的发生,当某个或多个文件描述符上的事件满足条件时,epoll_wait 会返回这些文件描述符。
  4. 处理事件

    • 处理返回的事件,执行相应的读写操作,或根据应用程序逻辑进行其他处理。

使用场景:

epoll 特别适合用于高并发的网络服务器中,比如 Web 服务器、聊天服务器等。这些应用通常需要处理大量并发连接,并且每个连接可能频繁进行 I/O 操作。epoll 能够有效地提升这些应用的性能。

总之,epoll 是在 Linux 环境下构建高性能网络服务器的重要工具,它通过高效的事件通知机制帮助开发者更好地管理大量并发 I/O 操作。

1.3 为什么在项目实现中,总是将类的定义(包括成员变量和成员函数的声明)放在头文件(.h文件)中,而将成员函数的实现放在源文件(.cpp文件)中?

在C++编程中,通常将类的定义(包括成员变量和成员函数的声明)放在头文件(.h文件)中,而将成员函数的实现放在源文件(.cpp文件)中。这种做法有几个重要的原因:

1.3.1 代码分离与清晰度
  • 头文件:主要用于定义类的接口(即类的定义),包括类的成员函数声明和成员变量。这使得头文件的内容比较简洁,便于其他开发者了解类的结构和使用方法,而不需要关注具体的实现细节。
  • 源文件:用于实现类的具体行为,即成员函数的实现。这种分离使得代码更容易维护和阅读,因为实现细节被隐藏在源文件中,头文件仅展示类的接口。
1.3.2 编译时间优化
  • 当类的实现放在 .cpp 文件中时,只有在实现发生变化时才需要重新编译 .cpp 文件。如果类的实现都放在 .h 文件中,那么每次该头文件发生变化时,所有包含该头文件的文件都需要重新编译,这可能会大幅增加编译时间。
1.3.3 信息隐藏与封装性
  • 将实现细节隐藏在 .cpp 文件中可以更好地实现信息隐藏(encapsulation),这是一种面向对象编程的关键原则。外部代码只需要了解类的接口(即头文件中的内容),而不需要知道类是如何实现的。这也有助于保护类的实现不被意外修改或依赖。
1.3.4 防止重复定义
  • 如果将类的定义和实现都放在头文件中,那么在多个源文件中包含这个头文件时,可能会导致重复定义的问题。而将实现放在 .cpp 文件中,每个源文件只会包含一次相应的实现,从而避免了这个问题。

复现过程中遇到的问题

1. 解决“E: 无法定位软件包 mysql-workbench-community”问题

用这个指令:
sudo apt install mysql-workbench-community
会报错“E: 无法定位软件包 mysql-workbench-community”问题
解决方法为改用这个指令:
apt-get install mysql-workbench
成功:
在这里插入图片描述
分析下可能的原因:使用 mysql-workbench 是因为它在 Ubuntu 默认的软件源中,而 mysql-workbench-community 需要从 MySQL 官方仓库中获取。如果没有配置 MySQL 官方仓库,系统会找不到 mysql-workbench-community 包,导致错误信息的出现。

2. 解决"正在设定ttf-mscorefonts-installer"

在这里插入图片描述
这里如果直接关了会导致后续包安装时会出现非法占用
解决方案:
按tab将光标移动到确定键上 然后回车就完事了


http://www.mrgr.cn/news/18729.html

相关文章:

  • tailwindcss在vue2中安装配置流程
  • Linux动态监控系统
  • 新型蜜罐有哪些?未来方向如何?
  • 浅谈安科瑞充电桩收费运营云平台在丹阳农商批发市场B区快充站的应用
  • 中资优配:“迪王”,拔得头筹!
  • Python实现贝叶斯优化器(Bayes_opt)优化卷积神经网络-双向长短时记忆循环神经网络分类模型(CNN-BiLSTM分类算法)项目实战
  • 【赵渝强老师】大数据技术的理论基础
  • openGauss闪回恢复
  • 海南云亿商务咨询有限公司抖音电商服务的可靠之选
  • 一夜之间删库跑路?Runway到底在做什么?
  • NIO笔记02-ByteBuffer
  • 环保专包二级资质延续申请全流程介绍
  • 无线麦克风推荐哪些品牌,无线麦克风十大排名,领夹麦克风推荐
  • P-Tuning v2:一种普遍有效的提示调整方法
  • C++入门基础,看这篇就足够了!
  • 安卓在新进程中开启服务并与原进程通信
  • Postgres容器使用
  • 地铁X光危险品检测数据集
  • 医院安全用电全套解决方案
  • HTTP 请求方法(method)介绍