muduo 学习
1.muduo网络库服务器编写
/*
muduo网络库给用户提供了两个主要的类
TcpServer和TcpClient,分别用于服务器和客户端的编程。epoll + thread pool
好处: 能够将网络I/O代码和业务代码分离,提高并发性。业务:用户的连接和断开 用户的可读写事件
*/
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <iostream>
#include <functional>
#include <string>using namespace std;
using namespace muduo;
using namespace muduo::net;
using namespace placeholders;
/*
基于muduo网络库开发服务器程序
1. 组合TcpServer 对象
2. 创建EventLoop事件循环指针;
3. 明确TcpServer 构造函数需要什么参数,输出Chatserver的构造函数
4. 在当前服务器类的构造函数中,注册处理连接的回调函数和处理读写事件的回调函数;
5. 设置合适的服务端线程数量,muduo会自己分配IO线程和workr线程;
*/
class ChatServer
{
public:ChatServer(EventLoop* loop, //事件循环 Reactorconst InetAddress& listenAddr, // ip+portconst string& nameArg) // 服务器名字:_server(loop,listenAddr,nameArg),_loop(loop){ // 服务器注册用户连接的创建和断开回调_server.setConnectionCallback(std::bind(&ChatServer::onConnection,this,_1));// 给服务器注册用户读写事件回调_server.setMessageCallback(std::bind(&ChatServer::onMessage,this,_1,_2,_3));// 设置服务器端的线程数量 一个I/O线程 3个worker线程_server.setThreadNum(4);}// 开启事件循环void start(){_server.start();}private: // 专门处理用户的连接创建和断开 epoll listenfd acceptvoid onConnection(const TcpConnectionPtr& conn){if(conn->connected()){cout << conn->peerAddress().toIpPort() << "->" <<conn->localAddress().toIpPort() << "state:online" << endl; }else{cout << conn->peerAddress().toIpPort() << "->" <<conn->localAddress().toIpPort() << "state:offline" << endl; conn->shutdown(); // 调用用于关闭 TCP 连接// _loop->quit(); 退出服务器 这是用于退出 事件循环 的方法 这通常意味着 关闭整个服务器 或者停止继续处理来自网络接口的事件。}}// 专门处理读写事件void onMessage(const TcpConnectionPtr& conn, //连接Buffer* buffer, //缓冲区Timestamp time //接收到数据的事件信息 ){string buf = buffer->retrieveAllAsString();cout << "recv data " << buf << "time: " << time.toString() << endl;conn->send(buf);}TcpServer _server;EventLoop *_loop; // epoll};int main()
{EventLoop loop; // epollInetAddress addr("192.168.169.132", 8080);ChatServer server(&loop, addr, "Chatserver");server.start(); //listenfd 通过epoll_ctl->epoll 添加到epoll上面loop.loop(); // epoll_wait 以阻塞方式等待新用户连接,或者已连接用户的读写操作;return 0;
}
整体在,muduo库提供的两个接口下进行, 需要注意的两点:
1.更多关注IO网络编程和业务分离,构造函数通过两个回调函数去调用具体的处理函数;我们更多关注的就是这两个 回调函数;
2.bind是一个指的注意的点,因为对于成员函数而言,第一个参数是this,但是作为回调函数,没有示例对象也就没有this,这个时候用bind去绑定解决这个问题,然后后面的_1,_2,_3就是占位符,去代替这个回调函数的参数;
编译运行指令:
g++ -o server muduo_server.cpp -lmuduo_net -lmuduo_base -lpthread
然后
./server
另外开启一段终端
telnet 192.168.... port 去实现通信
客户端连接上后,服务端会显示connection的提示符也就是之前在onConnection定义的;
项目开始正是编写,先把CMakelist写好,以前用makefile但是现在cmake可以看作makefile升级,有了它可以自动生成makefile;
这段 CMakeLists.txt
文件用于配置一个基于 CMake
的项目,项目名称为 mymuduo
,其目标是编译出一个名为 muduo
的共享库(动态库)。以下是对每一部分的详细解释:
cmake_minimum_required(VERSION 2.5)
project(mymuduo)# mymuduo 最终编译成so动态库, 设置动态库路径, 放在根目录的lib目录下
set(LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
# 设置调试信息 以及启动c++11语言标准
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11")# 定义参与编译的的源文件
aux_source_directory(. SRC_LIST)
# 编译动态库
add_library((muduo SHARED ${SRC_LIST}))
1. cmake_minimum_required(VERSION 2.5)
:
-
该指令指定了
CMake
的最低版本要求。在这里,要求使用CMake
2.5 或更高版本来生成项目。
2. project(mymuduo)
:
-
定义项目名称为
mymuduo
。 -
CMake
会根据项目名称生成默认的构建目录和工程文件等。
3.set(LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
:
-
设置生成的 动态库输出目录,路径为
${PROJECT_BINARY_DIR}/lib
。 -
${PROJECT_BINARY_DIR}
是CMake
自动定义的变量,表示生成目录(即构建目录),通常是build
目录。 -
最终生成的动态库会存放在
build/lib
目录中。
4. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11")
:
-
为 C++ 编译器设置编译选项:
-
-g
:生成调试信息,用于调试程序。 -
-std=c++11
:启用 C++11 标准。
-
-
${CMAKE_CXX_FLAGS}
是CMake
的内置变量,用于存储当前项目的所有编译选项。这行代码在现有选项的基础上追加新的编译标志。
5. aux_source_directory(. SRC_LIST)
:
-
将当前目录下(
.
)的所有源文件(如.cpp
文件)添加到变量SRC_LIST
中。 -
SRC_LIST
最终包含了所有需要编译的源文件列表。
6. add_library(muduo SHARED ${SRC_LIST})
:
-
编译一个名为
muduo
的 共享库(动态库)。 -
SHARED
表示生成动态库(.so
文件,Linux 下动态库后缀)。 -
${SRC_LIST}
是前面收集的所有源文件列表,作为动态库的编译输入文件。
2. noncopyable
就像注释一样,通过将基类拷贝构造函数和赋值函数delete,派生类就无法进行;
但是对于析构和构造还是可以正常的;并且对于类继承是private缺省,对于struct是public缺省;
这里需要注意的一点是,并不是说派生类对象没有自己的析构和构造,而是说派生类创建实例的过程,构造是由内而外,也就是说从基类构造开始再到派生类构造函数,析构刚好相反;
#pragma once/*noncopyable被继承后,派生类对象可以正常构造和析沟,但是派生类对象无法进行拷贝构造和赋值操作;
*/
class noncopyable
{public:noncopyable(const noncopyable&) = delete;noncopyable& operator=(const noncopyable&) = delete;private:protected:noncopyable() = default;~noncopyable() = default;
};
如果派生类尝试调用 noncopyable
的拷贝构造函数,编译会报错,因此派生类的拷贝和赋值都会失败。
4. 总结
noncopyable
禁止拷贝构造和赋值运算符后,所有继承自它的派生类默认也会禁止拷贝构造和赋值。- 即使派生类试图定义自己的拷贝构造和赋值运算符,也不能绕过
noncopyable
的= delete
限制,因为基类的拷贝构造和赋值被删除,无法调用。 - 这种机制确保派生类也具备不可拷贝和赋值的特性,通常用于需要独占资源(如日志类、单例模式)等场景。
3. Logger
Logger.h
1.用enum枚举定义了不同的日志级别;
2. 日志类使用的是单例模式,也就是说这个类只能有一个实例,通过static实现的,它是类的函数而不属于对象;
然后还定义了设置日志级别的成员函数和写日志的函数;
3. 最后的一个点就是,并利用 宏(#define
) 来简化日志的记录操作。代码中的每个宏都通过不同的日志级别(INFO、ERROR、FATAL、DEBUG)来记录日志信息,且宏内通过 Logger
类的实例来处理日志写入
#pragma once// 定义日志级别 INFO ---- ERROR ---- FATAL ----DEBUG#include "noncopyable.h"
#include <string>// LOG_INFO("%s, %d", arg1, arg2)
#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); \} 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, ...)
#endifenum 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(){}
};
LOG_INFO
、LOG_ERROR
、LOG_FATAL
和LOG_DEBUG
都是类似的宏定义,使用了 C 语言的变长参数(...
)来实现日志信息的动态生成。通过snprintf
将格式化的日志信息写入到buf
中,然后调用logger.log(buf)
输出日志。- 宏内的
Logger& logger = Logger::instance()
获取Logger
类的唯一实例(因为Logger
是单例模式),并通过setLogLevel
设置日志级别。 snprintf
格式化日志内容并存储在buf
数组中。##__VA_ARGS__
语法用于处理可变参数列表,可以传入任意数量的参数并格式化它们。logger.log(buf)
调用Logger
实例的log
方法来写日志。
- 只有在
MUDEBUG
宏被定义时,LOG_DEBUG
才会被启用,这是一种控制调试日志是否启用的机制。 - 如果
MUDEBUG
被定义(通常是在调试模式下),LOG_DEBUG
宏会记录调试信息;否则,它就什么也不做(通过定义空的#define LOG_DEBUG
)。 - 是的,使用宏后
LOG_DEBUG
的代码块会自动执行,只要MUDEBUG
被定义。 do-while(0)
保证了语法正确性,在任何上下文中都可以安全调用日志宏。- 宏展开在编译前完成,是编译器的一种优化手段,用于减少日志调用的复杂性
源文件cpp
基本 对头文件类里面定义的函数进行实现,记得前缀;
就是设置级别,然后按照级别写日志
#include "Logger.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;default:break;}// print time and msgstd::cout << "print time: " << msg << std::endl;
}
单例模式 (Singleton Pattern)
单例模式是一种常见的设计模式,目的是确保某个类只有一个实例,并提供全局访问点来获取该实例。通常用于管理共享资源或全局状态,例如日志记录器、数据库连接池、配置管理等。
单例模式的主要特征
- 私有化构造函数:类的构造函数通常是私有的,防止外部代码通过
new
创建多个实例。 - 静态成员:通常在类中有一个静态的指针(或引用)来保存唯一的实例。
- 公共的静态方法:提供一个静态方法来获取该唯一实例。这个方法通常会创建实例(如果尚未创建)并返回该实例的指针。
4. Timestamp
把日志类里面输出时间的类的函数也加上
#pragma once#include <iostream>
#include <string>
class Timestamp
{
public:Timestamp();explicit Timestamp(int64_t microSecondsSinceEpoch);static Timestamp now();std::string toString() const;
private:int64_t microSecondsSinceEpoch_;
};
#include "Timestamp.h"
#include <iostream>
#include <time.h>
Timestamp::Timestamp():microSecondsSinceEpoch_(0) {}Timestamp::Timestamp(int64_t microSecondsSinceEpoch): microSecondsSinceEpoch_(microSecondsSinceEpoch){}Timestamp Timestamp::now()
{time_t ti = time(NULL);return Timestamp(ti);
}std::string Timestamp::toString() const
{char buf[128] = {0};tm* tm_time = localtime(µSecondsSinceEpoch_);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;return 0;
}
5. InetAddress
封装socket地址类型
InetAddress.hpp
#pragma once#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>// 封装socket地址类型
class InetAddress
{public:// 构造函数,使用端口和IP地址初始化InetAddress对象,默认IP为127.0.0.1explicit InetAddress(uint16_t port, std::string ip="127.0.0.1");// 构造函数,使用sockaddr_in结构体初始化InetAddress对象explicit InetAddress(const sockaddr_in& addr): addr_(addr){}// 返回IP地址的字符串表示std::string toIP() const;// 返回IP地址和端口号的字符串表示std::string toIpPort() const;// 返回端口号uint16_t toPort() const;// 返回sockaddr_in结构体的指针const sockaddr_in* getSockAddr() const{return &addr_;}private:// 存储sockaddr_in地址信息sockaddr_in addr_;
};
#include "InetAddress.h"
#include <iostream>
#include <strings.h>
#include <string.h>InetAddress::InetAddress(uint16_t port, std::string ip)
{bzero(&addr_, sizeof addr_); //使用 bzero 函数清空 addr_,确保没有残留数据。addr_.sin_family = AF_INET; // 设置地址族为 IPv4(AF_INET)。addr_.sin_port = htons(port); // 使用 htons 函数将端口号转换为网络字节序,确保跨平台的一致性。addr_.sin_addr.s_addr = inet_addr(ip.c_str()); // 将输入的字符串类型 IP 地址转换为网络字节序,并赋值给 sin_addr.s_addr。
}std::string InetAddress::toIP() const
{// addr_char buf[64] = {0};::inet_ntop(AF_INET, &addr_.sin_addr, buf, sizeof(buf)); // 使用 inet_ntop 函数将网络字节序的 IP 地址转换为可读的字符串格式,并存储在 buf 中,最后返回该字符串。return buf;
}std::string InetAddress::toIpPort() const // 将 IP 地址和端口号组合成一个字符串,格式为 "ip:port"。
{// ip::portchar 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); // 这一行代码的作用是将端口号格式化为字符串并追加到已有的 IP 地址字符串后面。return buf;
}uint16_t InetAddress::toPort() const
{return ntohs(addr_.sin_port); //使用 ntohs 函数将端口号从网络字节序转换为主机字节序并返回。
}
主机和网络字节序之间的转换函数;一般为了跨平台,都用网络字节序存;
ntohs;ntohl;htons;ntonl ;
s->short;l->long;
ip一般32位,因此用htonl,ntohl;
inet_addr:
将字符串形式的 IP 地址转换为网络字节序的 in_addr 结构体中的 s_addr 成员,这实际上是一个 uint32_t 类型。
inet_ntoa
inet_ntop
都是用于将网络字节序的 IP 地址转换为字符串格式的函数,但它们有一些重要的区别:
6.Channel
到这就需要搞清楚TcpServer\ Channel\ EventLoop之间的关系,和Reactor模型具体如何对应;
从这个框架结构看出来,reactot和Demultiplex 合在一起就很想epoll机制,注册sockfd和感兴趣事件; 其中reactor则存储是事件和事件处理器绑定的一个集合,更多的操作是由事件分发器Demultiplex完成的,它的功能更像epoll,比如说注册sockfd和感兴趣时间,然后监听到有事件发生,返回发生的事件给Reactor让它去找到对应的事件handler去处理;
在muduo库中具体实现一个EventLoop就包含了reactor和Demultiplex(poller),然后Channel是event, 也就是说一个poller监控着多个sockfd然后对应channel,相当于一个epoll监控多个sockfd;
所以poller包含Channel;
然后一个线程一个epoll--------one thread one loop;
因为对服务器来说单线程不是很充分利用资源;作为一个高并发服务器
到这才是真正的框架;
相当于poller可以监控多个sockfd,每个sockfd对应一个channel,用于注册sockfd和感兴趣的事件,因此Channel类就需要设置sockfd相应的事件状态和返回相应的事件状态,并且调用具体发生事件的回调函数
Channel 类的作用
Channel
类本质上是一个事件管理单元,它将一个sockfd
与其感兴趣的事件(如可读、可写、异常等)关联,并在事件发生时触发回调函数。
Channel 和 sockfd 的绑定
可以将 sockfd
和 Channel
看作一一对应的关系,你的比喻“将 sockfd
和 Channel
做了 map 绑定在一起”是非常形象的。这种关系的核心作用如下:
- 管理状态:
Channel
保存sockfd
以及当前感兴趣的事件类型,如EPOLLIN
(可读)、EPOLLOUT
(可写)。 - 处理回调:当某个
sockfd
发生感兴趣的事件时,Channel
会调用对应的回调函数来处理事件。 - 解耦逻辑:
Channel
将文件描述符和具体的事件处理逻辑解耦,使得poller
(如epoll
)只需要管理事件的分发,而不关心具体的业务逻辑。
工作机制总结
-
Channel 类负责:
- 保存
sockfd
和感兴趣的事件(EPOLLIN
、EPOLLOUT
等)。 - 提供注册回调函数的方法,如
setReadCallback
。 - 当事件发生时,通过
handleEvent
调用相应的回调函数。
- 保存
-
Poller 类负责:
- 管理
Channel
与sockfd
的映射。 - 使用
epoll_ctl
将sockfd
注册到epoll
实例。 - 调用
epoll_wait
等待事件发生并通知相应Channel
处理。
- 管理
总结
你描述的 Channel 将 sockfd 与感兴趣的事件绑定并调用回调函数的理解是准确的。这种设计体现了职责分离和事件驱动编程思想:
Channel
负责具体sockfd
的事件管理和回调。Poller
负责高效监听和分发事件。
#pragma once#include "noncopyable.h"
#include "Timestamp.h"
#include <memory>#include <functional>class EventLoop;
// class Timestamp;/* 理清楚 EventLoop\ Channel\ poller之间的关系 Channel 理解为通道,封装了sockfd和其他感兴趣event,如EPOLLIN、EPOLLOUT事件还绑定了监听返回的具体事件;
*/
class Channel : noncopyable
{
public:using EventCallback = std::function<void()>;using ReadEventCallback = std::function<void(Timestamp)>;Channel(EventLoop* loop, int fd);~Channel();// fd得到poller通知后处理事件的void handleEvent(Timestamp receiveTime);// 设置回调函数对象void setReadCallback(ReadEventCallback cb) {readCallback_ = std::move(cb);} // move左值转右值void setwriteCallback(EventCallback cb) { writeCallback_ = std::move(cb);}void setCloseCallback(EventCallback cb) { closeCallback_ = std::move(cb);}void setErrorCallback(EventCallback cb) { errorCallback_ = std::move(cb);}// 防止当Channel被手动remove掉,还在执行回调操作void tie(const std::shared_ptr<void>&);int fd() const{return fd_;}int events() const{return events_;}int set_revents(int revt) { revents_ = revt; } // epoll监听事件,所以channel有一个对外接口让epoll设置到底什么事件发生// 设置fd相应的事件状态void enableReading() {events_ |= kReadEvent; update();}void disableReading() {events_ &= ~kReadEvent; update();}void enableWriting() {events_ |= kWriteEvent; update();}void disableWriting() {events_ &= ~kWriteEvent; update();}void disableAll() {events_ &= kNoneEvent; update();}// 返回fd当前的事件状态bool isNoneEvent() const{ return events_ == kNoneEvent;}bool isWriting() const{ return events_ & kWriteEvent;}bool isReading() const { return events_ & kReadEvent;}int index() { return index_;}void set_index(int idx) { index_ = idx;}// one loop per threadEventLoop* ownerLoop(){return loop_;}void remove();
private:void update();void handleEventWithGuard(Timestamp receiveTime);static const int kNoneEvent;static const int kReadEvent;static const int kWriteEvent;EventLoop* loop_;const int fd_; // poller监听的对象int events_; // 注册fd感兴趣事件 epoll_ctlint revents_; // poller返回具体发生的事件int index_; std::weak_ptr<void> tie_;bool tied_;// 因为Channl通道里面,能够获知fd最终发生的具体的事件revents,所以它负责具体事件的回调操作;ReadEventCallback readCallback_;EventCallback writeCallback_;EventCallback closeCallback_;EventCallback errorCallback_;
};
有了前面的了解之后就可以开始channel类编写了,成员变量就包括:所属loop_, poller监听的fd_,poll而返回具体发生的revents_ 以及index;
这个用的是回调也就和EventHandler对应上了。
利用Channel向EventLoop下的Poller注册或者删除 对应的sockfd和感兴趣事件;
#include "Channel.h"
#include "EventLoop.h"
#include <sys/epoll.h>
#include <functional>
#include "Logger.h"const int Channel::kNoneEvent = 0;
const int Channel::kReadEvent = EPOLLIN | EPOLLPRI;
const int Channel::kWriteEvent = EPOLLOUT;// EventLoop: ChannelList Poller
Channel::Channel(EventLoop* loop, int fd) : loop_(loop), fd_(fd), events_(0), revents_(0), index_(-1), tied_(false)
{}Channel::~Channel()
{
// if (loop_->isInLoopThread())
// {
// assert(!loop_->hasChannel(this));
// } //这个操作就是判断当前这个loop是否是在这个线程下面,以及当前这个channel是否实在这个loop下面,因为目前都是高并发服务器,多线程}void Channel::tie(const std::shared_ptr<void>& obj)
{tie_ = obj;tied_ = true;
}// 该方法的作用是,当改变channel所表示的fd的事件后,update负责在poller里面更改fd相应的事件 epoll_ctl
// EventLoop => ChannelList Poller 独立
void Channel::update()
{// 通过Channel所属的EventLoop调用poller的相应方法,注册fd的event事件;// add code...// loop_->updateChannl(this);}// 在Channel所属的EventLoop中,把当前的channel删除
void Channel::remove()
{ // add code ..// loop_->removeChannel(this);
}void Channel::handleEvent(Timestamp receiveTime)
{if(tied_){std::shared_ptr<void> guard = tie_.lock();if(guard){handleEventWithGuard(receiveTime);}} else{handleEventWithGuard(receiveTime);}
}// 根据poller通知的channel发生的具体事件,由channel负责调用具体回调操作
void Channel::handleEventWithGuard(Timestamp receiveTime)
{LOG_INFO("channel handleEvent revents:%d\n", revents_);if(revents_ && EPOLLHUP && !(revents_ & EPOLLIN)){if(closeCallback_){closeCallback_();}}if(revents_ & (EPOLLERR)){if(errorCallback_){errorCallback_();}}if(revents_ & (EPOLLIN | EPOLLPRI)){if(readCallback_){readCallback_(receiveTime);}}if(revents_ & EPOLLOUT){if(writeCallback_){writeCallback_();}}
}