对照项目文件目录可得,项目主要内容如下:
1、lock锁机制
2、threadpool封装线程池
3、http解析get和post请求
4、CGImysql数据库连接池
5、timer定时器机制
6、log日志机制
其他内容如下:
1、主函数以及webserver
2、root静态资源页面
3、test_pressure压力测试
4、config项目配置
哇咔咔 终于到第一个项目了
主要参考以下内容:
GitHub - qinguoyi/TinyWebServer: Linux下C++轻量级Web服务器学习
小白视角:一文读懂社长的TinyWebServer | HU (huixxi.github.io)
web服务器项目部分问题汇总 - 知乎 (zhihu.com)
Web服务器---TinyWebServer代码详细讲解(log模块)_tinywebserver讲解_才文嘉的博客-CSDN博客
log是日志模块,用以记录运行状态、错误异常、访问数据等等信息。
设计一个异步写日志的模块,能顺利写日志但是又不要占用主线程时间去写
了解:
同步日志,日志写入函数与工作线程串行执行,当单条日志比较大的时候同步模式会阻塞整个处理流程,服务器所能处理的并发能力下降。
生产者-消费者模型:并发编程中的经典模型。生产者与消费者共享一个缓冲区,其中生产者往缓冲区中push消息,消费者从缓冲区中pop消息。
阻塞队列:将生产者-消费者模型进行封装,使用环形数组实现队列,作为两者共享的缓冲区
循环数组 及 实现_Monkey Ji的博客-CSDN博客
异步日志:将所写日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志文件中
线程安全:线程安全就是某个函数在并发环境中调用时,能够处理好多个线程之间的共享变量,使程序能够正确执行完毕。也就是说我们想要确保在多线程访问的时候,我们的程序还能够按照我们的预期的行为去执行,那么就是线程安全了
多用于复杂项目中
单例模式可用于编写日志类
分别是懒汉和饿汉模式。顾名思义,懒汉模式,即非常懒,不用的时候不去初始化,所以在第一次被使用时才进行初始化;饿汉模式,即迫不及待,在程序运行时立即初始化。
shellmad-c++_44 单例模式的原理及实现_哔哩哔哩_bilibili
单例模式:只允许创建一个实例,最简单的实现方式就是:私有化它的构造函数,以防止外界创建单例类的对象;使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例
单例模式---只允许创建一个实例--将类的构造函数私有化,使得用户不能在公共区域创建对象
但是外部就根本没办法直接实例化调用该类,只能类内部调用--在类内部使用new创建一个公有化的函数,然后让该函数返回一个该类的指针,这样外部就可以通过这个函数调用该类了。
class Singleton {
public:
Singleton* CreatObject() {
return new Singleton;
}
~Singleton() {
printf("Singleton析构函数");
};
private:
Singleton() {
printf("Singleton构造函数");
};
};
int main()
{
//Singleton S1;
system("pause");
return 0;
}
----但是这时必须先实例化对象才能调用CreatObject函数
把该函数设置为静态函数,静态函数的作用范围是全局整个文件,这样外部就可以调用了
#include
#include
#include
#include
#include
#include
using namespace std;
//只能产生一个实例
class Singleton {
public:
static Singleton* CreatObject() {
return new Singleton();
};
~Singleton() {
printf("Singleton析构函数");
};
private:
Singleton() {
printf("Singleton构造函数");
};
};
int main()
{
//Singleton S1;
Singleton* S1 = Singleton::CreatObject();
Singleton* S2 = Singleton::CreatObject();
system("pause");
return 0;
}
但是这样的话实际上并不能保证主函数调用时该类对象指针的唯一性。
设置一个私有化的静态对象指针,在外部初始化这个指针为空。在静态成员函数CreatObject函数中,如果该静态指针为空就创建对象指针,否则直接返回对象指针。这样就确保了在外部使用时,该对象的唯一性。
#include
#include
#include
#include
#include
#include
using namespace std;
//只能产生一个实例
class Singleton {
public:
static Singleton* CreatObject() {
if (m_Object == nullptr) {
m_Object = new Singleton();
}
return m_Object;
};
~Singleton() {
printf("Singleton析构函数");
};
private:
Singleton() {
printf("Singleton构造函数");
};
static Singleton* m_Object;
};
Singleton* Singleton::m_Object = nullptr;
int main()
{
//Singleton S1;
Singleton* S1 = Singleton::CreatObject();
Singleton* S2 = Singleton::CreatObject();
system("pause");
return 0;
}
这时候就只能创建一个对象--以上就是最简单的单例模式--但是这样做会造成内存泄漏--只构造了没有析构
可以使用delete手动释放--这样做的话太过于原始(需要自己手动释放),并且还不安全(不线程安全)
所以可以把该静态成员函数中的创建对象指针改为创建一个静态对象成员(生命周期是整个程序执行完),然后返回该成员的地址。这样也可以创建一个对象
#include
#include
#include
#include
#include
#include
using namespace std;
//只能产生一个实例
class Singleton {
public:
static Singleton* CreatObject() {
static Singleton obj;
return &obj;
};
~Singleton() {
printf("Singleton析构函数");
};
private:
Singleton() {
printf("Singleton构造函数");
};
//static Singleton* m_Object;
};
//Singleton* Singleton::m_Object = nullptr;
int main()
{
//Singleton S1;
Singleton* S1 = Singleton::CreatObject();
Singleton* S2 = Singleton::CreatObject();
system("pause");
return 0;
}
返回一个地址就需要用指针去接收,用户就有可能对该指针进行delete造成错误(由于不是new出来的,delete无效程序崩溃),所以直接静态成员函数返回一个引用更好。这样delete就会无效。
但是,需要用引用来接收才能是唯一创建一个实例(如果不使用引用来接收,直接使用对象指针接收,=号相当于一种拷贝构造函数,其是一种声明。所以可以产生第二个对象)。
所以现在又需要对拷贝构造函数进行私有化设置。或者直接对拷贝构造=delete,进行禁用。又或者把默认的运算符重载给禁用了。
以下是对拷贝构造函数进行私有化设置
#include
#include
#include
#include
#include
using namespace std;
class Singleton{
public:
~Singleton(){
printf("~Singleton() destruct");
}
static Singleton& CreateObject()
{
static Singleton obj;
return obj;
}
private:
Singleton(){
printf("Singleton() Construct");
}
//拷贝构造函数私有化,就不能在类外利用拷贝构造创建对象了
Singleton(Singleton& obj){
printf("Singleton(Singleton& obj) Construct");
}
};
int main(){
Singleton& p0bj1=Singleton::CreateObject();
//下面这句话是使用拷贝构造函数进行创建对象,要将拷贝构造函数私有化,这样就会利用拷贝构造创建第二个实例
Singleton p0bj2 = Singleton::CreateObject();
return 0;
}
下面是把默认的运算符重载给禁用了以及把拷贝构造=delete,进行禁用
#include
#include
#include
#include
#include
using namespace std;
class Singleton{
public:
~Singleton(){
printf("~Singleton() destruct");
}
static Singleton& CreateObject()
{
static Singleton obj;
return obj;
}
//禁用,不让编译器使用拷贝构造
Singleton(Singleton& obj) = delete;
//运算符重载禁用
Singleton &operator=(Singleton& obj) = delete;
private:
Singleton(){
printf("Singleton() Construct");
}
//拷贝构造函数私有化,就不能在类外利用拷贝构造创建对象了
//Singleton(Singleton& obj){
//printf("Singleton(Singleton& obj) Construct");
//}
};
int main(){
Singleton& p0bj1=Singleton::CreateObject();
//下面这句话是使用拷贝构造函数进行创建对象,要将拷贝构造函数私有化,这样就会利用拷贝构造创建第二个实例
Singleton p0bj2 = Singleton::CreateObject();
return 0;
}
1class single{
2private:
3 //私有静态指针变量指向唯一实例
4 static single *p;
5
6 //静态锁,是由于静态函数只能访问静态成员
7 static pthread_mutex_t lock;
8
9 //私有化构造函数
10 single(){
11 pthread_mutex_init(&lock, NULL);
12 }
13 ~single(){}
14
15public:
16 //公有静态方法获取实例
17 static single* getinstance();
18
19};
20
21pthread_mutex_t single::lock;
22
23single* single::p = NULL;
24single* single::getinstance(){
25 if (NULL == p){
26 pthread_mutex_lock(&lock);
27 if (NULL == p){
28 p = new single;
29 }
30 pthread_mutex_unlock(&lock);
31 }
32 return p;
33}
为什么要用双检测,只检测一次不行吗?
如果只检测一次,在每次调用获取实例的方法时,都需要加锁,这将严重影响程序性能。双层检测可以有效避免这种情况,仅在第一次创建单例的时候加锁,其他时候都不再符合NULL == p的情况,直接返回已创建好的实例。
前面的双检测锁模式,写起来不太优雅,《Effective C++》(Item 04)中的提出另一种更优雅的单例模式实现,使用函数内的局部静态对象,这种方法不用加锁和解锁操作。
这种方法就是上面将单例模式提到过的把该静态成员函数中的创建对象指针改为创建一个静态对象成员(生命周期是整个程序执行完),然后返回该成员的地址。
1class single{
2private:
3 single(){}
4 ~single(){}
5
6public:
7 static single* getinstance();
8
9};
10
11single* single::getinstance(){
12 static single obj;
13 return &obj;
14}
如果使用C++11之前的标准,还是需要加锁,C++11就不用加锁了
1class single{
2private:
3 static pthread_mutex_t lock;
4 single(){
5 pthread_mutex_init(&lock, NULL);
6 }
7 ~single(){}
8
9public:
10 static single* getinstance();
11
12};
13pthread_mutex_t single::lock;
14single* single::getinstance(){
15 pthread_mutex_lock(&lock);
16 static single obj;
17 pthread_mutex_unlock(&lock);
18 return &obj;
19}
饿汉模式不需要用锁,就可以实现线程安全。原因在于,在程序运行时就定义了对象,并对其初始化。之后,不管哪个线程调用成员函数getinstance(),都只不过是返回一个对象的指针而已。所以是线程安全的,不需要在获取实例的成员函数中加锁。
1class single{
2private:
3 static single* p;
4 single(){}
5 ~single(){}
6
7public:
8 static single* getinstance();
9
10};
11single* single::p = new single();
12single* single::getinstance(){
13 return p;
14}
15
16//测试方法
17int main(){
18
19 single *p1 = single::getinstance();
20 single *p2 = single::getinstance();
21
22 if (p1 == p2)
23 cout << "same" << endl;
24
25 system("pause");
26 return 0;
27}
项目中完成的是懒汉模式,即有日志需求的时候,才会创造出一个log实例。Log::get_instance()是一个静态函数,可以通过类命名域直接调用来产生出单例。这个函数的返回值就是这个单例的指针,于是可以通过指向来完成对于日志写函数的调用。
一些思考
C++类中一个函数调用另一个函数
c++面向对象-类的函数调用(一)_类函数调用_精神小伙_Steven的博客-CSDN博客
block_queue.h
代码实际上是对queue的实现(就是里面每个函数都加了锁),当然也可以用stl的queue实现,这个queue的底层是一个数组,最大值为1000。里面存放即将刷入文件的日志内容
循环数组实现的阻塞队列,m_back = (m_back + 1) % m_max_size;
线程安全,每个操作前都要先加互斥锁,操作完后,再解锁
#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H
#include
#include
#include
#include
#include "../lock/locker.h"
using namespace std;
template
class block_queue
{
public:
block_queue(int max_size = 1000)
{
if (max_size <= 0)
{
exit(-1);
}
m_max_size = max_size;
//阻塞队列的string数组
m_array = new T[max_size];
m_size = 0;
m_front = -1;
m_back = -1;
}
void clear()
{
m_mutex.lock();
m_size = 0;
m_front = -1;
m_back = -1;
m_mutex.unlock();
}
~block_queue()
{
m_mutex.lock();
if (m_array != NULL)
delete [] m_array;
m_mutex.unlock();
}
//判断队列是否满了
bool full()
{
m_mutex.lock();
if (m_size >= m_max_size)
{
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//判断队列是否为空
bool empty()
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return true;
}
m_mutex.unlock();
return false;
}
//返回队首元素
bool front(T &value)
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_front];
m_mutex.unlock();
return true;
}
//返回队尾元素
bool back(T &value)
{
m_mutex.lock();
if (0 == m_size)
{
m_mutex.unlock();
return false;
}
value = m_array[m_back];
m_mutex.unlock();
return true;
}
int size()
{
int tmp = 0;
m_mutex.lock();
tmp = m_size;
m_mutex.unlock();
return tmp;
}
int max_size()
{
int tmp = 0;
m_mutex.lock();
tmp = m_max_size;
m_mutex.unlock();
return tmp;
}
//往队列添加元素,需要将所有使用队列的线程先唤醒
//当有元素push进队列,相当于生产者生产了一个元素
//若当前没有线程等待条件变量,则唤醒无意义
bool push(const T &item)
{
m_mutex.lock();
if (m_size >= m_max_size)
{
m_cond.broadcast();
m_mutex.unlock();
return false;
}
m_back = (m_back + 1) % m_max_size;
m_array[m_back] = item;
m_size++;
m_cond.broadcast();
m_mutex.unlock();
return true;
}
//pop时,如果当前队列没有元素,将会等待条件变量
bool pop(T &item)
{
m_mutex.lock();
while (m_size <= 0)
{
if (!m_cond.wait(m_mutex.get()))
{
m_mutex.unlock();
return false;
}
}
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}
//增加了超时处理
bool pop(T &item, int ms_timeout)
{
struct timespec t = {0, 0};
struct timeval now = {0, 0};
gettimeofday(&now, NULL);
m_mutex.lock();
if (m_size <= 0)
{
t.tv_sec = now.tv_sec + ms_timeout / 1000;
t.tv_nsec = (ms_timeout % 1000) * 1000;
if (!m_cond.timewait(m_mutex.get(), t))
{
m_mutex.unlock();
return false;
}
}
if (m_size <= 0)
{
m_mutex.unlock();
return false;
}
m_front = (m_front + 1) % m_max_size;
item = m_array[m_front];
m_size--;
m_mutex.unlock();
return true;
}
private:
locker m_mutex;
cond m_cond;
T *m_array;
int m_size;
int m_max_size;
int m_front;
int m_back;
};
#endif
写日志线程,这一部分也比较简单就是新建一个线程,这个线程不断while当日志队列有日志就从里面取出来写到文件去,这个过程记得加锁就行。
项目默认的是同步写,这个可以测试性能。
#include
#include
#include
#include
#include "log.h"
#include
using namespace std;
Log::Log()
{
m_count = 0;
m_is_async = false;
}
Log::~Log()
{
if (m_fp != NULL)
{
fclose(m_fp);
}
}
//异步需要设置阻塞队列的长度,同步不需要设置
bool Log::init(const char *file_name, int close_log, int log_buf_size, int split_lines, int max_queue_size)
{
//如果设置了max_queue_size,则设置为异步
if (max_queue_size >= 1)
{
m_is_async = true;
m_log_queue = new block_queue(max_queue_size);
pthread_t tid;
//flush_log_thread为回调函数,这里表示创建线程异步写日志
cout << tid << endl;
pthread_create(&tid, NULL, flush_log_thread, NULL);
}
m_close_log = close_log;
m_log_buf_size = log_buf_size;
m_buf = new char[m_log_buf_size];
memset(m_buf, '\0', m_log_buf_size);
m_split_lines = split_lines;
time_t t = time(NULL);
struct tm *sys_tm = localtime(&t);
struct tm my_tm = *sys_tm;
const char *p = strrchr(file_name, '/');
char log_full_name[256] = {0};
if (p == NULL)
{
snprintf(log_full_name, 255, "%d_%02d_%02d_%s", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, file_name);
}
else
{
strcpy(log_name, p + 1);
strncpy(dir_name, file_name, p - file_name + 1);
snprintf(log_full_name, 255, "%s%d_%02d_%02d_%s", dir_name, my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday, log_name);
}
m_today = my_tm.tm_mday;
m_fp = fopen(log_full_name, "a");
if (m_fp == NULL)
{
return false;
}
return true;
}
void Log::write_log(int level, const char *format, ...)
{
struct timeval now = {0, 0};
gettimeofday(&now, NULL);
time_t t = now.tv_sec;
struct tm *sys_tm = localtime(&t);
struct tm my_tm = *sys_tm;
char s[16] = {0};
switch (level)
{
case 0:
strcpy(s, "[debug]:");
break;
case 1:
strcpy(s, "[info]:");
break;
case 2:
strcpy(s, "[warn]:");
break;
case 3:
strcpy(s, "[erro]:");
break;
default:
strcpy(s, "[info]:");
break;
}
//写入一个log,对m_count++, m_split_lines最大行数
m_mutex.lock();
m_count++;
if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) //everyday log
{
char new_log[256] = {0};
fflush(m_fp);
fclose(m_fp);
char tail[16] = {0};
snprintf(tail, 16, "%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);
if (m_today != my_tm.tm_mday)
{
snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
m_today = my_tm.tm_mday;
m_count = 0;
}
else
{
snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
}
m_fp = fopen(new_log, "a");
}
m_mutex.unlock();
va_list valst;
va_start(valst, format);
string log_str;
m_mutex.lock();
//写入的具体时间内容格式
int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
int m = vsnprintf(m_buf + n, m_log_buf_size - 1, format, valst);
m_buf[n + m] = '\n';
m_buf[n + m + 1] = '\0';
log_str = m_buf;
m_mutex.unlock();
if (m_is_async && !m_log_queue->full())
{
m_log_queue->push(log_str);
}
else
{
m_mutex.lock();
fputs(log_str.c_str(), m_fp);
m_mutex.unlock();
}
va_end(valst);
//cout << "完成缓存区写入\n";
//cout << log_str << "\n";
}
void Log::flush(void)
{
m_mutex.lock();
//强制刷新写入流缓冲区
fflush(m_fp);
m_mutex.unlock();
}
类创建的对象使用什么来接收?
C++死磕基础之指针篇(二)--对象指针简介_c++ 类对象指针 包含哪些信息?_菠萝印象威的博客-CSDN博客
使用对象指针
为什么静态变量要类内定义,类外初始化?
为什么static成员变量一定要在类外初始化?_静态成员类外初始化_sevencheng798的博客-CSDN博客
可以保证static成员变量只被定义一次。(所有对象共享同一份数据,如果在类内初始化了,该类再一次创建对象将变成初始值,会修改其他对象对该静态变量操作)
为什么静态常量成员可以直接类内赋值?
static const int 因为静态常量成员是常量,不允许修改,这种情况下是否所有的对象共享同一份数据已经不重要了,因为都是同一常量数据,而且如果不允许直接赋值,那么这个常量就没有意义了。