手写C++muduo库(Cmake,nocopyable,logger,TimeStamp,InetAddress)

1,087 阅读6分钟

文件夹muduo-master>muduo下面有base(放公共的网络文件)和net(存放和网络相关的,比如poller是具体的多路事件分发器),我们写的时候就不这么复杂了,主要窥探的是它的网络模块。

我们创建的项目工程名:mymuduo,把所有的代码都放到这里面。

1. 构建项目编译CMake文件

我们这是一个网络库,并不是一个可以直接执行的一个应用服务,所以我们最终是把它编译成动态库,Muduo库本身是编译成静态库的,muduo库的安装真的很难,因为Muduo库用了boost库里面的很多东西,而我们在写的时候就全部脱离boost库了,用c++11把现有的muduo库里和boost库相关的都干掉,用c++本身来完成,使用muduo库直接编译成so库和头文件提供给用户使用

cmake_minimum_required(VERSION 2.5) #要求当前系统环境必须装的cmake环境>=2.5,才可以编译当前的项目
project(mymuduo)    #启动mymuduo

# cmake => makefile   make
# mymuduo最终编译成so动态库,设置动态库路径,放在根目录的lib文件夹下面
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
# 设置调试信息 以及 启动C++11语言标准
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11 -fPIC")    # 有些人本地的编译环境gcc,g++版本如果不是非常新的话,默认C++的语法没有打开,应该再加个-std=c++11

# 定义参与编译的源代码文件,把当前根目录下的名字源文件组合起来放在变量SRC_LIST里面
aux_source_directory(. SRC_LIST)
# 编译生成动态库mymuduo
add_library(mymuduo SHARED ${SRC_LIST})

生成libmuduo.so

2. noncopyable代码

不管是用接口类TcpServer编写服务器还是用TcpClient编写客户端程序,还是在看源码很多相关的类eventloop,socked,acceptor,channel,connection,可以发现每一个类都是从noncopyable继承而来。

image.png noncopy 不可复制的意思
我们进去看看究竟

image.png 把拷贝构造函数和赋值函数都delete掉了,默认的构造函数和析构函数
TCPServer的对象可以创建,因为派生类对象的创建会调用基类的构造函数,基类的构造和析构是保护的,派生类是可以访问的。
基类的拷贝构造函数和赋值函数直接delete掉了,那当我们对TCPServer的对象进行拷贝构造和赋值的时候,肯定要先调用基类部分的拷贝构造和赋值,然后才是派生类的拷贝构造和赋值,但是基类的拷贝构造和赋值已经删掉了,所以说,从noncopyable继承而来的好处是直接让其派生类都不可拷贝构造和赋值。 这个写法我们要学习啊!!!
有些人会觉得搞得这么麻烦干什么,也可以不从noncopyable继承而来,可以把TcpServer的拷贝构造和赋值在这里面delete掉,当然可以,但是这是一个大的项目,里面有100个类,让这么多类都不能做拷贝构造和赋值,要写100次把拷贝构造和赋值delete掉吗?这样的设计非常丑陋。

重写noncopyable.h

#pragma once   // 防止头文件被重复包含

/*
noncopyable被继承以后,派生类对象可以正常的构造和析构,
但派生类对象无法进行拷贝构造和赋值操作
*/
class noncopyable
{
public:
    noncopyable(const noncopyable&) = delete;
    noncopyable& operator=(const noncopyable&) = delete;
protected:
    noncopyable() = default;
    ~noncopyable() = default;
};

3. Logger日志代码

很多时候当软件应用以后gdb调试还是有很多不便的,日志是我们来处理问题最直接的最佳的路径,muduo库的日志设计的还不是非常的好,在许多c++的日志里面都是用格式化字符的方式进行日志的输出,可以很好的按作者的风格进行输出,可以很好的组织输出的格式。

Logger.h

#pragma once

#include<string>

#include "noncopyable.h"

//LOG_INFO("%s %d", arg1, arg2)
//__VA_ARGS__获取可变参的宏
//logmsgFormat:字符串,后面...是可变参
//为了防止造成意想不到的错误用do-while(0)
#define LOG_INFO(logmsgFormat,...)\
    do\
    {\
        Logger &logger = Logger::instance();\
        logger.setLogLevel(INFO);\
        char buf[1024] = {0};\
        snprintf(buf,1024,logmsgFormat,##__VA_ARGS__);\
        logger.log(buf);\
    }while(0)
#define LOG_ERROR(logmsgFormat,...)\
    do\
    {\
        Logger &logger = Logger::instance();\
        logger.setLogLevel(ERROR);\
        char buf[1024] = {0};\
        snprintf(buf,1024,logmsgFormat,##__VA_ARGS__);\
        logger.log(buf);\
    }while(0)
#define LOG_FATAL(logmsgFormat,...)\
    do\
    {\
        Logger &logger = Logger::instance();\
        logger.setLogLevel(FATAL);\
        char buf[1024] = {0};\
        snprintf(buf,1024,logmsgFormat,##__VA_ARGS__);\
        logger.log(buf);\
        exit(-1);\
    }while(0)
#ifdef MUDEBUG
#define LOG_DEBUG(logmsgFormat,...)\
    do\
    {\
        Logger &logger = Logger::instance();\
        logger.setLogLevel(DEBUG);\
        char buf[1024] = {0};\
        snprintf(buf,1024,logmsgFormat,##__VA_ARGS__);\
        logger.log(buf);\
    }while(0)
#else
    #define LOG_DEBUG(logmsgFormat,...)
#endif

// 定义日志的级别,基本上日志分为这几个级别:INFO打印重要的流程信息;
// ERROR并不是所有的ERROR都得exit;
// FATAL这种问题出现系统无法继续向下运行,就得输出关键的日志信息然后exit;
// DEBUG在系统正常运行会默认把DEBUG关掉,在需要输出DEBUG日志才会打开开关 
enum LogLevel
{
    INFO,   //普通信息
    ERROR,  //错误信息
    FATAL,  //core信息
    DEBUG, //调试信息
};

// 输出一个日志类
class Logger : noncopyable
{
public:
    // 获取日志唯一的实例对象
    static Logger& instance();
    // 设置日志级别
    void setLogLevel(int level);
    // 写日志
    void log(std::string msg);
private:
    int logLevel_;
};

我们系统的变量很多都写_开头,我们书写定义成员变量把_放在末尾,去和系统的变量不产生冲突,而且为了区分函数内的变量(局部变量)

Logger.cc

#include "Logger.h"

#include "Timestamp.h"

#include<iostream>

// 获取日志唯一的实例对象
Logger& Logger::instance()
{
    static Logger logger;
    return logger;
}
// 设置日志级别
void Logger::setLogLevel(int level)
{
    logLevel_ = level;
}
// 写日志 打印顺序:[级别信息] time msg
void Logger::log(std::string msg)
{
    switch(logLevel_)
    {
        case INFO:
            std::cout << "[INFO]";
            break;
        case ERROR:
            std::cout << "[ERROR]";
            break;
        case FATAL:
            std::cout << "[FATAL]";
            break;
        case DEBUG:
            std::cout << "[DEBUG]";
            break;
    }

    //打印时间和msg
    std::cout <<  Timestamp::now().toString() << ":" << msg << std::endl;
}

4. TimeStamp时间代码

image.png 这个类里面有2个主要方法和一个成员变量

image.png

image.png

image.png

image.png

手写Timestamp
Timestamp.h

#pragma once

#include<iostream>
#include<string>

class Timestamp
{
public:
    Timestamp();    //默认构造
    explicit Timestamp(int64_t microSecondsSinceEpoch);   //带参数的构造,带参数的构造函数都加了explicit关键字:避免隐式对象转换
    static Timestamp now(); //获取当前时间
    std::string toString() const;   //获取当前时间的年月日时分秒的输出
private:
    int64_t microSecondsSinceEpoch_;
};

带参数的构造函数都加了explicit关键字:避免隐式对象转换,防止程序最后的行为和我们预想的不一样。 Timestamp.cc

#include "Timestamp.h"

#include<time.h>

Timestamp::Timestamp():microSecondsSinceEpoch_(0){}

Timestamp::Timestamp(int64_t microSecondsSinceEpoch)
    :microSecondsSinceEpoch_(microSecondsSinceEpoch)
    {}
Timestamp Timestamp::now()
{
    return Timestamp(time(NULL));   //获取当前时间
}
std::string Timestamp::toString()const
{
    char buf[128] = {0};
    tm *tm_time = localtime(&microSecondsSinceEpoch_);
    snprintf(buf,128,"%4d/%02d/%02d %02d:%02d:%02d",
    tm_time->tm_year + 1900,
    tm_time->tm_mon + 1,
    tm_time->tm_mday,
    tm_time->tm_hour,
    tm_time->tm_min,
    tm_time->tm_sec);
    return buf;
} 
// #include<iostream>
// int main()
// {
//     std::cout<<Timestamp::now().toString()<<std::endl;
// }

5. InetAddress

image.png TCPServer就是我们用muduo库编程 服务器程序的入口的类,我们经常自定义一个类,把TCPServer对象组合一下。

image.png eventloop就是事件循环,相当于多路事件分发器,相当于epoll,InetAddress,需要这个对象来打包IP地址和端口号作为TCPServer对象的构造函数的参数。

我们来看看InetAddress

image.png

image.png

image.png

image.png

image.png InetAddress.h

#pragma once

#include<arpa/inet.h>
#include<netinet/in.h>
#include<string>

class InetAddress
{
public:
    explicit InetAddress(uint16_t port = 8888, std::string ip = "127.0.0.1");
    explicit InetAddress(const sockaddr_in &addr)
        :addr_(addr)
    {}
    std::string toIp()const;
    std::string toIpPort()const;
    uint16_t toPort()const;

    const sockaddr_in* getSockAddr() const { return &addr_; }
    void setSockAddr(const sockaddr_in &addr) { addr_ = addr; }
private:
    sockaddr_in addr_;
};

InetAddress.cc

#include "InetAddress.h"

#include<strings.h>
#include<string.h>

InetAddress::InetAddress(uint16_t port,std::string ip)
{
    bzero(&addr_,sizeof addr_); //清零
    addr_.sin_family = AF_INET;
    addr_.sin_port = htons(port); //把本地字节序转成网络字节序,两个不同的端要通信的时候,系统都有可能不一样,我是小端你是大端,网路字节都是大端,我们需要都转成网络字节序,通过网络传送到对端后,再将网络字节序转成本地字节序,这样互相传输的数据都能识别了
    addr_.sin_addr.s_addr = inet_addr(ip.c_str());


}
std::string InetAddress::toIp()const
{
    char buf[64] = {0};
    ::inet_ntop(AF_INET,&addr_.sin_addr,buf,sizeof buf);//读出整数的表示网络字节序转成本地字节序
    return buf;
}
std::string InetAddress::toIpPort()const
{
    char buf[64] = {0};
    ::inet_ntop(AF_INET,&addr_.sin_addr,buf,sizeof buf);
    size_t end = strlen(buf);
    uint16_t port = ntohs(addr_.sin_port);
    sprintf(buf+end,":%u",port);
    return buf;
}
uint16_t InetAddress::toPort()const
{
    return ntohs(addr_.sin_port);
}

// #include<iostream>
// int main()
// {
//     InetAddress addr(8080);
//     std::cout<<addr.toIpPort()<<std::endl;
    
// }