muduo网络库:18---muduo简介之(muduo库的由来、编译安装、目录结构、代码结构、线程模型)
一、由来2010年3月我写了一篇《学之者生,用之者死——ACE历史与简 评》1,其中提到“我心目中理想的网络库”的样子:·线程安全,原生支持多核多线程。 ·不考虑可移植性,不跨平台,只支持Linux,不支持Windows。 ·主要支持x86-64,兼顾IA32。(实际上muduo也可以运行在ARM 上。) ·不支持UDP,只支持TCP。 ·不支持IPv6,只支持IPv4。 ·不考虑广域网应...
·
一、由来
-
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++网络库:
- 与前两次不同,这次一开始就想好了库的名字,叫muduo(木铎)(这个名字的由来可以参阅陈硕的一篇访谈:http://www.oschina.net/question/28_61182)
- 并在Google code上创建了项目:http://code.google.com/p/muduo
- muduo以git为版本管理工具,托管于:http://github.com/chenshuo/muduo
- muduo的主体内容在2010年5月底已经基本完成,8月底发布0.1.0版, 2012年11月的最新版本是0.8.2
为什么需要网络库?
- 使用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
- 陈硕先生的Github有一个项目展示了如何使用CMake和普通makefile编译基于muduo的程序,可参阅:https://github.com/chenshuo/muduo-tutorial
演示案例
- 待续
四、目录结构
- 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的示例项目。由于License等原因没有放到 muduo发行版中,可以单独下载:
- https://github.com/chenshuo/muduo-udns:基于UDNS的异步DNS解析
- https://github.com/chenshuo/muduo-protorpc:新的RPC实现,自动管理对象生命期
七、线程模型
- 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例子),待日后慢慢改进
更多推荐
已为社区贡献9条内容
所有评论(0)