博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
muduo网络库源码分析——整体架构
阅读量:2179 次
发布时间:2019-05-01

本文共 7586 字,大约阅读时间需要 25 分钟。

muduo的源代码中,虽然不考虑可移植性,但还是划分了很多小的类(Channel、Socket、TcpConnection、Acceptor,不知道是不是参考了java中的概念),类之间大量通过boost::bind()注册回调函数,感觉比继承还要难理解。

但是无论如何,muduo所强调的关于现代C++编程技术和多线程服务端编程理念都是非常值得学习的。本文的主要目的:从整体架构上分析muduo的源代码,让希望了解它的人能够快速入门。让更多的人可以此为基础,更快捷的实践现代C++编程和Linux并发网络编程相关的技术。

一、经典的服务器设计模式Reactor模式

大多数人学习Linux网络编程的起点可能都是从《UNP》开始的,书中描述的服务端程序架构基本上是一个大的while循环,程序阻塞在accept或poll函数上,等待被监控的socket描述符上出现预期的事件。事件到达后,accept或poll函数的阻塞解除,程序向下执行,根据socket描述符上出现的事件,执行read、write或错误处理。

整体架构如下图所示:
这里写图片描述

muduo的软件架构采用的也是Reactor模式,只是整个模式被分成多个类,并且支持以线程池的方式实现多线程并发处理,所以显得有些复杂。整体架构如下图所示:

这里写图片描述

二、分析Muduo中几个主要的类

muduo是一个支持多线程编程的网络库,它封装了和Linux线程、网络socket相关的十几个API,支持客户端和服务端编程。这里先介绍和服务端编程编程相关的几个类对象。

1、TcpServer、Acceptor和EventLoop

TcpServer对象一般运行在用户代码的主线程,它的生命周期应该和用户服务器程序的生命周期一致。TcpServer对象基本上是用户代码和Muduo库之间的总界面。它对内管理多个成员对象、创建线程池、将新建连接分发不同线程处理,对外为用户代码提供客户端连接建立、消息接收和发送的接口。

这里写图片描述
TcpServer中有三个主要的成员类,分别是:AcceptorEventLoopThreadPoolEventLoop*。其中:
Acceptor负责管理服务器的监听socket
EventLoopThreadPool用于创建和管理线程池
EventLoop*是一个指针,它指向一个用户代码中创建的EventLoop对象,为TcpServer专用,相当于是为主线程提供的Loop循环。
这里我一直不明白既然是给TcpServer专用,这个EventLoop对象为啥要在客户代码中创建,然后将对象指针传递给TcpServer,而不是像Acceptor那样放在TcpServer中自动创建)。

这里写图片描述

在TcpServer的构造函数中,会自动创建并初始化Acceptor对象。其中,Acceptor对象的构造函数首先会创建一个用于服务器程序的监听socket描述符,并为其bind()服务器侧的IP地址和监听端口。另外,Acceptor对象还提供一个封装了listen() API的函数Acceptor::listen()。

void Acceptor::listen(){    acceptSocket_.listen(); // 此函数最终调用listen() API函数,启动监听    // 此函数将监听socket对应的Channel对象放入轮询管理器poller的监控描述符集合中    // 当监听socket收到客户端接入请求后,监听socket对应的Channel对象(acceptChannel_)    // 的handleEvent()函数,最终会调用到Acceptor::handleRead() (此函数将在稍后介绍)    acceptChannel_.enableReading(); }

Acceptor::listen()函数在TcpServer::start()函数的最后一行被调用。

这里写图片描述

当然,这里说调用可能并不准确,因为loop_->runInLoop()函数会判断:如果当前正在运行的线程就是loop_对象所属的线程,则直接执行 Acceptor::listen()函数,否则 Acceptor::listen()函数被封装成函数对象放入pendingFunctors_容器中,等待Loop_所属的线程运行时再被执行。(这里其实我也有点不理解,因为TcpServer中的loop_指向的就是用户代码中创建的EventLoop对象,难道有可能用户调用TcpServer::start()函数的线程与用户创建EventLoop对象的线程不是同一个线程???

这里写图片描述

 

讲到这里,我们可以看到传统的服务器初始化部分(创建socket、bind端口和IP、启动监听listen)基本上就完成了。另外,这里还有两个很重要的细节。首先Acceptor内部管理了一个Socket对象(acceptSocket_)和一个Channel(acceptChannel_)对象。Socket对象封装了监听socket描述符,它向下封装了和socket相关的API接口。而Channel对象总是和Socket对象成对出现,它向上提供了一些回调函数的注册接口,这些回调函数用于处理socket描述符上出现的各种状态事件,例如:POLLIN、POLLOUT、POLLERR等等。(这里,其实我也有些疑问,既然不考虑移植,为啥要区分Socket对象和Channel对象,把它们合并了不是更简单)。了解了Socket和Channel的关系以后,后面在分析TcpConnection这个类的时候,我们会发现,每个TcpConnection用于表示一个客户端的连接,所以TcpConnection类中也会有一对Socket和Channel成员。(我的想法是,Socket、Channel、TcpConnection完全可以合并成一个类,像现在这样分成3个类,一堆回调函数注册来注册去,没想明白这样做的好处是什么???

再说第二个需要注意的细节:Acceptor还提供了一个函数void Acceptor::handleRead(),这个函数的主要工作有两个

// 为了便于说明问题,这是简化后的代码// 此函数中还有一个防止系统描述符耗尽的小技巧,这里没有展示,感兴趣的同学可参考陈大侠的书自行了解void Acceptor::handleRead() {    int connfd = acceptSocket_.accept(&peerAddr);    newConnectionCallback_(connfd, peerAddr);}

代码的第一行代码int connfd = acceptSocket_.accept(&peerAddr)最终会调用到系统API函数accept(),用于接收一个客户端的连接请求。 

第二行的newConnectionCallback_是一个回调函数指针,它指向TcpServer::newConnection()函数,用于处理接收到的连接请求。

Acceptor::handleRead()函数本身在Acceptor的构造函数中会被注册给acceptChannel_对象的readCallback_指针。当acceptSocket_(监听socket)描述符发现客户端的连接请求时,acceptChannel_对象的readCallback_就会被调用,即Acceptor::handleRead()函数被调用。

void Channel::handleEvent(Timestamp receiveTime){    handleEventWithGuard(receiveTime);}void Channel::handleEventWithGuard(Timestamp receiveTime){    if (revents_ & (POLLIN | POLLPRI | POLLRDHUP))    {        // acceptChannel对象的readCallback_指向Acceptor::handleRead()函数        if (readCallback_) readCallback_(receiveTime);    }}

如果我前面的描述够清晰的话,看到这里,通过muduo库,从创建服务端监听socket到调用accept() API获取客户端连接请求的过程应该就比较清晰了。接下来需要分析的是newConnectionCallback_,也就是TcpServer::newConnection()函数如何创建并管理一个客户端连接。新建一条Tcp连接的整体函数调用如下所示:

// 为了便于说明问题,这是简化后的代码void Acceptor::handleRead(){    int connfd = acceptSocket_.accept(&peerAddr);    if (connfd >= 0)    {        if (newConnectionCallback_)        {            newConnectionCallback_(connfd, peerAddr); // 这里调用TcpServer::newConnection()函数        }    }}void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr){    // 从线程池中获取一个EventLoop(因为EventLoop对应一个线程),这里相当于获取了一个线程    EventLoop* ioLoop = threadPool_->getNextLoop();    // 为当前接收到的连接请求创建一个TcpConnection对象,将TcpConnection对象与分配的(ioLoop)线程绑定    TcpConnectionPtr conn(new TcpConnection(ioLoop,connName, sockfd, localAddr, peerAddr));    // 将(用户定义的)连接建立回调函数、消息接收回调函数注册到TcpConnection对象中    // 由此可知TcpConnection才是muduo库对与网络连接的核心处理    conn->setConnectionCallback(connectionCallback_);    conn->setMessageCallback(messageCallback_);    // 对于新建连接的socket描述符,还需要设置期望监控的事件(POLLIN | POLLPRI),    // 并且将此socket描述符放入poll函数的监控描述符集合中,用于等待接收客户端从此连接上发送来的消息    // 这些工作,都是由TcpConnection::connectEstablished函数完成。    ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));}// 为了便于说明问题,这是简化后的代码void TcpConnection::connectEstablished(){  channel_->enableReading(); // 将新建连接的Channel对象加入到POLLER轮询管理器  // 此函数对应TcpServer::connectionCallback_,最终指向一个用户服务器定义的回调函数  connectionCallback_(shared_from_this());}void Channel::enableReading() { events_ |= (POLLIN | POLLPRI); update(); }void Channel::update(){ loop_->updateChannel(this);}void EventLoop::updateChannel(Channel* channel){  poller_->updateChannel(channel); }

以上就是建立一条Tcp连接的大致流程,如果要删除Tcp连接,函数的调用流程如下:

void TcpConnection::handleClose(){    setState(kDisconnected);    channel_->disableAll();    TcpConnectionPtr guardThis(shared_from_this());    connectionCallback_(guardThis); // 回调用户定义的连接处理函数    // must be the last line    closeCallback_(guardThis); // closeCallback_对应的函数是TcpServer::removeConnection()}void TcpServer::removeConnectionInLoop(const TcpConnectionPtr& conn){    size_t n = connections_.erase(conn->name());    ioLoop->queueInLoop(std::bind(&TcpConnection::connectDestroyed, conn));}void TcpConnection::connectDestroyed(){  if (state_ == kConnected)  {    setState(kDisconnected);    channel_->disableAll();    connectionCallback_(shared_from_this()); // 回调用户定义的连接处理函数  }  channel_->remove(); // 本连接对应的描述符不在需要监控,从poll中删除。}

前面详细分析的TcpServer和Acceptor对象的结构和行为,并且大致了解了Socket、Channel和TcpConnection三者之间的关系,整个Muduo库中与Reactor模式相关的软件架构也就大致清楚了,最后再看看EventLoop这个类。muduo的对于并发处理采用的是one thread one loop方式,所以,每个线程都唯一对应一个EventLoop对象。

这里写图片描述

EventLoop中包含了两个比较重要的函数,首先是void EventLoop::loop(),它为每个工作线程提供了一个大的while循环,如下:

void EventLoop::loop(){    quit_ = false;    while (!quit_)    {        // 轮询,得到发生状态变化的Channel对象集合        poller_->poll(kPollTimeMs, &activeChannels_);        // 遍历每个发生状态变化的Channel对象,执行对象的状态处理函数        for (ChannelList::iterator it = activeChannels_.begin();        it != activeChannels_.end(); ++it)        {            currentActiveChannel_ = *it;            currentActiveChannel_->handleEvent(pollReturnTime_);        }        // 执行pendingFunctors_容器中由外部线程注入的函数对象        doPendingFunctors();    }}

第二个重要的函数是void EventLoop::runInLoop(const Functor& cb),此函数的作用是为外部线程提供接口,将函数注入到EventLoop所属的线程中执行。

void EventLoop::runInLoop(const Functor& cb){    if (isInLoopThread())    {        cb();    }    else    {        queueInLoop(cb);    }}void EventLoop::queueInLoop(const Functor& cb){    {        MutexLockGuard lock(mutex_);        pendingFunctors_.push_back(cb);    }    if (!isInLoopThread() || callingPendingFunctors_)    {        wakeup();    }}

到此为止,服务器端创建并初始化监听socket描述符、轮询(poll())并获取(accept())监听socket描述符上的新建连接请求,为新建连接分配EventLoop工作线程的相关处理我们都已经分析了一遍。

接下来看看muduo库中和线程池管理器相关的三个类:EventLoopThread、Thread和EventLoopThreadPool。除主线程外,muduo库通过线程池管理器EventLoopThreadPool为每个线程创建一个EventLoopThread对象,每个EventLoopThread类中包含一个EventLoop对象指针(该对象建立在线程入口函数 void EventLoopThread::threadFunc() 的堆栈上,所以EventLoopThread中包含的EventLoop对象的生命周期应该和该线程相同)。另外,EventLoopThread类中还包含一个Thread类,它的作用主要是封装了和线程相关的系统API(例如:pthread_create()、pthread_detach()),总的看,这几个类的逻辑相对简单,就不深入分析了。

这里写图片描述

转自:

你可能感兴趣的文章
solver及其配置
查看>>
JAVA多线程之volatile 与 synchronized 的比较
查看>>
Java集合框架知识梳理
查看>>
笔试题(一)—— java基础
查看>>
Redis学习笔记(三)—— 使用redis客户端连接windows和linux下的redis并解决无法连接redis的问题
查看>>
Intellij IDEA使用(一)—— 安装Intellij IDEA(ideaIU-2017.2.3)并完成Intellij IDEA的简单配置
查看>>
Intellij IDEA使用(二)—— 在Intellij IDEA中配置JDK(SDK)
查看>>
Intellij IDEA使用(三)——在Intellij IDEA中配置Tomcat服务器
查看>>
Intellij IDEA使用(四)—— 使用Intellij IDEA创建静态的web(HTML)项目
查看>>
Intellij IDEA使用(五)—— Intellij IDEA在使用中的一些其他常用功能或常用配置收集
查看>>
Intellij IDEA使用(六)—— 使用Intellij IDEA创建Java项目并配置jar包
查看>>
Eclipse使用(十)—— 使用Eclipse创建简单的Maven Java项目
查看>>
Eclipse使用(十一)—— 使用Eclipse创建简单的Maven JavaWeb项目
查看>>
Intellij IDEA使用(十三)—— 在Intellij IDEA中配置Maven
查看>>
面试题 —— 关于main方法的十个面试题
查看>>
集成测试(一)—— 使用PHP页面请求Spring项目的Java接口数据
查看>>
使用Maven构建的简单的单模块SSM项目
查看>>
Intellij IDEA使用(十四)—— 在IDEA中创建包(package)的问题
查看>>
Redis学习笔记(四)—— redis的常用命令和五大数据类型的简单使用
查看>>
Win10+VS2015编译libcurl
查看>>