【校招项目】基于C++11的muduo库

news2024/11/25 14:54:39

基于C++11的网络库

文章目录

  • 基于C++11的网络库
    • 项目介绍
    • 项目地址
    • 项目特点
    • 开发环境
    • 并发模型
    • 构建项目
    • 运行案例
    • 模块讲解
      • Channel
      • Poller
      • EventLoop
      • Buffer
      • Timer
      • HTTP
      • 异步日志
      • 内存池
      • 数据库连接池
    • 优化计划
    • 感谢

项目介绍

本项目是参考 muduo 实现的基于 Reactor 模型的多线程网络库。使用 C++ 11 编写去除 muduo 对 boost 的依赖,内部实现了一个小型的 HTTP 服务器,可支持 GET 请求和静态资源的访问,且附有异步日志监控服务端情况。

项目已经实现了 Channel 模块、Poller 模块、事件循环模块、HTTP 模块、定时器模块、异步日志模块、内存池模块、数据库连接池模块。

项目地址

https://github.com/Shang/A-Tiny-Network-Library

项目特点

  • 底层使用 Epoll + LT 模式的 I/O 复用模型,并且结合非阻塞 I/O 实现主从 Reactor 模型。
  • 采用「one loop per thread」线程模型,并向上封装线程池避免线程创建和销毁带来的性能开销。
  • 采用 eventfd 作为事件通知描述符,方便高效派发事件到其他线程执行异步任务。
  • 基于自实现的双缓冲区实现异步日志,由后端线程负责定时向磁盘写入前端日志信息,避免数据落盘时阻塞网络服务。
  • 基于红黑树实现定时器管理结构,内部使用 Linux 的 timerfd 通知到期任务,高效管理定时任务。
  • 遵循 RALL 手法使用智能指针管理内存,减小内存泄露风险。
  • 利用有限状态机解析 HTTP 请求报文。
  • 参照 Nginx 实现了内存池模块,更好管理小块内存空间,减少内存碎片。
  • 数据库连接池可以动态管理连接数量,及时生成或销毁连接,保证连接池性能。

开发环境

  • 操作系统:Ubuntu 18.04.6 LTS
  • 编译器:g++ 7.5.0
  • 编辑器:vscode
  • 版本控制:git
  • 项目构建:cmake 3.10.2

并发模型

image.png

项目采用主从 Reactor 模型,MainReactor 只负责监听派发新连接,在 MainReactor 中通过 Acceptor 接收新连接并轮询派发给 SubReactor,SubReactor 负责此连接的读写事件。

调用 TcpServer 的 start 函数后,会内部创建线程池。每个线程独立的运行一个事件循环,即 SubReactor。MainReactor 从线程池中轮询获取 SubReactor 并派发给它新连接,处理读写事件的 SubReactor 个数一般和 CPU 核心数相等。使用主从 Reactor 模型有诸多优点:

  1. 响应快,不必为单个同步事件所阻塞,虽然 Reactor 本身依然是同步的;
  2. 可以最大程度避免复杂的多线程及同步问题,并且避免多线程/进程的切换;
  3. 扩展性好,可以方便通过增加 Reactor 实例个数充分利用 CPU 资源;
  4. 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性;

构建项目

安装Cmake

sudo apt-get update
sudo apt-get install cmake

下载项目

git clone git@github.com:Shangyizhou/tiny-network.git

执行脚本构建项目

cd ./tiny-network && bash build.sh

运行案例

这里以一个简单的回声服务器作为案例,EchoServer默认监听端口为8080

cd ./example
./EchoServer

执行情况:

img

http模块有一个小型的HTTP服务器案例,也可以执行。其默认监听8080

cd ./src/http && ./HttpServer

img

模块讲解

这里的某些模块会配置使用 muduo 的源码讲解,有些使用的是本项目的源码,不过实现思路是一致的。这里的源码详解更详细一点,后面只给出部分设计。

Channel模块

Poller模块

EventLoop模块

Buffer模块

定时器模块

HTTP模块

异步日志模块

内存池模块

数据库连接池模块

Channel

Channel模块

Channel 对文件描述符和事件进行了一层封装。平常我们写网络编程相关函数,基本就是创建套接字,绑定地址,转变为可监听状态(这部分我们在 Socket 类中实现过了,交给 Acceptor 调用即可),然后接受连接。

但是得到了一个初始化好的 socket 还不够,我们是需要监听这个 socket 上的事件并且处理事件的。比如我们在 Reactor 模型中使用了 epoll 监听该 socket 上的事件,我们还需将需要被监视的套接字和监视的事件注册到 epoll 对象中。

可以想到文件描述符和事件和 IO 函数全都混在在了一起,极其不好维护。而 muduo 中的 Channel 类将文件描述符和其感兴趣的事件(需要监听的事件)封装到了一起。而事件监听相关的代码放到了 Poller/EPollPoller 类中。
Channel 重要成员

/**
* const int Channel::kNoneEvent = 0;
* const int Channel::kReadEvent = EPOLLIN | EPOLLPRI;
* const int Channel::kWriteEvent = EPOLLOUT;
*/
static const int kNoneEvent;
static const int kReadEvent;
static const int kWriteEvent;

EventLoop *loop_;   // 当前Channel属于的EventLoop
const int fd_;      // fd, Poller监听对象
int events_;        // 注册fd感兴趣的事件
int revents_;       // poller返回的具体发生的事件
int index_;         // 在Poller上注册的情况

std::weak_ptr<void> tie_;   // 弱指针指向TcpConnection(必要时升级为shared_ptr多一份引用计数,避免用户误删)
bool tied_;  // 标志此 Channel 是否被调用过 Channel::tie 方法

// 保存着事件到来时的回调函数
ReadEventCallback readCallback_; 	// 读事件回调函数
EventCallback writeCallback_;		// 写事件回调函数
EventCallback closeCallback_;		// 连接关闭回调函数
EventCallback errorCallback_;		// 错误发生回调函数

Poller

Poller模块

我们编写网络编程代码的时候少不了使用IO复用系列函数,而muduo也为我们提供了对此的封装。muduo 有 Poller 和 EPollPoller 类分别对应着epoll和poll。

而我们使用的接口是Poller,muduo 以Poller 为虚基类,派生出 Poller 和 EPollPoller 两个子类,用不同的形式实现 IO 复用。

class Poller : noncopyable
{
 public:
  // Poller关注的Channel
  typedef std::vector<Channel*> ChannelList;

  Poller(EventLoop* loop);
  virtual ~Poller();

  /**
   * 需要交给派生类实现的接口
   * 用于监听感兴趣的事件和fd(封装成了channel)
   * 对于Poller是poll,对于EPollerPoller是epoll_wait
   * 最后返回epoll_wait/poll的返回时间
   */
  virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels) = 0;

  // 需要交给派生类实现的接口(须在EventLoop所在的线程调用)
  // 更新事件,channel::update->eventloop::updateChannel->Poller::updateChannel
  virtual void updateChannel(Channel* channel) = 0;

  // 需要交给派生类实现的接口(须在EventLoop所在的线程调用)
  // 当Channel销毁时移除此Channel
  virtual void removeChannel(Channel* channel) = 0;
  
  // 需要交给派生类实现的接口
  virtual bool hasChannel(Channel* channel) const;
  
  /** 
   * newDefaultPoller获取一个默认的Poller对象(内部实现可能是epoll或poll)
   * 它的实现并不在 Poller.cc 文件中
   * 如果要实现则可以预料其会包含EPollPoller PollPoller
   * 那么外面就会在基类引用派生类的头文件,这个抽象的设计就不好
   * 所以外面会单独创建一个 DefaultPoller.cc 的文件去实现
   */
  static Poller* newDefaultPoller(EventLoop* loop);

  // 断言是否在创建EventLoop的所在线程
  void assertInLoopThread() const
  {
    ownerLoop_->assertInLoopThread();
  }

 protected:
  // 保存fd => Channel的映射
  typedef std::map<int, Channel*> ChannelMap;
  ChannelMap channels_;

 private:
  EventLoop* ownerLoop_;
};

重写方法靠派生类实现,这里我们可以专注一下 newDefaultPoler 方法。其内部返回默认实现的 Poller,可以是 epoll 实现的,也可以是 select 或 poll 实现的。

EventLoop

EventLoop模块

EventLoop 可以算是 muduo 的核心类了,EventLoop 对应着事件循环,其驱动着 Reactor 模型。我们之前的 Channel 和 Poller 类都需要依靠 EventLoop 类来调用。

Channel 负责封装文件描述符和其感兴趣的事件,里面还保存了事件发生时的回调函数
Poller 负责I/O复用的抽象,其内部调用epoll_wait获取活跃的 Channel
EventLoop 相当于 Channel 和 Poller 之间的桥梁,Channel 和 Poller 之间并不之间沟通,而是借助着 EventLoop 这个类。
这里上代码,我们可以看见 EventLoop 的成员变量就有 Channel 和 Poller。

class EventLoop
{
	...
	std::unique_ptr<Poller> poller_;

	// scratch variables
	ChannelList activeChannels_;
	Channel* currentActiveChannel_;
};

其实 EventLoop 也就是 Reactor模型的一个实例,其重点在于循环调用 epoll_wait 不断的监听发生的事件,然后调用处理这些对应事件的函数。而这里就设计了线程之间的通信机制了。

最初写socket编程的时候会涉及这一块,调用epoll_wait不断获取发生事件的文件描述符,这其实就是一个事件循环。

while (1)
{
    // 返回发生的事件个数
    int n = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); 

    // 这个循环的所有文件描述符都是发生了事件的,效率得到了提高
    for (i = 0; i < n; i++)
    {
        //客户端请求连接时
        if (ep_events[i].data.fd == serv_sock) 
        {
        	// 接收新连接的到来
        }
        else //是客户端套接字时
        {
        	// 负责读写数据
        }
    }
}

Buffer

Buffer模块

TcpConnection 类负责处理一个新连接的事件,包括从客户端读取数据和向客户端写数据。但是在这之前,需要先设计好缓冲区。

非阻塞网络编程中应用层buffer是必须的:非阻塞IO的核心思想是避免阻塞在read()或write()或其他I/O系统调用上,这样可以最大限度复用thread-of-control,让一个线程能服务于多个socket连接。I/O线程只能阻塞在IO-multiplexing函数上,如select()/poll()/epoll_wait()。这样一来,应用层的缓冲是必须的,每个TCP socket都要有inputBuffer和outputBuffer。
TcpConnection必须有output buffer:使程序在write()操作上不会产生阻塞,当write()操作后,操作系统一次性没有接受完时,网络库把剩余数据则放入outputBuffer中,然后注册POLLOUT事件,一旦socket变得可写,则立刻调用write()进行写入数据。——应用层buffer到操作系统buffer
TcpConnection必须有input buffer:当发送方send数据后,接收方收到数据不一定是整个的数据,网络库在处理socket可读事件的时候,必须一次性把socket里的数据读完,否则会反复触发POLLIN事件,造成busy-loop。所以网路库为了应对数据不完整的情况,收到的数据先放到inputBuffer里。——操作系统buffer到应用层buffer。

muduo 的 Buffer 类作为网络通信的缓冲区,像是 TcpConnection 就拥有 inputBuffer 和 outputBuffer 两个缓冲区成员。而缓冲区的设计特点:

其内部使用std::vector保存数据,并提供许多访问方法。并且std::vector拥有扩容空间的操作,可以适应数据的不断添加。
std::vector内部分为三块,头部预留空间,可读空间,可写空间。内部使用索引标注每个空间的起始位置。每次往里面写入数据,就移动writeIndex;从里面读取数据,就移动readIndex。

在这里插入图片描述

class Buffer : public muduo::copyable
{
public:
    static const size_t kCheapPrepend = 8; // 头部预留8个字节
    static const size_t kInitialSize = 1024; // 缓冲区初始化大小 1KB

    explicit Buffer(size_t initialSize = kInitialSize)
        : buffer_(kCheapPrepend + initialSize), // buffer分配大小 8 + 1KB
            readerIndex_(kCheapPrepend), // 可读索引和可写索引最开始位置都在预留字节后
            writerIndex_(kCheapPrepend) 
    {
        assert(readableBytes() == 0);
        assert(writableBytes() == initialSize);
        assert(prependableBytes() == kCheapPrepend);
    }

	/*......*/

	// 可读空间大小
	size_t readableBytes() const
	{ return writerIndex_ - readerIndex_; }
	
	// 可写空间大小
	size_t writableBytes() const
	{ return buffer_.size() - writerIndex_; }
	
	// 预留空间大小
	size_t prependableBytes() const
	{ return readerIndex_; }
	
	// 返回可读空间地址
	const char* peek() const
	{ return begin() + readerIndex_; }

	/*......*/
	
private:
	std::vector<char> buffer_; // 缓冲区其实就是vector<char>
	size_t readerIndex_; // 可读区域开始索引
	size_t writerIndex_; // 可写区域开始索引
};

Timer

定时器模块

定时器功能相关的类由 Timestamp,Timer,TimerQueue类组成,muduo 库还有一个 Timeld 类方便对定时器进行索引,本项目里没有加上这个类。
一个定时器所需的功能:

  1. 定时器到期后需要调用回调函数
  2. 我们需要让定时器记录我们设置的超时时间
  3. 如果是重复事件(比如每间隔5秒扫描一次用户连接),我们还需要记录超时时间间隔
class Timer : noncopyable
{
private:
    const TimerCallback callback_;  // 定时器回调函数
    Timestamp expiration_;          // 下一次的超时时刻
    const double interval_;         // 超时时间间隔,如果是一次性定时器,该值为0
    const bool repeat_;             // 是否重复(false 表示是一次性定时器)
};

使用timerfd实现定时功能
linux2.6.25 版本新增了 timerfd 这个供用户程序使用的定时接口,这个接口基于文件描述符,当超时事件发生时,该文件描述符就变为可读。这种特性可以使我们在写服务器程序时,很方便的便把定时事件变成和其他I/O事件一样的处理方式,当时间到期后,就会触发读事件。我们调用响应的回调函数即可。

int timer_create(int clockid,int flags); // 成功返回0

此函数用于创建timerfd,我们需要指明使用标准事件还是相对事件,并且传入控制标志。

  • CLOCK_REALTIME:相对时间,从1970.1.1到目前时间,之所以说其为相对时间,是因为我们只要改变当前系统的时间,从1970.1.1到当前时间就会发生变化,所以说其为相对时间
  • CLOCK_MONOTONIC:绝对时间,获取的时间为系统最近一次重启到现在的时间,更该系统时间对其没影响
  • flag:TFD_NONBLOCK(非阻塞),TFD_CLOEXEC

HTTP

HTTP模块

异步日志

异步日志模块

为什么要实现非阻塞的日志

如果是同步日志,那么每次产生日志信息时,就需要将这条日志信息完全写入磁盘后才会执行后续程序。而磁盘 IO 是比较耗时的操作,如果有大批量的日志信息需要写入就会阻塞网络库的工作。
如果是异步日志,那么写日志消息只需要将日志的信息先进行存储,当累计到一定量或者经过一定时间时再将这些日志信息批量写入磁盘。而这个写入过程靠后台线程去执行,不会影响处理事件的其他线程。
经过对比可以得到,异步日志的方式对性能更加友好,而且可以减少磁盘 IO 函数的操作,一次写入更多的数据,提高效率。

muduo日志库的设计
muduo 日志消息的组成:时间戳、线程ID、日志级别、日志正文、源文件名、行号(下面的例子省略了线程 TID)。

2022/11/30 00:57:016530 INFO EventLoop start looping - EventLoop.cc:70
muduo 日志库由前端和后端组成。

前端主要包括:Logger、LogStream、FixedBuffer、SourceFile。
后端主要包括:AsyncLogging、LogFile、AppendFile。

内存池

内存池模块

数据库连接池

数据库连接池模块

优化计划

  1. 计划使用 std::chrono 实现底层时间戳
  2. 使用优先级队列管理定时器结构
  3. 覆盖更多的单元测试

感谢

  • 《Linux高性能服务器编程》
  • 《Linux多线程服务端编程:使用muduo C++网络库》
  • https://github.com/chenshuo/muduo

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/84168.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何高效存储中高频多因子

2022年12月1日晚7点半&#xff0c;DolphinDB 进行了以「中高频多因子库存储最佳实践」为主题的直播&#xff0c;吸引了众多专业观众的注意。本文将回顾直播精彩内容。 DolphinDB 的因子库&#xff1a; 为了使广大用户更方便地实现因子计算和管理&#xff0c;助力更高效的投研…

[附源码]Node.js计算机毕业设计大学生专业实习管理系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

C/C++ 高精度(加减乘除)算法简单实现

文章目录前言一、基本原理1、存储方式2、计算方式二、辅助方法1、字符串转高精度2、整型转高精度3、比较4、打印三、算法实现1、加法2、减法3、乘法4、除法四、使用示例1、加法2、减法3、乘法4、除法总结前言 由于上一章《C/C 高精度&#xff08;加减乘除&#xff09;算法实现…

NetSuite资产负债表编制技巧

在最近的项目上&#xff0c;发现大家对NetSuite资产负债的编制参数缺乏足够的了解&#xff0c;导致对客户需求的支支吾吾&#xff0c;产生了沟通上的浪费。所以今朝对三个典型应用做个介绍。 1. 如何在资产负债表中选择“会计期间区间”&#xff1f; 2. 期初、发生、结余报表…

【元宇宙欧米说】Web3如何为漫画产业创造更多玩法和可能性

漫画和NFT的碰撞会产生什么火花&#xff1f;NFT如何为传统的漫画收藏增加额外价值&#xff1f;Web3时代如何为漫画带来更多玩法和可能性&#xff1f; 12月15日下午三点&#xff0c;MadManga 创始人Jun将以“Web3如何为漫画产业创造更多玩法和可能性”为题&#xff0c;与大家共…

构建文本摘要Baseline并且开始训练

构建文本摘要Baseline并且开始训练 基于前面word2vec的原理与训练实践、seq2seq模型的原理与实践以及attention机制&#xff0c;已经分别写了相关的文章来记录&#xff0c;此篇文章就是基于前面所学&#xff0c;开始着手训练文本摘要模型&#xff0c;当然仅是一个比较普通的ba…

[附源码]Node.js计算机毕业设计大学体育馆预约系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

vue框架搭建大屏自适应方案

vue框架搭建大屏自适应方案 1.可使用flexible.js rem实现宽高&#xff0c;字体自适应 附上flexible.js代码 (function flexible(window, document) {var docEl document.documentElement;var dpr window.devicePixelRatio || 1;// adjust body font sizefunction setBody…

NocasRule负载均衡与服务实例的权重设置

NocasRule负载均衡 .yml 配置文件配置 server:port: 8080 spring:application:name: orderservicecloud:nacos:server-addr: localhost:8848 #nocas服务地址discovery:cluster-name: HZ #集群名字 userservice: #要做配置的微服务名称ribbon:NFLoadBalancerRuleClassName: com…

游戏开发57课 性能优化14

5. 内存优化 内存优化目的是加快IO&#xff0c;防止卡主线程&#xff0c;防止频繁操作&#xff08;创建/删除&#xff09;内存&#xff0c;避免内存碎片化和占用过高。 5.1 缓存法 与CPU的缓存计算类似&#xff0c;思路是将需要重复创建的对象缓存起来&#xff0c;销毁时将它…

安装、启动与停止Apache服务

安装、启动与停止Apache服务 安装Apache相关软件 [rootcentos7 ~]# rpm -q httpd [rootcentos7-1 ~]# mkdir /opt/centos //创建目录/opt/centos [rootcentos7-1 ~]# mount /dev/cdrom /opt/centos //挂载光盘到/opt/centos 下 mount: /dev/sr0 写保护…

Spring Boot 3.0.0正式发布,Banner不再支持图片增强可观测性

本文已被https://yourbatman.cn收录&#xff1b;女娲Knife-Initializr工程可公开访问啦&#xff1b;程序员专用网盘https://wangpan.yourbatman.cn&#xff1b;技术专栏源代码大本营&#xff1a;https://github.com/yourbatman/tech-column-learning&#xff1b;公号后台回复“…

openCV(一)基础背景

1 认识计算机视觉 2012年AlexNet模型在ImageNet图像分类中获得比赛冠军&#xff0c;深度学习开始在计算机视觉领域流行。早期的计算机视觉主要集中在重建方面&#xff0c;2012年以后在感知和重建两个领域都受到了深度学习的影响。应用场景包括自动驾驶、机器视觉、安防监控、其…

猿如意中的【PostgreSQL 数据库】工具详情介绍

猿如意中的【PostgreSQL 数据库】工具详情介绍 一、工具名称 PostgreSQL 数据库 二、下载安装渠道 PostgreSQL 数据库V14.2 通过CSDN官方开发的【猿如意】客户端进行下载安装。 2.1 什么是猿如意&#xff1f; 猿如意是一款面向开发者的辅助开发工具箱&#xff0c;包含了效…

jenkins-pipeline与变量

本文介绍如何在pipeline中使用变量 使用jenkins预定义的环境变量 jenkins预先定义了一些环境变量&#xff0c;在pipeline中使用${env.key}来调用 另外安装了第三方插件&#xff0c;会有新的环境变量&#xff0c;可以使用插件Environment Inject来查看 在pipeline中使用预定义…

Java二维数组项目练习

T1.显示所有书店客户的信息 示例代码 public static void main(String[] args) {String[][] users{{"1100","18","100"},{"1101","24","834"},{"1102","13","20000"},{"1103…

软件测试——用例篇

文章目录为什么在测试前要设计测试用例基于需求设计测试用例等价类边界值错误猜测法场景法因果图正交法为什么在测试前要设计测试用例 测试用例是执行测试的依据。可以复用&#xff08;回归测试的时候&#xff09;衡量需求的覆盖率自动化测试的依据有借鉴意义&#xff0c;后续…

OH----原子量的妙用--保护usb时序

1、问题&#xff1a; 展锐平台&#xff0c;usb otg高概率不能正确检测识别到 2、思路&#xff1a; usb使用musb控制器&#xff0c;展锐的平台处理代码是musb_sprd.c&#xff0c;在这个文件中对usb mode做检测和切换&#xff0c;log级别跳到最高&#xff0c;在probe中的关键函…

用 Taichi 加速 Python:提速 100+ 倍!

Python 已经成为世界上最流行的编程语言&#xff0c;尤其在深度学习、数据科学等领域占据主导地位。但是由于其解释执行的属性&#xff0c;Python 较低的性能很影响它在计算密集&#xff08;比如多重 for 循环&#xff09;的场景下发挥作用&#xff0c;实在让人又爱又恨。如果你…

PAT(乙级)2022年冬季考试

此前先后花了十元去做了乙级题&#xff0c;从最开始分别是70&#xff0c;35&#xff0c;43&#xff0c;33&#xff08;途中做了RobpCom,只搞定了签到题&#xff09;&#xff0c;想着报今年的冬季赛&#xff0c;但是报名费有点高啊&#xff0c;加上做下来感觉不怎么样&#xff0…