您的位置:首页 > 理论基础 > 计算机网络

muduo网络库学习(一)对io复用的封装Poller,面向对象与基于对象

2017-10-23 18:34 381 查看
高效并发的网络框架大多离不开io多路复用函数,Linux下有三种

select

poll

epoll

关于三者的区别可以参考 linux网络编程—–几种服务器模型及io多路复用函数

前段时间看Libevent源码时也学习过对epoll/poll/select的封装,但是毕竟c语言写的库,是通过函数指针实现多态。我学习的muduo源码是c++11版本的,利用c++进行封装。

首先复习一下C++面向对象和基于对象的区别

面向对象

面向对象的三大特点:封装,继承,多态缺一不可

封装:数据和处理数据的函数统一起来,封装在一个class中

继承:通过继承某个类派生出一个新类,被继承的类称作基类,派生出的类称作派生类。派生类是对基类的补充,二者之间满足一定的归属关心,如动物(基类),鸟(派生类)。继承可以是public/private/protected继承,也可以是虚继承(用于解决多重继承带来的重复问题),基类可以是抽象基类(不能被实例化),但是派生类需要重新实现基类定义的每个纯虚函数。

多态:在继承的基础上通过基类指针指向派生类的实例化对象,达到调用派生类虚函数的目的,多态又被叫做运行时多态,是在运行期根据基类指针实际指向的对象类型判断调用哪个函数的方式。

基于对象

无继承,无多态,只有封装

利用类封装好的接口实现对数据的操作

如何禁止编译器自动生成拷贝构造函数/赋值运算符

继承boost::noncopyable

自定义空基类,基类中将两个函数放在private域,派生类private继承该基类

在自己的private域中声明两个函数,不予实现

c++11版本采用第2中,boost版本采用第1中,第三种效果不好,因为错误是在链接期发现,前两个是在编译期

现如今大多C++程序都是基于对象的,面向对象只在整个程序中占一小部分比重。

muduo采用的也是基于对象的手法,但是对io多路复用的封装采用的是面向对象,即定义一个基类,派生出不同的派生类。muduo只派生了poll/epoll两个类封装,因为二者在实现上有相似性,可以共用基类。

基类Poller主要用于设计统一接口,两个派生类EPollPoller/PollPoller用于实现各自的操作,Poller定义如下

/* 禁止编译器自动生成拷贝构造函数/赋值操作运算符 */
class Poller : noncopyable
{
public:
typedef std::vector<Channel*> ChannelList;

Poller(EventLoop* loop);
virtual ~Poller();

/// Polls the I/O events.
/// Must be called in the loop thread.
/*
* 监听函数,对于epoll是epoll_wait,对于poll是poll
* 返回epoll_wait/poll返回的时间
*/
virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels) = 0;

/// Changes the interested I/O events.
/// Must be called in the loop thread.
/* 更新监听事件,增删改对fd的监听事件 */
virtual void updateChannel(Channel* channel) = 0;

/// Remove the channel, when it destructs.
/// Must be called in the loop thread.
/* 删除监听事件 */
virtual void removeChannel(Channel* channel) = 0;

virtual bool hasChannel(Channel* channel) const;

static Poller* newDefaultPoller(EventLoop* loop);

void assertInLoopThread() const
{
ownerLoop_->assertInLoopThread();
}

protected:
/*
* Channel,保存fd和需要监听的events,以及各种回调函数(可读/可写/错误/关闭等)
* 类似libevent的struct event
*/
typedef std::map<int, Channel*> ChannelMap;
/* 保存所有事件Channel,类似libevent中base的注册队列 */
ChannelMap channels_;

private:
/*
* EventLoop,事件驱动主循环,用于调用poll函数
* 类似libevent的struct event_base
*/
EventLoop* ownerLoop_;
};


类中采用前向声明,即在定义Poller之前声明一下
class Channel;
,用处是避免让头文件
#include <muduo/net/Channel.h>
从而增加依赖性,因为头文件中并没有使用Channel,只是定义了这个类型的变量,所以只声明就好了,而在成员函数的实现中需要使用Channel的接口,这就需要让编译器知道Channel是怎么定义的,就需要在
.cpp
文件中
#include <muduo/net/Channel.h>
。另外,因为Channel是Poller的成员变量,当Poller析构时也会调用Channel的析构函数,这就需要让编译器知道Channel析构函数的定义,所以Poller的析构函数需要在
.cpp
中定义。

以上也是大多muduo类采用的方法,这种方法可以降低依赖关系,如果Channel文件改变,不需要重新编译Poller文件

派生类EPollPoller的实现就是重新实现基类Poller声明的纯虚函数,简单的调用epoll的接口。在poll返回后也会将就绪的fd(muduo是由Channel管理,libevent是由struct event管理)添加到激活队列中

/*
* 对epoll函数的封装,继承自Poller
*/
class EPollPoller : public Poller
{
public:
EPollPoller(EventLoop* loop);
virtual ~EPollPoller();

/* epoll_wait */
virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels);
/* ADD/MOD/DEL */
virtual void updateChannel(Channel* channel);
/* DEL */
virtual void removeChannel(Channel* channel);

private:
static const int kInitEventListSize = 16;

/* EPOLL_CTL_ADD/MOD/DEL转成字符串 */
static const char* operationToString(int op);

/* epoll_wait返回后将就绪的文件描述符添加到参数的激活队列中 */
void fillActiveChannels(int numEvents,
ChannelList* activeChannels) const;

/* 由updateChannel/removeChannel间接调用,执行epoll_ctl */
void update(int operation, Channel* channel);

typedef std::vector<struct epoll_event> EventList;

int epollfd_;
EventList events_;
};


类的声明中的EventList记录着所有监听的epoll_event,
.cpp
中就是实现上述函数,进行增删改等,主要记录一些没接触过的知识

epoll_create1

int epoll_create(int size);
- 创建epollfd,早期linux引入的创建监听epollfd的函数,传入的参数size作为给内核的一个提示
- 内核会根据这个size分配一块这么大的数据空间用来监听事件(struct epoll_event)
- 当在使用的过程中出现大于size的值时,内核会重新分配内存空间。
- 目前这个size已经没有作用,内核可以动态改变数据空间大小,但仍然需要传入大于0的数

int epoll_create1(int flag);
- 创建epollfd,新版linux引入的函数,当flag为0时和不带size的epoll_create效果一样
- 目前flag只支持EPOLL_CLOEXEC,在创建的过程中将返回的epollfd描述符设置CLOSE-ON-EXEC属性
- 当程序exec执行新程序时自动close epollfd


static_assert

static_assert(bool flag, char *msg);

- 编译期断言,程序在编译的过程中执行
- 若flag为真,什么也不做
- 若flag为假,产生一条编译错误,输出错误信息msg,错误位置为当前行号

static_assert可以增加编译期对程序的控制,准确定位出错的可能


assert

assert(bool flag);

- 运行期DEBUG模式下的断言
- 若flag为真,什么也不做
- 若flag为假,终止程序

注意assert只有在debug模式下才会有效,在release模式下这条语句就被编译器删除了
对此,通常在assert后面有一个(void)n;等语句

int fd = channel_->fd();
assert(channels_[fd] == channel);
(void)fd;
如果以release模式下运行,assert被删除,编译器会发出警告通知,提示变量fd未使用
而如果在设置编译条件时将警告提升为错误,那么编译就不会继续进行
(void)fd;意为将fd转为void类型,简单使用一下fd,消除警告
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  muduo poll epoll 源码
相关文章推荐