您的位置:首页 > 其它

从epoll构建muduo-4 加入Channel

2013-04-15 12:56 316 查看
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 引入了内存泄漏,代码中有注释。

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