从epoll构建muduo-4 加入Channel

来源:互联网 发布:计步器软件免费下载 编辑:程序博客网 时间:2024/05/16 16:01

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

  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行,下面这两句调用

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版本是这样的

 50     ev.data.fd = listenfd; 51     ev.events = EPOLLIN; 52     epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);

当前版本

 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位长度的内容。

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()返回事件后,有两块逻辑要执行

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 引入了内存泄漏,代码中有注释。

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


原创粉丝点击