您的位置:首页 > 其它

从epoll构建muduo-4 加入Channel

2015-12-04 18:10 459 查看
mini-muduo版本传送门

version 0.00 从epoll构建muduo-1 mini-muduo介绍

version 0.01 从epoll构建muduo-2 最简单的epoll

version 0.02 从epoll构建muduo-3 加入第一个类,顺便介绍reactor

version 0.03 从epoll构建muduo-4 加入Channel

version 0.04 从epoll构建muduo-5 加入Acceptor和TcpConnection

version 0.05 从epoll构建muduo-6 加入EventLoop和Epoll

version 0.06 从epoll构建muduo-7 加入IMuduoUser

version 0.07 从epoll构建muduo-8 加入发送缓冲区和接收缓冲区

version 0.08 从epoll构建muduo-9 加入onWriteComplate回调和Buffer

version 0.09 从epoll构建muduo-10 Timer定时器

version 0.11 从epoll构建muduo-11 单线程Reactor网络模型成型

version 0.12 从epoll构建muduo-12 多线程代码入场

version 0.13 从epoll构建muduo-13 Reactor + ThreadPool 成型

mini-muduo v 0.03版本,这是个版本最重要的修改是加入了一个名为Channel的类。完整可运行的示例可从github下载,使用命令git checkout v0.03可切换到此版本,在线浏览此版本到这里

介绍一下Channel类,先看其声明,这里特别要注意_events和_revents,前者是要关注的事件,后者是发生的事件,不仔细看容易混淆。名字的来源是poll(2)的struct pollfd

[cpp] view
plaincopyprint?





 7 class Channel  

 8 {  

 9     public:       

10         Channel(int epollfd, int sockfd);  

11         ~Channel();  

12         void setCallBack(IChannelCallBack* callBack);  

13         void handleEvent();  

14         void setRevents(int revent);  

15         int getSockfd();  

16         void enableReading();  

17     private:  

18         void update();  

19         int _epollfd;  

20         int _sockfd;  

21         int _events;  

22         int _revents;  

23         IChannelCallBack* _callBack;  

24 };  

按照作者描述"每个Channel对象自始至终只负责一个文件描述符的IO事件分发"。我是这么理解的,Channel把socket文件描述符和关心这个描述符的回调捆绑在了一起,之前的v0.01版本,程序在调用epoll_wait获得事件后,直接就进行了事件处理,现在通过添加Channel,程序终于可以将事件处理程序写在一个单独的函数中,然后将这个函数注册到Channel上。这个注册的过程比较关键,在TcpServer.cc的117和118行,下面这两句调用

[cpp] view
plaincopyprint?





117     pChannel->setCallBack(this);  

118     pChannel->enableReading();  

117行负责把回调指针传递给Channel(muduo使用的是boost::function),这个指针指向实现IChannelCallback接口的一个对象,也就是TcpServer本身了。setCallBack只是将这个指针保存在成员变量_callBack中,等待后续调用。

118行enableReading()的实现有两步,首先将_events(注意和_revents区别)里加入EPOLLIN标记,然后通过update()将事件真正注册到epollfd上,这是最关键的步骤。调用epoll_ctl注册的过程和v0.01版本有微小的差别,这个差别也很关键。

v0.01版本是这样的

[cpp] view
plaincopyprint?





50     ev.data.fd = listenfd;  

51     ev.events = EPOLLIN;  

52     epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);  

当前版本

[cpp] view
plaincopyprint?





45     struct epoll_event ev;  

46     ev.data.ptr = this;  

47     ev.events = _events;  

48     epoll_ctl(_epollfd, EPOLL_CTL_ADD, _sockfd, &ev);  

差别在于ev.data.fd = listenfd 和 ev.data.ptr = this

使用man epoll_ctl来查看一下epoll_event的定义,data字段是一个union,所以可以存放任何64位长度的内容。

[cpp] view
plaincopyprint?





typedef union epoll_data {  

    void        *ptr;  

    int          fd;  

    uint32_t     u32;  

    uint64_t     u64;  

} epoll_data_t;  

  

struct epoll_event {  

    uint32_t     events;      /* Epoll events */  

    epoll_data_t data;        /* User data variable */  

};  

data字段的作用是当事件发生后,可以让epoll的使用者获得这个事件的信息,v0.01版本只保存了一个sockef描述符在里面,这样我们必须通过再额外建立一个"描述符->回调"映射才能找到注册的函数,然后去调用它。v0.03直接将Channel的指针保存到data字段中,直接解决了这个问题,因为一个Channel对象可以保存任意的信息,回调放到其成员变量中即可,当然了Channel中也保存着socket描述符。

在epoll_wait()返回事件后,有两块逻辑要执行

[cpp] view
plaincopyprint?





122         vector<Channel*> channels;  

123         int fds = epoll_wait(_epollfd, _events, MAX_EVENTS, -1);  

...         ...  

129         for(int i = 0; i < fds; i++)  

130         {  

131             Channel* pChannel = static_cast<Channel*>(_events[i].data.ptr);  

132             pChannel->setRevents(_events[i].events);  

133             channels.push_back(pChannel);  

134         }  

135   

136         vector<Channel*>::iterator it;  

137         for(it = channels.begin(); it != channels.end(); ++it)  

138         {  

139             (*it)->handleEvent();  

140         }  

第一步129行到134行 ,遍历所有的事件,从其data字段中拿出和这个socket相关的Channel指针,并且将其_revents(注意和_events区别)字段填充好,最后将Channel插入到vector中
第二步136行到140行,遍历vector,逐一调用其中的handleEvent方法。handleEvent方法里会直接调用_callBack的OnIn方法,把事件送给注册好的回调进行处理。

这里之所以分成两个步骤而不是一边遍历fds一边调用handleEvent(),是由于后者会添加或删除Channel,从而造成fds在遍历期间改变大小,这是非常危险的。作者在书中有提及(P285)。

v0.03版本重要修改介绍完了,下面是一些小改动和注意事项

1加入了防止头文件重复引用的#ifndef系列宏

2加入前置声明 和专门用于前置声明的Declear.h和宏定义Define.h

3 引入了内存泄漏,代码中有注释。

经过这个版本,代码的问题还很多,后续改进。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: