使用 C++ 11 编写去除 muduo 对 boost 的依赖
C++11 特性
std::function和std::bind
boost库是muduo网络库依赖的第三方库。
在c++11标准出来之后,新增了std::function和std::bind,功能与boost::function和boost::bind相同。
因此就可以用标准库中的函数代替boost库中的函数,以此去除对boost库的依赖。
智能指针
线程
Thread中
std::shared_ptr<std::thread> thread_;
该thread_直接与std::thread联系
ThreadPool中
std::vector<std::unique_ptr<Thread>> threads_;
该threads_与Thread联系
底层使用 Epoll + LT 模式的 I/O 复用模型,并且结合非阻塞 I/O 实现主从 Reactor 模型。
I/O多路复用
LT和ET
是什么
LT:水平触发模式。只要内核缓冲区有数据就⼀直通知,只要socket处于可读状态或可写状态,就会⼀直返
回sockfd;是默认的⼯作模式,⽀持阻塞IO和⾮阻塞IO
ET:边沿触发模式。只有状态发⽣变化才通知并且这个状态只会通知⼀次,只有当socket由不可写到可写或
由不可读到可读,才会返回其sockfd;只⽀持⾮阻塞IO
epoll
是什么
epoll 是 Linux 内核的可扩展 I/O 事件通知机制,是一种I/O多路复用方式。
其他的多路复用方式有哪些,它们的区别,为什么选择用epoll
其他的多路复用方式
- select
- poll
区别
-
文件描述符集合的存储位置
对于 select 和 poll 来说,所有⽂件描述符都是在⽤户态被加⼊其⽂件描述符集合的,每次调⽤都需要将整个集合
拷⻉到内核态;epoll 则将整个⽂件描述符集合维护在内核态,每次添加⽂件描述符的时候都需要执⾏⼀个系统调
⽤。系统调⽤的开销是很⼤的,⽽且在有很多短期活跃连接的情况下,由于这些⼤量的系统调⽤开销,epoll 可能
会慢于 select 和 poll。 -
文件描述符集合的表示方法
select 使⽤线性表描述⽂件描述符集合,⽂件描述符有上限;poll使⽤链表来描述;epoll底层通过红⿊树来描述,
并且维护⼀个就绪列表,将事件表中已经就绪的事件添加到这⾥,在使⽤epoll_wait调⽤时,仅观察这个list中有没
有数据即可。 -
判断是否有⽂件描述符就绪
select 和 poll 的最⼤开销来⾃内核判断是否有⽂件描述符就绪这⼀过程:每次执⾏ select 或 poll 调⽤时,它们会
采⽤遍历的⽅式,遍历整个⽂件描述符集合去判断各个⽂件描述符是否有活动;epoll 则不需要去以这种⽅式检
查,当有活动产⽣时,会⾃动触发 epoll 回调函数通知epoll⽂件描述符,然后内核将这些就绪的⽂件描述符放到就
绪列表中等待epoll_wait调⽤后被处理。
epoll为何高效
- 红黑树
- 就绪队列
- 回调函数
各自的适用场景
当监测的fd数量较⼩,且各个fd都很活跃的情况下,建议使⽤select和poll;当监听的fd数量较多,且单位时间仅部
分fd活跃的情况下,使⽤epoll会明显提升性能。
并发模型
非阻塞I/O
相关代码
socket()的参数设置为SOCKNONBLOCKING
// A***eptor.***
static int createNonblocking()
{
int sockfd = ::socket(AF_I***, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_TCP);
...
return sockfd;
}
A***eptor::A***eptor(EventLoop *loop, const I***Address &ListenAddr, bool reuseport)
...
a***eptSocket_(createNonblocking()),
...
}
a***ept4()的参数设置为SOCKNONBLOCKING
// Socket.***
int Socket::a***ept(I***Address *peeraddr) {
...
int connfd = ::a***ept4(sockfd_, (sockaddr *)&addr, &len, SOCK_NONBLOCK | SOCK_CLOEXEC);
...
}
同步I/O
主从reactor模型
基于自实现的双缓冲区实现异步日志,由后端线程负责定时向磁盘写入前端日志信息,避免数据落盘时阻塞网络服务。
Buffer
异步日志
为什么要实现非阻塞的日志
- 如果是同步日志,那么每次产生日志信息时,就需要将这条日志信息完全写入磁盘后才会执行后续程序。而磁盘 IO 是比较耗时的操作,如果有大批量的日志信息需要写入就会阻塞网络库的工作。
- 如果是异步日志,那么写日志消息只需要将日志的信息先进行存储,当累计到一定量或者经过一定时间时再将这些日志信息批量写入磁盘。而这个写入过程靠后台线程去执行,不会影响处理事件的其他线程。
日志库设计
日志库由前端和后端组成。
- 前端主要包括:Logger、LogStream、FixedBuffer、SourceFile。
- 后端主要包括:AsyncLogging、LogFile、AppendFile。
Logger
Logger 类为用户提供使用日志库的接口,其内部有一个 Impl(实现类)具体实现功能。Logger 内部定义了日志的等级。
FiexedBuffer
之前谈到了实现异步日志需要先将前端消息储存起来,然后到达一定数量或者一定时间再将这些信息写入磁盘。而 muduo 使用 FixedBuffer 类来实现这个存放日志信息的缓冲区。FixedBuffer 的实现在 LogStream.h 文件中。
针对不同的缓冲区,muduo 设置了两个固定容量。
const int kSmallBuffer = 4000; // 4KB
const int kLargeBuffer = 4000*1000; // 4MB
LogStream 类用于重载正文信息,一次信息大小是有限的,其使用的缓冲区的大小就是 kSmallBuffer。而后端日志 AsyncLogging 需要存储大量日志信息,其会使用的缓冲区大小更大,所以是 kLargeBuffer。
LogStream
LogStream 重载了一系列 operator<< 操作符,将不同格式数据转化为字符串,并存入 LogStream::buffer_。
AsyncLogging
现在开始讲解 muduo 日志库的后端设计了。前端主要实现异步日志中的日志功能,为用户提供将日志内容转换为字符串,封装为一条完整的 log 消息存放到 FixedBuffer 中;而实现异步,核心是通过专门的后端线程,与前端线程并发运行,将 FixedBuffer 中的大量日志消息写到磁盘上。
- AsyncLogging 提供后端线程,定时将 log 缓冲写到磁盘,维护缓冲及缓冲队列。
- LogFile 提供日志文件滚动功能,写文件功能。
- FileUtil 封装了OS 提供的基础的写文件功能。
双缓冲区
之前说过 FixedBuffer 有不同的大小,而 4MB 的就被 AsyncLogging 所用。前端在生成一条日志消息的时候会调用 AsyncLogging::append,将日志信息加入到 AsyncLogging::Buffer 中。而前端会有不同的线程调用日志库,因此 append 操作需要加锁保证互斥。
muduo 采用双缓冲区实现异步日志,它设置了两个 FixedBuffer<kLargeBuffer> Buffer
来储存前端的日志信息。如果当前的缓冲区不够放下日志信息,它就会将此缓冲区加入到 Buffer 数组中(为后端使用)。然后将预备缓冲区 nextBuffer 作为新的缓冲区使用。
如果后 nextBuffer 也不够使用了,那么就会新分配一个缓冲区记录日志信息,不过这种情况极少发生。如果日志写的速度很快,但是 IO 函数速度很慢,那么前端日志缓冲区就会积累,就会产生这种情况
LogFile
LogFile 主要职责:提供对日志文件的操作,包括滚动日志文件、将 log 数据写到当前 log 文件、flush log数据到当前 log 文件。
FileUtil
封装了 FILE 对文件操作的方法。以组合的形式被 LogFile 使用。
基于红黑树实现定时器管理结构,内部使用 Linux 的 timerfd 通知到期任务,高效管理定时任务。
定时器
利用有限状态机解析 HTTP 请求报文。
HTTP
参照 Nginx 实现了内存池模块,更好管理小块内存空间,减少内存碎片。
内存池
数据库连接池可以动态管理连接数量,及时生成或销毁连接,保证连接池性能。
数据库
在回声服务器和静态资源http服务器中并没有被使用到,用测试程序测试了单线程和多线程情况下使用连接池和不使用之间的性能差距
TCP
TcpConnection的生存周期管理,为什么用shared_ptr管理
参考文章