目录
概念
阻塞和非阻塞
\(\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事件对应的事件处理器EventHandlerreactor主要存储了事件和事件对应的处理器
epoll
select和poll的缺点
select的缺点:
- 能监视的最大文件描述符存在限制(1024),而且由于是轮询的方式,一旦监视的文件描述符增多,性能越差
- 内核于用户空间的内存拷贝问题,产生大量开销
select
返回整个句柄数据,应用程序需要便利整个数组才能知道那个句柄发生事件- 水平触发模式,如果没有完成一个已经就绪的文件描述符进行IO,那么下次
select
还是会通知这个描述
和select
相比,poll
采用的是链表存储文件描述符,除了第一点其余三个缺点均存在。
epoll的原理和优势
与poll/select机制完全不同,
epoll
在Linux
内核申请一个简易的文件系统,IO效率高
主要分成3个流程
- 调用
epoll_create()
建立一个epoll
对象 - 调用
epoll_ctl
向epoll
对象中 - 调用
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
封装了
fd
、events
、revents
以及一组回调
fd
:往poller
上注册的文件描述符events
:事先关注的事件revent
:文件描述符所返回的事件,根据相应的事件触发相应的回调。
分为两种channel
,一个是用于listenFd
用于接收连接,一种是connFd
是已建立连接的客户端,其中listenFd
封装为acceptorChannel
,connFd
封装为connectionChannel
;
Poller和EPollPoller - Demultiplex
poller
中记录了一个表unorder_map<int,channel*>
,如果有事件发生,就找到对应的channel
,其中就记录了详细的事件回调
EventLoop - Reactor
保存了一系列的
Channel
,即ChannelList
,存储了活跃的Channel
还有非常重要的weakFd
,std::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_
,记录所有的连接
初始化流程
- 构建
TcpServer
对象,在TcpServer
构造函数中:Acceptor->setNewConnectionCallback
,设置的就是Tcp::newConnection
,有新用户连接时,响应的就是这个函数 - 通过轮询算法选择
subLoop
,newConnection
创建TcpConnection
对象并且注册回调,closeCallback->TcpServer::removeConnection
,
ioloop->runInLoop->TcpConnection::connectionEstablished
- 调用
start()
函数用于启动,首先调用threadPool.start(startCallback)
,最终就是创建子线程并开启loop.loop()
; - 然后调用
_loop_->runInLoop(std::bind(&Acceptor::listen, _acceptor_.get()));
,就是将Acceptor
注册 - 开启
baseloop.loop()