网络编程基本流程图如下:
在网络通信中io会有阻塞和非阻塞形式,阻塞一般出现在网络线程中,可以通过fcntl将网络套接字设置成非阻塞模式,默认创建的套接字为阻塞模式
如果套接字设置成非阻塞模式,在解析力啊read write就会表现出非阻塞模式
非阻塞和阻塞io主要差异,在数据未到达时,非阻塞体现在当有数据是立刻返回,反之阻塞
具体如下图所示:
阻塞io模型+多线程方式:
每一个线程处理一个fd连接
优势:处理及时; 缺陷:线程利用率低,线程的数量有限的
io多路复用(网络线程),用一个线程来监测多个io事件
水平触发的时候,io函数既可以阻塞也可以非阻塞形式的,因为在数据在没有接受完成时,会不断的发送过来
边缘触发的时候,io函数只能是非阻塞的形式
这里就引出io多路复用的epoll,在Linux下多路复用io接口select/poll的增强版本
epoll重要数据结构:
struct eventpoll {
...
struct rb_root rbr; // 管理epoll监听的事件
struct list_head rdllist; // 保存着 epoll_wait 返回满⾜条件的事件
...
};
struct epitem {
...
struct rb_node rbn; // 红⿊树节点
struct list_head rdllist; // 双向链表节点
struct epoll_filefd ffd; // 事件句柄信息
struct eventpoll *ep; // 指向所属的eventpoll对象
struct epoll_event event; // 注册的事件类型
...
};
struct epoll_event {
__uint32_t events;
epoll_data_t data; // 保存 关联数据
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64; }epoll_data_t;
主要函数:
epoll_create系统调用
int epoll_create(int size);
size参数高速内核在epoll这个对象会处理事件大致数量,而不是能够处理的事件的最大数。
在现有的linux版本中,size参数已经没有意义
返回值:epoll对象句柄,在之后针对给epoll操作需要通过该句柄进行标识该epoll对象
epoll_ctl系统调用
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
epoll_ctl向epoll对象添加、修改或删除事件
返回:0表示执行成功,-1表示错误,根据errno错误码判断错误类型。
op类型:
EPOLL_CTL_ADD 添加新的事件到epoll中
EPOLL_CTL_MOD 修改epoll中的事件
EPOLL_CTL_DEL 删除epoll中 的事件
event.events取值:
EPOLLIN 表示该连接上有数据可读(tcp连接远端主动关闭连接,也是可读时间,因为需要处理发送来的FIN包)
EPOLLOUT 表示该连接上可写发送(主动向上游服务器发起非阻塞tcp连接,连接建立成功事件相当于可写事件)
EPOLLRDHUP 表示tcp连接的远端关闭或半关闭连接
EPOLLPRI 表示连接上有紧急数据需要读
EPOLLERR 表示连接发生错误
EPOLLHUP 表示连接被挂起
EPOLLET 表示触发方式设置为边缘触发,系统默认为水平触发
EPOLLONESHOT 表示该事件只处理一次,下次需要处理时需要重新加入epoll
epoll_wait系统调用
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
收集epoll监控的事件中已经发生的事件,如果epoll中没有任何一个事件发生,则最多等待timeout毫秒后返回
返回:表示当前发生的事件个数
返回0 表示本次没有事件发生
返回-1 表示出现错误,需要检查errno错误码判断错误类型
附加:
events这个数据需要在用户态分配内存,内核负责把就绪事件复制到该数组中,
maxevents 表示本次可以返回的最大事件数目,一般设备为events数组的长度
timeout 表示在没有检测到事件发生时最多等待的时间;如果设置为0,检测到rdllist为空立刻返回;
如果设置为-1,一直等待
记录
所有添加到epoll中的事件都会与网卡驱动程序建立回调关系,相应的事件发生时会调用这里的回调方法(ep_poll_callback),它会把这样的事件放在rdllist双向链表中
reactor模型:
组成方式:非阻塞io+io多路复用
特征:基于事件循环,以事件驱动或者事件回调的方式来实现业务逻辑
理解:具体的网络连接转化成事件的处理,一个事件循环检测多个io事件的处理
int listenfd = socket();
bind(listenfd, addr, sizeof(addr));
listen(listenfd);
int epfd = epoll_create(0);
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd); // acceptor
while (1) //事件循环
{
struct epoll_event events[1024];
int nevents = epoll_wait(epfd, events, 1024, -1); // 数据准备阶段
for (int i = 0; i < nevents; i++)
{
epoll_event *e = events[i];
if (e->data.fd == = listenfd) //建⽴
{
// fd = socket ()
epoll_ctl(epfd, EPOLL_CTL_ADD, fd);
}
else
{
if (e->events & EPOLLIN)
{
// FIN包 客户端主动断开连接
// 数据的到达
read(e->data.fd, buff);
if (buff == FIN包)
{
close(fd)
return
}
decode();
compute();
encode();
epoll_ctl(epfd, EPOLL_CTL_MOD, events | EPOLLOUT);
}
if (e->events & EPOLLOUT)
{
// 数据的发送完毕
write(e->data.fd, buff, size);
}
if (e->events & EPOLLERR)
{
// close 连接断开
}
}
}
}
在redis中有体现reactor模式,在6.0版本中添加了多线程处理机制
reactor模式还有
单reactor模型+任务队列+线程池 添加线程池处理,对于业务进行解耦合,异步处理运行更高效
多reactor多进程模式 nginx采用多进程运行方式,多进程监听listen一个端口,进程间共享一把锁,每个进程都有一个reactor模式,进程获得了锁的同时也获得了对这个连接的处理
多reactor+消息队列+线程池 在多业务场景中使用较多