前言
项目简介:
Linux下C++轻量级Web服务器,助力初学者快速实践网络编程,搭建属于自己的服务器.
- 使用 线程池 + 非阻塞socket + epoll(ET和LT均实现) + 事件处理(Reactor和模拟Proactor均实现) 的并发模型
- 使用状态机解析HTTP请求报文,支持解析GET和POST请求
- 访问服务器数据库实现web端用户注册、登录功能,可以请求服务器图片和视频文件
- 实现同步/异步日志系统,记录服务器运行状态
- 经Webbench压力测试可以实现上万的并发连接数据交换
以下是该项目中比较简单的代码的源码注释:
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;
}
webserver.h
#ifndef WEBSERVER_H
#define WEBSERVER_H
// 导入必要的头文件,用于处理网络编程、文件描述符、epoll机制、线程池等功能
#include <sys/socket.h> // 套接字相关
#include <netinet/in.h> // IP地址相关
#include <arpa/inet.h> // IP转换相关
#include <stdio.h> // 标准输入输出
#include <unistd.h> // POSIX 操作系统 API,如close等
#include <errno.h> // 错误处理
#include <fcntl.h> // 文件控制(非阻塞设置)
#include <stdlib.h> // 标准库函数,如malloc等
#include <cassert> // 断言,用于调试
#include <sys/epoll.h> // epoll相关
// 导入线程池和HTTP连接相关模块
#include "./threadpool/threadpool.h"
#include "./http/http_conn.h"
// 常量定义
const int MAX_FD = 65536; // 最大文件描述符数量
const int MAX_EVENT_NUMBER = 10000; // epoll 监听的最大事件数
const int TIMESLOT = 5; // 定时器的最小超时时间单位(秒)
class WebServer
{
public:
// 构造函数
WebServer();
// 析构函数
~WebServer();
// 初始化函数,设置服务器端口、数据库信息、日志等
void 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);
// 初始化线程池
void thread_pool();
// 初始化数据库连接池
void sql_pool();
// 初始化日志系统
void log_write();
// 设置触发模式(边沿触发或水平触发)
void trig_mode();
// 设置监听事件(epoll监听)
void eventListen();
// 事件循环,处理服务器运行中的各类事件
void eventLoop();
// 处理定时器的回调,管理客户端连接
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();
// 处理信号(如关闭服务器、超时等)
bool dealwithsignal(bool& timeout, bool& stop_server);
// 处理读事件
void dealwithread(int sockfd);
// 处理写事件
void dealwithwrite(int sockfd);
public:
// 基础参数
int m_port; // 服务器端口号
char *m_root; // 服务器根目录
int m_log_write; // 日志写入方式
int m_close_log; // 是否关闭日志
int m_actormodel; // 事件处理模式(Reactor/Proactor)
// 管道文件描述符(用于处理信号)
int m_pipefd[2];
// epoll 文件描述符
int m_epollfd;
// 所有客户端的 HTTP 连接数据
http_conn *users;
// 数据库相关
connection_pool *m_connPool; // 数据库连接池
string m_user; // 数据库用户名
string m_passWord; // 数据库密码
string m_databaseName; // 数据库名称
int m_sql_num; // 数据库连接数量
// 线程池相关
threadpool<http_conn> *m_pool; // 线程池指针
int m_thread_num; // 线程数量
// epoll 事件相关
epoll_event events[MAX_EVENT_NUMBER]; // 用于存储epoll等待到的事件
// 套接字相关
int m_listenfd; // 监听套接字
int m_OPT_LINGER; // 是否使用优雅关闭连接(linger选项)
int m_TRIGMode; // 触发模式(边沿或水平触发)
int m_LISTENTrigmode; // 监听套接字的触发模式
int m_CONNTrigmode; // 连接套接字的触发模式
// 定时器相关
client_data *users_timer; // 用户定时器数据
Utils utils; // 工具类,管理定时器和信号
};
#endif
Config.cpp
这里面也只有一个解析命令行的代码:
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;
}
}
}
这段代码是 Config::parse_arg
函数的实现,它用于解析命令行参数,将传入的命令行参数根据不同的标志(如 -p
, -l
, -m
等)转换为对应的配置值。
下面是详细的解释:
1. 函数签名
void Config::parse_arg(int argc, char *argv[])
- 这是一个
Config
类的成员函数,名字是parse_arg
。 argc
是命令行参数的个数,argv
是存储命令行参数的字符数组。argc
和argv
通常在main
函数中作为参数传递。- 这个函数的作用是根据命令行输入的参数,解析并设置类中的相关配置项。
2. 定义变量
int opt;
const char *str = "p:l:m:o:s:t:c:a:";
opt
:用于存储解析到的选项字符(如-p
,-l
等)。str
:定义了命令行参数选项的格式。每个字符代表一个参数的标志,后面的冒号(:
)表示该选项需要一个参数。例如,'p'
后面有冒号,因此-p
选项必须带有一个参数。
3. getopt
函数
while ((opt = getopt(argc, argv, str)) != -1)
getopt
是一个用于解析命令行参数的标准库函数,它依次解析由argv
传入的参数,并根据str
中定义的选项返回对应的标志字符(如p
、l
等)。- 当
getopt
返回-1
时,表示已经没有更多的选项可供处理。
4. switch
语句处理选项
- 根据
getopt
返回的标志字符(存储在opt
中),switch
语句分别处理不同的命令行选项。
switch (opt)
{
case 'p':
PORT = atoi(optarg);
break;
// 其他选项处理
}
- 每个
case
语句处理对应的标志选项(如-p
,-l
等)。 atoi(optarg)
:optarg
是getopt
提供的当前选项的参数值(它是一个字符串)。atoi
函数用于将字符串转换为整数。例如,当用户输入-p 8080
时,optarg
是"8080"
,而atoi(optarg)
将其转换为整数8080
。- 这些参数分别赋值给类中的成员变量(如
PORT
,LOGWrite
,TRIGMode
等)。
5. 解析的命令行选项
-p
:解析端口号,赋值给PORT
。-l
:日志写入方式,赋值给LOGWrite
。-m
:触发模式,赋值给TRIGMode
。-o
:设置 linger 选项,赋值给OPT_LINGER
。-s
:数据库连接池的连接数,赋值给sql_num
。-t
:线程池中的线程数,赋值给thread_num
。-c
:是否关闭日志,赋值给close_log
。-a
:选择处理模型,赋值给actor_model
。
6. 默认行为
- 如果传入了未定义的选项(即不在
str
中的标志),default
部分将不做任何处理。
Config.h
#ifndef CONFIG_H
#define CONFIG_H
#include "webserver.h"
using namespace std;
class Config
{
public:
Config();
~Config(){};
void parse_arg(int argc, char*argv[]);
//端口号
int PORT;
//日志写入方式
int LOGWrite;
//触发组合模式
int TRIGMode;
//listenfd触发模式
int LISTENTrigmode;
//connfd触发模式
int CONNTrigmode;
//优雅关闭链接
int OPT_LINGER;
//数据库连接池数量
int sql_num;
//线程池内的线程数量
int thread_num;
//是否关闭日志
int close_log;
//并发模型选择
int actor_model;
};
#endif
lst_timer.h
宏定义部分
#ifndef LST_TIMER
#define LST_TIMER
- 这部分是 头文件保护,防止该头文件被重复包含。
#ifndef
和#define
保证当头文件已经被包含时,编译器不会再次包含它,避免重复定义。
头文件引用部分
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/epoll.h>
// ... 省略部分 include
#include "../log/log.h"
- 引入了一系列与系统 I/O、网络、信号处理、线程和日志相关的头文件。这些头文件提供了网络编程和多线程编程所需的基础功能。
client_data
结构体
struct client_data
{
sockaddr_in address; // 客户端的地址信息(IP 和端口)
int sockfd; // 客户端 socket 文件描述符
util_timer *timer; // 指向与该客户端关联的定时器
};
client_data
结构体保存了与每个客户端连接相关的数据,包括它的sockaddr_in
地址信息,sockfd
(用于通信的 socket),以及关联的util_timer
定时器。
util_timer
类
class util_timer
{
public:
util_timer() : prev(NULL), next(NULL) {}
public:
time_t expire; // 定时器到期时间,类型是 time_t(通常为秒)
void (* cb_func)(client_data *); // 定时器到期后执行的回调函数,传入 client_data 指针
client_data *user_data; // 关联的客户端数据
util_timer *prev; // 指向前一个定时器
util_timer *next; // 指向下一个定时器
};
util_timer
是一个定时器类,它代表一个定时器节点。expire
表示该定时器的到期时间。cb_func
是定时器到期时要执行的回调函数,它接受client_data
类型的参数。user_data
是与该定时器关联的客户端数据。prev
和next
指向双向链表中的前一个和后一个定时器节点。
sort_timer_lst
类
class sort_timer_lst
{
public:
sort_timer_lst();
~sort_timer_lst();
void add_timer(util_timer *timer);
void adjust_timer(util_timer *timer);
void del_timer(util_timer *timer);
void tick();
private:
void add_timer(util_timer *timer, util_timer *lst_head);
util_timer *head;
util_timer *tail;
};
sort_timer_lst
是一个管理定时器的类。它使用一个升序排序的双向链表来管理多个定时器,保证定时器按照到期时间顺序排列。add_timer()
用于将新的定时器添加到链表中。adjust_timer()
用于调整定时器的时间(比如延长某个连接的超时时间)。del_timer()
用于删除某个定时器。tick()
会遍历定时器链表,检查每个定时器是否到期并执行回调函数。add_timer()
私有函数是一个辅助函数,用于将定时器插入到链表中正确的位置。
Utils
类
class Utils
{
public:
Utils() {}
~Utils() {}
void init(int timeslot);
// 对文件描述符设置为非阻塞模式
int setnonblocking(int fd);
// 注册文件描述符到 epoll 实例中,支持 ET 模式和 EPOLLONESHOT
void addfd(int epollfd, int fd, bool one_shot, int TRIGMode);
// 信号处理函数
static void sig_handler(int sig);
// 设置某个信号的处理函数
void addsig(int sig, void(handler)(int), bool restart = true);
// 定时器处理函数,每次定时触发 SIGALRM 信号
void timer_handler();
// 显示错误信息给客户端
void show_error(int connfd, const char *info);
public:
static int *u_pipefd; // 用于信号通信的管道
sort_timer_lst m_timer_lst; // 管理定时器的链表
static int u_epollfd; // 全局的 epoll 文件描述符
int m_TIMESLOT; // 定时器的时间间隔
};
Utils
类封装了工具函数,如设置非阻塞文件描述符、处理信号、管理定时器等。setnonblocking()
:将某个文件描述符设置为非阻塞模式。addfd()
:将文件描述符添加到epoll
事件监听列表中,支持 ET 模式和EPOLLONESHOT
(即一次触发模式)。sig_handler()
:信号处理函数,用于处理收到的信号。addsig()
:设置信号处理函数,可选restart
表示收到信号后是否自动重启系统调用。timer_handler()
:定时器处理函数,用于处理定时任务并重新设置定时器。show_error()
:向客户端发送错误信息。
回调函数 cb_func
void cb_func(client_data *user_data);
- 这是一个定义在全局作用域中的回调函数,作用是当定时器到期时处理相应的客户端连接(例如超时关闭连接),它会接收一个指向
client_data
结构体的指针作为参数。具体的实现代码通常会关闭客户端连接并释放资源。
总结:
- 这个头文件定义了一个基于定时器链表的超时管理系统,通常用于 Web 服务器管理客户端连接超时。
- 每个客户端有自己的
client_data
结构体,存储其 socket 和定时器。 util_timer
类是定时器节点,sort_timer_lst
类是定时器管理器,负责对定时器链表进行排序、添加、删除等操作。Utils
类封装了信号处理、文件描述符管理以及定时器的处理机制。