C++ Webserver从零开始:代码书写(十)——完成Locker类和Log类封装

2024-02-20 23:20

本文主要是介绍C++ Webserver从零开始:代码书写(十)——完成Locker类和Log类封装,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


前言

这是我们正式开始写代码的第一章,经历了前面那么多的内容,我们终于可以上手写代码了。前面那么多基础知识,如果大家都看了,理解了更好。如果说看的一知半解也不用担心,基础知识是学不完的,而且如果不加以使用,那么你学的基础知识就会非常快的忘掉。只有将学到东西拿来用,才能真正地掌握。

但是基础知识又不能没有,不然写项目的过程中会非常痛苦,你会发现你基本每一行代码都不知道是什么意思,然后再去查回来再写,就非常容易掉进局部的细节里出不来,而无法纵观全局,领会项目的设计思想和代码结构。所以,如果到了这一章的小伙伴,你还完全不了解Webserver的话,我是十分建议你去把前面的内容稍微看一看。当然,如果你有足够的时间,那么我是十分推荐把游双学长的《Linux高性能服务器编程》通读一遍的,读过以后再来写代码就会轻松许多。

好了,话不多说,我们开始今天的内容。


Locker类


写一个项目,很多同学不知道具体从哪开始写起。我目前的经验是,我会进入项目的main函数里,然后一层一层地看它的include的依赖关系,直到找到最里面的那一层,也就是最底层,然后从最底层开始,一个文件一个文件地写。这样的好处是,你整个书写代码地过程会非常清晰,你会知道每一行代码的实现原理。缺点是,你可能会陷入不知道自己在写什么的困境中,也不知道自己写的东西具体起到了一个什么作用。所以,我们可以在整个项目全部写完之后,再来一遍自顶向下的梳理。这样就即明白了每行代码的原理,也了解了整个项目的整体结构。

按照这样的思路,我们来实现一下Locker类,Locker类是最底层的工具类,用以保证其他代码的同步。

Locker类主要使用的API都是include<semaphore.h>里的,这部分的API都在

C++ Webserver从零开始:基础知识(八)——多线程编程-CSDN博客

RAII

RALL:"Resource Acquisition is Initialization",即”资源获取即初始化“

在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制,当一个对象创建时会调用构造函数,当对象超出作用域时会自动调用析构函数。所以在RAII指导下,我们应该用类来管理资源,将资源和对象的生命周期绑定

RAII核心思想是将资源或者状态与对象的生命周期绑定,通过C++的语言机制,实现资源和状态的安全管理,智能指针是RAII最好的例子

代码

/*author:Benxaomin*date:20240219* */
#ifndef LOCKER_H
#define LOCKER_H#include<exception>
#include<pthread.h>
#include<semaphore.h>using namespace std;
class sem{
public:sem() {if (sem_init(&m_sem, 0, 0) != 0) {throw exception();}}sem(int val) {if (sem_init(&m_sem, 0, val) != 0) {throw exception();}}~sem() {sem_destroy(&m_sem);}bool wait() {return sem_wait(&m_sem) == 0;}bool post() {return sem_post(&m_sem) == 0;}
private:sem_t m_sem;
};class locker{
public:locker() {if (pthread_mutex_init(&m_mutex, NULL) != 0) {throw exception();}}~locker() {pthread_mutex_destroy(&m_mutex);}bool lock() {return pthread_mutex_lock(&m_mutex) == 0;}bool unlock() {return pthread_mutex_unlock(&m_mutex) == 0;}/*获得互斥锁的指针*/pthread_mutex_t *get() {return &m_mutex;}private:pthread_mutex_t m_mutex;
};class cond{
public:cond() {if (pthread_cond_init(&m_cond, NULL) != 0) {throw exception();}}~cond() {pthread_cond_destroy(&m_cond);}bool wait(pthread_mutex_t *m_mutex) {return pthread_cond_wait(&m_cond, m_mutex) == 0;}bool timewait(pthread_mutex_t *m_mutex, struct timespec *m_abstime) {return pthread_cond_timedwait(&m_cond, m_mutex, m_abstime) == 0;}bool signal() {return pthread_cond_signal(&m_cond) == 0;}bool broadcast() {return pthread_cond_broadcast(&m_cond) == 0;}
private:pthread_cond_t m_cond;
};#endif


LOG类

顾名思义,LOG类就是项目的日志系统。所谓日志,即由服务器自动创建,并记录运行状态,错误信息,访问数据的文件。

日志的实现有两种,一种是同步日志,一种是异步日志;

同步日志:日志写入函数与工作线程串行执行,由于涉及I/O操作,同步日志会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在访问峰值时,写日志可能会成为系统的瓶颈

异步日志:将工作线程所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志


在异步日志中,每个工作线程当有日志需要处理时,将所需写的内容所在内存加入一个阻塞队列,然后就不管了。而日志系统会单独分配一个写线程,不断地从阻塞队列中获得任务并写入日志文件中。

从上面地日志工作流程描述中我们可以发现,这是一个典型的生产者-消费者模型。其中工作线程时生产,写线程是消费者。

那么,生产者-消费者模型的临界区(缓冲区)是什么呢?在我们日志系统中,这个临界区就是一个队列。 在本项目中,我们使用循环队列来实现。


循环队列代码

因为循环队列代码大部分重复且简单,就不分文件编写了

/*
author:Benxaomin
date:20240219
*/#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H#include<iostream>
#include<stdio.h>
#include<pthread.h>
#include<sys/time.h>
#include"../lock/locker.h"using namespace std;template<class T>
class block_queue{
public:/*初始化阻塞队列*/block_queue(int max_size) {if (max_size <= 0) {exit(-1);}m_max_size = max_size;T* m_array = new T[max_size];m_size = 0;m_front = -1;m_back = -1;}/*删除new出的T数组*/~block_queue() {m_mutex.lock();if (m_array != NULL) {delete []m_array;}m_mutex.unlock();}/*清空队列*/void clear() {m_mutex.lock();m_size = 0;m_front = -1;m_back = -1;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 (m_size == 0) {m_mutex.unlock();return true;}m_mutex.unlock();return false;}/*获得队首元素*/bool front(T &value) {m_mutex.lock();/*注意下面的if判断不能用empty,因为empty函数也有加锁操作,加两次锁会导致死锁*/if (size == 0) {m_mutex.unlock();return false;}//TODO:个人感觉这行逻辑出错,后面部分是原代码  value = m_array[m_front];value = m_array[(m_front + 1) % m_max_size];m_mutex.unlock();return true;}/*获得队尾元素*/bool back(T& value) {m_mutex.lock();if (size == 0) {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(T& item) {m_mutex.lock();if (m_size >= m_max_size) {cond.broadcast();m_mutex.unlock();return false;}m_back = (m_back + 1) % m_max_size;m_array[m_back] = item;m_size++;cond.broadcast();m_mutex.unlock();return true;}/*调用pop的是消费者,负责把生产者的内容写入文件*/bool pop(T& item) {m_mutex.lock();while (m_size <= 0) {if (!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};//tv_sec :从1970年1月1日 0点到现在的秒数 tv_nsec:tv_sec后面的纳秒数struct timeval now = {0,0};//tv_sec: 从1970年1月1日 0点到现在的秒数 tu_usec:tv_sec后面的微妙数gettimeofday(&now,nullptr);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;}}//TODO:这一块代码的意义不知道在哪里,留着DEBUGif (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_max_size;int m_size;int m_front;int m_back;
};#endif


log

接下来我们可以开始日志代码的书写,解释一下,为了循序渐进地进行代码书写和思考,我不会把整个LOG文件的代码全部放上来,而是分知识点一部分一部分写,这样的话读者读起来会更加清楚,写起来也会有顺序而不是无头苍蝇。但缺点是,写完之后大家要自己把代码整合到一个文件中。为了提供参考,我应该会把整个项目的文件传到github。如果你能感受到我的良苦用心,可以给我点个关注当支持~


单例模式:

单例模式是最常用的设计模式之一,单例模式保证了一个类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

实现思路:

  1. 私有化构造函数
  2. 使用类的私有静态指针变量指向类的唯一实例
  3. 创建一个共有静态方法获取该实例

单例模式也分为两种,一种是懒汉模式:顾名思义,懒汉模式非常懒,当没有人用它的时候它就不初始化,只有被第一次使用时才去初始化;另一种是饿汉模式:与懒汉模式相反,程序运行时就立刻创建实例进行初始化。

经典的懒汉模式一般要使用双检测锁。但C++11之后,可以使用静态局部变量初始化,就不再需要锁,编译器会负责线程安全的问题。(非常建议大家看一看C++11之前的单例模式的代码书写,学习思路)


单例模式代码

#ifndef LOG_H
#define LOG_H#include<stdio.h>
using namespace std;class Log{
public:
/*日志单例模式2:创建一个共有静态方法获得实例,并用指针返回*/
static Log *get_instance() {static Log instance;//C++11以后懒汉模式无需加锁,编译器会保证局部静态变量的线程安全return &instance;
}private:
/*日志单例模式1:私有化构造函数,确保外界无法创建新实例*/
Log();
~Log();private:
FILE *m_fp;//打开log的文件指针
long long m_count = 0;//日志行数记录
bool m_is_async;//是否是异步};
#endif
#include"log.h"
using namespace std;Log::Log() {m_count = 0;m_is_async = false;
}Log::~Log() {if (m_fp != NULL) {fclose(m_fp);}
}

Log初始化

初始化部分代码没有什么很关键的知识点,但是会有一些比较新的API,我就一次性挂出来了,大家自行搜索学习吧,放入文章里太臃肿了。

/*数据类型*/
FILE
time_t
struct tm//结构体
va_list
/*API*/
void* memset(void* ptr, int value, size_t num);
char* strrchr(const char* str, int character);
struct tm* localtime(const time_t* timer);
FILE* fopen(const char* filename, const char* mode);
int snprintf(char* str, size_t size, const char* format, ...);
int vsnprintf(char* str, size_t size, const char* format, va_list args);
void va_start(va_list ap, last_arg);

init()代码

public:
static void *flush_log_thread(void* args) {Log::get_instance()->async_write_log();}
bool init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0);private:
void* async_write_log() {string single_log;/*循环从阻塞队列里获取资源*/while (m_log_queue->pop(single_log)) {m_mutex.lock();fputs(single_log.c_str(), m_fp);//将c_str()输出到m_fp指向的文件中m_mutex.unlock();}}
private:FILE *m_fp;//打开log的文件指针long long m_count = 0;//日志行数记录bool m_is_async;//是否是异步block_queue<string> *m_log_queue;//阻塞队列int m_close_log;//关闭日志int m_log_buf_size;//日志缓冲区大小char *m_buf;//缓冲区int m_split_lines;//日志最大行数int m_today;//日志按天分类,记录当前是哪一天char log_name[128];//log文件名char dir_name[128];//地址名locker m_mutex;
bool Log::init(const char *file_name, int close_log, int log_buf_size = 8192, int split_lines = 5000000, int max_queue_size = 0) {if (max_queue_size >= 1) {m_is_async = true;//异步写入m_log_queue = new block_queue<string>(max_queue_size);//创建阻塞队列/*创建一个新线程,执行异步写入文件函数*/pthread_t tid;pthread_create(&tid, NULL, flush_log_thread, NULL);}/*初始化各值*/m_close_log = close_log;m_split_lines = split_lines;m_log_buf_size = log_buf_size;m_buf = new char[m_log_buf_size];memset(m_buf, '\0', m_log_buf_size);/*创建strcut tm变量接收当下时间*/time_t t = time(NULL);struct tm *sys_tm = localtime(&t);struct tm my_tm = *sys_tm;/*在filename里面查找'/',未找到返回 nullptr,找到返回最后一个的位置的指针*/const char *p = strrchr(file_name, '/');char log_full_name[256] = {0};//创建一个局部缓冲区对文件名命名/*下面是命名规则代码:日志文件命名为:年_月_日_文件名*/if (p == nullptr) {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 == nullptr) {return false;}return true;
}

write_log()

Log分级:

  • Debug,调试代码时的输出,在系统实际运行时,一般不使用。
  • Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。
  • Info,报告系统当前的状态,当前执行的流程或接收的信息等。
  • Erro,输出系统的错误信息

Log分文件:

当新的一天时创建新文件

当原文件日志写满时创建新文件

write_log()代码

public:
void flush(void) {m_mutex.lock();fflush(m_fp);m_mutex.unlock();}
void write_log(int level, const char *format, ...);//类外:
#define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
#define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}
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,"[error]");break;default:strcpy(s,"[info]");break;}/*开始写入*/m_mutex.lock();m_count++;/*如果是新的一天了,或者日志行数到上限了,创建新日志*/if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) {char new_log[256] = {0};flush();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();/*可变参数定义初始化,在vsprintf时使用,作用:输入具体的日志内容*/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 - n - 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);
}

这篇关于C++ Webserver从零开始:代码书写(十)——完成Locker类和Log类封装的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/729867

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

内核启动时减少log的方式

内核引导选项 内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般在前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"_

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能