muduo

讲讲muduo的channel

对比一下,epoll在监听fd及其发生事件的时候,需要通过epoll_ctl将fd和该fd感兴趣的事件注册到epoll多路复用模块中。当epoll监听到事件发生后,会将发生事件的fd和fd对应的事件返回(通过event结构体)。
channel类采用面对对象的思想,对fd,fd感兴趣的事件以及监听到具体发生的事件封装起来。同时channel保存了不同事件对应的处理函数。
channel提供了修改感兴趣事件的方法,设置处理函数的方法,以及发生不同事件时调用不同处理函数的方法。

channel包装了事件,那么事件的分发交给谁?

channel的分发由eventloop负责,每个channel都会属于一个eventloop。channel类中会有一个eventloop指针。

channel提供了对感兴趣事件的修改,那修改后是如何通知到多路复用组件的?

channel提供了update方法,该方法会通过eventloop调用到poller,poller来更新

channel会和一个具体的tcp connection绑定,是怎么判断tcp connection是不是存在的呢?

channel会用一个weak_ptr指向一个对象,在调用自己处理事件的函数时,会使用weak_ptr.lock方法尝试转化为一个shared_ptr方法。如果成功说明对象存在,不成功说明对象失败。

muduo库支持什么样的多路复用组件?

muduo库支持Poller和EPollPoller,我只实现了EPollPoller。

EPollPoller类提供了什么样的功能?

对比一下,epoll在监听fd及其发生事件的时候,需要通过epoll_ctl将fd和该fd感兴趣的事件注册到epoll多路复用模块中。当epoll监听到事件发生后,会将发生事件的fd和fd对应的事件返回(通过event结构体)。
EpollPoller采用面对对象的思想将epoll封装了起来,将epoll_create封装在EpollPoller的构造函数中,epoll_ctl封装在update中,epoll_wait封装在poll中。

epoll函数需要一个epoll_event数组保存返回的事件,你在封装中使用的是什么?为什么?

我在封装中使用的vector,vector具备动态扩容的机制。当返回活跃事件的个数等于vector长度的时候,动态扩容为原来的两倍。该步封装在了epoll_ctl中。

poller怎么更新channel的?

poller根据通过index来。
channel中会保存一个index,该index表示channel所处的状态。-1表示新的channel,1添加,2删除。
如果当前channel,是没有被添加过或者是删除状态,则将其用add添加。并修改状态位添加。
否则,判断有没有感兴趣的事件类型,没有则直接设置为删除状态。有则,使用mod修改,不改变状态。
注意index的设置是通过poller来的。

Epollpoller怎么返回感兴趣的事件?

在注册到epoll中的时候,event_data中的ptr会携带对应的channel指针。从而在返回感兴趣的事件中,直接用指针能得到channel对象。

为什么更新需要index?

判断channel的状态,逻辑调用清晰。

epollPoller删除channel怎么删除的?

从poller map中保存的删除对应的channel,根据状态在多路复用组件上删除对应的channel。将channel状态设置为new。

muduo库eventloop类是怎么设计的?用来干什么?

采用面对对象的设计思想,设计了channel和poller两个组件。还需要一个类将它们调度起来,eventloop类是网络服务器中负责循环的重要模块,它能做到持续监听、持续获取监听结果、持续处理监听结果对应的事件。

eventLoop经常向其他loop注册事件执行,是通过哪些成员达成这一目的的?

eventLoop中有两个比较的成员,一个是wakeupFd,一个是pendingFunctors。在eventLoop初始化的时候,wakeupFd就被注册到了eventLoop自己所属的poller中,并关注epollin读事件。此后想要唤醒eventLoop,则向该wakeupFd写入数据。
同时,会向loop的pendingFunctors中添入想要执行的回调函数。
eventLoop循环被唤醒后,会去执行pendingFunctors中的所有回调函数。
这里的wakeupFd使用的是eventFd。

eventloop的loop循环中执行了哪些方法?

执行了两种方法,一种是channel发生并注册的对应事件,一种是其余loop添加的回调函数。

eventloop采用了one loop per thread的方法,如何保证添加的回调loop在对应的线程中执行呢?

muduo库保存了一个线程局部变量threadId,并将该值保存在了eventloop对象中。判断的时候读取当前threadId和eventloop中的threadId进行比较,如果一致就执行。不一致,加入到pendingFunctors中,并唤醒对应的eventloop执行。

muduo的one loop per thread设计是怎么实现的?

muduo有一个EventLoopThread,其中保存了loop对象和thread的对象,它们是一对一的。
具体的,EventLoopThread的stratLoop方法,会启动该thread对象的运行。thread对象绑定的线程方法会在栈上创建一个EventLoop对象并将其loop方法开启,之后返回EventLoop对象的执行。两者之间通过锁和信号量同步。
该loop对象是放在线程的栈中的,会随着线程消亡而消亡,其所对应的EventLoop对象也会消亡。

怎么防止一个线程创建多个eventloop?

eventloop中设置一个线程 thread local变量。是指针。 指针指向该线程创建好的eventloop变量,如果不为null,则说明已经创建好了。

muduo库会创建无数个线程吗?

不会,muduo提供了一个EventLoopThreadPool类。该类是一个EventLoopThread线程池。这个类在开始的时候会指定线程的个数。并通过start方法,将所有线程启动,并保存每个线程的eventloop指针。

EventLoopThreadPool作用在哪里?

它有一个getNextLoop方法,通过轮询的方式,返回一个loop,共baseloop使用。

Acceptor类做了什么事情?

类比我们在开发一个普通server的时候,需要创建一个socket,将该socket和ip地址+port绑定,并开启为监听状态。当有新连接来临的时候,调用accept接受连接。
因此Acceptor类中有一个socket用来监听连接,该socket被封装为channel并注册在poller中。于是,有一个acceptChannel和一个eventloop。
该eventloop运行在baseloop中。
当acceptChannel被注册在poller中时,会关注它的读事件,读回调是创建方为它设置的。其实际作用是,选择一个subloop,将新连接添加到他的poller中。

TcpServer类做了什么事情?

TcpServer中有一个loop对象,该loop为baseloop,tcpserver的acceptor对象运行在baseloop上。当有新的连接通过acceptor返回后,会被TcpServer打包成一个tcpconnection对象。
之后TcpServer从自己所拥有的线程池线程中选择一个线程,使用其中的loop,称为subloop来服务该tcpconnection。当其套接字发生相应的事件,执行相应的回调。
所以,TcpServer中有loop对象,acceptor对象,EventLoopThreadPool线程池和tcpconnection连接。

Acceptor是在tcpserver中的,它会接受新的连接,这个回调是TcpServer设置的,具体讲讲。

这个回调函数是TcpServer中的newConnection。该函数将本端(调用系统函数)和对端(传入)ip+port得到,之后从线程池里面轮询得到一个eventloop。之后,构造一个tcpconnection对象。将该对象的各种信息回调设置给tcpconnection。注意,这里的回调比较有意思,其传递链为用户设置给TcpServer=>TcpConnection=>Channel=>Poller=>notify channel调用回调。
并通过得到的eventloop,异步执行TcpConnection的connectEstablished。该方法是将tcpconnection放到subloop中执行。

TcpServer有个启动过程,启动的时候他干了什么?

启动的时候,先启动subloop线程池,再将acceptor的accept fd开启listen,并在loop的epoll中使能读事件。

你说了创建,tcpconnection的关闭回调的连接是客户给的吗?

不是,是tcpserver自己设置的。它会在自己记录的map中去掉tcpconnection,同时在ioloop中唤醒,并调用TcpConnection::connectDestroyed方法。

说说TcpConnection类

类比我们在开发一个普通server的时候,当有一个新的连接到来的时候,我们会接受连接并将其注册到epoll中。TcpConnection对新来的连接进行了封装。
它拥有的对象为连接的socket,将socket封装的channel,以及server设置的连接,读写等等回调函数。

TcpConnection发送数据的方法怎么调用的?在哪里调用?

send方法可以使用在定义server onMessage的时候。
send中有sendInLoop的函数,该函数在同步/异步的状态下执行发送信息的操作。
进入sendInLoop的逻辑中,若第一次发送数据,则使用write系统函数向channel的fd中写数据。
如果发送完成,则在loop中注册一个写完成的回调。
如果没有发送完成,则将让channel关注写操作,并唤醒epoll,执行写操作。
(注意:这里会对缓冲区的数据长度做判断(原有的+新来的),如果超过了水位线,则触发高水位回调。)
channel写操作,它将buffer中的数据仅可能的写,如果写完了,则关闭写事件,并执行一个写结束回调。

TcpConnection调用shutdown是什么时候?它是怎么执行的?

shutdown方法可以使用在定义server onMessage的时候,发送完消息。
在自己的线程执行shutDownInLoop。
如果channel没有写事件,则说明channel里面没有数据。关闭socket的写端,会触发epoll中的EPOLLUP事件,调用closeback回调。该closeback是tcp的handelclose(置为关闭状态,取消所有感兴趣的事件)方法。
这里有一点比较有趣,如果还有在写的操作,shutDownInLoop不会执行。只是在这里设置为了kDisconnecting。在write写完数据之后,会判断状态,如果是kDisconnecting再去执行shutdownInLoop。

TcpConnection调用connectEstablished是什么时候?它是怎么执行的?

Tcpserver调用了这个方法,当连接到来后,在初始化该资源后,server会强制执行该方法。
连接成功,将connecting改编为connected。
将channel与tcpconnection绑定,防止conn被销毁后,还在channel中调用。
注册channel读事件。调用连接建立的回调。

TcpConnection调用connectDestroyed是什么时候?它是怎么执行的?

该方法通过tcpserver的removeConnection最终被注册到了TcpConnection的closeCallBack中。
设置状态为断开连接。将channel所有感兴趣的事件关闭。将channel从poller中删除。

TcpConnection有不同的注册事件,分别的作用是什么讲讲?

handleRead读数据读到buffer中,调用设置好的messageCallback_。如果读到数据为0,说明触发读事件但是没有发过来的了,则调用handelclose关闭。
handleWrite从buffer将数据写到fd中。写之后,判断有没有都写完,写完调用一个全部完成的回调。如果状态正在等待关闭连接,再去执行shutdownInLoop。
handclose设置为关闭状态。

TcpConnection继承自enable_shared_from_this,为什么?

TcpConnection是唯一一个继承自enable_shared_from_this的类。
TcpConnection的生命周期比较模糊,库使用方会看到TcpConnection,同时由于回调函数的链条为:TcpServer=>TcpConnection=>Channel=>Poller=>notify channel。库使用方在构造TcpServer的时候,会写好连接建立/断开,可读写事件的回调,该回调中的事件需要使用TcpConnection。为了防止TcpConnection对象销毁带来的函数调用错误,使用shared_from_this()方法将智能指针传递到这些事件回调中,从而实现合理的生存期控制。

TcpConnection定义了不同的状态,你能讲讲在什么时候设置的吗?

创建的时候设置成了kConnecting
在将channel与tcpconnection绑定,并将其设置为关注读事件后,设置为kConnected。
在调用shutdown后,变为kDisconnecting状态。
在触发closeback后,变为kDisconnected状态。
设置这些状态是有用的,比如在写消息的时候,如果状态正在等待关闭连接,说明之前想要关闭连接的时候,因为写操作没有关闭,现在再去执行shutdownInLoop。

channel有个buffer类,说说这个类。


Buffer类封装了用户缓冲区,以及向其中读写数据等方法。
初始化状态。CheapPrepend记录整块数据的长度。
一段时间后,向buffer中写入数据。数据分区为read(可读),write(可写)。其中,readIdx标志可读区域起始位置,writeIdx标志可写区域起始位置。
从可读区域中读取一部分数据后,会有部分buffer区域空闲。
继续向数据区写入数据,可读区域变大,但是超出了buffer的大小。但是算上空白区域,buffer还可以容纳整个数据。
整体数据向前调整。
继续写入数据,但是这次算上空白区域也无法满足数据要求,则扩容。

posted @ 2024-05-21 23:57  qirmcmww  阅读(17)  评论(0)    收藏  举报