本文简单分析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是0000 0111 0000 0000,稍微有点复杂,但其实也不复杂,为什么这么说呢,并有没有发现,有对events_按某一位进行操作的函数,所以,其实三个1和一个1的效果是一样的。具体就不展开论述了,因为都是差不多的操作。
聊完了events_这个标志,那么我们开始看看别的吧。
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一起被销毁,而数据还没处理完毕。