muduo 总结
muduo 网络库整体架构简析

· muduo是一个高质量的Reactor网络库,它的设计特点:Reactor In Threads + One Loop Per Thread
解释:有一个 Main Reactor 负责 accept 连接,然后以轮询的方式在 Reactor Pool 中找到合适的 sub Reactor,将该连接分发给它,且该连接的所有操作都在这个 Sub Reactor 所处的线程中完成。使用轮询的方式是尽量将多个连接分发到多个线程,充分利用 CPU。
· muduo是一个高质量的Reactor网络库,采用one loop per thread + thread pool架构实现。
· EventLoop、Poller、Channel、线程的关系:事件循环类 EventLoop 与线程是一一对应关系,负责 IO 事件的分派。它有一个内部类 Poller 作为 IO 多路复用,还绑定了多个封装了 fd 的 Channel。Channel 通过 EventLoop 向 Poller 注册和更新事件。
· EventLoop 执行任务:EventLoop 还包含一个任务队列,它在一次事件循环中处理完 IO 事件就会依次取出任务执行。如果队列中的任务处理时涉及到别的线程的资源,会将该任务添加到对应线程的任务队列中,交由它执行。
【好处】将资源固定地交由一个线程来处理,减少了锁的复杂性。
由于 EventLoop 是在处理完 IO 事件后才执行计算任务的。若它阻塞在 epoll_wait 就无法处理这些计算任务。因此当 EventLoop 的线程阻塞时,会其他线程会通过某种通信方式唤醒该线程。
备注: IO 事件指的是网络收发数据的 Read 和 Write 操作。任务队列里的任务是:当 Main Reactor 获取到一个新的连接,会将其打包成 Channel,并创建一个 TcpConnection 对象,负责管理该连接 Channel。然后将 TcpConnection 对象分配给 subloop 处理。Main Reactor 通过 queueInLoop 将回调加入 Sub Reactor 的任务队列,由于 Sub Reactor 阻塞,因此 queueInLoop 会通过 wakeup 对其进行唤醒。
while (!quit_)
{
activeChannels_.clear();
pollRetureTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
for (Channel *channel : activeChannels_)
{
// Poller监听哪些channel发生了事件 然后上报给EventLoop 通知channel处理相应的事件
channel->handleEvent(pollRetureTime_);
}
doPendingFunctors();
}
· Channel 理解:Channel 封装了 fd 及对 fd 事件相关方法的处理回调。** Event Loop 与 Channel 是一对多的关系。**
· TcpConnection 包含一个 Socket 和一个 Channel。Channel 的具体回调函数,是由 TcpConnection 在构造的时候为 Channel 注册的。TcpConnection 的读写事件回调 handleRead 和 handle Write 会借助 Buffer 缓冲区类保存输入、输出数据。这样做的目的是解决 TCP 协议收发数据时的阻塞问题(基于 TCP 的流协议拓展)。维护一个应用层的 inputBuffer 和 outputBuffer 能够保证数据的可靠发送,应用层只需调用一次 send() 就可以发送全部数据。
· Acceptor 理解:Acceptor 是接受连接的类,与 Channel 是一一对应的关系。它先对 Channel 里的 listenfd 进行 bind, listen 初始化,然后调用 Channel 的方法注册到事件循环中。当 listenfd 有新连接时,Channel 会进行读回调进行 accept 连接,然后调用上层注册的 newConnectionCallback 回调函数。
· Acceptor 与 TcpServer 的关系:Acceptor 与 TcpServer 是一一对应关系,Acceptor 是 TcpServer 的内部类。
· Acceptor、TcpServer 、TcpConnection 关系:当 Acceptor 建立一个连接后,TcpServer 对这个连接创建一个 TcpConnection 对象,并设置好对应的回调函数。TcpServer 维护一个 TcpConnection 的 map 来管理所要连接的 Client。
· TcpServer 与 EventLoopThreadPool 关系:TcpServer 通过线程池处理并发请求。线程池的每个线程都对应一个 EventLoop。每个连接到来会自动分配一个线程来创建 TcpConnection,由此来提高服务器的请求处理能力。
· Connector 与 TcpClient:Connector 与 TcpClient 如同 Acceptor 与 TcpServer 的关系。
Buffer 类
Muduo 设计与实现之一:Buffer 类的设计
一、Buffer 类的设计
为什么非阻塞网络编程中应用层 buffer 是必须的?
非阻塞 IO 的核心思想是避免阻塞在 read() 或 write() 或其他 IO 系统调用上,这样才能最大限度地让一个线程能服务于多个 socket 连接。IO 线程只能阻塞在 epoll_wait() 上。基于这个思想,应用层的缓冲是必须的,每个 TCP socket 都要有输入缓冲区、输出缓冲区。
TcpConnection 必须要有输出缓冲区?
考虑如下场景:若程序想通过 TCP 连接发送 100k 字节的数据,但是在 writ() 调用中,OS 只接收了 80k 字节(受滑动窗口的影响)。由于程序不能阻塞,要尽快交出控制权。因此,如果没有缓冲区 20k 数据将丢失。
对于应用程序而言,它只负责生成数据,只需调用 TcpConnection::send() 就认为数据迟早会发送出去。数据的真正分发工作由网络库负责。
· 网络库应该使用输出缓冲区接收剩余的 20k 字节数据,然后注册 POLLOUT 事件。若有 POLLOUT 事件发生,说明发送 socket 缓冲区为空,可以再剩余的数据再次发送。
· 发送完 20k 字节后,网络库停止关注 POLLOUT,以免造成 busy loop(关注的事件一直触发称为busy loop)。
· 若 20k 字节未发送前,又有新的 50k 字节需要 send(),网络库会先将这 50k 字节添加在 20k 字节之后,等 socket 可写的时候一并写入。
· 若输入缓冲区还有待发送的数据,而应用程序向关闭,网络库不能立刻关闭连接,而要等数据发送完毕。
【备注:muduo 把"主动关闭连接"这件事情分成两步走,首先关闭本地”写“端(shutdown 操作),等对方关闭之后,再关本地”读“端。因为要防止在本地关闭连接的时候,对方发送了消息而本地未收到】
TcpConnection 必须要有输入缓冲区
由于 TCP 是无边界的字节流协议,所以服务端单次接收到的数据可能会出现数据不完整情况。因此网络库在处理 ”socket 可读“事件的时候,为了避免反复触发 POLLIN 事件,造成 busy-loop 必须一次性把 socket 的数据读完(从内核 buffer 读到应用层 buffer)。
Buffer 的设计要点:
· 首先是一块连续的内存;
· 其次,其 size() 可以自动增长,以适应不同大小的情况;
muduo 的 Buffer 是线程安全的吗?
不是线程安全的。
· 对于输入 buffer,只有 TcpConnection 所属的 IO 线程才能操作输入buffer,因此它不必是线程安全的。
· 对于输出 buffer,它不会暴露给用户程序,而是通过调用 TcpConnection::send() 来发送数据,而 send() 是安全的。解释如下:send() 只会发生在该 TcpConnection 所属的 IO 线程。如果调用 send() 是当前 IO 线程,那么它会通过调用 TcpConnection::sendInLoop() 操作输出 buffer。如果调用 send() 是其他 IO 线程,那么它会通过 runInLoop() 把 sendInLoop() 调用转移到所属 IO 线程。
muduo Buffer 的数据结构
readIndex 和 writeIndex 将缓冲区分成 3 个部分 —— prependable + readable + writable
1)readIndex 和 writeIndex 在挪动过程中再次相等时,会重新归位。在 prependable = 8B 的位置。
2)Buffer 具有自动增长的功能;
3)内部腾挪。若经过若干次读写,readIndex 移到了比较靠后的位置,留下了巨大的 prependable 的空间,buffer 会将已有的数据移到前面去。
基于 C++11 重写 muduo
Reactor 模式 由 四部分构成:Event、Reactor、Demuliplex(事件分发器) 和 Eventhandler(事件处理器).
· Event:注册事件和其对应的处理方法给 Reactor,Reactor 里有 sockfd 以及对应的 event
· Reactor:向其 Demuliplex(事件分发器) 添加,启动反应堆
· Demuliplex:开启事件循环,将发生事件的 event 返回给 Reactor
· EventHandler:Reactor 调用 event 对应的事件处理器 EventHandler 执行相应的回调函数
muduo 的 Reactor 模型:
· channel 封装了 Event
· EventLoop 就是一个 Reactor
· Poller 就是一个 Demultiplex
· channel 里边有对应的回调函数

channel 必须借助 EventLoop 才能与 Poller 进行数据传输。
muduo 有 2 种线程:主线程的 mainLoop 专门负责处理新用户的连接,线程池的工作线程专门负责处理对应连接的所有读写事件。
TcpServer
TcpServer 将新用户连接回调 newConnectionCallBack 传给 Acceptor,将读写事件回调传给 TcpConnection。
EventLoop 以及相关类
Channel 此类负责打包 sockfd,不同 fd 对应的 loop 提醒 channel 执行的回调也不同。
Poller
此类是一个抽象接口类,向下提供不同的 IO 复用函数的接口
EpollPoller
此类用 poll() 函数监听和管理 channel,并将 sockfd 有事件发生的 channel 返回给 EventLoop,由 EventLoop 通知 channel 执行相应的回调。
EventLoop
此类相当于一个 Reactor 的角色,向 Poller 的 map 表里注册 fd 事件对应的 channel,并接收有事件发生的 cahnnel,并通知 channel 执行相应的回调。
EventLoopThread
EventLoopThread 在创建时,就创建了一个线程,并传递了线程回调函数 threadFunc。调用 EventLoopThread::startLoop 时,就调用线程的开始函数 start() ,就会执行线程回调函数 threadFunc,其逻辑是生成一个 loop。【即 startLoop 是让绑定的线程创建一个loop,同时运行线程跟 loop】
EventLoopThreadPool
如果工作在多线程中,baseLoop_ 默认以轮询的方式分发 channel 给 subloop
总结:
· muduo 库采用 one loop per thread + thread pool 模型
· 总体是基于 Reactor 模型,EventLoop 就是一个 Reactor 、Poller 就是一个事件分发器
· sockfd 极其对应的回调操作被打包成 channel,由 EventLoop 将其添加到 Poller 的 map 表里,Poller 将发生事件的 channel 返回在 EventLoop 的 List 表里,EventLoop 通知 channel 执行相应的回调操作;
· muduo 的 TcpServer 负责传入用户写的回调操作
· 此时 fd 分为 2 种 —— acceptfd 和 connfd,分别和其对应的事件打包成 channel,acceptfd 感兴趣的事件是新用户连接事件,因此 acceptChannel 被添加到 baseLoop 的Poller 里边。当有新用户连接时,执行 acceptfd 对应的回调操作,并得到 connfd,此时将 connfd 和其感兴趣的事件添加到 IOLoop 里的 Poller 里边,当对应的事件发生时,调用相关的回调操作。
深入理解 EventLoop
https://cloud.tencent.com/developer/user/1147827/search/article-muduo
EventLoop(一)
1 EventLoop、Channel、Poller的关系
Channel 负责注册(update())与响应 IO 事件(有根据事件的处理回调函数 handleEvent())。
Channel 是 Acceptor、Connector、EventLoop、TimerQueue、TcpConnection 成员
一个 EventLoop 对象对应一个 Poller 成员对象。对于EPollPoller 来说,一个 channel对应一个 fd、一个struct epoll_event。而一个 EPollPoller 有多个 fd,所以 EPollPoller 有存放 fd 与 Channel 的映射表 ChannelMap channels_,存放 fd 的 epoll_event 数组容器 std::vector<epoll_event> EventList; EventList events_;
只有 wakeupChannel_ 生存期由 EventLoop 控制,timerfdChannel_ 生存期由 TimeQueue 管理。其余以由 ChannelList 管理 typedef std::vector<Channel *> ChannelList;,ChannelList activeChannels_; // Poller返回的活动通道
一个线程只能对应一个 EventLoop,称为 IO 线程。
EventLoop(二)
wakeup
虽然一个 EventLoop 对应一个线程,但是A 线程的 EventLoop 的 quit() 函数可以被 B 线程调用(跨线程调用)。若不是当前 IO 线程(A 线程)调用 EventLoop 的 quit(),而该 EventLoop 对应的 A 线程此时可能阻塞在 poll() 的位置,因此 EventLoop 需要先唤醒( wakeup() ) A 线程,唤醒的具体操作就是向该 eventLoop 发送一个写的数据,这样线程 A 在 poll() 处才能被唤醒。
备注:一般情况下,poll() 会超时返回
void EventLoop::quit()
{
quit_ = true;
if (!isInLoopThread())
{
wakeup();
}
}

enableReading() / enableWriting()
wakeupChannel 设置完读/写回调函数后,注册读事件 enableReading / 写事件 enableWriting。
Channel 设置读/写事件的流程如下:
调用 Channel 的 update(),进而调用 EventLoop 的 update(),再调用 Poller 的 update(),由 Poller 将 Channel 的读事件进行注册。Poller::update() 会根据 events_ 值进行相应的读写操作。
谈谈 EventLoop::loop() 的流程
EventLoop::loop() 内部调用 poll(),poll() 阻塞,直至有事件发生 —— 或timerfd_ 超时,或 socket 有数据可读/可写。非 IO 线程调用 EventLoop::quit() ,进而调用 wakeup(),往wakeupFd_ 写 8 个字节数据,此时 EventLoop 对应的 EPollPoller 监听到注册的 wakeupFd_ 可读【EventLoop 自己给自己的 wakeupFd_ 写 8 字节数据,以唤醒自己的 IO 线程】。EventLoop 对应的 EPollPoller,调用 poll() ,进而调用 fullActiveChannels(),设置此 channel 的 revents_。然后将此 channel 压入 EventLoop::activeChannels_ 中返回。
在 EventLoop 里,对每个 activeChannels 执行回调函数。

EventLoop::loop、runInLoop、queueInLoop、doPendingFunctors
关于 doPendingFunctors 的补充说明:
1)不是简单地在临界区内依次调用 Functor,而是把回调列表 swap出去到 functors。这样既可以减小临界区长度(不会阻塞其他线程的 queueInLoop()),也避免了死锁(因为functor 可能再次调用 queueInLoop())。
2)由于 doPendingFunctors() 调用的 Functor 可能再次调用 queueInLoop(cb),这时,queueInLoop() 就必须 wakeup(),否则新增的 cb 可能不能及时调用。
3)muduo 没有反复执行 doPendingFunctors() 直到为空,是因为担心 IO 线程陷入死循环。
Acceptor
包括 acceptSocket_、acceptChannel_(用于监听 socket 的可读事件发生),Channel::handleEvent() 回调 Acceptor::handleRead()。而回调函数做的就是建立新连接。
TcpServer 与 Acceptor 的关系
TcpServer 在构造函数中初始化 acceptor_ 成员,并将 newConnetion 回调函数传给 acceptor_。调用 TcpServer::start(),开始 accepto_::listen()
void TcpServer::start()
{
threadPool_->start(threadInitCallback_); // 启动底层的loop线程池
loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
}
acceptor_ 绑定一个 channel,并将自己的 handleRead() 回调传给 channel。若 acceptor_ 的 channel 发生了读事件,即 acceptor_ 监听到新连接请求,channel 的读函数会调用 acceptor 的读回调 handleRead()。而 handleRead() 的实现就是调用 TcpServer::newConnection,因为在 TcpServer 创建之处,就创建了唯一用于监听的 acceptor,并将 newConnection 设置给该 acceptor。
现在来看看 TcpServer::newConnection 的逻辑:
创建 TcpConnection 对象,给一个 name,在缓存中保留这个映射关系。
而创建一个 TcpConnection 对象的同时,会记录对端 ip::port 及本端 ip::port,这唯一标识一个 TcpConnection 对象,同时为该对象其绑定一个 socketfd 及 channel。该 Tcp 连接来的数据都会发送到此 sockfd【内核根据四元组,将网卡数据拷贝到对应的 socketfd】。
现在看看 TcpConnection 逻辑:
1)含有连接建立函数 connectEstablished(),其核心是通过其绑定的 channel 向 poller 注册 channel 的读事件 channel->enbaleReading()。
2)含有连接销毁函数 connectDestroyed(),其核心是把 channel 的所有感兴趣事件从 poller 删除掉。
3)含有读函数 handleRead(),当 TcpConnection 绑定的 fd 有数据到达时,将 fd 的数据读取到 buffer 中,并调用读事件回调 messageCallback_() 。
EventLoop(四)
EventLoopThread 与 EventLoopThreadPool
任何一个线程,只要创建并运行了 EventLoop,都称为 IO 线程。
muduo 并发模型:one loop per thread + threadpool (计算线程池)
主线程不是 IO 线程,因此其调用 loop->runInLoop(runInThread) ,调用的是 queueInLoop(runInThread),将 runInThread 添加到队列,然后 wakeup() IO 线程,IO 线程在 doPendingFunctors() 中取出队列的 runInThread() 执行。
EventLoopThreadPool(IO 线程池类)
IO 线程池的功能是开启若干个 IO 线程,让 IO 线程处于事件循环的状态。
在 mainReactor + ThreadPool (subReactors) 模式下,TcpServer 和 Acceptor 的 loop_ 就是 baseLoop_。即 mainReactor 处理监听事件,已连接套接字事件轮询给线程池中的 subReactors 处理。注意:创建一个 TcpConnection 对象时,需要绑定一个 subReactor
EventLoop(五):TcpConnection 生存期管理(连接关闭)
1)监听套接字可读事件为 POLLIN;
2)已连接套接字可读为 POLLIN;
3)已连接套接字可写为 POLLOUT;
4)客户端 close/shutdown 关闭连接为 POLLIN/POLLHUP

将 TcpConnectionPtr 在 connections_ 中 erase 掉时,并不会马上析构 TcpConnection 对象(引用计数不为 0)。因为此时正处于 Channel::handleEvent() 中。若析构了 TcpConnection,那么它的成员 channel_ 也会析构。所以 TcpConnection 的对象生存期要大于 handleEvent() 函数。
EventLoop(五):TcpConnection::send()、shutdown()、handleRead()、handleWrite()
当某个 TcpConnection 发生可读事件,调用 TcpConnection::handleRead(),底层是调用将内核接收缓冲区数据读到 inputBuffer_ 中,接着调用消息处理回调 messageCallback_,从 inputBuffer_ 中读取数据。
若用户要发送数据,则调用 TcpConnection::send()。若是在 TcpConnection 所在的线程调用 send,则直接调用 sendInLoop,其底层是通过 write() 将要发送数据写入缓冲区;否则,会调用 runInLoop,唤醒所属 IO 线程。由于没有 activeChannels_ ,IO 直接调用 doPendingFunctors(),调用 sendInLoop()。
首先尝试 write 入内核发送缓冲区,若内核发送缓冲区满,则将未写完的数据添加到 outputBuffer,并关注 POLLOUT 事件,当内核发送缓冲区不为满时,即发生可写事件,再次调用 handleWrite()。
outputBuffer 的好处:只要程序调用了 send(),就可以认为数据迟早会到达。若程序向关闭连接,此时网络库不能立刻关闭连接,而是要等数据发送完毕。所以 TcpConnection 只提供 shutdown,以保证收发数据的完整性。【考点:shutdown 的原理分析】
===========================================================================================
谈谈 std::thread join()
The function returns when the thread execution has completed.This synchronizes the moment this function returns with the completion of all the operations in the thread: This blocks the execution of the thread that calls this function until the function called on construction returns (if it hasn't yet).
- 谁调用了join()函数? d2这个线程对象调用了join()函数,因此必须等待d2的下载任务结束了,d2.join()函数才能得到返回。
- d2在哪个线程环境下调用了join()函数? d2是在主线程的环境下调用了join()函数,因此主线程要等待d2的线程工作做完,否则主线程将一直处于block状态;这里不要搞混的是d2真正做的任务(下载)是在另一个线程做的,但是d2调用join()函数的动作是在主线程环境下做的。
int main()
{
cout << "主线程开始运行\n";
std::thread d2(download2);
download1();
d2.join();
process();
}

浙公网安备 33010602011771号