前言
项目地址
项目介绍
源码详细分析
项目路径如下:
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];
}
#include "webserver.h"
:包含WebServer类的声明。- 构造函数
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文件描述符、监听文件描述符、管道文件描述符。
- 删除动态分配的
users
和users_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 + 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
:设置监听和连接的触发模式。- LT(水平触发):0
- ET(边缘触发):1
- 根据
m_TRIGMode
的值来设置监听和连接的触发模式。
这段代码的作用:
WebServer::trig_mode
函数用于设置服务器的监听(m_LISTENTrigmode
)和连接(m_CONNTrigmode
)的触发模式。触发模式有两种:LT(水平触发,Level-Triggered)和ET(边缘触发,Edge-Triggered),分别用0
和1
表示。
根据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 多路复用机制。
-
LT(Level-Triggered,水平触发):
- 工作方式:在水平触发模式下,只要文件描述符上还有数据未处理,
epoll
会反复通知应用程序。因此,只要某个事件没有被处理,下一次调用epoll_wait
时,仍会返回该事件。 - 特点:
- 容易编程,适合大部分场景。
- 可能导致重复处理同一事件。
- 场景:适用于要求及时处理事件的场景,编程简单,但效率相对较低。
- 工作方式:在水平触发模式下,只要文件描述符上还有数据未处理,
-
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);
else
Log::get_instance()->init("./ServerLog", m_close_log, 2000, 800000, 0);
}
}
- 日志写入函数
WebServer::log_write
:初始化日志系统。- 根据
m_log_write
的值来选择不同的日志初始化方式。
- 根据
数据库连接池初始化
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
方法,初始化数据库读取表。
线程池初始化
void WebServer::thread_pool()
{
// 线程池
m_pool = new threadpool<http_conn>(m_actormodel, m_connPool, m_thread_num);
}
- 线程池初始化函数
WebServer::thread_pool
:创建并初始化线程池对象m_pool
。
事件监听函数
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;
}
- 事件监听函数
WebServer::eventListen
:设置监听socket和epoll事件。- 创建监听socket
m_listenfd
,并设置相关选项(如SO_LINGER
)。 - 将socket绑定到指定的地址和端口,并开始监听。
- 初始化工具类
utils
。 - 创建epoll实例
m_epollfd
,并将监听socketm_listenfd
加入epoll监听列表。 - 创建UNIX域套接字对
m_pipefd
,用于进程间通信,并将其加入epoll监听列表。 - 设置信号处理函数,忽略
SIGPIPE
信号,处理SIGALRM
和SIGTERM
信号。 - 启动定时器
alarm
。
- 创建监听socket
定时器相关函数
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;
// reactor
if (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;
// 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
{
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服务器的核心逻辑。具体来说,它负责:
-
事件管理和处理:通过
epoll
管理所有的网络事件,包括客户端连接、读写操作、以及异常和超时事件。 -
连接处理:实现新客户端连接的接收和管理,包括创建连接对象,并将其注册到
epoll
中进行监听。 -
读写操作:根据服务器的模式(
Reactor
或Proactor
),处理客户端的读写请求。在Reactor
模式下,主要通过线程池异步处理请求;在Proactor
模式下,则在读写完成后直接处理业务逻辑。 -
定时器管理:使用定时器对客户端连接进行超时控制,通过调整和删除定时器来管理连接的生命周期,确保资源及时释放。
-
信号处理:处理系统信号(如终止信号、定时信号),用于控制服务器的停止和定时任务的执行。
总体来说,这个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 多路复用机制,用于监控多个文件描述符,以便在这些文件描述符上发生事件时通知应用程序进行相应处理。相比于传统的 select
和 poll
,epoll
在处理大量文件描述符时表现更为高效,特别是在高并发场景下。
epoll
的主要特点:
-
高效性:
epoll
使用的是基于事件通知的机制,只有发生事件的文件描述符才会被返回,因此在大量文件描述符中只有少数有事件发生时,epoll
的性能优势显著。epoll
在内核空间维护了一个事件表,避免了每次调用都要传递整个文件描述符集合,减少了内核与用户态之间的数据拷贝。
-
水平触发和边缘触发:
- 水平触发(Level-triggered, LT):默认模式,只要某个文件描述符上有事件发生,
epoll_wait
就会返回该文件描述符,直到事件被处理。 - 边缘触发(Edge-triggered, ET):更为高效,但要求更细致的处理。当文件描述符状态从无事件变为有事件时才会通知,适用于减少系统调用频率,提高程序效率。
- 水平触发(Level-triggered, LT):默认模式,只要某个文件描述符上有事件发生,
-
对文件描述符数量的支持:
epoll
能够支持大规模的文件描述符集合,理论上上限是系统的最大文件描述符数,而select
和poll
通常有较小的文件描述符限制。
epoll
的工作流程:
-
创建
epoll
实例:- 使用
epoll_create
或epoll_create1
函数创建一个epoll
实例,返回一个epoll
文件描述符。
- 使用
-
注册事件:
- 使用
epoll_ctl
函数将需要监控的文件描述符添加到epoll
实例中,并指定要监听的事件类型(如可读、可写、异常等)。
- 使用
-
等待事件发生:
- 使用
epoll_wait
函数等待事件的发生,当某个或多个文件描述符上的事件满足条件时,epoll_wait
会返回这些文件描述符。
- 使用
-
处理事件:
- 处理返回的事件,执行相应的读写操作,或根据应用程序逻辑进行其他处理。
使用场景:
epoll
特别适合用于高并发的网络服务器中,比如 Web 服务器、聊天服务器等。这些应用通常需要处理大量并发连接,并且每个连接可能频繁进行 I/O 操作。epoll
能够有效地提升这些应用的性能。
总之,epoll
是在 Linux 环境下构建高性能网络服务器的重要工具,它通过高效的事件通知机制帮助开发者更好地管理大量并发 I/O 操作。
复现过程中遇到的问题
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将光标移动到确定键上 然后回车就完事了