Ractor
- 一种同步网络模型,以IO多路复用和非阻塞IO实现。将对IO的操作转为为各类事件的操作(多数为读就绪事件和写就绪事件)
- 多路复用实现对IO的监测
- 非阻塞IO实现对IO的操作
封装过程
- 关联事件及其对应的处理器
- 1.对新到连接的处理
- 2.对被动断开的处理
- 3.对读事件的处理
- 4.对写事件的处理
- 事件控制接口
- 1.注册事件
- 2.注销事件
- 3.改变事件
- 事件循环
编程过程
为什么使用非阻塞IO?
非阻塞IO的特点是在数据未准备好(未从内核拷贝到协议栈)时,就可以返回当前操作的结果。而阻塞IO会直到数据准备好才返回。在多路复用中如果出现了阻塞的现象,那势必影响到其他fd的事件循环处理。
IO函数在操作IO的时候会返回错误码,可以根据错误码对网络状态进行分析,来选择对应的处理方式。所以说IO函数本身也可以用来监测IO的状态,也可以用来操作IO读写
- connect
- errno = EINPROGRESS 表示正在建立连接
- errno = EISCONN 表示连接建立成功
- write = -1
- error = EPIPE 表示写端被关闭,写失败
- errno = EWOULDBLOCK 表示当前写缓存区写不下这么多
- read = -1
- errno = EWOULDBLOCK 表示当前读缓存区为空,数据未准备好
- errno = EINRT 表示系统发生了中断,正在处理其他事务
- errno = other 都属于非良性的故障码,直接关闭连接
连接的建立
- 处理来自客户端的链接
int epollfd = epoll_create(1);
struct epoll_event events[POLL_SIZE] = {0};
// 注册监听listenfd的可读事件
struct epoll_event ev;
ev.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listen, &ev);
// 触发可读事件时,通过accept进行接收
int client = accept(listenfd, addr, sz);
struct epoll_event ev;
ev.events = EPOLLIN; // 给新的链接绑定可读事件,如果绑定可写事件,则必然会触发
epoll_ctl(epollfd, EPOLL_CTL_ADD,clientfd, &ev);
// 进入事件循环 timeout = 0表示非阻塞, timeout = -1表示永久阻塞
epoll_wait(fd, events, maxevents, timeout);
- 处理连接到外部的请求
int confd = socket(AF_INET, SOCK_STREAM, 0 );
connect(confd, &remoteAddr, sz);
// 注册当前fd的写事件
struct epoll_event ev;
ev.event = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &ev);
//事件循环如果触发可写,则说明可以发送请求,即连接成功过
连接断开
需要处理的场景,必然是对端主动请求关闭链接,调用close或者shutdown。shutdown可以选择关闭读端或写端,触发EPOLLRHUP(读端被关闭事件),EPOLLHUP(读写端都关闭);close的调用会引发四次挥手。
- 对close的处理过程,在被动方进入close_wait的时候协议栈会向读缓冲区写入文件结束符EOF,此时可以通过read=0去感知close请求
if(read == 0)
{
before_close(); // 关闭连接前的一些操作
close(fd);
}
- 对shutdown的处理
if(events[i].event == EPOLLRHUP)
{
// 读端关闭时,还可以写入
close_read();
close(fd);
}
if(events[i].event == EPOLLHUP)
{
before_close(); // 关闭连接前的一些操作
close(fd);
}
数据到达
即触发了可读事件
if(events[i] == EPOLLIN)
{
while(1)
{
// 这里使用非阻塞IO进行IO操作
int n = read(fd, buf, sz);
if(n < 0)
{
}
else if( n == 0)
{
close(fd);
}
}
}
数据发送
触发了可写事件
if(events[i] == EPOLLOUT)
{
int n = write(fd, buf, sz);
if(n == -1) // 写入失败
{
if(errno == EINTR)
continue;
if(errno == EWOULDBLOCK)
{
// 写缓存空间不足,此时需要给当前fd重新注册写事件,下次事件循环时再尝试写
struct epoll_wait ev;
ev.events = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
}
else
{
close(fd);
}
}
else if(n > 0) // 触发写事件,且写入成功
{
// 写成功之后删除当前写事件,减少cpu符合
// 是否需要写入根据实际请求判断即可,没必要一直对事件进行监听
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
}
}
reactor的应用
- 单线程应用reactor---redis
- redis对单reactor模型进行了对事件响应进行了优化。采用单线程read多线程write,提高了事件循环的效率
- 为什么read单线程,write多线程?跟这两个函数的效果有关:read是读取预期数量的字节数,返回实际读取到的字节数,一次read可能读不全,如果采用多线程,可能单个线程取不到完整的请求包,影响业务逻辑;write是尝试写入指定字节数,如果成功,则全部写入,如果失败,则一个都不写入,所以不存在多线程写入错乱的问题。
- 事件响应的分配采用round robin的方式。保证负载均衡
- 多线程应用reactor---memcached
- CPU有N核就使用N个reactor对象(one eventloop per thread,每个线程都有一个事件循环),其中专门分配一个reactor去处理accept连接,提高了新到连接的响应速度,也避免了accept的惊群现象(新版本的linux内核中已经实现了惊群避免?)
- 由于是多线程操作,适用于加锁粒度小的场景,不影响效率
- 多进程应用reactor---nginx
- 主进程创建listenfd
- fork子进程,子进程可以共享主进程创建的listenfd
- 在用户层通过共享队列加锁实现线程同步,保证只有一个线程成功accept,避免了惊群现象
- 取得锁的线程在成功accept之后,对clientfd进行事件绑定
- nginx主要实现的反向代理,没有解析请求包的需求,只需要实现转发功能,所以在这里采用的是epoll的边缘触发模式,可以更快的响应到新到的请求