一、webserver.cpp部分
1、epoll触发模式设计部分
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; // 连接模式为边沿触发
}
}
代码解释
这个方法 WebServer::trig_mode() 根据成员变量 m_TRIGMode 的值来设置监听和连接的触发模式:
这段代码是一个 Web 服务器中设置触发模式的方法,用于配置监听(listen)和连接(connection)触发模式的行为。触发模式(Trigger Mode)在网络编程中主要指的是事件通知机制的不同处理方式,通常包括电平触发(Level Triggered,LT)和边沿触发(Edge Triggered,ET)。
触发模式简介
(1)电平触发(Level Triggered, LT):
在事件驱动编程中,电平触发是一种触发模式,当文件描述符(比如 socket)有数据可读或可写时,会一直触发直到所有数据被处理完毕。
简单理解,就是“有就一直通知”。
(2)边沿触发(Edge Triggered, ET):
边沿触发是另一种触发模式,只在状态变化时(从无数据到有数据,或者从不能写到能写时)触发一次。
简单理解,就是“状态变化才通知一次”。
具体含义
m_TRIGMode:是一个成员变量,决定了服务器的触发模式。它的值范围为 0 到 3,对应四种不同的组合。
m_LISTENTrigmode 和 m_CONNTrigmode:分别表示监听和连接的触发模式。0 表示电平触发(LT),1 表示边沿触发(ET)。
具体配置如下:
m_TRIGMode = 0(LT + LT):
(1)监听模式和连接模式均为电平触发。
适用于需要较高稳定性和简单性,处理每个事件的服务器。
m_TRIGMode = 1(LT + ET):
(2)监听模式为电平触发,连接模式为边沿触发。
监听状态下,服务器可以持续接收连接请求;处理连接时,只在状态变化时处理,减少不必要的系统调用。
m_TRIGMode = 2(ET + LT):
(3)监听模式为边沿触发,连接模式为电平触发。
适用于需要高效处理连接请求,监听状态下减少系统调用,处理连接时持续处理所有数据。
m_TRIGMode = 3(ET + ET):
(4)监听模式和连接模式均为边沿触发。
适用于高性能服务器,最大化减少系统调用次数,但需要更复杂的事件处理逻辑。
2、日志管理部分
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);
}
}
代码解释
这段代码定义了 WebServer 类中的 log_write() 方法,用于初始化日志系统。具体作用是根据不同的配置参数,设置日志文件的初始化行为。我们将详细解释这段代码的含义。
变量和函数解释
m_close_log:这是一个成员变量,用于表示是否关闭日志。如果值为 0,则表示日志功能是开启的。
*m_log_write:*这是另一个成员变量,用于表示日志写入方式。通常有不同的写入模式,比如同步写入或异步写入。
Log::get_instance():这是一个单例模式的实例获取方法,用于获取 Log 类的唯一实例。
Log::init():这是 Log 类的初始化方法,用于初始化日志系统的各种参数。
初始化日志
日志关闭检查:首先检查 m_close_log 是否为 0。如果是 0,则继续初始化日志;否则,什么都不做。
初始化参数:
*./ServerLog:*日志文件的路径。
*m_close_log:*是否关闭日志(这里传递的是 0,表示开启日志)。
*2000:*日志缓冲队列大小或其他相关参数,具体含义根据日志库的实现不同而不同。
*800000:*日志文件的最大大小,超过这个大小会进行日志滚动。
*800 或 0:*根据 m_log_write 的值,设置不同的日志写入方式参数。
如果 m_log_write 为 1,则传递 800,这可能表示异步日志写入时的缓冲区大小或异步队列长度。
如果 m_log_write 为 0,则传递 0,可能表示同步日志写入。
总结
这段代码主要是根据 Web 服务器的配置,初始化日志系统。它检查日志是否开启,并根据不同的日志写入方式,设置相应的初始化参数。这有助于确保服务器在运行时正确记录日志信息,以便后续的调试和监控。
实例分析
例如,当 m_close_log 为 0 且 m_log_write 为 1 时,日志系统会被初始化为异步写入模式,使用一个大小为 800 的缓冲区。反之,如果 m_log_write 为 0,则日志系统会被初始化为同步写入模式,不使用异步缓冲区。
这个设计通过不同的初始化参数配置,可以适应不同的日志写入需求,既可以支持高性能的异步日志写入,也可以支持更简单的同步日志写入,灵活性很高。
3、初始化数据库
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() 方法,用于初始化数据库连接池和读取数据库表的信息。让我们逐行解释这段代码。
变量和函数解释
m_connPool:这是一个成员变量,表示数据库连接池的实例。
connection_pool::GetInstance():这是一个单例模式的实例获取方法,用于获取 connection_pool 类的唯一实例。
m_user、m_passWord、m_databaseName:这些是成员变量,表示数据库的用户名、密码和数据库名。
3306:这是数据库服务的端口号,通常 MySQL 的默认端口是 3306。
m_sql_num:这是成员变量,表示数据库连接池中初始连接的数量。
m_close_log:这是成员变量,表示是否关闭日志功能。
users:这是一个指向用户管理相关对象的指针,负责用户相关操作。
initmysql_result:这是 users 对象中的一个方法,用于初始化用户数据,具体操作是从数据库中读取表信息。
初始化数据库连接池
m_connPool = connection_pool::GetInstance();
m_connPool->init("localhost", m_user, m_passWord, m_databaseName, 3306, m_sql_num, m_close_log);
获取连接池实例:通过 connection_pool::GetInstance() 获取数据库连接池的唯一实例。
初始化连接池:
“localhost”:数据库服务器的地址。
m_user:数据库用户名。
m_passWord:数据库密码。
m_databaseName:数据库名。
3306:数据库端口号。
m_sql_num:连接池中初始连接的数量。
m_close_log:是否关闭日志。
这个过程会创建一个数据库连接池,并配置数据库连接的相关参数,为后续的数据库操作提供多个可复用的数据库连接。
初始化数据库读取表
users->initmysql_result(m_connPool);
初始化用户数据:通过 users 对象的 initmysql_result 方法初始化用户数据。
参数 m_connPool:传递已经初始化的数据库连接池对象,users 对象会使用连接池中的连接来执行数据库查询操作。
总结
连接池初始化:该方法首先通过单例模式获取数据库连接池实例,并进行初始化。初始化过程中设置了数据库服务器地址、用户名、密码、数据库名、端口号、初始连接数和日志配置等。
用户数据初始化:随后调用 users 对象的 initmysql_result 方法,使用连接池中的连接读取用户数据表的信息。
4、线程池初始化
void WebServer::thread_pool()
{
// 线程池
m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
}
代码解释
这段代码定义了 WebServer 类中的 thread_pool() 方法,用于初始化一个线程池。具体来说,该方法创建了一个新的 threadpool 对象,并将其指针赋值给成员变量 m_pool。
变量和函数解释
m_pool:这是 WebServer 类的一个成员变量,用于存储线程池对象的指针。
threadpool<http_conn>:这是一个模板类,表示一个专门用于处理 http_conn 类型任务的线程池。
m_actormodel:这是一个成员变量,表示线程池使用的并发模型(例如 Reactor 或 Proactor 模型)。
m_connPool:这是一个成员变量,表示数据库连接池的指针。
m_thread_num:这是一个成员变量,表示线程池中线程的数量。
线程池初始化
m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
创建线程池对象:使用 new 运算符创建一个新的 threadpool 对象。该对象是一个模板类的实例,专门用于处理 http_conn 类型的任务。
传递参数:
m_actormodel:并发模型参数,用于初始化线程池。
m_connPool:数据库连接池指针,线程池中的线程可能会使用这个连接池来处理数据库操作。
m_thread_num:线程池中的线程数量。
总结
这段代码的主要作用是初始化 Web 服务器的线程池,使得服务器能够并发处理多个 HTTP 连接。通过将线程池与数据库连接池结合,服务器能够高效处理大量客户端请求,同时进行数据库操作。
线程池的工作原理
一个线程池通常包含以下几个关键部分:
线程队列:保存所有工作线程。
任务队列:保存所有等待执行的任务。
工作线程:从任务队列中获取任务并执行。
线程池在 Web 服务器中扮演的重要角色包括:
提升性能:通过复用线程,减少了频繁创建和销毁线程的开销。
控制并发数:通过限制线程池中的线程数量,防止系统资源耗尽。
高效管理资源:结合数据库连接池,可以高效管理数据库连接,提升整体系统性能。
示例分析
假设有以下成员变量:
m_actormodel = 1(假设这是 Proactor 模型)
m_connPool 指向已经初始化的数据库连接池
m_thread_num = 8
调用 thread_pool() 方法后:
创建一个包含 8 个线程的线程池,使用 Proactor 模型。
线程池中的线程将使用 m_connPool 中的连接来处理数据库操作。
通过这种方式,服务器可以高效处理 HTTP 请求和数据库操作,提高系统的并发处理能力和响应速度。
5、初始化网络编程
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));
}
/* 创建监听socket文件描述符,用于接收客户端发来的http请求报文 */
int ret = 0;
/* 创建监听socket的TCP/IP的IPV4 socket地址 */
struct sockaddr_in address;//sockaddr_in为结构体名称
bzero(&address, sizeof(address));//bzero(&s,int n):将s的所指的内存区域前n 个字节全部设为零。
address.sin_family = AF_INET;//设置地址簇为AF_INET:TCP/IPv4地址簇,与PF_INET对应(协议簇)
address.sin_addr.s_addr = htonl(INADDR_ANY);//sockaddr_in(变量名).sin_addr.s_addr:将IP字符串转化为网络字节序(大端)
address.sin_port = htons(m_port);//htons(m_port):将端口号从主机字节序转化为网络字节序
int flag = 1;
/*setsockopt(int sockfd , int level, int optname, void *optval, socklen_t *optlen):设置套接字描述符的属性。
sockfd:要设置的套接字描述符。
level:选项定义的层次。或为特定协议的代码(如IPv4,IPv6,TCP,SCTP),或为通用套接字代码(SOL_SOCKET)。
optname:选项名。level对应的选项,一个level对应多个选项,不同选项对应不同功能。
optval:指向某个变量的指针,该变量是要设置新值的缓冲区。可以是一个结构体,也可以是普通变量
optlen:optval的长度。
原文链接:https://blog.csdn.net/qq_41960161/article/details/122705626
*/
//SO_REUSEADDR是让端口释放后立即就可以被再次使用
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
/* 绑定socket和它的地址 */
//sockaddr:sockaddr的缺陷是:sa_data把目标地址和端口信息混在一起了;sockaddr分开存放目标地址和端口信息
//bind()将address所指的socket地址分配给未命名的m_listenfd文件描述符,最后一项指出该address的长度,成功命名返回0,否则-1和设置errno状态
ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
assert(ret >= 0);//assert函数是一个宏。括号里面的表达式如果为真,程序正常执行。如果函数形式的宏的参数表达式比较等于零(即表达式为false),则会向标准错误设备写入一条消息,并调用中止,从而终止程序执行。。
ret = listen(m_listenfd, 5);//命名成功监听m_listenfd,5(backlog典型值)是内核监听队列的最大长度
assert(ret >= 0);
utils.init(TIMESLOT);
//epoll创建内核事件表
/* 用于存储epoll事件表中就绪事件的event数组 */
epoll_event events[MAX_EVENT_NUMBER];
/* 创建一个额外的文件描述符来唯一标识内核中的epoll事件表 */
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;
}
代码详细解释
这段代码定义了 WebServer 类中的 eventListen 方法,用于初始化网络编程所需的基本步骤,包括创建监听 socket,设置 socket 选项,绑定地址,开始监听,以及配置 epoll 和信号处理等。以下是详细的解释:
具体解释
(1)创建监听 socket:
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(m_listenfd >= 0);
使用 socket 函数创建一个监听 socket,协议族为 IPv4,类型为流式 socket,协议为 TCP。
###############注:#######################
socket()函数用于创建一个新的套接字(socket),它是网络编程中的基础函数之一。这个函数的参数定义了套接字的特性,包括协议族、套接字类型和协议。具体来说:
int socket(int domain, int type, int protocol);
参数详细解释
domain(协议族):
AF_INET(或 PF_INET):表示使用 IPv4 地址协议。
AF_INET6(或 PF_INET6):表示使用 IPv6 地址协议。
AF_UNIX(或 PF_UNIX):用于本地通信(同一台机器上的进程间通信)。
AF_NETLINK:用于内核与用户空间通信。
AF_PACKET:用于底层套接字(直接与网络接口进行通信)。
在你的代码中,使用的是 PF_INET,也就是 AF_INET,表示使用 IPv4 地址协议。
type(套接字类型):
SOCK_STREAM:提供面向连接的稳定数据传输(即 TCP)。这是一个双向、可靠、基于字节流的通信方式。
SOCK_DGRAM:提供无连接的数据报服务(即 UDP)。这是一个不可靠的、基于消息的通信方式。
SOCK_RAW:提供原始网络协议访问。允许直接访问下层协议,如 IP 或 ICMP。
SOCK_SEQPACKET:提供有序的、可靠的、基于连接的报文传递。
在你的代码中,使用的是 SOCK_STREAM,表示创建一个 TCP 套接字。
protocol(协议):
通常设置为 0,表示使用默认的协议。对于 SOCK_STREAM 类型的套接字,默认协议是 TCP,对于 SOCK_DGRAM 类型的套接字,默认协议是 UDP。
可以指定特定的协议,例如 IPPROTO_TCP 表示 TCP 协议,IPPROTO_UDP 表示 UDP 协议。
示例解释
m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(m_listenfd >= 0);
PF_INET:表示使用 IPv4 协议。
SOCK_STREAM:表示创建一个 TCP 套接字。
0:表示使用默认的协议(TCP)。
这行代码的作用是创建一个 IPv4 地址族的 TCP 套接字,并将其文件描述符赋值给 m_listenfd 变量。如果创建失败(返回值小于 0),程序将终止并输出断言失败信息。
总结
socket() 函数的参数共同决定了创建的套接字的类型和行为,包括使用的地址族(协议族)、套接字的类型(面向连接或无连接)以及使用的具体协议。通过合理设置这些参数,程序可以创建满足特定网络通信需求的套接字。
############################################################################
(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 选项,以决定连接关闭时的行为。
###############注:#######################
setsockopt() 函数用于设置套接字选项。通过该函数,可以控制套接字的行为和属性,如端口重用、超时设置等。setsockopt() 的函数原型如下:
int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
参数详细解释
socket:要设置选项的套接字文件描述符。
level:指定选项所在的协议层次。
SOL_SOCKET:通用套接字选项。
IPPROTO_TCP:TCP 选项。
IPPROTO_IP:IP 选项。
option_name:需要设置的选项名称。常用的选项名称有:
SO_REUSEADDR:允许重用本地地址。
SO_RCVBUF:接收缓冲区大小。
SO_SNDBUF:发送缓冲区大小。
SO_LINGER:控制延迟关闭行为。
TCP_NODELAY:禁用 Nagle 算法。
option_value:指向包含新选项值的缓冲区。
option_len:option_value 的长度。
示例解释
在你的代码中,出现了多次 setsockopt() 函数调用,下面对每个调用进行解释。
// 优雅关闭连接
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 选项
SO_LINGER 选项用于控制套接字关闭时的行为。struct linger 定义如下:
struct linger {
int l_onoff; // 是否启用 linger 选项
int l_linger; // 延迟关闭的时间(秒)
};
l_onoff = 0,表示禁用延迟关闭。
l_onoff = 1,表示启用延迟关闭,l_linger 指定延迟关闭的时间(秒)。
在你的代码中,根据 m_OPT_LINGER 的值,分别设置不同的 SO_LINGER 选项:
当 m_OPT_LINGER == 0 时,设置 l_onoff = 0,l_linger = 1,表示禁用延迟关闭。
当 m_OPT_LINGER == 1 时,设置 l_onoff = 1,l_linger = 1,表示启用延迟关闭,延迟时间为 1 秒。
SO_REUSEADDR 选项
SO_REUSEADDR 选项允许重用本地地址。设置该选项可以使套接字在绑定(bind())之前可以重用本地地址。这个选项非常有用,例如当服务器重启时,可以立即绑定同一个端口。
int flag = 1;
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
SO_REUSEADDR:允许重用本地地址。
flag = 1:启用该选项。
总结
setsockopt() 函数通过设置不同的选项名称和选项值,可以灵活地控制套接字的行为和属性。在网络编程中,合理使用 setsockopt() 函数,可以提高程序的性能和灵活性。
##############################################################
(3)初始化地址结构体:
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);
初始化一个 sockaddr_in 结构体,用于存储 IP 地址和端口号。
(4)设置端口复用:
int flag = 1;
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
设置 SO_REUSEADDR 选项,以便在程序重启时立即重用端口。
(5)绑定 socket 和地址:
ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
assert(ret >= 0);
将地址结构体绑定到监听 socket 上。
(6)开始监听:
ret = listen(m_listenfd, 5);
assert(ret >= 0);
开始监听 socket,等待客户端连接。
(7)初始化定时器:
utils.init(TIMESLOT);
TIMESLOT 是一个时间间隔参数,用于设置定时器的触发频率。在你的代码中,TIMESLOT 被用来初始化工具类和设置定时信号,以实现定时任务的功能。这些定时任务通常包括检查连接的超时状态、发送心跳包等,确保服务器的正常运行。
(8)创建 epoll 内核事件表:
epoll_event events[MAX_EVENT_NUMBER];
m_epollfd = epoll_create(5);
assert(m_epollfd != -1);
######################################################################
m_epollfd = epoll_create(5); 是用来创建一个新的 epoll 实例,并返回一个用于访问该实例的文件描述符。在这个函数调用中,参数 5 是一个提示值,表示这个 epoll 实例需要处理的文件描述符的预期数量。
epoll_create 的详细解释
语法
int epoll_create(int size);
参数
size:这是一个提示值,表示期望的监听的文件描述符的数量。在 Linux 2.6.8 之后的内核中,这个参数已经没有实际的作用了,但是它仍然是一个必需的参数,可以传递一个大于 0 的整数即可。
返回值
成功时返回一个新的 epoll 文件描述符。
失败时返回 -1,并设置 errno 来指示错误。
在代码中的作用
m_epollfd = epoll_create(5);
assert(m_epollfd != -1);
这行代码的作用是创建一个 epoll 实例,并将返回的文件描述符赋值给 m_epollfd。assert(m_epollfd != -1); 用来检查 epoll_create 是否成功,如果失败则程序会终止。
epoll 的概念
epoll 是 Linux 内核提供的一种多路复用 I/O 的机制,相对于传统的 select 和 poll,epoll 更加高效,适合用于大规模并发连接的网络应用。epoll 提供了一种高效的事件通知机制,能够避免 select 和 poll 在处理大量文件描述符时的低效问题。
示例代码
创建一个 epoll 实例,并注册文件描述符进行事件监控的示例代码如下:
#include <sys/epoll.h>
#include <unistd.h>
#include <assert.h>
int main() {
// 创建 epoll 实例
int epoll_fd = epoll_create(5);
assert(epoll_fd != -1);
// 注册文件描述符(例如监听 socket 文件描述符)到 epoll 实例
int listen_fd = /* 创建和绑定并监听的 socket 文件描述符 */;
struct epoll_event ev;
ev.events = EPOLLIN; // 监控读事件
ev.data.fd = listen_fd;
int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev);
assert(ret != -1);
// 事件循环
struct epoll_event events[10];
while (true) {
int nfds = epoll_wait(epoll_fd, events, 10, -1);
for (int i = 0; i < nfds; ++i) {
if (events[i].data.fd == listen_fd) {
// 处理新的连接
} else {
// 处理其他已连接的 socket 事件
}
}
}
close(epoll_fd);
return 0;
}
在这个示例中,我们创建了一个 epoll 实例,注册了一个监听 socket 文件描述符用于监控读事件,并进入事件循环处理所有注册的事件。
######################################################################
(9)将监听文件描述符添加到 epoll 实例中:
utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
http_conn::m_epollfd = m_epollfd;
####################################################################################
这两行代码的目的是将 m_pipefd 和 m_epollfd 的值赋给 Utils 类的静态成员变量 u_pipefd 和 u_epollfd。这样做的目的是为了在 Utils 类中可以方便地访问和使用这两个文件描述符。
详细解释
cpp
Copy code
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
m_pipefd: 这是在 WebServer 类中创建的一个用于进程间通信的管道文件描述符数组。m_pipefd[0] 通常用于读取端,m_pipefd[1] 通常用于写入端。
m_epollfd: 这是在 WebServer 类中创建的一个 epoll 实例的文件描述符,用于管理和监控多个文件描述符上的事件。
Utils::u_pipefd: 这是 Utils 类的一个静态成员变量,用于存储 m_pipefd 的值,使得 Utils 类中的其他方法可以访问和使用这个管道文件描述符。
Utils::u_epollfd: 这是 Utils 类的一个静态成员变量,用于存储 m_epollfd 的值,使得 Utils 类中的其他方法可以访问和使用这个 epoll 文件描述符。
代码示例
Utils.h
cpp
Copy code
class Utils {
public:
static int* u_pipefd;
static int u_epollfd;
// 其他成员函数和变量
};
Utils.cpp
cpp
Copy code
#include "Utils.h"
// 静态成员变量定义
int* Utils::u_pipefd = nullptr;
int Utils::u_epollfd = -1;
WebServer.cpp
cpp
Copy code
#include "Utils.h"
void WebServer::eventListen() {
// 初始化和设置 socket、epoll 等...
// 将管道文件描述符和 epoll 文件描述符赋值给 Utils 类的静态成员变量
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
// 其他初始化和设置...
}
这样做的好处
共享资源: Utils 类中的静态成员变量可以被该类的所有对象共享,因此在 Utils 类中可以方便地访问和使用 m_pipefd 和 m_epollfd,无需在每个对象中重复存储这些文件描述符。
简化代码: 通过将这些文件描述符存储在静态成员变量中,可以简化代码结构,使得在其他地方使用这些文件描述符时更加方便和直观。
集中管理: 将这些关键的文件描述符集中管理,可以更好地控制它们的生命周期和使用,提高代码的可维护性。
总结
这两行代码的目的是将 WebServer 类中的关键文件描述符赋值给 Utils 类的静态成员变量,使得 Utils 类中的方法可以方便地使用这些文件描述符。这是一种常见的设计模式,用于共享和集中管理关键资源。
#######################################################################################
(10)创建 socketpair 用于进程间通信:
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);
##########################################################################################
这段代码的目的是创建一个双向的 UNIX 域套接字对,并将其中的一端设置为非阻塞模式,然后将另一端添加到 epoll 实例中,以便进行事件监控。以下是详细的解释:
代码解释
创建 UNIX 域套接字对
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_pipefd);
socketpair(int domain, int type, int protocol, int sv[2]):
domain:协议族,这里是 PF_UNIX,表示 UNIX 域协议。
type:套接字类型,这里是 SOCK_STREAM,表示面向连接的数据传输(类似于 TCP)。
protocol:通常为 0,表示默认协议。
sv:整数数组,存储创建的两个套接字文件描述符。
socketpair 函数创建一对相互连接的套接字,可以用于进程间通信。这里创建了 m_pipefd[0] 和 m_pipefd[1]。
错误检查
assert(ret != -1);
assert:用于检查 socketpair 函数的返回值是否为 -1。如果为 -1,表示创建套接字对失败,程序终止执行。
将 m_pipefd[1] 设置为非阻塞模式
utils.setnonblocking(m_pipefd[1]);
utils.setnonblocking(int fd):假设 setnonblocking 是一个工具函数,用于将文件描述符 fd 设置为非阻塞模式。设置为非阻塞模式后,I/O 操作(如读写)将立即返回,而不会阻塞等待。
将 m_pipefd[0] 添加到 epoll 实例中
utils.addfd(m_epollfd, m_pipefd[0], false, 0);
utils.addfd(int epollfd, int fd, bool one_shot, int trigMode):假设 addfd 是一个工具函数,用于将文件描述符 fd 添加到 epoll 实例 epollfd 中进行事件监控。
epollfd:epoll 实例的文件描述符。
fd:要添加的文件描述符,这里是 m_pipefd[0]。
one_shot:一个布尔值,表示是否启用 EPOLLONESHOT 选项。
trigMode:触发模式,0 表示水平触发(LT),1 表示边沿触发(ET)。
addfd 函数将 m_pipefd[0] 添加到 epoll 实例中,以便 epoll 可以监控其事件(如可读或可写事件)。
总结
这段代码通过 socketpair 函数创建了一对相互连接的 UNIX 域套接字(m_pipefd[0] 和 m_pipefd[1]),并将 m_pipefd[1] 设置为非阻塞模式。然后,它将 m_pipefd[0] 添加到 epoll 实例中进行事件监控。这种设计通常用于在网络服务器中实现进程或线程间的高效通信和事件驱动的非阻塞 I/O 操作。
#################################################################################################
(11)设置信号处理:
utils.addsig(SIGPIPE, SIG_IGN);
utils.addsig(SIGALRM, utils.sig_handler, false);
utils.addsig(SIGTERM, utils.sig_handler, false);
###############################################################################
这段代码片段用于设置信号处理器,具体解释如下:
代码解释
utils.addsig(SIGPIPE, SIG_IGN);
utils.addsig(int sig, sighandler_t handler):假设 addsig 是一个工具函数,用于设置信号处理器。
SIGPIPE:表示管道破裂的信号,通常在向已经关闭的套接字写数据时会触发。
SIG_IGN:表示忽略该信号,即当程序收到 SIGPIPE 信号时,将其忽略而不做任何处理。
这里的代码将对 SIGPIPE 信号的默认处理方式设置为忽略,通常是为了避免因为管道破裂而导致程序异常退出。
utils.addsig(SIGALRM, utils.sig_handler, false);
utils.addsig(int sig, void (*handler)(int), bool restart):
假设 sig_handler 是一个静态成员函数或全局函数,用于处理 SIGALRM 信号。
SIGALRM:表示定时器到期的信号。
utils.sig_handler:信号处理函数,用于处理收到 SIGALRM 信号时的行为。
false:表示不重启被信号中断的系统调用(如果有的话)。
这段代码将 SIGALRM 信号的处理方式设置为调用 utils.sig_handler 函数来处理,该函数可能用于定时器到期后的特定操作。
utils.addsig(SIGTERM, utils.sig_handler, false);
utils.addsig(int sig, void (*handler)(int), bool restart):
同上。
SIGTERM:表示终止进程的信号。
utils.sig_handler:信号处理函数,用于处理收到 SIGTERM 信号时的行为。
false:表示不重启被信号中断的系统调用(如果有的话)。
这段代码将 SIGTERM 信号的处理方式设置为调用 utils.sig_handler 函数来处理,通常是为了优雅地终止进程,释放资源等操作。
总结
上述代码片段通过调用 utils.addsig 函数来设置三种不同信号的处理方式:
SIGPIPE:忽略信号,避免由管道破裂引起的异常退出。
SIGALRM:设置定时器到期时的处理函数。
SIGTERM:设置终止进程时的处理函数。
###########################################################################################
(12)设置定时器:
alarm(TIMESLOT);
(13)设置工具类中的静态成员:
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
总结
eventListen 方法实现了 Web 服务器在初始化时所需的所有网络配置步骤,包括创建 socket、设置 socket 选项、绑定地址、监听、设置 epoll 和信号处理等。这些步骤是实现一个高效、可靠的 Web 服务器的基础。
二、http_conn.cpp部分
1、从数据库中读取用户表的数据(用户名和密码)
void http_conn::initmysql_result(connection_pool *connPool)
{
// 先从连接池中取一个连接
MYSQL *mysql = NULL;
connectionRAII mysqlcon(&mysql, connPool);
// 在user表中检索username,passwd数据,浏览器端输入
if (mysql_query(mysql, "SELECT username,passwd FROM user"))
{
LOG_ERROR("SELECT error:%s\n", mysql_error(mysql));
}
// 从表中检索完整的结果集
MYSQL_RES *result = mysql_store_result(mysql);
// 返回结果集中的列数
int num_fields = mysql_num_fields(result);
// 返回所有字段结构的数组
MYSQL_FIELD *fields = mysql_fetch_fields(result);
// 从结果集中获取下一行,将对应的用户名和密码,存入map中
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
}
代码解释
这段代码定义了 http_conn 类中的 initmysql_result 方法,用于从数据库中读取用户表的数据(用户名和密码),并将这些数据存储到一个映射(map)中。这个方法使用了一个数据库连接池 connPool 来获取和管理 MySQL 连接。
详细解释
(1)获取数据库连接:
MYSQL *mysql = NULL;
connectionRAII mysqlcon(&mysql, connPool);
MYSQL *mysql = NULL;:声明一个指向 MYSQL 结构的指针 mysql。
connectionRAII mysqlcon(&mysql, connPool);:使用资源获取即初始化(RAII)模式,从连接池 connPool 中获取一个 MySQL 连接,并在 mysqlcon 的生命周期结束时自动释放连接。
执行 SQL 查询:
if (mysql_query(mysql, "SELECT username,passwd FROM user"))
{
LOG_ERROR("SELECT error:%s\n", mysql_error(mysql));
}
mysql_query(mysql, “SELECT username,passwd FROM user”):执行 SQL 查询,检索 user 表中的 username 和 passwd 数据。
如果查询失败,记录错误日志。
(2)获取结果集:
MYSQL_RES *result = mysql_store_result(mysql);
mysql_store_result(mysql):从 MySQL 服务器获取查询结果集,并存储在 result 中。
(3)获取结果集的元数据:
int num_fields = mysql_num_fields(result);
MYSQL_FIELD *fields = mysql_fetch_fields(result);
mysql_num_fields(result):获取结果集中的列数。
mysql_fetch_fields(result):获取结果集的字段结构数组。
(4)处理结果集:
while (MYSQL_ROW row = mysql_fetch_row(result))
{
string temp1(row[0]);
string temp2(row[1]);
users[temp1] = temp2;
}
mysql_fetch_row(result):从结果集中获取下一行数据。返回一个指向字符串数组的指针,其中每个字符串表示该行中对应字段的值。
将每行的用户名和密码存储到 users 映射中,users[temp1] = temp2;。
总结
这段代码的主要功能是从数据库中读取用户数据,并将其存储在一个映射中,以便于后续的验证和使用。使用连接池可以有效管理数据库连接,避免频繁创建和销毁连接带来的开销。
RAII 模式
connectionRAII 是一个用于管理资源的类,确保资源在使用完成后正确释放。它的构造函数从连接池获取一个连接,并将其赋值给 mysql 指针,而析构函数则负责将连接归还给连接池。这种模式保证了资源的正确管理,避免了资源泄漏。