【无标题】Muduo库源码剖析(十)——总结

Muduo网络库的核心代码模块

Channel

封装fd的对应事件变化情况,和关注事件

fd 、events、 revents、 callbacks,

两种channel: listenfd-acceptorChannel, connfd-connectionChannel

Poller 和 EPollPoller - Demultiplex

std::unordered_map channels_

EventLoop - Reactor

负责 Channel 和Poller通信

std::vector activeChannels_

std::unique_ptr poller_

int wakeupFd_; // 当mainloop获取一个新用户的channel,通过轮询算法选择一个subloop,通过该fd唤醒对应subloop来执行Channel回调

std::unique_ptr wakeupChannel_;

Thread 和 EventLoopThread

c++11线程封装 以及 one loop per thread 的封装

EventLoopThreadPool

getNextLoop()RR方式获取下一个 subLoop

Socket

Acceptor

特殊的TcpConnection? 封装listenfd相关操作如bind listen, 在baseLoop中进行检测。

Buffer

参考Netty的实现, prendable, readerindex, writerindex

TcpConnection

封装 成功建立连接的connfd,一个connfd唯一对应一个TcpConnection ,关键成员有:Socket对象、Channel对象、发送和接受缓冲区;

TcpConnectionChannel各种事件回调的设置

TcpServer

关键成员有:AcceptorEventLoopThreadPoolstd::unordered_mapconnections_

重点总结

应用技术

C++11、 Socket、 Reactor模型、多线程、epoll

项目描述

在对源码剖析的同时也针对Muduo网络库进行基于C++11的重新实现,保留关键部分,使其不用依赖boost库就可以实现主要功能。

此项目是一个基于Muduo库和C++11的高性能网络库,能够让用户实现稳定、可靠、高性能、高并发的服务器应用程序

主要工作

  • 采用 multi-Reactorsone loop per thread + nonblocking IO的网络模型实现IO处理和业务处理的分离;
  • 对C++11线程类进行封装实现 one loop per thread的一对一绑定关系;
  • 使用C++11特性取代boost库在muduo中的使用,如shared_ptr、bind、atomic等
  • 使用eventfd()来实现 mainLoopsubLoop的线程间通知操作,相比于使用任务队列需要加锁降低了开销

项目核心逻辑介绍

我自己对Muduo库网络部分核心做了一个总结

【无标题】Muduo库源码剖析(十)——总结_第1张图片

Muduo库采用one loop per thread + nonblocking IO网络模型,在mainLoop中 关注listenfd的读事件,并且将该listenfd封装成一个特殊的 TcpConnection 类即 Acceptor ,因为其读事件的处理是调用 accept 建立连接,并且将连接发送到一个subLoop上,选择subLoop的方法采用轮询的方法。

EventLoop模块

首先是最重要的Reactor模块,在Muduo库中也就是EventLoop 类的,其中主要的功能负责调度和分发网络事件即调用 epoll_wait 监视fd 感兴趣的事件,并执行对应的回调函数,在Muduo中 将这一个逻辑流程进行面向对象的拆解,将epoll_wait 封装到 EPollPoller中,将fd 以及其感兴趣的事件和对应回调封装到Channel中。EventLoop 负责 Channel 和 Poller的通信EventLoop::loop() 调用 Poller进行检测事件,当有事件到来,再通过EventLoop调用 Channel中由TcpConnection和 用户 设置的相应事件回调。

eventfd()

注意到这里每一个Loop 与 其构造时的线程一一对应,在subLoop初始化时,会自动创建一个 eventfd()来供mainLoop唤醒它本身,这样就不需要使用生产者消费者队列来管理任务,从而避免subLoop争抢任务而要将队列加锁带来的开销。

缓冲区Buffer

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

muduo库 connfd 采用触发模式是LT,这样优点是

  • 不会丢失数据或者消息
    应用没有读取完数据,内核是会不断上报的

  • 低延迟处理
    每次读数据只需要一次系统调用;照顾了多个连接的公平性,不会因为某个连接上的数据量过大而影响其他连接处理消息

  • 跨平台处理

    像select一样可以跨平台使用

Muduo几大网络组件的抽象如下

【无标题】Muduo库源码剖析(十)——总结_第2张图片

项目多处使用C++11特性如原子变量实现无锁编程,以及智能指针对资源进行安全管理。

个人收获

能更加熟练地运用C++11新特性,如bind、智能指针和原子变量等语法,同时我对于one loop per thread + nonblocking IO网络模型的理解得到了提升。此外,在编码中对类的封装和设计的实践,让我对设计模式、基于事件驱动的编程方法以及事件回调有了更具体的认识。

遇到问题

原子变量未初始化

服务器没有启动监听Acceptor::listen,排查后发现

TcpServer::start() 中 执行该函数的判断变量没有初始化,导致判断条件不成立,无法执行Acceptor::listen

即控制TcpServer::start()只执行一次的**原子变量started_**未初始化,数值为65538

accept问题

调用aceept处出错,错误码为 errno = 22

通过 perror查询发现是invalid argument,查阅资料后发现具体是因为

  1. accept函数参数不合法

  2. 对返回的connfd没设置非阻塞,因为本项目是Reactor模型 one loop per thread + non-blocking IO

为了方便起见,不想再多写fcntl,于是采用accept4替代 accept

sockaddr_in addr;
socklen_t len = sizeof addr;
bzero(&addr, sizeof addr);
// sockfd是listen用
int connfd = ::accept4(sockfd_, (sockaddr*)&addr, &len, SOCK_CLOEXEC | SOCK_NONBLOCK);

杂项

ps -ef | grep testserver 查找程序对应的进程号

在这里插入图片描述

netstat -anpt 查看网络状态,如下表明./testserver在本机8888端口处于监听状态

【无标题】Muduo库源码剖析(十)——总结_第3张图片

你可能感兴趣的:(Muduo,网络编程,C/C++,服务器,linux,c++)