理解一个项目最好的方法便是从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;
}
}
}
至此,项目所涉及的模块与函数的功能我们已经大致了解,后续我们将具体地理解每一个模块函数,以及该模块所涉及到的模式(包括模式的重要性与功能)。