muduo学习笔记

发布时间 2023-06-09 11:50:33作者: DaoDao777999

概念

阻塞和非阻塞

\(\bf{ 网络 \rm{IO} 阶段一:}\\ 数据准备 \begin{cases} 阻塞:\quad调用 \rm{IO} 方法线程进入阻塞状态 \quad \\ \\非阻塞:\quad不会改变线程状态,通过返回值判断(需要将sockfd设置为非阻塞状态)\end{cases}\)

ssize_t  recv(int sockfd,void *buf,size_t : len,int : flag);

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程


同步和异步

\(\bf{网络\rm{IO}阶段二:}\\ 数据读写 \begin{cases} 同步:(订票→等待出票→拿票) \\ \\ 异步:(订票→干自己的事→出票(机场通知拿票))\end{cases}\)

同步:

需要自己将数据放到缓冲区

char buf[1024]={0};
int size =  recv(sockfd, buf, 1024, 0);
if( size>0 ){
    ...
}

异步:

应用程序自己往下执行,由操作系统将数据放入缓冲区并发出通知。
预先设置通知方式,操作系统提供异步IO接口。 Linux下(aio_read,aio_write)

同步和异步的区别:请求发出后,是否需要等待结果,才能继续执行其他操作。

总的来说,同步和异步关注的是任务完成消息通知的机制,而阻塞和非阻塞关注的是等待任务完成时请求者的状态。

Unix/Linux上的5种IO模型

典型的一次IO两个阶段:数据就绪和数据读写

阻塞 Blocking


进程阻塞于read

非阻塞 non-blocking


进程调用read,判断EAGAIN反复调用

IO复用 IO multiplexing


进程阻塞于select/poll/epoll等待套接字变为可读,阻塞于read读数据

信号驱动 signal-driven


进程继续执行,阻塞于read读数据。于非阻塞IO的区别在于提供了消息通知机制,无需用于进程轮询查询

异步 asynchronous

典型异步非阻塞状态,Node.js采用的IO模型

网络服务器设计

Reactor模型

sequenceDiagram participant Event participant Reactor participant Demultiplex participant EventHandler Event ->> Reactor:注册Event和Handler loop 事件集合 Reactor -> Reactor:Event集合 end Reactor ->>Demultiplex:向Epoll add/mod/del Event Reactor ->>Demultiplex:启动反应堆 loop 事件发生器 Demultiplex ->Demultiplex:开启事件循环epoll_wait end Demultiplex ->> Reactor:返回发生事件的Event Reactor ->> EventHandler:调用Event事件对应的事件处理器EventHandler

reactor主要存储了事件和事件对应的处理器

epoll

select和poll的缺点

select的缺点:

  1. 能监视的最大文件描述符存在限制(1024),而且由于是轮询的方式,一旦监视的文件描述符增多,性能越差
  2. 内核于用户空间的内存拷贝问题,产生大量开销
  3. select返回整个句柄数据,应用程序需要便利整个数组才能知道那个句柄发生事件
  4. 水平触发模式,如果没有完成一个已经就绪的文件描述符进行IO,那么下次select还是会通知这个描述

select相比,poll采用的是链表存储文件描述符,除了第一点其余三个缺点均存在。


epoll的原理和优势

与poll/select机制完全不同,epollLinux内核申请一个简易的文件系统,IO效率高

主要分成3个流程

  • 调用epoll_create()建立一个epoll对象
  • 调用epoll_ctlepoll对象中
  • 调用epoll_wait收集发生事件的sockfd

触发模式

LT模式

epoll默认的模式

数据没有读完就会一直上报

ET模式

数据只会上报一次

muduo采用的是LT模式

大概原因:

  • 不丢失数据

    • 没有读完,内核会不断上报
  • 低延时处理

    • 每次读数据只需要一次系统调用,照顾了连接的公平性
  • 跨平台

    • select一样可以跨平台使用

关键组件

noncpoyable

很多类都继承了这个类,是为了让类无法拷贝构造和赋值

class noncopyable{
    public:
    noncopyable(const noncopyable& )=delete;
    noncopyable& operator=(const noncopyable&)=delete;
    protected:
    noncopyable()=default;
    ~noncopyable()=default;
}

Channel

封装了fdeventsrevents以及一组回调

  • fd:往poller上注册的文件描述符
  • events:事先关注的事件
  • revent:文件描述符所返回的事件,根据相应的事件触发相应的回调。

分为两种channel,一个是用于listenFd用于接收连接,一种是connFd是已建立连接的客户端,其中listenFd封装为acceptorChannelconnFd封装为connectionChannel


Poller和EPollPoller - Demultiplex

poller中记录了一个表unorder_map<int,channel*>,如果有事件发生,就找到对应的channel,其中就记录了详细的事件回调


EventLoop - Reactor

保存了一系列的Channel,即ChannelList,存储了活跃的Channel
还有非常重要的weakFdstd::unique_ptr<Channel> wakeUpChannel,一个weakupFd隶属于一个loop,二者一一对应,驱动loop是通过往weakupFd中写入数据,weakupfd也封装成了Channel注册在了EPollPoller


Thread和EventLoopThread


EventLoopThreadPool

getNextLoop():通过轮询算法获取下一个subloop
一个Thread对应一个loop=> one loop peer thread


Acceptor

主要封装了listenFd相关的操作,包括socket bind listen,然后扔给baseloop


Buffer

缓冲区,应用写数据->写入缓冲区->Tcp发送缓冲区->send

	+------------------+-------------------+---------------+
 	| 	preable bytes  |  readable bytes   |	writable   |
 	+------------------+-------------------+---------------+
 	|				   |				   |			   |
 	0				readIndex			writeIndex		  size


TcpConnection

一个成功连接的客户端对应一个TcpConnection
封装了socket channel callback,发送、接收缓冲区


TcpServer

封装了Acceptor EventLoopThreadPool和一堆callback
connectionMap connection_,记录所有的连接

初始化流程

  1. 构建TcpServer对象,在TcpServer构造函数中:Acceptor->setNewConnectionCallback,设置的就是Tcp::newConnection,有新用户连接时,响应的就是这个函数
  2. 通过轮询算法选择subLoopnewConnection创建TcpConnection对象并且注册回调,closeCallback->TcpServer::removeConnection
    ioloop->runInLoop->TcpConnection::connectionEstablished
  3. 调用start()函数用于启动,首先调用threadPool.start(startCallback),最终就是创建子线程并开启loop.loop()
  4. 然后调用_loop_->runInLoop(std::bind(&Acceptor::listen, _acceptor_.get()));,就是将Acceptor注册
  5. 开启baseloop.loop()