高级IO
以前的都是拷贝接口。write什么的就是将字符串拷贝到发送缓冲区中。
应用层等待接收缓冲区填写数据的过程算是IO吗?算
IO=等待+拷贝数据;
真正的IO的过程就是拷贝的过程。比如等待鱼上钩的时候也算是钓鱼(adj),当把鱼拿上来的时候也是钓鱼(v);
-
那么什么叫做高效的IO呢?硬件不再变化,只在软件上提高效率。
单位时间内,等待的比重低
IO话题:
- 减少等的比重
- 改变等待的方式
五种IO的模型
阻塞IO是最常见的IO模型。
非阻塞等待就是轮询检测
信号驱动:
SIGIO(29)
多路转接:
select,poll ,epoll
只负责等,让recvfrom
来拷贝。异步IO:OS给你准备好了,你直接处理就行,都不用你亲自拷贝。
几乎所有的IO函数,核心功能就是两类,等和拷贝。
只要等和拷贝有一个方面你是参与的,就是同步IO,两个都不用管的就是异步IO。
-
什么叫做等事件就绪?
IO事件就绪,读事件就绪和写事件就绪,发送和接收缓冲区中是否有数据满足条件。
OS缓冲区的存在有预读的存在,提高了效率。实现了应用层和硬件之间的解耦。
非阻塞IO
一个文件描述符默认是阻塞IO。fcntl();
实验代码,后续是轮询检测的现象分析。
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
- 实验:实现函数SetNoBlock(),将文件描述符设置为非阻塞。
errno是C语言上的,write等是系统调用接口,为什么也可以使用errno呢?因为Linux使用C写的。
在非阻塞情况下,读取数据,如果数据没有就绪,系统是以出错的形式返回的,不算读出错,不是错误。
没有就绪和真正的错误使用的都是同样的方式进行标识。
-
如何进一步区分呢?
使用
errno
进行区分。#define EAGAIN 11
,再试一次。
期望读1024,但是你输入的只有几个。底层就告诉你的意思是我没读够,下次再试试吧!errno=11;
读取成功就不会设置errno,然后还是上一次的数据就是上一次读取因条件不足的值11,如果返回值大于0就没人关心errno了。如果提前设置errno=0,一旦输入errno就是0.
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if(fl<0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);//设为非阻塞
}
int main()
{
SetNonBlock(0);
while(1){
errno=0;//如果得到设置(read返回值是-1的同时将errno设置)
char buffer[10];
//重点是read
ssize_t s = read(0, buffer, sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
write(1, buffer, strlen(buffer));
printf("read success, s: %d, errno: %d\n", s, errno);
}
else{
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("数据没有准备好,再试试吧!\n");
printf("read failed, s: %d, errno: %d\n", s, errno);
//做做其他事情
sleep(1);
continue;
}
}
}
return 0;
}
IO的多路转接
1.select接口介绍
-
select,只负责一件事情就是等待,得到fd就绪,通知上层进行读取或者写入。
-
select有没有读取和写入数据的功能呢?没有
read,write本身也具备等待的功能,但是只能传入一个fd,select能够同时等待多个fd.
NAME
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing
SYNOPSIS
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, //maxfd+1
fd_set *readfds, //只关心读就绪,
fd_set *writefds,//只关心写就绪,
fd_set *exceptfds, //只关心异常就绪(网络异常)
struct timeval *timeout);//
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
fd_set是一个位图结构,比特位的位置代表哪一个sock,注意:比特位的位置和比特位的内容是两回事。
-
我们想要什么样的等待结果呢?
读就绪,写就绪,异常就绪(网络异常)
核心工作(读为例):
用户告知内核,你要帮我关心
哪些fd
上的读事件就绪。位图位置设置为1内核告知用户,你所关心的
那些fd
上的读事件已经就绪
所以内个位图结构fd_set
是一个输入输出型参数。
比特位的位置代表那一个sock,
比特位的内容:输入时用户告诉呃逆和要帮我关心哪些fd集合;输出时内核告诉用户你关心的哪些fd上面的事件已经就绪。
- 通过参数的设置可以调整
只关心读或者写
还是先关心读再关心写。
功能就是通知你所要的功能是否已经就绪。
- select的等待策略是什么呢?
struct timeval *timeout
timeout参数设置
NULL:阻塞式返回
timeout={0,0}; 非阻塞,没有就绪返回立马就走
timeout={5,0}; 5s超时,5秒之内等待,5秒之外timeout就返回了。也是输入输出型参数
select返回值>0,就绪的fd有几个;=0,超时了;<0,错误了。
- 对fd_set进行操作的系统调用接口如下,不需要我们对位图结构进行按位操作。
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
-
fd_set是一个数据类型,定义对象的时候就需要开辟空间,取8字节长度,空间的大小就有了那么类型的大小是多大呢?
2. select执行过程
- 模拟一次:
1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
2)若fd= 5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) (从后往前第几位)
3)若再加入fd= 2, fd=1,则set变为0001,0011
4)执行select(6,&set,0,0,0)阻塞等待 (用户告诉内核)
5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。内核告诉和用户
注意:没有事件发生的fd=5被清空
因为select使用输入输出型参数标识不同的含义,意味着后面每一次都要对fd_set进行重新设置。比如5,这次没准备就绪返回了,但是下次可能就就绪了,就需要你下一次进行重新设置传入。
-
每次设置select之后,程序怎么知道都有哪些fd呢?(避免fd文件描述符泄漏)
用户必须定义第三方数组或者其他容器结构来把历史fd全部保存起来。
准备编码
-
accept的本质叫做通过listen_sock获取新连接,并不是在那里等待。
前提是listen_sock上面有新连接,accept并不知道有无新连接,所以进行阻塞式等待,
站在多路转接的角度,我们认为新连接到来,对于listen_sock就是读事件就绪。
-
对于所有的服务器最开始的时候,只有listen_sock,先将他设置进数组中,并获取最大fd。
-
我们需要一个数组将所有的文件描述符储存起来,
fd_array[NUM]
-
我们服务器上的所有fd,包括listen_sock都要交给select进行检测,因为他可以等待多个文件描述符,使得有较高的概率随时获取到鱼,也就是可以保证服务器随时都有资源就绪进行获取。
recv write read send accept只负责自己最核心的工作,真正的读写是把连接获取上来accept。
实验:测试timeout
-
阻塞式等待
int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
-
非阻塞式等待(设置时间):
struct timeval timeout = {5, 0};
int n = select(max_fd + 1, &rfds, nullptr, nullptr, &timeout);//每个五秒进行一次检测
struct timeval timeout = {0, 0};//轮询检测,一直询问,非阻塞式等待
int n = select(max_fd + 1, &rfds, nullptr, nullptr, &timeout);
采用阻塞式等待,然后采用telnet进行连接,尽管现在还没有accept拿链接,但是底层连接建立好了,三次握手完成了, 你不读的情况下就一直通知你有资源就绪了。相当于tcp中的PSH。
到此处,就可以将就绪的事件ACCEPT,那么如何得知哪个时间就绪了呢?好在我们的rfds是输入输出醒的参数位图,所以只需要遍历一次即可。但是合法的fd不一定是就绪的fd。就需要采用位图接口FD_ISSET();
判断这个标志位是否在rfds中。成功的话,一定是读事件就绪了,就绪的fd在fd_array中保存,此时read,recv一定不会被阻塞。
-
读事件就绪就一定可以recv,read吗?
可能是监听套接字,listen_sock,是需要我们accept的。如果不是的话就是普通文件描述符读事件就绪,可以read recvfrom一定已经好了不会阻塞了。
-
可是这次读取一次能读完吗?没有粘包问题吗?
epoll那里搭建逻辑之后再处理。
-
读取普通文件描述符,recv返回值为0,说明对端已经关闭连接,我们需要减获取的这个连接关闭,并且在fd_array中将这位设置为-1.下一次遍历就不用关注他了。
telnet中ctrl+]调出命令行执行quit
-
-
accept之后获取新连接,连接到来并不意味着数据到来。谁清楚哪些fd上面可以读取了呢?select
-
那么如何交给在前面写的select呢?
无法直接将fd设置select,但是我们有fd-array[],在这里我们先将他设置进数组。下一次遍历时就可以遍历到我们这里设置的文件描述符 ,帮助我们监控下图中的连接了。
如果满载了就将获取到的fd关闭,满载了。
-
- 看到读取普通文件描述符的现象后:
我们实现了单进程的方式完成了多个连接的处理,只进行了读事件是否就绪的判定
socket就绪条件
读就绪
监听的socket上有新的连接请求。
socketTCP通信中,对端关闭连接,此时对该socket读返回0.
socket内核中,接收缓冲区中的字节数,大于等于低水位SO_RCVLOWAT。此时可以无阻塞的读改文件描述符,并且返回值大于0.
socket上有未处理的错误。
写就绪
socket上有未读取的错误
socket使用非阻塞connect连接成功或失败之后
对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
socket内核中,发送缓冲区中的字节数,大于等于低水位SO_RCVLOWAT。此时可以无阻塞的写,并且返回值大于0.
select总结
- 优点:
可以一次等待多个fd,可以让我们等待的时间重叠,钓鱼有多个鱼竿,在一定程度可以提高IO的效率。单进程排队成本非常低,而且是聚合在一起。
-
输入输出型参数每次都要重新设置数组,每次完成之后需要遍历检测,合法非法都测试了。
-
fd_set
:位图,有大小,他能够让select同时检测的fd是有上限的,云服务器能打开的fd是十万个。 -
select底层OS需要
轮询式的检测
哪些fd上的哪些事件就绪了。所以第一个参数max_fd+1,操作系统喜欢左闭右开,[0,6)->[0,5]
-
select可能会较为高频率的进行用户到内核,内核到用户的频繁拷贝问题,检测就绪和修改的问题。
poll
NAME
poll, ppoll - wait for some event on a file descriptor
SYNOPSIS
#include <poll.h>
int poll(struct pollfd *fds,
nfds_t nfds,
int timeout);//timeout(毫秒为单位):0永远等待,时间范围之内阻塞等待。-1代表永久阻塞。
返回值是非0revents结构体的数量
//poll不再使用位图了,解决了select检测文件符上限的问题。数组结构并不约束大小,你可以定义很大的空间。使用的是结构体struct polled{},不再使用select参数值传递的方式,接口更方便。将输入输出分开。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events *///用户->内核
short revents; /* returned events *///内核->用户
};
- events和revents的取值:很明显各个事件都是系统定义的宏
如何要设置类似select中的对于哪些时间是否读,写就绪的判断,只需要events|=POLLIN;event |= POLLOUT
即可完成。位操作检测特定事件是否发生:if(revents & POLLIN)
因为将二者拆开,避免了像select会存在来回覆盖的问题。
poll监控标准输入
#include <iostream>
#include <unistd.h>
#include <poll.h>
int main()
{
struct pollfd rfds;
rfds.events = POLLIN;
rfds.fd = 0;
rfds.revents = 0;
while (1)
{
int n = poll(&rfds, 1, -1); // 阻塞等待
// int n=poll(&rfds,1,0);//非阻塞轮询
// int n=poll(&rfds,1,1000);//每隔一秒看一眼
std::cout << n << std::endl; // 返回值的理解
switch (n)
{
case 0:
std::cout << "time out..." << std::endl;
break;
case -1:
std::cout << "poll error" << std::endl;
break;
default:
// std::cout<<"有事件来了"<<std::endl;
if (rfds.revents & POLLIN)
{
std::cout << rfds.fd << ":"
<< "事件已经就绪" << std::endl;
char buffer[128];
ssize_t s = read(rfds.fd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << "有人说#" << buffer << std::endl;
}
}
break;
}
}
return 0;
}
poll和select的区别
poll使用一个pollfd的指针实现:
pollfd结构包含了要监视的events(用户告诉内核)和发生的revent,不再使用select中的输入输出型参数,接口更加方便,是对二者进行分离。
不用受位图结构限制大小,可以malloc数组大小自己决定文件描述符检测的大小。
poll缺点
poll中监听的文件描述符数目增多时,
和select同,poll返回后需要轮询pollfd来获取就绪的描述符
每次调用poll都需要把大量的pollfd结构体从用户态拷贝到内核态。
同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监听的描述符数量的增多效率也会下降。
epoll(extend poll)
- 干什么的?
只负责等待手段不是目的,通过用户设置的某些fd,及其事件,告诉内核,让内核帮助用户关心,一旦就绪就通知上层。
为了处理大量的句柄而做了改进的poll,性能最好的多路IO就绪通知方案。
相关系统调用
epoll_create(int size);
返回值是一个句柄也就是一个fd,就是3.
- 提供了两个函数接口上,实现了select中用户和内核交互的输入输出参数的分离以及poll结构体中的event revent的分离。
epoll_ctl()
用户告诉内核,你要帮我关心哪些文件描述符上的哪些事情。只要调用一次,内核就永远记住了,一直关注着哪些文件描述符和事件。
NAME
epoll_ctl - control interface for an epoll descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_ctl(int epfd,
int op, //用户告诉内核新增或者删除关注哪个事件,是读是写
int fd,
struct epoll_event *event);
- op:想要让内核做什么
- struct epoll_events{}
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
//设置EPOLLIN或者EPOLLOUT来决定监测读就绪还是写就绪
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列里
epoll_wait()
内核告诉用户,哪些文件描述符已经就绪了。返回值就是有哪些fd就绪了,然后将就绪的fd们封装到epoll_events中返回。就不用像之前一样遍历一遍大数组,只要遍历返回值个就一定能覆盖所有就绪的事件。
NAME
epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
SYNOPSIS
#include <sys/epoll.h>
int epoll_wait(int epfd,
struct epoll_event *events,//输出型参数
int maxevents,
int timeout);
服务器的前半部分:
- tcp绑定端口Sock::Socket(),Bind(),Listen()
- 创建epoll模型epoll_create
- 将listen_sock和事件添加到内核当中epoll_ctl()
- 事件循环epoll_wait()
epoll的原理
OS进行调用epoll,会维护一棵红黑树结构,
凡是红黑树中的节点,对应的fd和事件events就是OS系统关心的!
-
进程是如何知道网卡驱动等硬件设备是否就绪的呢?
进程处于等待队列,OS进行安排,
键盘网卡等硬件通过中断的方式什么引脚发送到CPU,告诉系统中断号,对应中断方法将外设的数据拷贝到内核中,在各种缓冲区中,但是到内存中数据不代表就已经就绪了,数据已经存在于内存缓冲区中。
select和poll要求操作系统去遍历,检查文件描述符是否就绪,设置好位图统计几个好了,然后唤醒并交给进程。检验事件是否就绪的方式仍然是一个个遍历。
如果是epoll,底层支持回调机制,不想再遍历了;底层有数据就绪,原来的中断机制让OS一直在进行数据拷贝到内核缓冲区或者去干别的事情了,现在呢,你再让他多做一步,建立一个节点(相关与红黑树事件节点)连接到就绪队列上。此时我的进程还在等待挂起呢,以上步骤都是自动的。
然后呢,一旦有数据到内核中并且就绪了,就触发回调机制,这次的中断机制再让OS生成一个就绪事件节点,把这个节点放到就绪队列中,OS在下次再干这个事情时发现就绪了再唤醒等待队列中的进程就直接读取就完了。
综上:红黑树,就绪队列,回调机制
-
epoll_create()就是在内核当中,创建红黑树,创建就绪队列,告知OS我要使用回调机制策略了。
(有一个进程可以多次调用多次创建epoll模型。)
-
epoll_ctl();根据文件描述符向红黑树中插入一个节点,建立该fd的关联回调策略。
-
epoll_wait();以O(1)的时间复杂度检测是否有事件就绪,检测就绪队列是否为空。
三步当中,是在epoll_wait部分阻塞住的,当进程去访问就绪队列发现为空,也就是不就绪,OS就将进程的状态设置为非R状态,将PCB连接到某个队列当中,
等待各种数据就绪,OS发现就绪队列中有一系列节点了,(一个进程可以要求很多就绪所以有低水位这个概念),再找到这个进程将状态设置为R,放到运行队列当中,然后进程就可以拿到那些就绪的节点了。
- 要在某个资源下进行等待,OS角度就是,这个资源一定是被OS用描述组织结构体内核数据结构进行管理,一定存在一个队列,进程在等就是将进程放到管理该资源的结构体当中,当OS发现资源就绪了,就在这个资源结构体中直接找到你,再将你的状态设置为R放到运行队列当中,然后你才算是被唤醒接着拿到了资源。资源的等待队列有很多,在描述该资源的结构体当中。OS周期性的轮询检测是否就绪。
epoll_wait();返回值是就绪的事件个数,我们只需要处理已经就绪的东西就行了。
-
红黑树节点是根据fd为键值建立的搜索二叉树
-
为什么还需要红黑树呢?
可以将文件描述符和各种事件关系联系到一起。等价于select中的数组只不过是我们自己维护的,红黑树由OS进行维护。
-
事件和文件描述符之间的关系OS并不关心,由用户进行维护,所以提供了
epoll_data_t data
的字段。
012被占用,3是listen套接字,4是epoll文件描述符,5就是新连接的套接字,每增加一个连接,epoll就多一个需要维护,所有的事件就会添加到epoll模型当中。
把对方发来的数据打印出来,只关心读事件的就绪。
存在问题:读取时buffer是临时变量,在下一次循环过来资源已经被释放了,就绪的不是跟上一次一样的,没读完并且数据没向上交付。如何保证下一次和上一次就绪的是一样的。
- 实验现象:对新连接到来并不Accept处理,一直提醒我们要进行读取.
EPOLL工作方式:
LT模式:水平触发;ET模式:边缘触发
将触发方式从默认的水平触发改为边缘触发,在没有Accept的情况下,只通知了一次。
IO效率是由用户和OS共同完成的,你自己代码的读取逻辑也一定要配合。
当有快递送达,张三是只要是有你的包裹就一直通知你。我这次没读完,他还会通知你再来一次。
李四是当快递从无到有的时候给你打一个电话,除此之外不搭理你了。这种方式倒逼程序员一旦读取数据就要一直读完,读不完如果忘了就可能丢了。
-
改为ET模式之后,不ACCept
ET模式下的fd必须是非阻塞
ET模式,只会通知一次,recv/accept/read,准备读取时怎么保证能够读取完毕所有的连接的文件描述符呢?循环读取,因为我不知道有多少数据只能反复尝试去读。
- 那什么时候读取完毕呢?
比如每次只能读100字节,一共300字节,读完3次第四次还得读。可能会在读取的第四次我们会卡住(阻塞住了你以为还有,就一直在等),因为最后一次你要求的数据可能并没有办法提供,而你依然会继续读取,单进程的epollselect的话就会被挂起,你这个进程就废了。
- 如何解决呢?
将ET模式下的所有的fd,必须将该fd设置为非阻塞!如果没读到最后以出错的方式返回就行了,就不会再被挂起了。
ET设置为非阻塞不是接口的要求,而是程序员自己需要设计的。窗口大小刷新更快,使得可以传输更多的数据,在传输层协议中。
如果是ET模式下并且没有循环读取,如果此时应用层每次只读取一部分,另一部分只有客户端再一次请求才会再次读取,但是客户端发下一个请求需要服务器的响应,进而造成类似“死锁”的问题。你不发请求,我这头就不继续读。
epoll基于ET模式如何进行服务器设计
- 定制协议:epoll版本的计算器
分隔符X断开出子请求组成请求报文,响应也以分隔符X分开。
- 存在问题:
如何保证读到的数据能够一次性读完?如果读不完的话如何保证后续再读的时候能够仍然接上之前的缓冲区呢?临时的buffer是被所有文件fd共享的。
所以,
整体结构描述:Reactor
-
每一个sock都必须有自己独立的缓冲区。每个Event对应的sock中都要有inbuffer,outbuffer
-
虽然已经对等wait和拷贝read,在接口层面已经进行了接口层面的分离,但是在代码逻辑上依旧是偶合在一起的,需要通过回调的形式解耦。函数指针类型*callback。
-
epoll最大的又是在于就绪事件通知机制。
就绪事件派发逻辑,sock读写回调,写一个派发器函数
dispatcher
。这样的模式,一旦哪个事件就绪根据map中的sock找到对应的event回调函数,将内容写到或者读出接收发送缓冲区。这种机制就叫做Reactor,epoll升级而来。Event类中也需要回指Reactor的指针,这样更方便,即
Recotr:Events=1:n;
派发器逻辑实现了IO的解耦,一旦接收了新的连接,放到Reactor当中,他就自动的调用你之前定义的回调函数当中。
-
如何管理event和epoll对应的关系呢?
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events EPOLLIN.etc*/ epoll_data_t data; /* User data variable */ };
我们发现epoll_events中的字段有events和data,data中存在fd和ptr都可以实现一一对应关系,我们采用
unordered_map
使用fd进行标识,建立映射关系。一旦有事件就绪,就可以通过fd就可以找到事件Event的读缓冲区写缓冲区。
函数实现注意:
- 如果有出错事件或者对端连接断开了,就将它设置为读事件或者写事件就绪。代表是差错处理,将所有问题全部转化为读写函数去处理。
- 如果事件就绪,如果events的回调函数指针被设置,然后调用时将自己Event传入就可以了。直接调用回调方法,执行对应的读取。哪个event就绪就把那个传进来
- 根据你自己注册的所有的文件描述符。执行对应的回调方法就实现了IO方法的解耦。
升级为ET模式
底层到的连接可能是很多个,不只是一个,需要对各种连接循环读取,不能只Accept一次,所以要将对应的文件描述符设置为非阻塞fcntl()
->SetNoBlock();
-
Recver
-
读取:数据从套接字读到inbuffer中
读完,底层没数据了,和读取出错都是s<0设置。判断一下区分、要的是底层没数据(errno)
IO被信号打断时,再读一次。只要出错了就差错处理就行了,回调errorer()中。
-
分包(解决粘包问题):读取完整报文,不完整的下次再读,拆分放到tokens中。
从头往后找分隔符,找到一个切除之前的放到数组中,然后再清除读走的,再往下找,找不到了就结束了。
-
反序列化,提取报文有效参与计算和存储信息
-
业务逻辑,得到结果
-
构建响应,添加到outbuffer中。
-
直接或者间接发送。写事件就绪了才能发。写事件一般都是就绪的,用户不是就绪的。对于写事件一般是按需设置,使能读写EnableReadAndWrite()。写打开的时候默认就是就绪的,即使发送缓冲区已经满了,epoll只要用户重设OUT事件,EPOLLOUT至少会触发一次。
只要写打开,当底层缓冲区满了,EPOLLOUT自动被触发,会调用回调方法send将数据发送出去。
-
-
Sender
-
Errorer
Reactor叫做反应堆模式,通过多路转接方案,被动的采用事件派发的方式去反向的调用对应的回调函数。
最终实验效果:
- 进阶思路:
业务逻辑不止步于加法,recver分包与业务处理拼接到outbuffer中。现在仍然是单进程,引入线程池,data1 data2 而是任务task,线程池进行业务处理。
-
检测事件 ----epoll
-
派发事件----Dispatcher(事件派发dispatcher+IO:recver,sender) + 业务处理=>
半同步(自己参与IO)半异步(告诉别人整我拿结果)的处理。
如果我把IO也分出来让别人正,就是前设施。
-
连接----accepter
-
IO----recver sender
了就结束了。
-
反序列化,提取报文有效参与计算和存储信息
-
业务逻辑,得到结果
-
构建响应,添加到outbuffer中。
-
直接或者间接发送。写事件就绪了才能发。写事件一般都是就绪的,用户不是就绪的。对于写事件一般是按需设置,使能读写EnableReadAndWrite()。写打开的时候默认就是就绪的,即使发送缓冲区已经满了,epoll只要用户重设OUT事件,EPOLLOUT至少会触发一次。
只要写打开,当底层缓冲区满了,EPOLLOUT自动被触发,会调用回调方法send将数据发送出去。
-
Sender
-
Errorer
Reactor叫做反应堆模式,通过多路转接方案,被动的采用事件派发的方式去反向的调用对应的回调函数。
最终实验效果:
[外链图片转存中…(img-YYhjHkzP-1672655256249)]
- 进阶思路:
业务逻辑不止步于加法,recver分包与业务处理拼接到outbuffer中。现在仍然是单进程,引入线程池,data1 data2 而是任务task,线程池进行业务处理。
-
检测事件 ----epoll
-
派发事件----Dispatcher(事件派发dispatcher+IO:recver,sender) + 业务处理=>
半同步(自己参与IO)半异步(告诉别人整我拿结果)的处理。
如果我把IO也分出来让别人正,就是前设施。
-
连接----accepter
-
IO----recver sender