项目总结:muduo网络库的整体流程
Unix/Linux上的五种IO模型



好的网络服务器设计
在这个多核时代,服务端网络编程如何选择线程模型呢? 赞同libev作者的观点:one loop per
thread is usually a good model,这样多线程服务端编程的问题就转换为如何设计一个高效且易于使
用的event loop,然后每个线程run一个event loop就行了(当然线程间的同步、互斥少不了,还有其
它的耗时事件需要起另外的线程来做)。
event loop 是 non-blocking 网络编程的核心,在现实生活中,non-blocking 几乎总是和 IO
multiplexing 一起使用,原因有两点:
1.没有人真的会用轮询 (busy-pooling) 来检查某个 non-blocking IO 操作是否完成,这样太浪费
CPU资源了。
2.IO-multiplex 一般不能和 blocking IO 用在一起,因为 blocking IO 中
read()/write()/accept()/connect() 都有可能阻塞当前线程,这样线程就没办法处理其他 socket
上的 IO 事件了。
所以,当我们提到 non-blocking 的时候,实际上指的是 non-blocking + IO-multiplexing,单用其
中任何一个都没有办法很好的实现功能
muduo核心组件示意图

muduo大致流程
1. TcpServer 的初始化
TcpServer
是整个服务器的核心组件,负责处理客户端连接请求。- 初始化
TcpServer
对象时,会创建一个Acceptor
,它包含一个监听文件描述符(listenfd
)和一个acceptChannel
,用于监听新连接事件。 - 通过
setNewConnectionCallback
将TcpServer::newConnection
方法设置为Acceptor
的回调函数,当有新连接到来时,触发此回调。
2. 主线程的 EventLoop (mainLoop)
mainLoop
是主线程中的EventLoop
对象,负责监听Acceptor
的事件。TcpServer::start
方法会启动EventLoopThreadPool
,创建多个子线程,每个子线程运行一个独立的EventLoop
,称为subLoop
。- 主线程的
mainLoop
主要负责监听新的连接请求,而客户端的具体数据处理会分配给subLoop
。
3. EventLoopThreadPool 的工作机制
EventLoopThreadPool
是一个线程池,用于管理多个EventLoop
线程。- 通过
start
方法启动EventLoopThreadPool
,创建numThreads_
个线程,每个线程都运行一个subLoop
。 - 每个
subLoop
都是一个独立的EventLoop
,和主线程的mainLoop
平行工作,用于处理客户端的 I/O 事件。
4. Acceptor 的事件处理
Acceptor
的作用是接受新的客户端连接。- 它在主线程的
mainLoop
中监听listenfd
的读事件,一旦有新的连接请求到来,就会触发acceptChannel
的读回调,即handleRead()
方法。 handleRead()
方法会调用newConnectionCallback
,即TcpServer::newConnection
,来处理新的连接。
5. TcpServer::newConnection:处理新连接
- 在
newConnection
中,TcpServer
创建一个新的TcpConnection
对象来表示客户端连接。 - 然后,
TcpServer
使用轮询算法选择一个subLoop
(从多个子线程的EventLoop
中选择一个),将新的连接分配给该subLoop
。 - 具体步骤:
- 根据轮询算法选择
subLoop
,将新的connfd
转换为一个channel
。 - 将
TcpConnection
的回调函数注册到subLoop
上,以便在该子线程中处理客户端的事件。
- 根据轮询算法选择
6. Reactor 模式的实现(每个线程一个 Reactor
)
- 每个
EventLoop
充当一个Reactor
,负责在其线程中管理和处理所有事件。 - 主线程的
mainLoop
只负责新的连接请求的监听,而所有的客户端 I/O 事件都会在subLoop
中处理。 - 每个
subLoop
都包含一个EPollPoller
,用于监听事件,通过poll()
方法轮询事件并执行相应的回调。
7. 线程间的唤醒机制
- 当主线程将新的连接分配给某个
subLoop
时,需要唤醒该子线程,以便立即处理新的连接。 - 通过
eventfd
或socketpair
实现线程间的唤醒机制。 - 每个
subLoop
线程都有一个wakeUpFd
,主线程通过写入wakeUpFd
唤醒子线程。
8. TcpConnection 的回调处理
TcpConnection
表示一个客户端连接,负责具体的读写数据操作。- 它包含了两个主要的回调函数:
onConnection
:当连接建立时触发,业务逻辑可以选择在这里处理一些连接初始化工作。onMessage
:当有数据到达时触发,用于读取和处理客户端发送的数据。
- 这些回调会被注册到
subLoop
中,在subLoop
监听到客户端事件后,自动执行相应的回调函数。
9. 数据发送和接收
- 当客户端发送数据时,
onMessage
回调被触发,TcpConnection
会从connfd
中读取数据并进行处理。 - 当服务器需要发送数据给客户端时,
TcpConnection
负责将数据写入connfd
,并确保数据被正确发送给客户端。
10.定时器
---->eventloop中timerqueue对象创建完成,此时timerqueue中构造函数会建立一个timerqueue::m_timerfd和timerqueue::m_timerfdChannel,用于注册可读定时器事件和唤醒poller::poll()。
---->eventloop对象创建完成,
---->eventloop::runAfter() 这个是为用户提供操作的,用来在eventloop中设置一个定时操作。
--->eventloop::runAt()
---->timerqueue::addTimer() 到此我们可以看出来,eventloop::run_XXX系列函数归根结底就是在timerqueue中添加一个定时器并设置定时器回调函数,那么timerqueue是如何知道现在有定时器到时了呢?
---->eventloop::runInLoop(timerqueue::addTimerInLoop)
---->eventloop::queueInLoop(timerqueue::addTimerInLoop),此时任务队列不为空,eventloop::m_wakefd会立即唤醒poller::poll()执行任务队列中的函数(timerqueue::addTimerInLoop)
----->resetTimerfd(timerqueue::m_timerfd,expiration),到现在才只是完成了timerfd_settime()操作,经过expiration秒后,timerqueue::m_timerfd就会发起可读定时器事件,
---->poller::poll()被m_timerfd可读事件唤醒,通过timerqueue::m_timerChannel来处理这个可读定时器事件
------>timerqueue::m_timerChannel会回调timerqueue::handleRead()函数:1.找到所有过期的定时器集合,
2.运行这些过期定时器的回调函数。
至此,从eventloop::runAfter()到真正的定时器函数调用才最终完成。

11.连接的关闭与资源清理
- 当连接需要关闭时,
TcpConnection
会调用closeCallback
。 closeCallback
通常指向TcpServer::removeConnection
方法,用于将连接从subLoop
中移除,并执行清理工作。- 关闭操作完成后,
TcpConnection
对象会被销毁,释放连接资源。
总结
- 主线程通过
mainLoop
负责监听新连接请求。 - 新连接到来时,
TcpServer
将其分配给一个subLoop
,由子线程负责处理。 EventLoopThreadPool
提供了多个subLoop
线程,实现多线程处理能力。- 每个
subLoop
都是一个Reactor
,负责在各自的线程中管理客户端的 I/O 事件。 - 通过
eventfd
或socketpair
实现线程间的唤醒,使得主线程可以及时通知子线程处理新连接。 TcpConnection
用于管理每个客户端连接,包括数据的读写和连接的关闭。
这样的设计实现了一个高效的多线程网络服务器结构,充分利用了多核 CPU 的并行处理能力,实现了高性能的网络 I/O 处理。