从epoll构建muduo-4 加入Channel

来源:互联网 发布:大数据入门书籍 编辑:程序博客网 时间:2024/05/16 10:00

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?在CODE上查看代码片派生到我的代码片
  1.  7 class Channel  
  2.  8 {  
  3.  9     public:       
  4. 10         Channel(int epollfd, int sockfd);  
  5. 11         ~Channel();  
  6. 12         void setCallBack(IChannelCallBack* callBack);  
  7. 13         void handleEvent();  
  8. 14         void setRevents(int revent);  
  9. 15         int getSockfd();  
  10. 16         void enableReading();  
  11. 17     private:  
  12. 18         void update();  
  13. 19         int _epollfd;  
  14. 20         int _sockfd;  
  15. 21         int _events;  
  16. 22         int _revents;  
  17. 23         IChannelCallBack* _callBack;  
  18. 24 };  

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

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. 117     pChannel->setCallBack(this);  
  2. 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?在CODE上查看代码片派生到我的代码片
  1. 50     ev.data.fd = listenfd;  
  2. 51     ev.events = EPOLLIN;  
  3. 52     epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);  

当前版本

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. 45     struct epoll_event ev;  
  2. 46     ev.data.ptr = this;  
  3. 47     ev.events = _events;  
  4. 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?在CODE上查看代码片派生到我的代码片
  1. typedef union epoll_data {  
  2.     void        *ptr;  
  3.     int          fd;  
  4.     uint32_t     u32;  
  5.     uint64_t     u64;  
  6. } epoll_data_t;  
  7.   
  8. struct epoll_event {  
  9.     uint32_t     events;      /* Epoll events */  
  10.     epoll_data_t data;        /* User data variable */  
  11. };  
data字段的作用是当事件发生后,可以让epoll的使用者获得这个事件的信息,v0.01版本只保存了一个sockef描述符在里面,这样我们必须通过再额外建立一个"描述符->回调"映射才能找到注册的函数,然后去调用它。v0.03直接将Channel的指针保存到data字段中,直接解决了这个问题,因为一个Channel对象可以保存任意的信息,回调放到其成员变量中即可,当然了Channel中也保存着socket描述符。

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

[cpp] view plaincopyprint?在CODE上查看代码片派生到我的代码片
  1. 122         vector<Channel*> channels;  
  2. 123         int fds = epoll_wait(_epollfd, _events, MAX_EVENTS, -1);  
  3. ...         ...  
  4. 129         for(int i = 0; i < fds; i++)  
  5. 130         {  
  6. 131             Channel* pChannel = static_cast<Channel*>(_events[i].data.ptr);  
  7. 132             pChannel->setRevents(_events[i].events);  
  8. 133             channels.push_back(pChannel);  
  9. 134         }  
  10. 135   
  11. 136         vector<Channel*>::iterator it;  
  12. 137         for(it = channels.begin(); it != channels.end(); ++it)  
  13. 138         {  
  14. 139             (*it)->handleEvent();  
  15. 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 引入了内存泄漏,代码中有注释。

经过这个版本,代码的问题还很多,后续改进。


0 0
原创粉丝点击