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



    前言

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

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

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


    Locker类


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

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

    Locker类主要使用的API都是include里的,这部分的API都在

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

    RAII

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

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

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

    代码

    1. /*author:Benxaomin
    2. *date:20240219
    3. * */
    4. #ifndef LOCKER_H
    5. #define LOCKER_H
    6. #include
    7. #include
    8. #include
    9. using namespace std;
    10. class sem{
    11. public:
    12. sem() {
    13. if (sem_init(&m_sem, 0, 0) != 0) {
    14. throw exception();
    15. }
    16. }
    17. sem(int val) {
    18. if (sem_init(&m_sem, 0, val) != 0) {
    19. throw exception();
    20. }
    21. }
    22. ~sem() {
    23. sem_destroy(&m_sem);
    24. }
    25. bool wait() {
    26. return sem_wait(&m_sem) == 0;
    27. }
    28. bool post() {
    29. return sem_post(&m_sem) == 0;
    30. }
    31. private:
    32. sem_t m_sem;
    33. };
    34. class locker{
    35. public:
    36. locker() {
    37. if (pthread_mutex_init(&m_mutex, NULL) != 0) {
    38. throw exception();
    39. }
    40. }
    41. ~locker() {
    42. pthread_mutex_destroy(&m_mutex);
    43. }
    44. bool lock() {
    45. return pthread_mutex_lock(&m_mutex) == 0;
    46. }
    47. bool unlock() {
    48. return pthread_mutex_unlock(&m_mutex) == 0;
    49. }
    50. /*获得互斥锁的指针*/
    51. pthread_mutex_t *get() {
    52. return &m_mutex;
    53. }
    54. private:
    55. pthread_mutex_t m_mutex;
    56. };
    57. class cond{
    58. public:
    59. cond() {
    60. if (pthread_cond_init(&m_cond, NULL) != 0) {
    61. throw exception();
    62. }
    63. }
    64. ~cond() {
    65. pthread_cond_destroy(&m_cond);
    66. }
    67. bool wait(pthread_mutex_t *m_mutex) {
    68. return pthread_cond_wait(&m_cond, m_mutex) == 0;
    69. }
    70. bool timewait(pthread_mutex_t *m_mutex, struct timespec *m_abstime) {
    71. return pthread_cond_timedwait(&m_cond, m_mutex, m_abstime) == 0;
    72. }
    73. bool signal() {
    74. return pthread_cond_signal(&m_cond) == 0;
    75. }
    76. bool broadcast() {
    77. return pthread_cond_broadcast(&m_cond) == 0;
    78. }
    79. private:
    80. pthread_cond_t m_cond;
    81. };
    82. #endif


    LOG类

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

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

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

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


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

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

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


    循环队列代码

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

    1. /*
    2. author:Benxaomin
    3. date:20240219
    4. */
    5. #ifndef BLOCK_QUEUE_H
    6. #define BLOCK_QUEUE_H
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include"../lock/locker.h"
    12. using namespace std;
    13. template<class T>
    14. class block_queue{
    15. public:
    16. /*初始化阻塞队列*/
    17. block_queue(int max_size) {
    18. if (max_size <= 0) {
    19. exit(-1);
    20. }
    21. m_max_size = max_size;
    22. T* m_array = new T[max_size];
    23. m_size = 0;
    24. m_front = -1;
    25. m_back = -1;
    26. }
    27. /*删除new出的T数组*/
    28. ~block_queue() {
    29. m_mutex.lock();
    30. if (m_array != NULL) {
    31. delete []m_array;
    32. }
    33. m_mutex.unlock();
    34. }
    35. /*清空队列*/
    36. void clear() {
    37. m_mutex.lock();
    38. m_size = 0;
    39. m_front = -1;
    40. m_back = -1;
    41. m_mutex.unlock();
    42. }
    43. /*判断队列是否已满*/
    44. bool full() {
    45. m_mutex.lock();
    46. if (m_size >= m_max_size) {
    47. m_mutex.unlock();
    48. return true;
    49. }
    50. m_mutex.unlock();
    51. return false;
    52. }
    53. /*判断队列是否为空*/
    54. bool empty() {
    55. m_mutex.lock();
    56. if (m_size == 0) {
    57. m_mutex.unlock();
    58. return true;
    59. }
    60. m_mutex.unlock();
    61. return false;
    62. }
    63. /*获得队首元素*/
    64. bool front(T &value) {
    65. m_mutex.lock();
    66. /*注意下面的if判断不能用empty,因为empty函数也有加锁操作,加两次锁会导致死锁*/
    67. if (size == 0) {
    68. m_mutex.unlock();
    69. return false;
    70. }
    71. //TODO:个人感觉这行逻辑出错,后面部分是原代码 value = m_array[m_front];
    72. value = m_array[(m_front + 1) % m_max_size];
    73. m_mutex.unlock();
    74. return true;
    75. }
    76. /*获得队尾元素*/
    77. bool back(T& value) {
    78. m_mutex.lock();
    79. if (size == 0) {
    80. m_mutex.unlock();
    81. return false;
    82. }
    83. value = m_array[m_back];
    84. m_mutex.unlock();
    85. return true;
    86. }
    87. int size() {
    88. int tmp = 0;
    89. m_mutex.lock();
    90. tmp = m_size;
    91. m_mutex.unlock();
    92. return tmp;
    93. }
    94. int max_size() {
    95. int tmp = 0;
    96. m_mutex.lock();
    97. tmp = m_max_size;
    98. m_mutex.unlock();
    99. return tmp;
    100. }
    101. /*往队列中添加元素前需要先将所有使用队列的线程先唤醒*/
    102. /*阻塞队列封装了生产者消费者模型,调用push的是生产者,也就是工作线程*/
    103. bool push(T& item) {
    104. m_mutex.lock();
    105. if (m_size >= m_max_size) {
    106. cond.broadcast();
    107. m_mutex.unlock();
    108. return false;
    109. }
    110. m_back = (m_back + 1) % m_max_size;
    111. m_array[m_back] = item;
    112. m_size++;
    113. cond.broadcast();
    114. m_mutex.unlock();
    115. return true;
    116. }
    117. /*调用pop的是消费者,负责把生产者的内容写入文件*/
    118. bool pop(T& item) {
    119. m_mutex.lock();
    120. while (m_size <= 0) {
    121. if (!cond.wait(m_mutex.get())) {
    122. m_mutex.unlock();
    123. return false;
    124. }
    125. }
    126. m_front = (m_front + 1) % m_max_size;
    127. item = m_array[m_front];
    128. m_size--;
    129. m_mutex.unlock();
    130. return true;
    131. }
    132. bool pop(T& item,int ms_timeout) {
    133. struct timespec t = {0,0};//tv_sec :从1970年1月1日 0点到现在的秒数 tv_nsec:tv_sec后面的纳秒数
    134. struct timeval now = {0,0};//tv_sec: 从1970年1月1日 0点到现在的秒数 tu_usec:tv_sec后面的微妙数
    135. gettimeofday(&now,nullptr);
    136. m_mutex.lock();
    137. if (m_size <= 0) {
    138. t.tv_sec = now.tv_sec + ms_timeout/1000;
    139. t.tv_nsec = (ms_timeout % 1000) * 1000;
    140. if (!m_cond.timewait(m_mutex.get(), t)) {
    141. m_mutex.unlock();
    142. return false;
    143. }
    144. }
    145. //TODO:这一块代码的意义不知道在哪里,留着DEBUG
    146. if (m_size <= 0) {
    147. m_mutex.unlock();
    148. return false;
    149. }
    150. m_front = (m_front + 1) % m_max_size;
    151. item = m_array[m_front];
    152. m_size--;
    153. m_mutex.unlock();
    154. return true;
    155. }
    156. private:
    157. locker m_mutex;
    158. cond m_cond;
    159. T* m_array;
    160. int m_max_size;
    161. int m_size;
    162. int m_front;
    163. int m_back;
    164. };
    165. #endif


    log

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


    单例模式:

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

    实现思路:

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

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

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


    单例模式代码

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

    Log初始化

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

    1. /*数据类型*/
    2. FILE
    3. time_t
    4. struct tm//结构体
    5. va_list
    6. /*API*/
    7. void* memset(void* ptr, int value, size_t num);
    8. char* strrchr(const char* str, int character);
    9. struct tm* localtime(const time_t* timer);
    10. FILE* fopen(const char* filename, const char* mode);
    11. int snprintf(char* str, size_t size, const char* format, ...);
    12. int vsnprintf(char* str, size_t size, const char* format, va_list args);
    13. void va_start(va_list ap, last_arg);

    init()代码

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

    write_log()

    Log分级:

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

    Log分文件:

    当新的一天时创建新文件

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

    write_log()代码

    1. public:
    2. void flush(void) {
    3. m_mutex.lock();
    4. fflush(m_fp);
    5. m_mutex.unlock();
    6. }
    7. void write_log(int level, const char *format, ...);
    8. //类外:
    9. #define LOG_DEBUG(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(0, format, ##__VA_ARGS__); Log::get_instance()->flush();}
    10. #define LOG_INFO(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(1, format, ##__VA_ARGS__); Log::get_instance()->flush();}
    11. #define LOG_WARN(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(2, format, ##__VA_ARGS__); Log::get_instance()->flush();}
    12. #define LOG_ERROR(format, ...) if(0 == m_close_log) {Log::get_instance()->write_log(3, format, ##__VA_ARGS__); Log::get_instance()->flush();}
    1. void Log::write_log(int level, const char* format, ...) {
    2. struct timeval now = {0,0};
    3. gettimeofday(&now, NULL);
    4. time_t t = now.tv_sec;
    5. struct tm *sys_tm = localtime(&t);
    6. struct tm my_tm = *sys_tm;
    7. char s[16] = {0};
    8. switch (level)
    9. {
    10. case 0:
    11. strcpy(s,"[debug]");
    12. break;
    13. case 1:
    14. strcpy(s,"[info]");
    15. break;
    16. case 2:
    17. strcpy(s,"[warn]");
    18. break;
    19. case 3:
    20. strcpy(s,"[error]");
    21. break;
    22. default:
    23. strcpy(s,"[info]");
    24. break;
    25. }
    26. /*开始写入*/
    27. m_mutex.lock();
    28. m_count++;
    29. /*如果是新的一天了,或者日志行数到上限了,创建新日志*/
    30. if (m_today != my_tm.tm_mday || m_count % m_split_lines == 0) {
    31. char new_log[256] = {0};
    32. flush();
    33. fclose(m_fp);
    34. char tail[16] = {0};
    35. snprintf(tail, 16,"%d_%02d_%02d_", my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday);
    36. if (m_today != my_tm.tm_mday) {//新的一天
    37. snprintf(new_log, 255, "%s%s%s", dir_name, tail, log_name);
    38. m_today = my_tm.tm_mday;
    39. m_count = 0;
    40. } else {//日志写满了
    41. snprintf(new_log, 255, "%s%s%s.%lld", dir_name, tail, log_name, m_count / m_split_lines);
    42. }
    43. m_fp = fopen(new_log, "a");
    44. }
    45. m_mutex.unlock();
    46. /*可变参数定义初始化,在vsprintf时使用,作用:输入具体的日志内容*/
    47. va_list valst;
    48. va_start(valst, format);
    49. string log_str;
    50. m_mutex.lock();
    51. /*写每一行的开头格式*/
    52. int n = snprintf(m_buf, 48, "%d-%02d-%02d %02d:%02d:%02d.%06ld %s ",
    53. my_tm.tm_year + 1900, my_tm.tm_mon + 1, my_tm.tm_mday,
    54. my_tm.tm_hour, my_tm.tm_min, my_tm.tm_sec, now.tv_usec, s);
    55. int m = vsnprintf(m_buf + n, m_log_buf_size - n - 1, format, valst);
    56. /*加入换行和空格*/
    57. m_buf[n + m] = '\n';
    58. m_buf[n + m + 1] = '\0';
    59. log_str = m_buf;
    60. m_mutex.unlock();
    61. /*决定是异步写还是同步写*/
    62. if (m_is_async && !m_log_queue->full()) {
    63. m_log_queue->push(log_str);
    64. } else {
    65. m_mutex.lock();
    66. fputs(log_str.c_str(), m_fp);
    67. m_mutex.unlock();
    68. }
    69. va_end(valst);
    70. }

  • 相关阅读:
    Linux Vim批量注释和自定义注释
    张高兴的 MicroPython 入门指南:(一)环境配置、Blink、部署
    Windows错误处理
    球场输了?卡塔尔背地里可能赢麻了!
    在 Windows 上开发.NET MAUI 应用_1.安装开发环境
    react HashRouter 与 BrowserRouter 的区别及使用场景
    Python基于Django的汽车销售网站
    SpringBoot 基础篇——基于SpringBoot实现ssm/ssmp整合
    系统(层次)聚类
    Python Number degrees()实例讲解
  • 原文地址:https://blog.csdn.net/qq_52313711/article/details/136195089