陈硕 muduo
参考资料
muduo源码剖析


Muduo是一个基于Reactor模式的现代C++网络库,它采用非阻塞IO模型,基于时间驱动和回调,原生支持多核多线程,适合编写Linux服务端多线程网络应用程序。
muduo基于Reactor模式实现
Reactor模式也是目前大多数Linux端高性能网络编程框架和网络应用所选择的主要架构,例如内存数据库Redis和Java的Netty库等。

muduo 架构
整个架构依照reactor模式,如如下图:

所谓的reactor模型,是有一个循环的过程,监听对应事件是否触发,触发时调用对应的callback函数进行处理;
这里的事件在muduo中包括socket可读写事件、定时器事件。在其他网络库中如libevent也包括了signal、用户自定义事件

负责事件循环的部分muduo命名为EventLoop,其他库如netty、libevent也都有对应的组件。

负责监听事件是否触发的部分,在muduo中叫做Poller
muduo提供了epollpoll两种来实现,默认是epoll实现。 通过环境变量MUDUO_USE_POLL来决定是否使用poll:

Poller* Poller::newDefaultPoller(EventLoop* loop)
{// 通过此环境变量来决定使用poll还是epollif (::getenv("MUDUO_USE_POLL")){return new PollPoller(loop);}else{return new EPollPoller(loop);}
}

此外,图中的acceptor负责accept新连接,并将新连接分发到subReactor。这个组件在muduo中也叫做Acceptor


通过简单的例子介绍muduo的基本使用:

从最简单的echo server入手:

void onMessage(const muduo::net::TcpConnectionPtr& conn,muduo::net::Buffer* buf,muduo::Timestamp time)
{conn->send(buf);
}
int main()
{muduo::net::EventLoop loop;muduo::net::InetAddress listenAddr(2007);TcpServer server(&loop, listenAddr);server.setMessageCallback(onMessage);server.start();loop.loop();
}

echo-server的代码量非常简洁。一个典型的muduo的TcpServer工作流程如下:

  1. 建立一个事件循环器EventLoop
  2. 建立对应的业务服务器TcpServer
  3. 设置TcpServerCallback
  4. 启动server
  5. 开启事件循环

陈硕认为,TCP网络编程的本质是处理三个半事件,即:

  1. 连接的建立
  2. 连接的断开:包括主动断开和被动断开
  3. 消息到达,文件描述符可读。
  4. 消息发送完毕。这个算半个事件。

我们接下来分析下muduo是怎么处理和实现这三个半事件的:


连接的建立

我们使用linux的API编写一个简单的tcp服务器时,建立一个新的连接通常需要四步:
步骤1. socket() // 调用socket函数建立监听socket
步骤2. bind() // 绑定地址和端口
步骤3. listen() // 开始监听端口
步骤4. accept() // 返回新建立连接的fd

接下来我们分析一下这四个步骤在muduo中是何时进行的:
首先在tcpserver对象构建是,tcpserver的属性acceptor同时也被建立。在acceptor的构造函数中分别调用了socket函数和bind函数完成了步骤一二。 即 当tcpserver server(&loop,listenAddr); 执行结束时,监听socket已经建立好,并已绑定到对应地址和端口了

而当执行server.start()时,主要做了两个工作:

  1. 在监听socket上启动listen函数,也就是步骤3;
  2. 将监听socket的可读事件注册到EventLoop中。
    此时,程序已完成对地址的监听,但还不够,因为此时程序的主角EventLoop尚未启动。 当调用loop.loop()时,程序开始监听该socket的可读事件。
    当新连接请求建立时,可读事件触发,此时该事件对应的callbackEventLoop::loop()中被调用。 该事件的callback实际上就是Acceptor::handleRead()方法。

Acceptor::handleRead()方法中,做了三件事:

  1. 调用了accept函数,完成了步骤4,实现了连接的建立。得到一个已连接socket的fd
  2. 创建TcpConnection对象
  3. 将已连接socket的可读事件注册到EventLoop中。
    这里还有一个需要注意的点,创建的TcpConnnection对象是个shared_ptr该对象会被保存在TcpServer的connections中。这样才能保证引用计数大于0,对象不被释放。

至此,一个新的连接已完全建立好,其可读事件也已注册到EventLoop中了。


消息的读取

上节讲到,在新连接建立的时候,会将新连接的socket的可读事件注册到EventLoop中。 假如客户端发送消息,导致已连接socket的可读事件触发,该事件对应的callback同样也会在EventLoop::loop()中被调用。

事件的callback实际上就是TcpConnection::handleRead方法。 在TcpConnection::handleRead方法中,主要做了两件事:

  1. 从socket中读取数据,并将其放入inputbuffer
  2. 调用messageCallback,执行业务逻辑。
ssize_t n = inputBuffer_.readFd(channel_->fd(), &savedErrno);
if (n > 0)
{messageCallback_(shared_from_this(), &inputBuffer_, receiveTime);
}

messageCallback是在建立新连接时,将TcpServer::messageCallback方法bind到了TcpConnection::messageCallback的方法。
TcpServer::messageCallback就是业务逻辑的主要实现函数。通常情况下,我们可以在里面实现消息的编解码、消息的分发等工作,这里就不再深入探讨了。

在我们上面给出的示例代码中,echo-server的messageCallback非常简单,就是直接将得到的数据,重新send回去。在实际的业务处理中,一般都会调用TcpConnection::send()方法,给客户端回复消息。

这里需要注意的是,在messageCallback中,用户会有可能会把任务抛给自定义的Worker线程池处理。 但是这个在Worker线程池中任务,切忌直接对Buffer的操作。因为Buffer并不是线程安全的。

我们需要记住一个准则:

所有对IO和buffer的读写,都应该在IO线程中完成。

一般情况下,先在交给Worker线程池之前,应该现在IO线程中把Buffer进行切分解包等动作。将解包后的消息交由线程池处理,避免多个线程操作同一个资源。


消息的发送

用户通过调用TcpConnection::send()向客户端回复消息。由于muduo中使用了OutputBuffer,因此消息的发送过程比较复杂。
首先需要注意的是线程安全问题, 对于消息的读写必须都在EventLoop的同一个线程(通常称为IO线程)中进行: 因此,TcpConnection::send保证了线程安全性,它是这么做的:

void TcpConnection::send(const StringPiece& message)
{if (state_ == kConnected){if (loop_->isInLoopThread()){sendInLoop(message);}else{loop_->runInLoop(boost::bind(&TcpConnection::sendInLoop,this,     // FIXMEmessage.as_string()));}}
}

检测send的时候,是否在当前IO线程,如果是的话,直接进行写相关操作sendInLoop。 如果不在一个线程的话,需要将该任务抛给IO线程执行runInloop, 以保证write动作是在IO线程中执行的。我们后面会讲解runInloop的具体实现。

sendlnloop中,做了以下几件事:
1、假如OutputBuffer为空,则直接向socket写数据
2、如果向socket写数据没有写完,则统计剩余的字节个数,并进行下一步。没有写完可能是因为此时socket的TCP缓冲区已满了。
3、如果此时OutputBuffer中的旧数据的个数和未写完字节个数之和大于highWaterMark,则将highWaterMarkCallback放入待执行队列中
4、将对应socket的可写事件注册到EventLoop中

注意,直到发送的时候,才把socket的可写事件注册到了EventLoop中。之前只注册了可读事件。
连接socket的可写事件对应的callback是TcpConnection::handleWrite() 当某个socket的可写事件触发时,TcpConnection::handleWrite会做两个工作:

  1. 尽可能将数据从OutputBuffer中向socket中write数据
  2. 如果OutputBuffer没有剩余的,则将该socket的可写事件移除,并调用writeCompleteCallback

为什么要移除可写事件?
因为当OutputBuffer中没数据时,我们不需要向socket中写入数据。但是此时socket一直是处于可写状态的, 这将会导致TcpConnection::handleWrite()一直被触发。然而这个触发毫无意义,因为并没有什么可以写的。
所以muduo的处理方式是,当OutputBuffer还有数据时,socket可写事件是注册状态。当OutputBuffer为空时,则将socket的可写事件移除。


连接的中断
我们看下muduo对于连接的断开是怎么处理的。
连接的断开分为被动断开和主动断开。主动断开和被动断开的处理方式基本一致,因此本文只讲下被动断开的部分。
被动断开即远程端断开了连接,server端需要感知到这个断开的过程,然后进行的相关的处理。
其中感知远程断开这一步是在Tcp连接的可读事件处理函数handleRead中进行的:当对socket进行read操作时,返回值为0,则说明此时连接已断开。

接下来会做四件事情:

  1. 将该TCP连接对应的事件从EventLoop移除
  2. 调用用户的ConnectionCallback
  3. 将对应的TcpConnection对象从Server移除。
  4. close对应的fd。此步骤是在析构函数中被动触发的,当TcpConnection对象被移除后,引用计数为0,对象析构时会调用close。

muduo---C++网络编程库相关推荐

  1. muduo学习笔记:net部分之实现TCP网络编程库-Acceptor

    前述文章围绕base.net两个模块各种组件,已经形成了初具规模的Reactor事件处理框架.从现在开始,逐步实现一个非阻塞的TCP网络编程库.不同于传统的Reactor,将timers 做成循环中单 ...

  2. muduo学习笔记:net部分之实现TCP网络编程库-Buffer

    文章目录 为什么采用non-blocking网络编程中应用层buffer是必需的? Buffer 设计 Buffer::readFd() 线程安全 Muduo Buffer 的数据结构 Muduo B ...

  3. python网络编程库_Python网络编程——协程

    协程的概念 协程,又称微线程,纤程,也称用户级线程,在不开辟线程的基础上实现多任务,也就是在单线程的情况下完成多任务,多个任务按照一定顺序交替执行的,通俗理解只要在def里面只看到一个yield关键字 ...

  4. python网络编程库_python网络编程学习笔记(9):数据库客户端

    一.DB-API概述 python支持很多不同的数据库.由于不同的卖家服务器导致和数据库通信的网络协议各有不同.在python的早期版本中,每一种数据库都带有自己的python模块,所有这些模块以不同 ...

  5. c++网络编程:实现简单的聊天——基于服务器/客户端的模式

    c++ socket编程 服务器/客户端 最近在学c++网路编程,写个帖子记录自己的学习过程,部分内容参考一个博主的,附上他的链接: link 实现: 客户端往服务器端发送一条数据,服务器端接收数据并 ...

  6. 《Linux多线程服务端编程:使用muduo C++网络库》书摘6.6.2节

    6.6.2 常见的并发网络服务程序设计方案 W. Richard Stevens 的<UNIX 网络编程(第2 版)>第27 章"Client-ServerDesign Alte ...

  7. 新书预告:《Linux 多线程服务端编程——使用 muduo C++ 网络库》

    看完了 W. Richard Stevens 的传世经典<UNIX 网络编程>, 能照着例子用 Sockets API 编写 echo 服务, 却仍然对稍微复杂一点的网络编程任务感到无从下 ...

  8. Muduo 网络编程示例之四:Twisted Finger

    陈硕 (giantchen_AT_gmail) Blog.csdn.net/Solstice 这是<Muduo 网络编程示例>系列的第四篇文章. Muduo 全系列文章列表: http:/ ...

  9. Muduo 网络编程示例之三:定时器

    陈硕 (giantchen_AT_gmail) Blog.csdn.net/Solstice 这是<Muduo 网络编程示例>系列的第三篇文章. Muduo 全系列文章列表: http:/ ...

最新文章

  1. jqgrid ajax 请求参数,如何将csrf_令牌传递给jqgrid的editurl的post参数?
  2. 阿里P8连肝一周整理出这份python自动化测试实战PDF
  3. DIV的id和class
  4. 高内聚低耦合通俗理解_抱歉,请不要把“业务逻辑层”理解为“业务中台”
  5. 使用Cygwin登录Raspberry PI
  6. JS能力测评经典题之数组
  7. Bresenham画线算法详解及其OpenGL编程实现
  8. t470键盘拆解_thinkpad t470怎么样?thinkpad t470拆机图解全面评测
  9. MyEclipse配置jdk
  10. ARM9——五级流水线结构,以及PC指针
  11. 会编程的都来玩玩这个游戏吧,看看你的编程水平.
  12. 微信公众号开发——实现用户微信网页授权流程
  13. 物是人非事事休,欲语泪先流 的出处[z]
  14. Qt之GIF录制工具
  15. PDF怎样转换成JPG图片 PDF转换为JPG图片教程
  16. 简单实用 微信授权登陆(网页版)
  17. 【fread/fwrite】C语言API之fread/fwrite函数详解
  18. 计算机的冷启动与热启动
  19. Exercise11-Matplotlib
  20. 组织行为学对项目管理的意义(1)

热门文章

  1. vnpy入门操作2——CTP交易
  2. Linux操作系统实践期中考总结(选择题)
  3. PPP(Point to Point Protocol)——点对点协议
  4. Centos7 开启图形界面
  5. 基于java使用jsoup爬取网站投票数据的demo
  6. YOLOv6又快又准的目标检测框架 已开源
  7. 阿里云RDS数据库备份迁移遇到的问题
  8. html meter做个C盘,HTML基础教程:meter标签详细讲解
  9. 全国计算机竞赛能保送清华北大吗,通过参加学科竞赛获得保送清华北大的机会很少,还有必要参加吗?...
  10. php-用户名注册是否重复