【C++实现HTTP服务器项目记录】日志系统

一、单例模式

1. 概念

 - 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

2. 实现思路

 - 私有化该类的构造函数,以防止外界创建单例类的对象。
 - 使用类的私有静态指针变量指向该类的唯一实例。
 - 用一个公有的静态方法获取该实例。

3. 实现方式

 - 懒汉模式:在第一次使用该对象时进行初始化。
 - 饿汉模式:在程序运行时立即初始化。

4. 实现代码

(1)懒汉模式

#include <memory>
#include <mutex>
using namespace std;
class Single
{
public:
	Single(Single&) = delete;
	Single& operator=(const Single&) = delete;

	static Single* getInstance()
	{
		if (ptr_ == nullptr)
		{
			lock_guard<mutex> locker(mutex_);
			if (ptr_ == nullptr)
			{
				ptr_ = new Single;
			}
		}
		return ptr_;
	}

private:
	~Single() { cout << "~Single()" << endl; }
	Single() { cout << "Single()" << endl; }
	static Single* ptr_;
	static mutex mutex_;
};

Single* Single::ptr_ = nullptr;
mutex Single::mutex_;
// 双检测锁模式不够优雅,C++11编译器保证函数内的局部静态对象的线程安全性。
class Single 
{
private:
	Single() {}
	~Single() {}
public:
	static Single* getinstance();
};

Single* Single::getinstance()
{
	static Single obj;
	return &obj;
}

(2)饿汉模式

// 在程序运行时就定义并对其初始化,故不需要锁就可以实现线程安全。
class Single {
private:
	Single() {}
	~Single() {}
	static Single* p;
public:
	static Single* getinstance();
};

Single* Single::p = new Single();

Single* Single::getinstance() {
	return p;
}

二、生产者-消费者模型

1. 概念

若干个生产者线程,产生任务放入到任务队列中,任务队列满了就阻塞,不满的时候就工作。
若干个消费者线程,将任务从任务队列取出处理,任务队列中有任务就工作,没有任务就阻塞。
生产者和消费者是互斥关系,两者对任务队列访问互斥。
同时生产者和消费者又是一个相互协作与同步的关系,只有生产者生产之后,消费者才能消费。

2. 示例

例:使用条件变量实现生产者和消费者模型,生产者有5个,往链表头部添加节点,消费者也有5个,删除链表头部的节点。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 链表的节点
struct Node
{
    int number;
    struct Node *next;
};

// 指向链表头结点的指针
struct Node *head = NULL;
// 条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁
pthread_mutex_t mutex;

// 生产者的回调函数
void *producer(void *arg)
{
    // 一直生产
    while (1)
    {
        /****** 加锁 *****/
        pthread_mutex_lock(&mutex);

        // 创建一个链表的新节点
        struct Node *pnew = (struct Node *)malloc(sizeof(struct Node));
        pnew->number = rand() % 1000;
        // 节点添加到链表的头部
        pnew->next = head;
        head = pnew;
        printf("++++++++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());

        /****** 解锁 *****/
        pthread_mutex_unlock(&mutex);

        // 生产了任务, 通知所有消费者消费
        pthread_cond_broadcast(&cond);

        // 生产慢一点
        sleep(rand() % 3);
    }
    return NULL;
}

// 消费者的回调函数
void *consumer(void *arg)
{
    while (1)
    {
        /****** 加锁 *****/
        pthread_mutex_lock(&mutex);

        // if(head == NULL) 必须循环等待条件变量!
        while (head == NULL)
        {
            pthread_cond_wait(&cond, &mutex);
        }
        // 删除链表的头结点
        struct Node *pnode = head;
        printf("--------consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
        head = pnode->next;
        free(pnode);

        /****** 解锁 *****/
        pthread_mutex_unlock(&mutex);

        sleep(rand() % 3);
    }
    return NULL;
}

int main()
{
    // 初始化条件变量
    pthread_cond_init(&cond, NULL);
    // 初始化互斥锁变量
    pthread_mutex_init(&mutex, NULL);

    // 创建5个生产者线程, 5个消费者线程
    pthread_t ptid[5];
    pthread_t ctid[5];
    for (int i = 0; i < 5; ++i)
    {
        pthread_create(&ptid[i], NULL, producer, NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_create(&ctid[i], NULL, consumer, NULL);
    }

    // 释放资源
    // 阻塞等待子线程退出
    for (int i = 0; i < 5; ++i)
    {
        pthread_join(ptid[i], NULL);
    }

    for (int i = 0; i < 5; ++i)
    {
        pthread_join(ctid[i], NULL);
    }

    // 销毁条件变量
    pthread_cond_destroy(&cond);
    // 销毁互斥锁
    pthread_mutex_destroy(&mutex);

    return 0;
}

三、相关函数

1. time()

- 头文件:#include <time.h>
- 函数原型:time_t time(time_t *timer) 
- 函数使用:time_t t = time(NULL);
- 函数用途:得到机器的日历时间,单位:秒。

2. localtime()

 - 头文件:#include <time.h>
 - 函数原型:struct tm *localtime(const time_t *timer)  
 - 函数使用:struct tm *sys_tm = localtime(&t);
 - 函数用途:返回一个以tm结构体表达的机器时间信息。
struct tm
{
    int tm_sec;   // 秒,取值区间为[0-59]
    int tm_min;   // 分,取值区间为[0-59]
    int tm_hour;  // 时,取值区间为[0-23]
    int tm_mday;  // 日,取值区间为[1,31]
    int tm_mon;   // 月,取值区间为[0,11]
    int tm_year;  // 年,其值等于实际年份减去1900
    int tm_wday;  // 星期,从星期日算起,取值区间为[0-6]
    int tm_yday;  // 从今年1月1日到目前的天数,取值区间为[0-365]
    int tm_isdst; // ?
};

3. gettimeofday()

- 头文件:#include <sys/time.h>
- 函数原型:int gettimeofday(struct timeval*tv, struct timezone *tz);
- 函数使用:gettimeofday(&now, NULL);
- 函数用途:把目前的时间用tv结构体返回,当地时区的信息则放到tz所指的结构体中。
struct timeval
{
    long tv_sec;  // 秒
    long tv_usec; // 微秒
} ;

4. fflush()

- 头文件:#include<stdio.h>
- 函数原型:int fflush(FILE* stream);
- 函数使用:fflush(m_fp);
- 函数用途:用于清空文件缓冲区,如果文件是以写的方式打开的,则把缓冲区内容写入文件。
		   也用于标准输入(stdin)和标准输出(stdout),用来清空标准输入输出缓冲区。

四、实现代码

1. log.h

// log.h
#ifndef LOG_H
#define LOG_H

#include <stdio.h>
#include <iostream>
#include <string>
#include <stdarg.h>
#include <pthread.h>
#include "block_queue.h"

using namespace std;

class Log
{
public:
    // 懒汉式单例模式
    // C++11编译器保证函数内的局部静态对象的线程安全性
    static Log *get_instance()
    {
        static Log instance;
        return &instance;
    }
    // 异步写入方式公有方法
    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);
    // 向日志文件写入具体内容
    void write_log(int level, const char *format, ...);
    // 强制刷新缓冲区
    void flush(void);

private:
    // 构造函数
    Log();
    // 析构函数
    virtual ~Log();
    // 异步写入日志方法
    void *async_write_log()
    {
        string single_log;
        // 从阻塞队列中取出一条日志
        while (m_log_queue->pop(single_log))
        {
            /****** 加锁 ******/
            m_mutex.lock();

            // 写进日志文件
            fputs(single_log.c_str(), m_fp);

            /****** 解锁 ******/
            m_mutex.unlock();
        }
    }

private:
    // 日志文件所在最内层文件夹
    char dir_name[128];
    // 日志文件名
    char log_name[128];
    // 日志最大行数
    int m_split_lines;
    // 日志缓冲区大小
    int m_log_buf_size;
    // 日志行数
    long long m_count;
    // 由于日志按天分类,记录当前是哪一天
    int m_today;
    // 打开日志的文件指针
    FILE *m_fp;
    // 日志缓冲区
    char *m_buf;
    // 阻塞队列
    block_queue<string> *m_log_queue;
    // 是否同步标志位
    bool m_is_async;
    // 互斥锁
    locker m_mutex;
    // 是否关闭日志
    int m_close_log;
};

// 这四个宏定义在其他文件中使用,主要用于不同类型的日志输出
// __VA_ARGS__是一个可变参数的宏
// __VA_ARGS__宏前面加上##的作用在于,当可变参数的个数为0时,会把前面多余的","去掉,否则会编译出错。

// DEBUG
#define LOG_DEBUG(format, ...)                                    \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(0, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }

// INFO
#define LOG_INFO(format, ...)                                     \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(1, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }

// WARN
#define LOG_WARN(format, ...)                                     \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(2, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }

// ERROR
#define LOG_ERROR(format, ...)                                    \
    if (0 == m_close_log)                                         \
    {                                                             \
        Log::get_instance()->write_log(3, format, ##__VA_ARGS__); \
        Log::get_instance()->flush();                             \
    }

#endif

2. block_queue.h

/*
 * 异步写入方式:将生产者-消费者模型封装为阻塞队列。将所写的日志内容先存入阻塞队列,写线程从阻塞队列中取出内容,写入日志。
 */
#ifndef BLOCK_QUEUE_H
#define BLOCK_QUEUE_H

#include <iostream>
#include <stdlib.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 = 1000)
    {
        if (max_size <= 0)
        {
            exit(-1);
        }

        // 队列最大长度
        this->m_max_size = max_size;
        // 循环数组实现队列
        this->m_array = new T[max_size];
        // 队列长度
        this->m_size = 0;
        // 队首下标
        this->m_front = -1;
        // 队尾下标
        this->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) // 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) // 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()
    {
        return m_max_size;
    }

    // 往队列添加元素
    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;
    }

    // 取出队头元素
    bool pop(T &item) // 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) // item为传出参数
    {
        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

3. log.cpp

// log.cpp
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdarg.h>
#include "log.h"
#include <pthread.h>
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) // file_name为日志文件路径
{
    // 若设置了阻塞队列的最大长度 --> 异步写入方式
    if (max_queue_size >= 1)
    {
        // 设置为异步写入方式
        this->m_is_async = true;
        // 创建阻塞队列
        this->m_log_queue = new block_queue<string>(max_queue_size);
        // 创建写线程来异步写日志
        pthread_t tid;
        pthread_create(&tid, NULL, flush_log_thread, NULL); // flush_log_thread为其回调函数
    }
    // 是否关闭日志
    this->m_close_log = close_log;
    // 日志缓冲区大小
    this->m_log_buf_size = log_buf_size;
    // 日志缓冲区
    this->m_buf = new char[this->m_log_buf_size];
    // 初始化日志缓冲区
    memset(this->m_buf, '\0', this->m_log_buf_size);
    // 日志最大行数
    this->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
    {
        // p指针从"/"的位置向后移动一个位置
        strcpy(this->log_name, p + 1);
        // p - file_name + 1相当于日志文件路径的"./"
        strncpy(this->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, this->log_name);
    }
    // 日志要按天分类,记录当前是哪一天
    this->m_today = my_tm.tm_mday;
    // 打开log的文件指针
    this->m_fp = fopen(log_full_name, "a");
    if (this->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;
    }

    // 以下实现按天分类,超行分文件功能
    /****** 加锁 ******/
    this->m_mutex.lock();

    // 日志行数+1
    this->m_count++;

    // 日志不是今天 或 写入的日志行数是最大行的倍数 --> 创建新日志文件
    if (this->m_today != my_tm.tm_mday || this->m_count % this->m_split_lines == 0)
    {
        // 新日志名
        char new_log[256] = {0};
        // 刷新缓冲区
        fflush(this->m_fp);
        // 关闭日志文件
        fclose(this->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 (this->m_today != my_tm.tm_mday)
        {
            // 路径年_月_日_文件名
            snprintf(new_log, 255, "%s%s%s", this->dir_name, tail, this->log_name);
            // 时间改成今天
            this->m_today = my_tm.tm_mday;
            // 重置日志行数
            this->m_count = 0;
        }
        // 日志行数超过最大行限制
        else
        {
            // count/max_lines为"页号"
            // 路径年_月_日_文件名.页号
            snprintf(new_log, 255, "%s%s%s.%lld", this->dir_name, tail, this->log_name, this->m_count / this->m_split_lines);
        }
        // 重新设置打开日志的文件指针
        this->m_fp = fopen(new_log, "a");
    }

    /****** 解锁 ******/
    this->m_mutex.unlock();

    // 将传入的format参数赋值给可变参数列表类型valst,便于格式化输出
    va_list valst;
    va_start(valst, format);

    string log_str;

    /****** 加锁 ******/
    m_mutex.lock();
    // 写一条日志
    // 年-月-日 时:分:秒.微秒 [日志等级]:
    int n = snprintf(this->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);

    // 例:LOG_INFO("%s", "adjust timer once");
    int m = vsnprintf(this->m_buf + n, this->m_log_buf_size - 1, format, valst);
    this->m_buf[n + m] = '\n';
    this->m_buf[n + m + 1] = '\0';
    log_str = this->m_buf;

    /****** 解锁 ******/
    this->m_mutex.unlock();

    // 异步写入日志
    if (m_is_async && !m_log_queue->full())
    {
        // 将日志信息加入阻塞队列
        this->m_log_queue->push(log_str);
    }
    // 同步写入日志
    else
    {
        /****** 加锁 ******/
        m_mutex.lock();

        fputs(log_str.c_str(), m_fp);

        /****** 解锁 ******/
        m_mutex.unlock();
    }

    va_end(valst);
}

// 例子:使用多个输出函数连续进行多次输出到控制台时,上一个数据还在输出缓冲区中时,输出函数就把下一个数据加入输出缓冲区,结果冲掉了原来的数据,出现输出错误。
void Log::flush(void)
{
    /****** 加锁 ******/
    m_mutex.lock();

    // 强制将缓冲区内的数据写入指定的文件
    fflush(m_fp);

    /****** 解锁 ******/
    m_mutex.unlock();
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值