mainReactor就是我们代码中的mainLoop(baseLoop),subReactor就是代码中工作线程进行读写事件的处理,read读数据是底层做的,decode、compute和send是用户做的。从网络上read数据,然后从网络上send数据,这些是由muduo库完成的。decode、compute、encode是用户在OnMessage处理的业务逻辑,每一个loop对应一个线程,每一个线程执行一个独立的Reactor。
一、Channel类
Channel主要做的事情就是封装了fd,events,revents和一组回调:
- fd_表示要向poller上注册的文件描述符
- events_表示事先设置的fd所感兴趣的事件
- revents_表示poller最终给channel通知的fd上发生的事件,channel根据相应的事件来执行相应的回调
- index_用来标识Channel的状态
对于上层来说,如果有一个fd,就会把这个fd打包成channel通道,然后下发到poller上。
二、Poller/EpollPoller类
相当于是事件分发器Demultiplex
不管是Channel还是Poller都有一个EventLoop成员变量记录它所在的事件循环。
Poller的成员变量:
- channels_:是一个map,键是Channel打包的sockfd,值是包含这个sockfd对应的Channel,也就是说如果Poller检测到哪个fd有事件发生了,就可以用发生事件的fd在这个map表里面找到对应的Channel,Channel里记录了详细的事件回调。Channel和Poller是独立的不能直接互相通信,依赖EventLoop来通信。
- ownerLoop_:Poller所属的EventLoop,Poller会填充有事件发生的Channel到ownerLoop_的成员到activateChannels_
muduo库不管是监听的listenfd还是已连接用户返回的connfd,都会把这些fd打包成Channel,并写入fd感兴趣的事件,然后注册到相应loop地Poller上去,Poller在我们muduo网络库里实现了EpollPoller,底层就是Epoll,也就是底层都是通过epoll_ctl把相应的Channel里包含的fd注册到epoll上,当epoll返回以后,因为poller里面有std::unordered_map<int,Channel*> channels_
,就可以通过sockfd找到它所对应的Channel,同时调用相应的回调。Channel的callbacks是上层设置的,我们总共只有两种Channel(acceptorChannel和connChannel),因为总共只有两种fd,分别是用于监听新用户连接的listenfd和用于监听已连接用户读写事件的connfd。Channel和Poller是独立的不能直接互相通信,依赖EventLoop来通信。
三、 EventLoop类-reactor
EventLoop管理了一堆Channel和一个Poller,wakeupFd,这样Channel想把自己注册到Poller上,或者在Poller上修改自己感兴趣的事件,是通过EventLoop获取Poller对象,向Poller里进行设定,同样Poller检测到Socket有相应的事件发生,会通过EventLoop调用相应的Channel相应的fd所发生的事件的回调函数。
- activateChannels_:是一个vector,包含了所有发生事件的Channel,Poller会把有事件发生的Channel放入EventLoop::activateChannels
- wakeupFd_和wakeupChannel_:一个wakeupFd_隶属于一个EventLoop,每一个wakeupFd_都是注册在了EventLoop管理的Poller上。对于subloop来说,wakeupChannel_用于和mainloop通信,activateChannels用于已连接的客户端通信,如果activateChannels没有事件发生,subloop就会一直阻塞监听,而此时有新的客户连接,mainloop需要找一个subloop处理这个已连接用户的读写事件,就需要往这个subloop的wakeupFd_上写8字节数据唤醒
- pendingFunctors_:每一个回调函数都需要在发生事件的EventLoop所属的线程执行,如果当前loop是属于当前执行的线程,那直接执行回调函数,否则就把回调函数存到pendingFunctors这个vector里面,然后唤醒相应loop所在线程。然后被唤醒的线程会执行在pendingFunctor里所有的函数
loop最终执行的时候驱动底层的是一个事件分发器epoll_wait,只要没有线程发生,相应的loop线程阻塞在epoll_wait上,那么如果我们想唤醒某个loop所在的线程,就通过loop对象获取wakeupFd,向wakeupFd上写东西,相应的loop就会被唤醒,因为每一个loop的wakeupFd也封装成wakeupChannel注册在了自己的loop上。
四、Thread、EventLoopThread、EventLoopThreadPool类
1. EventLoopThreadPool(事件循环线程池)类的成员:
getNextLoop方法:通过轮询算法获取下一个subloop,如果用户没有使用setThreadNum设置subloop线程的数量,那就没有创建subloop,getNextLoop返回的一直是baseloop。
当我们通过setThreadNum设置底层的线程数量时,EventLoopThreadPool就会驱动底层开始创建线程,一个thread对应一个loop。
2. EventLoopThread(事件线程类)的成员:
EventLoopThread封装了线程Thread以及对应的事件循环EventLoop,Thread封装了std::thread
3. Thread(底层的线程)的成员:
4. 线程的执行逻辑
TcpServer启动的时候,会让自己的成员threadPool_也启动
线程池启动时,会创建事件循环线程EventLoopThread,让该EventLoopThread调用startLoop方法
接着会让事件循环对应的Thread类线程启动
这里的thread_.start方法就会调用thread_生成时传入的回调函数,即EventLoopThread::threadFunc
这样就启动了事件循环,即epoll_wait开始监听事件。
五、Acceptor类
Acceptor工作在mainLoop,主要封装了listenfd相关的操作:创建listenfd,绑定socket,bind,listen,listen成功以后就把listenfd打包成acceptChannel_扔给baseLoop监听事件
Acceptor成员变量:
- loop_:Acceptor所属的事件循环,即mainLoop
- acceptSocket_、acceptChannel_:封装了listenfd,用于监听新用户连接
六、Buffer类
缓冲区里有kCheapPrepend、readeridx、writeridx,这3个成员,[readeridx, writeridx]表示可读数据区间
prependable有8个字节,是头部信息,用于记录数据包的长度,刚开始readeridx和writeridx都在这8个字节处的位置,然后我们写数据进去,写好的东西就可以是待发送的东西,由Buffer专门进行在缓冲区读取数据进行发送,我们从缓冲区可以进行send发送,用户也可以不断从缓冲区读取数据,通过readeridx和writeridx处理
Buffer是缓冲区,对于non-blocking(非阻塞)的I/O,我们都需要设置缓冲区,涉及到应用写数据写到缓冲区,write函数将应用程序缓冲区的数据再写到TCP的发送缓冲区,最后发送数据
如果应用程序调用write直接写入TCP的发送缓冲区,写满了就要发送,应用程序就要暂停写入等待发送完成,这是同步的过程,效率比较慢。 如果有缓冲区的话,应用程序可以随意的向应用程序缓冲区写数据,等TCP缓冲区空了Buffer再将应用程序缓冲区中的数据写入TCP缓冲区。
应用产生数据快,tcp发送的慢,难道每一次发送数据都要等tcp发送完上一次的数据再发送当前数据?
写数据到Buffer,然后让系统去调用write从Buffer写入TCP发送缓冲区,或者系统调用read从TCP接收缓冲区写入Buffer。相当于应用发送或者接收数据和tcp真实发送或者接收数据就成异步的了,这样非常高效。
七、TcpConnection类
一个连接成功的客户端对应一个TcpConnection,封装了Socket,Channel,各种回调,高水位线的控制(不要发送过快),发送和接收缓冲区。
对于Channel执行的回调,都是由TcpConnection来设置的
八、TcpServer类
Acceptor得到新用户,才能通过EventLoopThreadPool::getNextLoop(),才能把新用户封装成TcpConnection,设置各种回调以后才能选择subLoop,把新的connection扔给相应的loop。