之前已经简单分析了Muduo库的软件架构:
http://blog.csdn.net/adkada1/article/details/54342275
接下来准备对Muduo库进行部分简化。
一、三个主要目标:
1、从Boost向C++11迁移。Muduo库采用了现代C++编程的思想,在几年前,这种思想必须通过Boost库才能够支持,而现在C++11标准日益普及,Boost库中很多优秀特性已经被吸收到C++11的标准中,支持C++11标准的编译器也更加成熟,而Boost库整体上又显得过于庞大,所以简化Muduo库的第一个目标就是向C++11迁移。
2、调整软件架构,简化代码处理逻辑。根据陈硕大侠的解释,Muduo库采用基于对象的编程思想而不是面向对象的编程。具体的原因在《Linux多线程服务端编程:使用muduo C++网络库》中有很多具体的分析,主要的原因大概是虚函数不适合做Muduo库对外的接口(关于这一点,熟悉COM的人应该更有发言权)。所以,Muduo库中几乎没有基类和继承(除了将poll和epoll同时继承Poller类),取而代之的是代码中存在大量的Boost::function和Boost::bind。关于虚函数不适合作为库的对外接口,这一点我完全同意,但是在库的内部,我觉得合理的使用虚函数和面向对象的思想,可以让代码逻辑简化。所以,第二个简化目标是库的内部采用面向对象的编程思想,简化代码处理逻辑。
3、利用Linux新特性,简化线程安全的需求。Muduo库推荐通过多线程实现高并发服务器,其中一个主线程负责accept客户端的新连接,然后以RR的方式将新建立的连接描述符分发给其它IO线程处理。这种方案的好处很多,包括不存在惊群的问题。但是,一个线程accept全部客户连接,在特殊的高并发场景下,可能还是会成为性能瓶颈。另外,多个线程之间交互的数据太多,为了保证线程安全,代码要考虑的东西也更多,逻辑也就更复杂。其实,Linux内核从3.9开始支持SO_REUSEPORT特性,该特性允许多个进程或线程通过不同的socket监听服务器的同一个端口,最终由内核决定哪个进程或线程accept客户端的新连接。这个特性可以有效防止一个主线程accept而出现的性能瓶颈问题,另外还可以有效减少线程之间的数据交互,降低开发者对于对于线程安全的思想负担。
在《Linux多线程服务端编程:使用muduo C++网络库》中,开篇谈到的就是对象的线程安全,在讨论了各种同步和互斥方案后,读者很自然的发现,其实最难的是如何做到线程安全的删除一个对象。然后继续分析,给出的方案是通过智能指针可以保证被管理的对象安全删除。但是,最后还有一个尾巴,就是智能指针本身不是线程安全的和普通的STL一样。这就好像是解决了一个老问题,又引入了一个新问题,而两个问题都同样致命。所以,我个人的看法是,对于C++而言,不能追求绝对的线程安全,通过合理的架构,减少不必要的线程安全的需求,才是需要不断追求的。所以,简化后的Muduo库,通过SO_REUSEPORT特性实现多线程并发监听,同时尽量降低多线程之间的数据交互,简化软件的复杂度。
二、整体架构描述
简化后的Muduo库和以前一样,支持服务器和客户端两种模型,同时又可分为单线程模式和多线程模式。整体架构如下图所示:
对于服务端编程,用户程序可以根据需要创建多个应用服务器对象(例如:TimeServer、EchoServer、HttpServer),每个应用服务器对象都分别包含一个TcpServer对象,每个TcpServer对象都包含代表本服务器端口的侦听套接字(ListenSocket),通过TcpServer.start()接口启动侦听套接字(ListenSocket),并将侦听套接字(ListenSocket)的描述符添加到EventLoop对象的Poller(轮询)管理器中,让Linux内核监控侦听套接字(ListenSocket)上发生的各种事件(新建连接、套接字出错等)。
如果采用单线程模式,用户只需要在主线程上创建一个EventLoop对象和多个服务器对象,并将EventLoop对象的指针传给每个服务器。每个服务器将需要监控的描述符注册到EventLoop对象的Poller(轮询)管理器中,然后启动EventLoop.loop(),轮询得到发生新事件(event)的描述符,并执行事件处理(handleEvent)。
如果采用多线程模式,用户在创建服务器对象的时候可以通过传递参数,通知TcpServer创建多个子线程(TcpServer通过调用ServerThread对象可创建多个子线程),每个子线程在自己的线程栈上创建TcpServer对象的副本和EventLoop对象,实现多个线程并发服务一个Server。(这里在每个线程栈上创建TcpServer对象副本的目的是为了保存本线程建立的TCP连接,不同线程建立的TCP连接,保存在不同的TcpServer对象副本中,减少线程之间的数据交互,简化Muduo库在线程安全方面的设计需求,如果应用层的业务逻辑需要不同线程中的两条TCP连接之间交互数据,则需要应用层自己考虑如何保证线程安全。)
三、主要类的设计描述
如图所示,服务类TcpServer和客户类TcpClient作为Tcp连接的所有者,它们有共同的基类LinkOwner。LinkOwner作为虚基类的作用就是定义服务器和客户端消息接收接口,TcpServer和TcpClient需要实现这些接口。
class LinkOwner : noncopyable
{
public:
LinkOwner(){};
virtual ~LinkOwner(){};
// 新连接建立
virtual void newConnection(int sockfd, const InetAddress& peerAddr) = 0;
// 连接删除
virtual void delConnection(TcpLinkSPtr& conn) = 0;
//当前连接上接收到新消息
virtual void rcvMessage(const TcpLinkSPtr& conn, Buffer* buf, Timestamp time) = 0;
// 当前连接的发送缓冲区已清空
virtual void writeComplete(const TcpLinkSPtr& conn) = 0;
// 当前连接的发送缓冲区已到达高水线
virtual void highWaterMark(const TcpLinkSPtr& conn, size_t highMark) = 0;
virtual EventLoop* getLoop() const = 0;
};
ListenSocket类用于Server端管理侦听套接字,它代替了Muduo库中原有的Acceptor类。
ConnectSocket类用于Client端管理连接套接字,它代替了Muduo库中原有的Connector类。
TcpLink类用于表示一条Tcp连接,即用于Server端,也用于Client端。它代替了Muduo库中原有的TcpConnection类。
FdEvent类是ListenSocket、ConnectSocket、TcpLink的虚基类,它用于管理描述符上的event事件,并定义事件处理的总入口handleEvent(),以及各种事件处理的虚接口,总体上代替了原有的Channel和Socket类。
EpollAdpt类封装了epoll相关的API接口,它代替了Muduo库中原有的EPollPoller类。
PollAdpt类封装了poll相关的API接口,它代替了Muduo库中原有的PollPoller类。
Poller类是EpollAdpt和PollAdpt的虚基类,作为轮询管理器,它定义了轮询接口polling(),以及向轮询管理器增加、修改和删除监控事件的接口addFdEvent()/modFdEvent()/delFdEvent()。
class Poller : public noncopyable
{
public:
/// 定义一个简单工厂方法用于创建Poller对象
static Poller* CreatePoller(EventLoop* loop, int PollType = 0);
/// Polls the I/O events.
virtual Timestamp polling(int timeoutMs, FdEventList* pActFdEvents)= 0;
/// add the interested I/O events.
virtual void addFdEvent(FdEvent* pFdEvent) = 0;
/// modify the interested I/O events.
virtual void modFdEvent(FdEvent* pFdEvent) = 0;
/// Remove the channel, when it destructs.
virtual void delFdEvent(FdEvent* pFdEvent) = 0;
};
EventLoop类基本不变
四、整体工作流程
1、每个TcpServer和TcpClient对象中都分别包含一个ListenSocket对象和ConnectSocket对象。
2、启动Server和Client后,ListenSocket和ConnectSocket将分别调用listen()和connect() API,并将各自的socket描述符通过Poller::addFdEvent()接口注册到轮询管理器Poller中。
3、EventLoop.loop()启动以后,先通过Poller::polling()接口获取当前处于活动状态的描述符事件(event),再通过FdEvent::handleEvent()实现event事件的分发处理。如果是Server端发现新建连接,则调用到ListenSocket::handleRead()函数。如果是Client端发现新建连接,则调用到ConnectSocket::handleWrite()函数。
4、ListenSocket::handleRead()接下来会调用TcpServer::newConnection()函数,而ConnectSocket::handleWrite()接下来会调用TcpClient::newConnection()函数。
5、最终TcpServer::newConnection()和TcpClient::newConnection()会为每个新建连接new一个TcpLink对象,并且每个TcpLink对象中会包含一个LinkOwner类指针,该指针指向了创建这个TcpLink对象的TcpServer对象或TcpClient对象。另外,这个TcpLink对象也会通过Poller::addFdEvent()接口将自己注册到轮询管理器Poller中。
6、通过前面的5步,Server端或Client端的一个Tcp连接对象(TcpLink)就建立好了。接下来,如果在这个Tcp连接上出现POLLIN、POLLOUT、POLLHUP事件,都会被EventLoop.loop()中的Poller::polling()查询到,然后通过TcpLink::hanleEvent(),将事件分发到TcpLink::handleRead()、TcpLink::handleWrite()、TcpLink::handleClose()最终通过LinkOwner::rcvMessage()、LinkOwner::writeComplete()、LinkOwner::delConnection()等接口调用到TcpServer或TcpClient对象。
修改后的相关代码已上传https://git.oschina.net/coolbaul/sim_muduo。欢迎感兴趣的同学一起讨论。