本解读文章对完整代码进行解读,从main.cpp着手主看代码,然后一步步剖解代码文件,但不会插入所有代码,所以需要搭配打开TingWebServer的源代码一起学习
源代码地址:TinyWebServer/webserver.cpp at master · qinguoyi/TinyWebServer
作者的原解读:最新版Web服务器项目详解 – 01 线程同步机制封装类
Tingwebserver 的配置与使用:c++ 经典服务器开源项目 Tinywebserver的使用与配置(百度智能云服务器安装ubuntu18.04可用公网ip访问)-CSDN博客
我们将按照这个关系图逐级解读代码
先看主代码: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;
}
这三行是在程序编译运行前就需要修改的,确保user,password和database是自己的数据库
string user = "root";
string passwd = "root";
string databasename = "qgydb";
这是config解析命令行的运行代码的,一遍的shell脚本代码格式为:
./server [-p port] [-l LOGWrite] [-m TRIGMode] [-o OPT_LINGER] [-s sql_num] [-t thread_num] [-c close_log] [-a actor_model]
而config是解析后面的-p,-l之类的指定参数,从而实现个性化运行服务器
//命令行解析
Config config;
config.parse_arg(argc, argv);
后面则是webserver的初始化,传入参数是config的解析命令,之后便是服务器启动的规范标准流程:日志->数据库->线程池-> 触发模式-> 事件监听-> 事件循环,服务器开始运行
- 服务器启动时,首先需要确保日志系统可以记录后续的事件。
- 然后初始化数据库连接池,确保后续操作(如请求处理)能够顺利与数据库交互。
- 接下来初始化线程池,以确保服务器能够并发处理客户端请求。
- 随后配置触发模式,确保事件处理方式符合需求。
- 事件监听是服务器开始接收请求的步骤,它依赖于前面所有资源的配置,包括日志、数据库连接、线程池和触发模式。
- 事件循环是服务器的主循环,它依赖于所有初始化过程(如监听端口、线程池等)完成才能启动。
这个顺序确保了每个系统组件的初始化都在其依赖的组件之前进行,从而避免出现资源不可用或配置错误的问题。
因为main.cpp中只是include了config .h这个头文件,我们就跟着引用头文件入手
命令行解析config.h+config.cpp
config.h非常简单并且参数都给了注解,提一嘴宏定义头文件,为了防止重复编译报错,所以采用这种方式
// config.h #ifndef CONFIG_H #define CONFIG_H
// 头文件的内容
#endif // CONFIG_H
现代编译器通常还支持一种更简洁的方式来避免头文件多次包含,那就是使用 #pragma once。它的效果与 #ifndef / #define 配合使用的头文件保护机制相同,但写法更简洁,且不容易出错
在config.cpp中,构造函数Config()定义了默认参数,服务器的初始定义状态,默认端口号为9006。
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;
}
…
default:
break;
}
}
使用解析命令行函数getopt(),返回的opt指向合法命令行选项,例如-p 返回 'p',而optarg 是 getopt 解析到的当前选项的参数值。它是一个 char* 类型的全局变量,用于存储当前选项对应的参数。
getopt 是一个标准库函数,用于解析命令行参数。它通过一个选项字符串(比如 str)定义程序支持的命令行参数,并自动处理参数解析。
- 参数说明:
- argc: 参数的数量(来自命令行)。
- argv: 参数的值(命令行中输入的具体内容)。
- str: 一个字符串,指定程序支持的命令行选项。每个字符代表一个选项,字符后面可能跟着一个冒号(:),表示该选项后面需要一个参数
主体文件:webserver.h+webserver.cpp
webserver .h
因为是主体文件,include了很多需要使用到的头文件和库,除了main中用到的运行函数和初始化之外,public里还有定时器函数和epoll的相关配置
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(); // 处理客户端数据
// 定时器配置
client_data *users_timer; // 定时器相关数据结构,管理连接的超时
Utils utils; // 工具类,用于处理定时器等
//定义在timer的lst_timer中
//epoll事件
bool dealwithsignal(bool& timeout, bool& stop_server); // 处理信号
void dealwithread(int sockfd); // 处理读操作
void dealwithwrite(int sockfd); // 处理写操作
epoll_event events[MAX_EVENT_NUMBER]; // 存放epoll事件的数组
int m_listenfd; // 监听套接字文件描述符
int m_OPT_LINGER; // SO_LINGER设置标志,是否延迟关闭套接字
int m_TRIGMode; // 总的触发模式(水平触发/边缘触发)
int m_LISTENTrigmode; // 监听套接字的触发模式(水平触发/边缘触发)
int m_CONNTrigmode; // 客户端连接的触发模式(水平触发/边缘触发)
webserver .cpp
WebServer::WebServer()
构造函数:
//http_conn类对象
users = new http_conn[MAX_FD];
http_conn 类是 Web 服务器项目中与每个客户端连接相关的核心类,承担着管理连接、处理 HTTP 请求和响应、以及与事件驱动机制(如 epoll)协同工作等任务。通过该类,服务器能够高效地处理大量的并发连接和 I/O 操作。
//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);
这段代码的目的是在程序启动时动态构建一个路径,该路径是当前工作目录路径(通过 getcwd 获取)加上一个固定的 /root 路径。例如,如果当前工作目录是 /home/user/project,那么构建出来的路径会是 /home/user/project/root。
- getcwd 获取当前工作目录。
- strcat 和 strcpy 拼接得到一个新的路径,存储在 m_root 中。
WebServer::~WebServer()
析构函数,关闭epoll,listen,和管道pipe,delete释放users http_conn和users_timer定时器和池空间m_pool
WebServer::init()
main()中的初始化server对象
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;//并发模型选择
}
trig_mode()
根据传入的m_TRIGMode值来设定服务器运行的模式,默认:LT+LT,通过改变 m_LISTENTrigmode 和m_CONNTrigmode 的值来控制。
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;
}
}
日志函数
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 //日志不可写,最后的0表示日志的条目
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
}
}
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);
}
thread_pool()
线程池初始化函数
void WebServer::thread_pool()
{
//初始化线程池
m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
}
eventLoop()
Server的逻辑中心,loop函数,处理
- 新客户端连接
- 客户端连接的关闭或错误
- 信号事件
- 客户端数据的读取和响应数据的写入
- 定时器任务
重点
因为webserver.cpp在构建的时候涉及函数间使用依赖的问题,eventLoop写在源代码的最下面,而笔者把本函数调度到最上面讲解是以为loop涉及大体流程和其他函数,在看loop函数的同时找到对应的使用函数能更好理清思路理解代码。
void WebServer::eventLoop()
{
bool timeout = false;
bool stop_server = false;//标记是否有定时器超时事件和服务器是否停止运行
while (!stop_server)//循环处理事件
{
//阻塞模式,等待事件发生(io复用)
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 = dealclientdata();//客户端连接
if (false == flag)
continue;
}
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{ //表示关闭,异常或错误
//服务器端关闭连接,移除对应的定时器
util_timer *timer = users_timer[sockfd].timer;
deal_timer(timer, sockfd);
}
//处理信号,在listen中创建了pipe管道并挂上监听,这里是监听到管道可读数据时的处理
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);
}
}
if (timeout)//计时器关闭
{
utils.timer_handler();
LOG_INFO("%s", "timer tick");日志记录
timeout = false;
}
}
}
eventListen()
网络编程步骤,设立监听器,socket listen,判断是否优雅关闭连接,设置 setsockopt
优雅关闭选项:setsockopt(m_listenfd, SOL_SOCKET, SO_LINGER, &tmp, sizeof(tmp));
setsockopt函数:int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd:要设置选项的套接字的文件描述符。
- level:选项所在的协议层,通常是 SOL_SOCKET(套接字层)或协议层(如 IPPROTO_TCP)。
- optname:要设置的选项的名称,例如 SO_REUSEADDR,SO_LINGER,SO_RCVBUF 等。
- optval:指向包含选项值的缓冲区的指针。
- optlen:选项值的大小。
SO_LINGER:设置套接字关闭时的延迟时间。
SO_LINGER 选项定义了在套接字被关闭时,应该如何处理剩余数据。其结构体定义如下:
struct linger { int l_onoff; // 是否启用延迟关闭(0为不启用,1为启用)
int l_linger; // 延迟关闭的时间(单位为秒) };
当 l_onoff 设置为 1 时,表示启用延迟关闭,l_linger 设置为指定的时间长度(单位是秒)。在此期间,套接字不会立即关闭,而是等待所有未发送完的数据发送完毕后再关闭连接。
当 l_onoff 设置为 0 时,表示禁用延迟关闭。此时,调用 close() 函数会立即关闭套接字,并且任何未发送的数据将会被丢弃。
利用setsockopt完成优雅关闭操作
注:uilts类为timer文件夹lst_timer.cpp中定义的类文件,这是一个封装的工具函数,webserver.cpp和其他文件中会大量用到,看到时可以去lst_timer.cpp查阅具体函数定义
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));
//设置端口复用
//SO_REUSEADDR:允许重用本地地址。常用于允许多个服务器进程绑定到同一端口,或者在程序异常退出 后快速绑定到同一端口。
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];//创建epoll数组
m_epollfd = epoll_create(5);//创建epoll实例用于监听
assert(m_epollfd != -1);
//将m_listenfd挂在监听m_epollfd的epoll实例中,m_LISTENTrigmode为触发模式
utils.addfd(m_epollfd, m_listenfd, false, m_LISTENTrigmode);
//m_epollfd赋值给http请求类
http_conn::m_epollfd = m_epollfd;
//创建了管道m_pipefd用于进程间通信
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);
//忽略 SIGPIPE 信号,有助于避免进程因管道错误而意外退出
utils.addsig(SIGPIPE, SIG_IGN);
//设置 SIGALRM 信号(定时器信号),定时器到期时发送
utils.addsig(SIGALRM, utils.sig_handler, false);
//设置 SIGTERM 信号(终止信号),外部请求终止程序时发出
utils.addsig(SIGTERM, utils.sig_handler, false);
//信号设定后触发调用handler函数,send进管道的写端
//定时器设置
alarm(TIMESLOT);
//工具类,信号和描述符基础操作,存储全局资源
Utils::u_pipefd = m_pipefd;
Utils::u_epollfd = m_epollfd;
}
timer()
为fd设置计时器,计时器函数都在timer目录的lst_ timer下,相关函数定义可以跳转翻看
//为新连接设置计时器
void WebServer::timer(int connfd, struct sockaddr_in client_address)
{
//users是webserver.h里http_conn *users;初始化http请求数据
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);//定时器挂载列表
}
adjust_timer()
调整计时器,延长连接时间
//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
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");
}
deal_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);
}
dealclientdata()
处理连接事件,根据不同的触发模式,accept创建新的套接字处理后续操作
//处理连接事件
bool WebServer::dealclientdata()
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
if (0 == m_LISTENTrigmode) //如果设置LT触发模式
{
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 //ET模式进行循环判断
{
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;
}
dealwithsignal()
处理信号
//处理信号,判断条件:是否停止服务器,是否计时器结束,否->处理
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;//信号处理完毕
}
dealwithread()
读事件处理函数,根据设定模式进行不同操作,逻辑相同,都是把读事件加入请求处理队列
//读事件处理函数
void WebServer::dealwithread(int sockfd)
{
//每个客户端都有一个计时器,确保连接超时进行释放资源操作
util_timer *timer = users_timer[sockfd].timer;
//reactor 如果设定为reactor模式下
if (1 == m_actormodel)
{
if (timer)
{
adjust_timer(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
{
//proactor
if (users[sockfd].read_once())//服务器主动从客户端读取数据并进行处理
{
LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));//打印日志,记录客户端IP
//若监测到读事件,将该事件放入请求队列
m_pool->append_p(users + sockfd);
if (timer)
{
adjust_timer(timer);//延迟连接时间
}
}
else
{
deal_timer(timer, sockfd);//处理失败,关闭连接
}
}
}
dealwithwrite()
写事件处理,和上述读事件处理相同
//处理写事件,和读事件代码逻辑相同,先判断何种触发模式,然后交给处理队列处理,关闭或者延长计时器
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);
}
}
}
总结webserver:
在看完webserver的代码后,我们知道了服务器的大体模型框架为:
先初始化日志,数据库,线程池,触发模式,然后eventlisten创建了socket套接字,进行服务器的常规化构建操作,通过socket,bind,listen。
然后setsocketopt设置是否优雅关闭连接,接着epoll创建内核事件表,将m_listenfd挂上监听树,与此同时,创建m_pipe管道,将读端一起挂上监听树,信号传输就是在管道间进行的,然后设置事件信号 ,如:计时器结束信号,进程结束信号,并忽略pipe间通信的常见错误信号以免干扰接受信号的准确度。
接着,在服务器接收到客户端发来的请求时,loop判断处理请求为请求连接,调用dealclient函数处理,创建clientfd建立客户端连接,然后dealclient函数调用timer函数设立定时器,timer函数中又将client数据给http_conn初始化,挂上监听树。
loop循环到pipe上的信号时进行信号处理,收到服务端关闭连接时,关闭计时器。
loop循环到来自客户端的请求时,判断事件为读或写事件,进行dealwithread/dealwithwrite处理,根据不同的设定模式进行reactor/proactor处理,将事件防放入请求队列中,让线程池进行处理。
最后如计时器结束调用utils.timer_handler处理计时器,while循环由服务器关闭连接,stop_server=true时跳出,服务器停止运行。
评论前必须登录!
注册