muduo库是在Linux环境下使用C++实现的一个多Reactor多线程的高性能网络服务器,作者陈硕,他还出了一本书《Linux多线程服务端编程:使用muduo C++网络库》来介绍muduo库的使用以及设计。有兴趣的读者可以阅读一下书中关于muduo库的设计部分,本篇文章就是基于这本书来介绍如何模拟实现一个muduo网络库。因为我认为学习技术,有了初步的了解以后就要想办法复刻模仿,学习别人的设计思路,复刻的过程就是动手实践,只有在实践中我们才能真正感受到难点在哪,以及优秀的地方在哪,这对我们学习相关知识比如计算机网络、IO多路转接、Reactor模式是很有帮助的。
比如在模拟实现muduo库的过程中,我最大的感受就是这个库很难,至少对于我这么一个刚学习完计算机网络、多路转接的学生来说是有挑战的,代码中使用了大量的回调函数,以及多线程并发执行的逻辑,我刚开始看的时候是很绕很乱的,理不清这些关系。但模拟实现完成以后,现在再回头看这些代码,确实是非常优秀的设计。本篇文章的目的是记录这几个月学习并模拟实现muduo库的过程,分享一下我个人对muduo库的认识,如果有不正确的地方欢迎指正。
muduo库是基于Reactor模式下使用epoll多路转接的方式设计出来的,所以在模拟实现moduo库之前,非常有必要补充一下这方面的背景知识。
epoll是多路转接中使用最多并且是最高效的方式,多路转接又叫多路复用,其实我认为多路复用比较形象好理解,多路复用是用来解决一个服务器如何更好地服务多个用户的问题。试想一下,如果我们写的服务器只能服务一个用户,那么效率太低了,而且也很浪费资源。
那如果要处理多个客户端的请求,首先想到的是多进程模型,也就是每来一个用户,服务器就创建一个新进程来为用户服务,但这种方式有明显的劣势,因为进程的开销太大了。如果进程不行的话,就有多线程模型,每来一个用户就创建一个线程,或者使用线程池的方式,可以避免频繁创建和销毁线程。但多线程也只能解决用户少的情况,如果用户量很大,连接数很多,一个服务器要维护成千上万个线程,其实开销也是特别大的,即使线程比进程更轻量级。
IO多路复用就和多进程模型、多线程模型不一样,因为一个服务就分配一个线程或者进程开销太大,所以IO多路复用是让多个事件复用同一个进程。用户与服务器的交互无非就是建立连接请求、发送IO请求,这些请求在服务器层面看来,本质都是一个个的事件,建立连接事件以及IO处理事件,IO多路复用是让一个进程管理多个事件,其实更进一步说明应该是,进程调用操作系统提供的多路复用的接口,比如select、poll和epoll,通过这些接口进程将需要管理的事件交给操作系统内核去监听,一旦有事件就绪,操作系统内核会以不同的方式通知进程,进程再去做相应的处理,这就是IO多路复用的原理。
epoll就是操作系统为我们提供的IO多路复用接口,在使用epoll多路复用时,操作系统会为我们创建一个epoll模型,这个模型由三部分组成:红黑树、就绪队列以及回调机制。epoll的底层实现原理是当进程向epoll模型输入需要管理的事件时,epoll模型会创建一个相应的红黑树节点,将该事件记录在红黑树上。然后操作系统会为红黑树上的每一个事件注册一个回调函数,当事件就绪时,红黑树上的事件节点会被删除,然后向就绪队列中插入一个新的节点,通过回调函数告诉上层的进程某某事件已经就绪了,就绪事件就放在就绪队列里,上层进程就可以对就绪事件进行相应的进一步处理了。这就是epoll模型的基本原理。
这里只是作为背景知识简单地介绍了一下epoll模型,详细的可以看我之前写过的博客:Linux多路转接之epoll
Reactor模式是对多路复用的进一步设计,如果单纯使用epoll的多路复用,进程调用epoll接口监听事件,如果有事件就绪还是由这个进程来执行就绪事件的对应操作。也就是说单纯的多路复用是将事件监听和就绪事件的处理合在一起的,而Reactor模式就是将它们分开来处理。Reactor模式又叫做dispatcher模式,dispatcher有分派的意思,其实是比较形象地形容了Reactor模式的,因为Reactor模式中一般可以分出两类角色,一类是Reactor角色,一类是handler角色,其中Reactor角色负责的是监听和分发事件,handler角色负责的是处理就绪事件。Reactor角色会等待多路复用返回就绪事件,一旦被通知有事件就绪,Reactor角色就会把就绪的事件交给handler角色去处理,然后Reactor角色就可以继续等待下一轮的就绪事件,这就是Reactor模式的原理。
Reactor的模式是灵活多变的,在不同的业务场景下,我们可以选择单个或者多个reactor角色,同时也可以选择单个或多个handler角色。其实handler角色通常情况下就是额外的进程和线程,因为Reactor角色接收到就绪事件以后肯定是分派给其它执行进程或者线程去处理就绪事件。所以Reactor模式又可以分为单Reactor单进程/线程
、单Reactor多进程/线程
、多Reactor单进程/线程
、多Reactor多进程/线程
4种方案。
由于muduo库使用的就是多Reactor多线程的方案,所以这里只介绍这种方案,其实这种方案听起来很复杂,但我个人认为是最好理解、最好实现的方案,因为它能做到分工明确。首先来看一下多Reactor多线程的模型图:
通过模型图可以看到,多Reactor多线程模型分了一个主Reactor和多个从属Reactor,当然也有一个主线程和多个子线程。它的执行逻辑是,主线程的主Reactor通过epoll监听连接事件,并且主Reactor只监听连接事件,当连接事件到来的时候,主线程获取连接,获取到的连接又是一个新的文件描述符,也就是一个新的事件,这个事件可能还会有新的IO事件,所以也必须被管理起来。这些新连接就被主Reactor分派给某个子线程的从属Reactor去管理。同样的,从属Reactor就负责监听这些连接事件的IO事件,当这些连接有IO请求的时候,就让Handler去处理这些请求。这就是多Reactor多线程模型的执行逻辑。
多Reactor多线程模型在muduo库中的体现是,首先会有一个主Reactor,它只负责监听连接事件,然后会维护一个线程池,线程池里的线程可以指定数量,当主Reactor监听到连接事件以后,就选定线程池里的一个线程,将该事件的文件描述符与子线程的从属Reactor绑定,子线程的从属Reactor就负责监听该文件描述符的IO事件,而主Reactor就继续去监听等待新的连接事件到来。这样分工是非常明确的,也是非常好理解。
介绍完两个重点的背景知识以后,我们可以对自己模拟实现的muduo库做一个功能划分,这里首先划分TCP服务器层的功能模块,因为这才是整个网络库的核心,所有的多Reactor多线程模型,所有的高性能的实现都是在TCP服务器里面体现的,这一层做好了,上层应用层选择需要的协议就可以了,比如HTTP协议,搭建一个HTTP协议并不是什么难点,所以放在最后再来说,重点还是放在TCP服务器上。
TCP服务器的功能模块我个人认为可以划分出三个部分,分别是工具部分、Reactor部分和最上层的TCPServer部分。工具部分有Buffer模块、Socket模块、Acceptor模块、定时器模块、线程池模块。之所以这些模块被我划分为工具部分,是因为我认为在这个项目中这些模块更多的是起到一个工具的辅助作用,最重点的还是Reactor部分。Reactor部分有Channel模块、Poller模块、Connection模块和EventLoop模块,这部分的四个模块,就是实现多Reactor多线程、完成连接管理的模块,我认为是项目的核心模块。最上层的TcpServer模块当然就是将这些各个模块整合起来,形成一个类或者接口,提供给外部调用。下面将分别介绍每个模块的大致功能,后面会详细介绍每一个模块的原理、作用以及具体实现。
Buffer模块:
Buffer模块是TCP服务器的缓冲区模块,缓冲区这个概念在计算机里是非常常见的,我们平时使用的软件比如操作系统、数据库,以及一些第三方库、组件等等,都有缓冲区的存在。缓冲区就是一段缓存数据的内存空间,在我们这个TCP服务器中,我们需要接收缓冲区来保存对方发送的请求报文,也需要发送缓冲区保存我们需要发送给对方的响应报文。所以Buffer模块就是为我们的TCP服务器维护了一段缓冲区。
Socket模块:
这个模块比较简单,就是对TCP服务器中要使用到的套接字接口进行封装,因为要符合面向对象的思想,让开发调用更方便,也为了让代码更美观和结构化,所以很有必要将socket套接字操作封装成一个单独的模块。
Acceptor模块:
Acceptor模块会跟主Reactor有比较大的联系,因为Acceptor模块是负责监听事件管理的模块,它通过Socket模块实现监听套接字的操作,创建了监听套接字以后,它就会accept监听获取连接,当有连接到来的时候,它就会通知主Reactor去处理连接。
定时器模块:
我们知道网络连接有长连接和短连接,我们的服务器也是可以接收长连接和短连接的,但是长连接不能占用套接字的时间太长,如果一个连接到来但一直没有与服务器有其它IO数据交互,就只是占用着连接不销毁,这其实是非常消耗服务器资源的,虽然一个连接长时间占用资源看起来影响不大,但如果有成千上万个连接到来,服务器的资源是有限的,就比如文件描述符的资源是有限的,如果有长连接长时间不通信还占用着文件描述符,那么如果文件描述符都被占满了,新的连接就无法到来了,所以我们必须设置一个定时器机制,虽然我支持你的长连接,但你不能占用我的连接资源那么长时间。既然你自己不关闭连接,我就设置一个定时功能,多长时间你还没跟我服务器通信我就直接关闭你这个连接,下次要通信你再重新发起连接吧。
这就是定时器模块的功能,它会记录每一个连接距离上一次发送数据给服务器的时间,然后设置一个超时时间,如果这个时间超过了超时时间,就自动销毁该连接,这就是muduo库的超时自动销毁连接机制。
线程池模块:
这个模块就是维护一个线程池,因为我们实现的muduo库是多Reactor多线程模型的,所以需要一个线程池来管理多个子线程,主Reactor接收到连接事件之后,就从线程池中选一个线程,让它去管理新连接后续的IO事件。
Channel模块:
Channel模块是用来管理监控事件的,其实我们服务器要监控的事件就是这四类:可读事件、可写事件、错误事件、连接关闭事件,所以Channel模块要对这些监控的事件进行管理,比如你要设置监听的事件是可读事件还是可写事件,还要设置每一类事件触发以后的回调函数,这样事件触发以后才能够调用对应的回调函数去处理。
Poller模块:
Poller模块是对epoll函数操作的封装,但这个不是简单的封装,事实上Poller模块和Channel模块是关联起来的,Poller模块需要管理所有epoll模型要监控的文件描述符,这些文件描述符其实就是一个个的事件,所以每个文件描述符都对应一个Channel对象,Poller模块监控这些文件描述符,一旦有事件就绪,就调用这些文件描述符的Channel对象函数,去执行相应的回调函数,因为Channel模块是设置了每一种事件的回调函数的。
EventLoop模块:
EventLoop模块就是实现Reactor的模块了,它是对Channel模块和Poller模块的整体封装,在服务器中每创建一个EvevtLoop对象,其实就是创建一个Reactor,但是EventLoop也不是简单封装Reactor操作,事实上EventLoop模块是特别核心的一个模块,因为它关联了很多其他模块,比如Poller模块、Channel模块、Connection模块等,所以这个模块简述不清,后面会详细介绍。
Connection模块:
Connection模块是管理连接的模块,主Reactor监听到新连接到来后,就会创建一个Connection对象,用这个对象来管理这个新来的连接。这些管理包括读取数据、发送数据、启动非活跃连接超时销毁任务、释放连接、关闭连接等操作。另外,Connection模块还有一个很重要的成员就是EventLoop对象,因为主Reactor监听到新连接到来,创建Connection对象以后,需要把这个连接交给从属Reactor去监听事件,所以这个EventLoop对象就是从属Reactor。
TCPServer模块:
这个模块就是整个TCP服务器最上层的模块,它实现了对另外两个部分所有模块的封装,然后提供接口给外界,外界只需要调用相应的接口就能启动这个TCP服务器。
由于篇幅有限,我也不希望全部挤在一篇文章里来写,所以我分开几个文章来记录这个项目,这篇文章只是初步对muduo库做一个介绍,简单了解muduo库的背景知识以及基本的结构,接下来将针对每个模块详细地介绍项目的设计和实现过程。