去年看muduo网络库时没有总结博文,导致前段时间用muduo时发现好多东西都模模糊糊,于是就花时间又一次翻阅了muduo源码,并用此篇博文记录下其网络库整体脉络,以便是后来者入手起来更方便,同时也可用作自己以后复习的资料
1.如何入手muduo库
其实在这之前也没有尝试去分析一个网络库的总脉络,更没有去尝试把它以博文的形式总结下来。那么我们该如何尝试去分析这样一个网络库的整体脉络呢?其实我有想过将muduo/net中的每一个功能类详细的一一介绍,但是后来我感觉没必要,这样只会让初学者更加找不着北。基于此种情况,我打算从muduo库提供给用户最外层的TcpServer类入手,然后由外到内的道出其整体脉络
2.由外到内分析muduo
(1)TcpServer类
muduo库为用户提供了一个TcpServer类(定义在TcpServer.h中),该类就是用来生成整个muduo网络库架构的原始类。用户只需要创建一个该类的对象,那么一个搭载这整个muduo网络库的网络架构就形成了。要揭开该类的神秘面纱,那就先来看看它的数据成员
TcpServer类数据成员
typedef std::map<string, TcpConnectionPtr> ConnectionMap;
EventLoop* loop_;
const string ipPort_;
const string name_;
boost::scoped_ptr<Acceptor> acceptor_;
boost::shared_ptr<EventLoopThreadPool> threadPool_;
ConnectionCallback connectionCallback_;
MessageCallback messageCallback_;
WriteCompleteCallback writeCompleteCallback_;
ThreadInitCallback threadInitCallback_;
AtomicInt32 started_;
int nextConnId_;
ConnectionMap connections_;
在介绍其数据成员之前得先为读者介绍muduo库中对几种网络“组件”的抽象
(1)TcpConnection:该类是对网络中各个TCP套接字连接的抽象
(2)EventLoop:该类算是对网络中I/O复用的整个流程块的抽象
(3)Acceptor:该类是对连接处理的一个抽象
(4)EventLoopThreadPool:该类是对多线程搭载多个EventLoop的抽象
好了介绍完上面那几个抽象类,我们在回过头来看TcpServer类的数据成员就会清楚明白很多,本篇博文中在介绍每个类的数据成员时我都不会也不可能做到细无巨细,所以我只会挑最重要的来介绍
在介绍之前请读者先在自己脑海中想象一下,如果让你实现一个TcpServer类,并通过该类直接就可以创建一个网络服务器的网络架构。那么该类中必须等有些什么数据成员呢?
(1)首先必须得有一个I/O复用吧,这因该是一个网络库最基础的底层架构了。所以muduo的TcpServer类中定义了对I/O复用抽象之后的EventLoop类变量loop_
(2)I/O复用有了,那么我们得有个监听套接字,并通过处理该套接字来获得新连接的类吧。没错上面所介绍的几个抽象类中的Acceptor就是我们这里所需要的哪个类,所以数据成员中定义了由boost库智能指针boost::scoped_ptr所管理的Acceptor对象
(3)我们获得连接以后因该如何管理维护这些连接(对应抽象类TcpConnection)呢?TcpServer类是通过定义一个std::map通过每一个连接的名字来找到对应的连接来维护管理TcpConnection的,对应数据成员中的connections_
(4)作为一个网络库,如何能够实现多个”one loop per thread”呢?EventLoopThreadPool的对象threadPool_就是来实现此功能的
(5)通过上面的介绍我想一个基本的TcpServer类的数据成员已经有了,唯一缺少的就是用户处理各种事件的回调函数了
(5)中所说的回调函数就是TcpServer类数据成员中的
//新连接回调
ConnectionCallback connectionCallback_;
//消息回调
MessageCallback messageCallback_;
//写完成回调
WriteCompleteCallback writeCompleteCallback_;
到此为止一个TcpServer类的基础数据成员我已介绍完了,那么TcpServer类通过使用这些数据成员从而可以为我们提供哪些接口类呢?
根据我本篇博文去翻从简的宗旨,只列出几个最基础和重要的接口
接口如下
//该接口用来设置server中需要运行多少个loop线程
void setThreadNum(int numThreads);
//调用该接口就意味着启动了该TcpServer架构
void start();
//调用该接口用来设定用户自定义消息回调
void setMessageCallback(const MessageCallback& cb);
//调用该接口用来设置用户自定义连接回调
void setConnectionCallback(const ConnectionCallback& cb);
//调用该接口用来设置用户自定义写完成回调
void setWriteCompleteCallback(const WriteCompleteCallback& cb);
(2)EventLoop类
上面对TcpServer数据成员的介绍顺序我感觉其实就是一个服务器设计中一个流程的走向图。所以我也打算按这个顺序来一一介绍muduo库的脉络
首先介绍的就是EventLoop(定义在EventLoop.h中)这个封装着I/O复用的类了。
EventLoop数据成员如下
boost::scoped_ptr<Poller> poller_;
boost::scoped_ptr<TimerQueue> timerQueue_;
boost::scoped_ptr<Channel> wakeupChannel_;
ChannelList activeChannels_;
std::vector<Functor> pendingFunctors_;
(1)不言而喻EventLoop首先一定得有个I/O复用才行,否则说它的一切都是扯淡,因为它的所有职责都是建立在I/O复用之上的,其I/O复用就是类Poller(在头文件Poller.h中)的对象poller_该类为原始I/O复用epoll和poll封装的基类,所以我们既可以用它来指向poll类型的I/O复用类也可以指向epoll类型的I/O复用类
(2)该EventLoop循环中不仅要支持普通读写事件,还应该支持定时事件,关于定时器的所有操作和组织定义都在类TimerQueue中
(3)当非io线程(搭载EventLoop的线程)想使某个任务放在io线程中来执行,那么就可以将其放到数据成员pendingFunctors_中来其对应的事件便是wakeupChannel_事件,即若此事件发生便会一次执行pendingFunctors_中的可调用对象
(4)activeChannels_中保存的是poller类中的poll调用返回的所有活跃事件集
EventLoop类提供的主要接口
//此接口为该类的核心接口,用来启动事件循环
void loop();
//此俩接口用来将非io线程内的任务放到pendingFunctors_中并唤醒wakeupChannel_事件来执行任务
void queueInLoop(const Functor& cb);
void runInLoop(const Functor& cb);
//此3个接口用来添加定时任务
TimerId runAt(const Timestamp& time, const TimerCallback& cb);
TimerId runAfter(double delay, const TimerCallback& cb);
TimerId runEvery(double interval, const TimerCallback& cb);
//除此之外EventLoop还提供了对Channel类型的各种操作
void updateChannel(Channel* channel);
void removeChannel(Channel* channel);
(3)Acceptor类
通过(2)描述,我们服务器的基本架构I/O复用已经有了,接着就是我们对新连接因该如何处理的问题了,对此muduo封装出了Acceptor类(在Accept.h中)。
Acceptor的主要数据成员
EventLoop* loop_;
Socket acceptSocket_;
Channel acceptChannel_;
NewConnectionCallback newConnectionCallback_;
int idleFd_;
(1)还是前面的分析方法Acceptor类为接受连接的抽象,那么它首先就得有数据成员acceptSocket_(对监听套接字的封装)和其对应的事件acceptChannel_
(2)接着就是监听事件放在哪个loop循环中,所以也定义了数据成员loop_
(3)当新连接到来时我们该如何处理?于是就有了newConnectionCallback_这个回调成员
(4)关于idlefd_我们稍后来说明它为什么会出现在Acceptor类中
Acceptor类提供的接口
//该接口用来设置新连接处理回调
void setNewConnectionCallback(const NewConnectionCallback& cb);
//该接口用来启动监听套接字
void listen();
好了,现在我来补充刚才(4)中为介绍用途的idlefd成员。该成员是打开一个空洞文件(/dev/null)后返回的文件描述符,我们都知道空洞文件既不能读也不能写,那Acceptor类中拥有这样一个成员的用意何在呢?要说明白这个得先让读者看看Acceptor类中的处理新连接到来的私有函数handleRead
handleRead内容如下
void Acceptor::handleRead()
{
loop_->assertInLoopThread();
InetAddress peerAddr;
int connfd = acceptSocket_.accept(&peerAddr);
if (connfd >= 0)
{
if (newConnectionCallback_)
{
newConnectionCallback_(connfd, peerAddr);
}
else
{
sockets::close(connfd);
}
}
else
{
if (errno == EMFILE)
{
::close(idleFd_);
idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
::close(idleFd_);
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
}
}
}
上述代码变为muduo库中处理监听套接字可读的处理函数,当接受到的connfd>0时说明接受套接字成功于是会接着去执行用户自定义新连接回调,相反当else时即接受连接发生了错误,当返回的错误码如上为EMFILE(超过最大连接数)时。如果不看上面的源码你会咋么办?要知道此时你的最大连接数达到了上限,而accept队列里可能还一直在增加新的连接等你接受,并前muduo用的是epoll的LT模式时,那么因为你连接达到了文件描述符的上限,此时没有可供你保存新连接套接字描述符的文件符了,那么新来的连接就会一直放在accept队列中,于是呼其对应的可读事件就会一直触发读事件(因为你一直不读,也没办法读走它),此时就会造成我们常说的busy loop。muduo使用了一个优雅的办法来解决此问题,具体如下
在Acceptor类中先用idleFd_保存一个空洞文件的文件描述符,剩下的就如上述代码中的else中一样,当文件描述符达到上限时,我们此时的确不能获取新连接,但是我们可以把提前拥有的idleFd_关掉,这样就腾出来文件描述符来接受新连接了,接受到新连接之后将其关闭,然后我们就再次获得一个空洞文件描述符保存到idleFd_中。这样当文件描述符再次达到上限时继续使用刚才的流程来处理,muduo就是通过这么一个简单的idleFd_解决了服务器中文件描述符达到上限后如何处理的大问题!