一、由来

  • 2010年3月陈硕先生写了一篇《学之者生,用之者死——ACE历史与简评》(文章参阅:https://blog.csdn.net/Solstice/article/details/5364096),其中提到“我心目中理想的网络库”的样子:

    • 线程安全,原生支持多核多线程
    • 不考虑可移植性,不跨平台,只支持Linux,不支持Windows。 ·主要支持x86-64,兼顾IA32。(实际上muduo也可以运行在ARM 上)
    • 不支持UDP,只支持TCP
    • 不支持IPv6,只支持IPv4
    • 不考虑广域网应用,只考虑局域网。(实际上muduo也可以用在广 域网上)
    • 不考虑公网,只考虑内网。不为安全性做特别的增强
    • 只支持一种使用模式:非阻塞IO+one event loop per thread,不支持阻塞IO
    • API简单易用,只暴露具体类和标准库里的类。API不使用nontrivial templates,也不使用虚函数
    • 只满足常用需求的90%,不面面俱到,必要的时候以app来适应 lib
    • 只做library,不做成framework
    • 争取全部代码在5000行以内(不含测试)
    • 在不增加复杂度的前提下可以支持FreeBSD/Darwin,方便将来用 Mac作为开发用机,但不为它做性能优化。也就是说,IO multiplexing使用poll和epoll
    • 以上条件都满足时,可以考虑搭配Google Protocol Buffers RPC
  • 在想清楚这些目标之后,陈硕开始第三次尝试编写自己的C++网络库:

为什么需要网络库?

  • 使用Sockets API进行网络编程是很容易上手的一项技术,花半天时间读完一两篇网上教程,相信不难写出能相互连通的网络程序
  • 例如下面这个网络服务端和客户端程序,它用Python实现了一个简单的“Hello”协议,客户端发来姓名,服务端返回问候语和服务器的当前时间

  • 上面两个程序使用了全部主要的SocketsAPI,包括socket、 bind、listen、accept、connect、recv、send、close、 gethostbyname等,似乎网络编程一点也不难嘛
  • 在同一台机器上运 行上面的服务端和客户端,结果不出意料:

  • 但是连接同一局域网的另外一台服务器时,收到的数据是不完整的。错在哪里?

  • 出现这种情况的原因是:高级语言(Java、Python等)的Sockets库并没有对Sockets API提供更高层的封装,直接用它编写网络程序很容易掉到陷阱里,因此我们需要一个好的网络库来降低开发难度。网络库的价值还在于能方便地处理并发连接(参阅后面的“详解muduo多线程模型”文章)

二、安装

  • 安装注意事项:
    • muduo使用了Linux较新的系统调用(主要是timerfd和eventfd),要求Linux的内核版本大于2.6.28
    • 我自己用Debian 6.0 Squeeze / Ubuntu 10.04 LTS作为主要开发环境(内核版本2.6.32),以g++ 4.4为主要编译器版本,在32-bit和64-bit x86系统都编译测试通过
    • muduo在Fedora 13 和CentOS 6上也能正常编译运行,还有热心网友为Arch Linux编写了AUR文件(http://aur.archlinux.org/packages.php?ID=49251
    • 如果要在较旧的Linux 2.6内核(例如Debian 5.0 Lenny、Ubuntu 8.04、CentOS 5等旧版本)上使用muduo,可以参考backport.diff来修改代码。不过这些系统上没有充分测试,仅仅是编译和冒烟测试通过
    • 另外muduo也可以运行在嵌入式系统中,我在Samsung S3C2440开 发板(ARM9)和Raspberry Pi(ARM11)上成功运行了muduo的多个示例。代码只需略作改动,请参考armlinux.diff
  • 安装过程如下:

第一步(安装前准备)

  • 第一步:muduo采用CMake为build system,CMake的安装如下:(CMake最好不低于2.8版,CentOS 6自带的2.6版也能用,但是无法自动识别Protobuf库)
sudo apt-get install cmake

sudo apt-get install g++

  • 第二步:muduo依赖于Boost,Boost的安装如下
sudo apt-get install libboost-dev libboost-test-dev

  •  第三步(可选):muduo有三个非必须的依赖库(curl、c-ares DNS、Google Protobuf)。如果安装了这三个库,cmake会自动多编译一些示例。安装方法如下:
sudo apt-get install libcurl4-openssl-dev libc-ares-dev
sudo apt-get install protobuf-compiler libprotobuf-dev

第二步(编译、安装muduo)

  • 第一步:下载muduo源码包
git clone https://github.com/chenshuo/muduo.git

  • 第二步:编译muduo,命令如下:
# 下载完成之后进入muduo根目录
cd muduo

# 编译muduo库和它自带的例子
./build.sh -j2

  • 编译完成之后:
    • 会在muduo源码根路径的上一级路径下生成一个build目录(下面全文我们以../build表示)
    • 生成的可执行文件位于:../build/release-cpp11/bin
    • 静态文件位于:../build/release-cpp11/lib

  • 第三步:安装muduo库
./build.sh install

  • 默认情况下:
    • muduo头文件安装在../build/release-install-cpp11/include目录下
    • 库文件安装在../build/release-install-cpp11/lib目录下
    • 以便muduo-protorpc和muduo-udns等库使用

第三步(测试)

  • 编译完成之后我们可以试着运行编译的例子(位于../build/release-cpp11/bin/目录下),查看能够运行成功
  • 此处我们以inspector_test为例:
    • (下图1)运行../build/release-cpp11/bin/inspector_test
    • (下图2)然后通过浏览器访问“192.168.0.101:12345”访问运行的服务器(其中192.168.0.101更换为你的Linux IP)
    • (下图3)或者输入“192.168.0.101:12345/proc/status”来访问该服务器的状态

三、编译带有muduo库的C/C++程序

  • muduo是静态链接的C++程序库(因为在分布式系统中正确安全地发布动态库的成本很高)
  • 编译带有muduo代码的程序,规则与命令如下:
    • 头文件:使用-I选项指出头文件路径(头文件路径就是上面muduo的头文件安装路径,文章划上去看)
    • 库文件:使用-L选项指出库文件路径(库文件路径就是上面muduo的库文件安装路径,文章划上去看)
    • 链接相应的静态库文件:-lmuduo_net、-lmuduo_base
g++ -o muduo_test muduo_test.c -I头文件路径 -L库文件路径 -lmuduo_net -lmuduo_base

演示案例

  • 待续

四、目录结构

  • muduo命名规则:
    • 源代码文件名与class名相同
    • 例如ThreadPool class的定义是muduo/base/ThreadPool.h,其实现位于muduo/base/ThreadPool.cc
  • muduo源码目录如下:

基础库

  • muduo/base是一些基础库,都是用户可见的类。内容如下:

网络核心库

  • muduo库代码结构:
    • muduo是基于Reactor模式的网络库,其核心是个事件循环EventLoop,用于相应计时器和IO事件
    • muduo采用基于对象(object-bases)而非面向对象(object-oriented)的设计风格
    • 其事件回调接口多以function+bind表达,用户在使用muduo的时候不需要继承其中的class
  • 网络核心库位于muduo/net和muduo/net/poller:一共不到4300行代码,以下灰底表示用户不可见的内部类

网络附属库

  • 网络库有一些附属模块,它们不是核心内容:
    • 在使用的时候需要链接相应的库,例如-lmuduo_http、-lmuduo_inspect等等
    • HttpServer和Inspector暴露出一个http界面,用于监控进程的状态,类似于Java JMX
  • 附属模块位于muduo/net/{http,inspect,protorpc}等处

五、代码结构

头文件和库文件

  • 对于muduo库而言,只需要掌握5个关键类:Buffer、EventLoop、TcpConnection、TcpClient、TcpServer
  • muduo的头文件明确分为客户可见和客户不可见两类。下面是安装之后暴露的头文件和库文件

  • 在上面我们安装的时候,安装方式为(以muduo源码根目录为基准):
    • 头文件:安装在../build/release-install-cpp11/include目录下
    • 库文件:安装在../build/release-install-cpp11/lib目录下

  • 下图是muduo的网络核心库的头文件包含关系:用户可见的为白底,用户不可见的为灰底:

  • muduo头文件中使用了前向声明(forward declaration),大大简化了头文件之间的依赖关系:
    • 例如Accrptor.h、Channel.h、Connection.h、TcpConnection.h都前向声明了 EventLoop class,从而避免包含EventLoop.h
    • 另外, 前向声明了Connector class,从而避免将内部类暴露给用户
    • 类似的做法还有TcpServer.h用到的Acceptor和EventLoopThreadPool、EventLoop.h用到的Poller和TimerQueue、TcpConnection.h用到的Channel和Socket等等

公开接口

  • 这里简单介绍各个class的作用,详细的介绍参见以后的文章
  • 公开接口有:
    • Buffer仿Netty ChannelBuffer的buffer class,数据的读写通过buffer 进行。用户代码不需要调用read()/write(),只需要处理收到的数据和 准备好要发送的数据(详情参阅“muduo Buffer类的设计与使用”)
    • InetAddress封装IPv4地址(end point),注意,它不能解析域名, 只认IP地址。因为直接用gethostbyname()解析域名会阻塞IO线程
    • EventLoop事件循环(反应器Reactor),每个线程只能有一个 EventLoop实体,它负责IO和定时器事件的分派。它用eventfd()来异步唤醒,这有别于传统的用一对pipe()的办法。它用TimerQueue作为计时器管理,用Poller作为IO multiplexing
    • EventLoopThread启动一个线程,在其中运行EventLoop::loop()
    • TcpConnection整个网络库的核心,封装一次TCP连接,注意它不能发起连接
    • TcpClient用于编写网络客户端,能发起连接,并且有重试功能
    • TcpServer用于编写网络服务器,接受客户的连接
  • 在这些类中:
    • TcpConnection的生命期依靠shared_ptr管理(即用户和库共同控制)。Buffer的生命期由TcpConnection控制。其余类的生命期由用户控制
    • Buffer和InetAddress具有值语义,可以拷贝;其他class 都是对象语义,不可以拷贝

内部实现

  • Channel是selectable IO channel,负责注册与响应IO事件,注意它 不拥有file descriptor。它是Acceptor、Connector、EventLoop、 TimerQueue、TcpConnection的成员,生命期由后者控制
  • Socket是一个RAIIhandle,封装一个filedescriptor,并在析构时关闭 fd。它是Acceptor、TcpConnection的成员,生命期由后者控制。 EventLoop、TimerQueue也拥有fd,但是不封装为Socket class
  • SocketsOps封装各种Sockets系统调用
  • Poller是PollPoller和EPollPoller的基类,采用“电平触发”的语意。 它是EventLoop的成员,生命期由后者控制
  • PollPoller和EPollPoller封装poll()和epoll()两种IO multiplexing后 端。poll的存在价值是便于调试,因为poll(2)调用是上下文无关的,用 strace(1)很容易知道库的行为是否正确
  • Connector用于发起TCP连接,它是TcpClient的成员,生命期由后者控制
  • Acceptor用于接受TCP连接,它是TcpServer的成员,生命期由后者控制
  • TimerQueue用timerfd实现定时,这有别于传统的设置 poll/epoll_wait的等待时长的办法。TimerQueue用std::map来管理Timer, 常用操作的复杂度是O(logN),N为定时器数目。它是EventLoop的成 员,生命期由后者控制
  • EventLoopThreadPool用于创建IO线程池,用于把TcpConnection分派到某个EventLoop线程上。它是TcpServer的成员,生命期由后者控制
  • 下图是muduo的简化类图,Buffer是TcpConnection的成员:

六、例子

  • muduo附带了十几个示例程序,编译出来有近百个可执行文件:
    • 这些例子位于examples目录,其中包括从Boost.Asio、Java Netty、Python Twisted等处移植过来的例子
    • 这些例子基本覆盖了常见的服务端网络编程功能点,从这些例子可以充分学习非阻塞网络编程

  • 在上面我们编译muduo时,编程生成的可执行文件的路径为:../build/release-cpp11/bin

七、线程模型

  • muduo的线程模型为one loop per thread+thread pool模型:
    • 每个线程最多有一个EventLoop,每个TcpConnection必须归某个EventLoop管理,所有的IO会转移到这个线程
    • 换句话说,一个file descriptor(文件描述符)只能由一个线程读写。TcpConnection所在的线程由其所属的 EventLoop决定,这样我们可以很方便地把不同的TCP连接放到不同的 线程去,也可以把一些TCP连接放到一个线程里
    • TcpConnection和 EventLoop是线程安全的,可以跨线程调用
  • TcpServer直接支持多线程,它有两种模式:
    • 单线程,accept()与TcpConnection用同一个线程做IO
    • 多线程,accept()与EventLoop在同一个线程,另外创建一个EventLoopThreadPool,新到的连接会按round-robin方式分配到线程池 中
  • 后面还会以Sudoku服务器为例再次介绍muduo的多线程模型

八、总结

  • muduo是陈硕先生对常见网络编程任务的总结,用它能很容易地编写多线程的TCP服务器和客户端
  • muduo代码估计还有一些bug,功能也不完善,例如不支持signal处理(Signal也可以通过signalfd()融入EventLoop中,见https://github.com/chenshuo/muduo-protorpc中的zurg slave例子),待日后慢慢改进
Logo

更多推荐