muduo源码剖析

muduo是陈硕大神个人开发的C++开源网络编程框架,其Github地址在https://github.com/chenshuo/muduo。muduo的定位是服务器端TCP网络编程库,整体基于Reactor模式实现。Reactor模式是目前大多数Linux端高性能网络编程框架和网络应用所选择的主要架构,例如Java的Netty、内存数据库Redis等。

在陈硕的《Linux多线程服务器端编程》一书中对muduo进行了详细的介绍,可以说是学习muduo源码和设计理念最好的资料了。
本文则从事件处理和消息传递等角度对muduo源码进行一个梳理,也是本人学习muduo源码的一个心得记录。

注意:在阅读本文之前需要对网络编程和事件循环有基本的了解和学习。

一个简单的例子

本文首先从最简单的echo server入手,来介绍muduo的基本使用,同时也方便后面概念的理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void onMessage(const muduo::net::TcpConnectionPtr& conn,
muduo::net::Buffer* buf,
muduo::Timestamp time)
{
conn->send(buf);
}

int main()
{
muduo::net::EventLoop loop;
muduo::net::InetAddress listenAddr(2007);
TcpServer server(&loop, listenAddr);
server.setMessageCallback(onMessage);
server.start();
loop.loop();
}

echo-server的代码量非常简洁。一个典型的muduo的TcpServer工作流程如下:

  1. 建立一个事件循环器EventLoop
  2. 建立对应的业务服务器TcpServer
  3. 设置TcpServer的Callback
  4. 开启事件循环

事件循环

我们暂时不考虑muduo的多线程模型,仅仅从单线程的角度对muduo源码进行分析。示例代码echo-server就是一个标准的单线程TcpServer。

众所周知,Reactor模式主要是基于事件循环EventLoop实现的。所谓EventLoop,简单来说其实就是一个无限的循环,在循环中不断检测是否有某个事件发生。当某事件发生时,则触发该事件对应的callback。

在echo-server的最后一步,使用了EventLoop::loop开启事件循环。那么我们看下EventLoop::loop到底做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void EventLoop::loop()
{
quit_ = false;
while (!quit_)
{
activeChannels_.clear();
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
++iteration_;
eventHandling_ = true;
for (ChannelList::iterator it = activeChannels_.begin();
it != activeChannels_.end(); ++it)
{
currentActiveChannel_ = *it;
currentActiveChannel_->handleEvent(pollReturnTime_);
}
currentActiveChannel_ = NULL;
eventHandling_ = false;
}
}

代码跟我们上面描述的基本一致,就是在while循环中不断地处理事件,里面最核心的就是两句代码:

  1. 获取当前时刻触发的事件:pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
  2. 分发处理对应的事件:currentActiveChannel_->handleEvent(pollReturnTime_);

而在网络编程中,需要处理的Socket的事件至少有三个:

  1. 新连接请求建立的事件
  2. 已连接socket的可读事件
  3. 已连接socket的可写事件

注意,我们在这里暂时只讨论网络Socket事件。在一个完备的网络编程框架中,信号、定时器也需要在事件循环中考虑到。
muduo本质上也是围绕这三个事件进行的消息处理。

新连接的建立

在我们编写一个简单的Tcp服务器时,建立一个新的连接通常需要四步:

1
2
3
4
1. socket() // 调用socket函数建立监听socket
2. bind() // 绑定地址和端口
3. listen() // 启动监听
4. accept() // 返回新建立连接的fd

我们接下来分析下,这四个步骤在muduo中都是何时进行的:

首先在TcpServer对象构建时,TcpServer的属性acceptor同时也被建立。在Acceptor构造函数中分别调用了socket函数和bind函数完成了步骤1步骤2
即,当TcpServer server(&loop, listenAddr);执行结束时,监听socket已经建立好。

当执行server.start()时,主要做了两个工作:

  1. 在监听socket上启动listen函数,也就是步骤3
  2. 将监听socket的可读事件注册到Poller中

当新连接请求建立时,可读事件触发,此时该事件对应的callback在EventLoop::loop()中被调用。
该事件的callback实际上就是Acceptor::handleRead()方法。
在Acceptor::handleRead()方法中,也做了两件事:

  1. 调用了accept函数,完成了步骤4,实现了连接的建立。
  2. 将新连接的socket的可读事件注册到Poller中

消息的读取

上节讲到,在新连接建立的时候,会将新连接的socket的可读事件注册到Poller中。
假如客户端发送消息,连接socket的可读事件触发,该事件对应的callback同样也会在EventLoop::loop()中被调用。

该事件的callback实际上就是TcpConnection::handleRead方法。
在TcpConnection::handleRead方法中,主要做了两件事:

  1. 从socket中读取数据,并将其放入inputbuffer中
  2. 调用messageCallback,执行业务逻辑。

messageCallback是在建立新连接时,将TcpServer::messageCallback方法bind到了TcpConnection::messageCallback的方法。

TcpServer::messageCallback就是业务逻辑的主要实现函数。通常情况下,我们可以在里面实现消息的编解码、消息的分发等工作,这里就不再深入探讨了。

在我们上面给出的示例代码中,echo-server的messageCallback非常简单,就是直接将得到的数据,重新send回去。在实际的业务处理中,一般都会调用TcpConnection::send()方法,给客户端回复消息。

消息的发送

用户通过调用TcpConnection::send()向客户端回复消息。由于muduo中使用了OutputBuffer,因此消息的发送过程比较复杂。

  1. 假如OutputBuffer为空,则直接向socket写数据
  2. 如果向socket写数据没有写完,则统计剩余的字节个数,并进行下一步
  3. 如果此时OutputBuffer中的旧数据的个数和未写完字节个数之和大于highWaterMark,则将highWaterMarkCallback放入待执行队列中
  4. 将对应socket的可写事件注册到Poller中

连接socket的可写事件对应的callback是TcpConnection::handleWrite()
当某个socket的可写事件触发时,TcpConnection::handleWrite会做两个工作:

  1. 尽可能将数据从OutputBuffer中向socket中write数据
  2. 如果OutputBuffer没有剩余的,则将该socket的可写事件移除,并调用writeCompleteCallback

为什么要移除该可写事件呢?
因为当OutputBuffer中没数据时,我们不需要向socket中写入数据。但是此时socket一直是处于可写状态的, 这将会导致TcpConnection::handleWrite()一直被触发。然而这个触发毫无意义,因为并没有什么可以写的。

所以muduo的处理方式是,当OutputBuffer有数据时,注册socket的可写事件。当OutputBuffer为空时,将socket的可写事件移除。

此外,highWaterMarkCallback和writeCompleteCallback一般配合使用,起到限流的作用。在《linux多线程服务器端编程》一书的8.9.3一节中有详细讲解。这里就不再赘述了

总结

个人认为,muduo源码对于学习网络编程和项目设计非常有帮助。虽然网络中也不乏对muduo的批评,例如定时器的设计、信号处理的缺失等问题,但是muduo在Reactor模式网络编程的实现、现代C++的使用以及框架设计等方面都做的非常优秀,基于这几个方面来说,muduo绝对是一个值得一探究竟的优质源码。
此外,不但是网络编程方面,如何将复杂的底层细节封装好,暴露出友好的通用业务层接口,如何设计类的职责,对象的生命周期管理等方面,muduo都给了我们一个很好的示范。

坚持原创技术分享,您的支持将鼓励我继续创作!