muduo源码分析——Channel

本文简单分析muduo的Channel类,我在学习muduo的时候,因为一开始看的很晕,后来找了很多文章来学习,这个Channel有很多种叫法“事件分发器”,“I/O选择器”等,但我还是喜欢叫它通道,为什么呢,因为,当I/O事件发生的时候,最终会回调到Channel的回调函数中,所以我更喜欢把它当作通道,当然这也是它的中文翻译。

每一个Channel都对应唯一的EventLoop,也就是一个I/O线程,所以一般不用担心Channel的线程安全问题,因为别的线程不会操作到当前的这个Channel。

我们看看Channel类的成员:

		private:
		bool update();
		void handleEventWithGuard(Timestamp receiveTime);//真正被调用的方法,用来执行判断事件类型,并执行绑定的函数

		static const int            kNoneEvent;//0
		static const int            kReadEvent;//=XPOLLIN | XPOLLPRI  其中XPOLLIN=0x0100|0x0200,XPOLLPRI=0x0400
		static const int            kWriteEvent;//0x0010

		EventLoop*                  loop_;//属于的EventLoop
		const int                   fd_;//对应的文件描述符,不过并不拥有
		int                         events_;
		int                         revents_; // it's the received event types of epoll or poll
		int                         index_; // used by Poller.
		bool                        logHup_;

		std::weak_ptr<void>         tie_;           //std::shared_ptr/std::shared_ptr可以指向不同的数据类型
		bool                        tied_;//应该是表明,上面的tie_是否指向了对象
		//bool                        eventHandling_;
		//bool                        addedToLoop_;
		ReadEventCallback           readCallback_;
		EventCallback               writeCallback_;
		EventCallback               closeCallback_;
		EventCallback               errorCallback_;
	};

关于这个events_我将他当作一个标志,标记通道的状态。

在TcpConnection中哪这个例子来看看

   if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)

这是在TcpConnection::sendInLoop函数中的一个判断,当channel没有在写,且输出缓冲区没有数据的时候,直接调用socket的write去写数据,我们看看这个iswriting函数

bool isWriting() const { return events_ & kWriteEvent; }

显然当isWriting这个函数返回false的时候,才会写数据。从前面的成员变量,我也注释了,kWriteEvent=0x0010,二进制是0000 0000 0001 0000,那么events必须是xxxx xxxx xxx0 xxxx才能够算出0这个结果(x表示0和1都可以,第5个数必须是0)。

我们看看Channel的初始化

Channel::Channel(EventLoop* loop, int fd__)
  : loop_(loop),
    fd_(fd__),
    events_(0),
    revents_(0),
    index_(-1),
    logHup_(true),
    tied_(false),
    eventHandling_(false),
    addedToLoop_(false)

可以看出,events_一开始被初始化为0了,所以一开始是可以写的,那么我们看看什么时候会导致events_第5个数变为1.

我找了如下几个会修改events_的函数:

bool Channel::enableReading() 
{ 
    events_ |= kReadEvent;//=XPOLLIN | XPOLLPRI  其中XPOLLIN=0x0100|0x0200,XPOLLPRI=0x0400
    return update();//最终将事件添加到epoll轮询器内
}

bool Channel::disableReading()
{
    events_ &= ~kReadEvent; //=XPOLLIN | XPOLLPRI  其中XPOLLIN=0x0100|0x0200,XPOLLPRI=0x0400
    
    return update();
}

bool Channel::enableWriting() 
{
    events_ |= kWriteEvent; //0x0010
    
    return update(); 
}

bool Channel::disableWriting()
{ 
    events_ &= ~kWriteEvent; //0x0010
    return update();
}

bool Channel::disableAll()
{ 
    events_ = kNoneEvent; //0
    return update(); 
}

注意到kReadEvent是在16进制第3位上的,所以看起来不关write的事了。
那么我们看看上面函数中events_和kWriteEvent进行操作的情况。其实就两个disable和enable。前面说了:
kWriteEvent=0000 0000 0001 0000
那么对于enable来说:events_ |=kWriteEvent,很明显会让events_的第五位变为1,所以当执行了enable后,对于下面这个函数来说:

bool isWriting() const { return events_ & kWriteEvent; }

明显会返回true,那么在最开始这个函数中:

   if (!channel_->isWriting() && outputBuffer_.readableBytes() == 0)

if判断就会失败,而不会执行里面的代码。
我觉得这个isWriting名字起得不好,因为它是想判断是否可以写,应该改叫iswritable会更容易让人理解,为什么这么说呢,
同样看看TcpConnection内的sentInLoop后半段

    if (!faultError && remaining > 0)
    {
        size_t oldLen = outputBuffer_.readableBytes();
        if (oldLen + remaining >= highWaterMark_
            && oldLen < highWaterMark_
            && highWaterMarkCallback_)
        {
            loop_->queueInLoop(std::bind(highWaterMarkCallback_, shared_from_this(), oldLen + remaining));//高水位了
        }
        outputBuffer_.append(static_cast<const char*>(data) + nwrote, remaining);//把没写的添加到输出缓存区
        //就在刚刚append了数据进去,所以现在是有数据可以写的,接着往下看
        if (!channel_->isWriting())//如果现在channel是不可以写的,因为没有数据
        {
            channel_->enableWriting();//但是,因为刚刚写了数据进去了,所以要enable。
        }
    }

看看我写在后半段的注释是不是很有道理,如果有偏差的地方,还请大佬们指正。

关于disable,通过~取反符号让kWriteEvent的第五位变为0 ,然后通过&操作符,让events_的第五位变为0。也就不难理解了。

kReadEvent

为什么要加个大标题呢,纯粹是为了转换目标,不然怕看着看着就搞混了。
kReadEvent是0000 0111 0000 0000,稍微有点复杂,但其实也不复杂,为什么这么说呢,并有没有发现,有对events_按某一位进行操作的函数,所以,其实三个1和一个1的效果是一样的。具体就不展开论述了,因为都是差不多的操作。

聊完了events_这个标志,那么我们开始看看别的吧。

handleEventWithGuard函数

void Channel::handleEventWithGuard(Timestamp receiveTime)
{
	LOGD(reventsToString().c_str());
	if ((revents_ & XPOLLHUP) && !(revents_ & XPOLLIN))//仅用于内核设置传出参数revents,表示设备被挂起,如果poll监听的fd是socket,表示这个socket并没有在网络上建立连接,比如说只调用了socket()函数,但是没有进行connect。
	{
		if (logHup_)
		{
			LOGW("Channel::handle_event() XPOLLHUP");
		}
		if (closeCallback_) closeCallback_();
	}

	if (revents_ & XPOLLNVAL)//仅用于内核设置传出参数revents,表示非法请求文件描述符fd没有打开
	{
		LOGW("Channel::handle_event() XPOLLNVAL");
	}

	if (revents_ & (XPOLLERR | XPOLLNVAL))//仅用于内核设置传出参数revents,表示设备发生错误
	{
		if (errorCallback_) 
            errorCallback_();
	}
    
	if (revents_ & (XPOLLIN | XPOLLPRI | XPOLLRDHUP))//读事件,读事件但表示紧急,Stream socket的一端关闭了连接
	{
		//当是侦听socket时,readCallback_指向Acceptor::handleRead
        //当是客户端socket时,调用TcpConnection::handleRead 
        if (readCallback_) 
            readCallback_(receiveTime);
	}

	if (revents_ & XPOLLOUT)
	{
		//如果是连接状态服的socket,则writeCallback_指向Connector::handleWrite()
        if (writeCallback_) 
            writeCallback_();
	}
	//eventHandling_ = false;
}

revents_也是一个标志,这个标志被初始化为了0,但是在外面调用的时候,会被设置。由于我是参考的flamingo,所以我参考的是flamingo的EPollPoller类

		channel->set_revents(events_[i].events);

events_在这里是EPollPoller的一个成员,不是Channel那个标志

		typedef std::vector<struct epoll_event> EventList;

		int                 epollfd_;
		EventList           events_;//eventslist是一个事件数组,用vector存储,元素类型为epoll_event的结构体

epoll事件可以参考如下代码,具体的含义上网查,不复制粘贴了,可以考出,revents_其实也是一个通过位来操控事件的标志,同events_类似。

#define POLLRDNORM  0x0100
#define POLLRDBAND  0x0200
#define POLLIN      (POLLRDNORM | POLLRDBAND)
#define POLLPRI     0x0400

#define POLLWRNORM  0x0010
#define POLLOUT     (POLLWRNORM)
#define POLLWRBAND  0x0020

#define POLLERR     0x0001
#define POLLHUP     0x0002
#define POLLNVAL    0x0004

那么Channel类的介绍基本就到这里了吧,至于Channel的生命结束,由TcpConnection负责销毁,因为TcpConnection持有了Channel的shared_ptr指针,shared_ptr会在最后自动调用析构函数。
然后就是tie_这个week_ptr指针,muduo通过lock这个指针,使得计数+1,那么便可以由代码来控制Channel的销毁,防止连接突然被onClose,导致Channel一起被销毁,而数据还没处理完毕。

你可能感兴趣的:(c++网络编程)