操作流程:
① 程序员定义某个事件的描述符集合(可读事件的描述符集合 / 可写事件的描述符集合 / 异常事件的描述符集合),初始化清空集合,对哪个描述符关心什么事件,就把这个描述符添加到相应事件的描述符集合中。
② 发起监控调用,将集合拷贝到内核中进行监控,监控的原理是轮询遍历判断。
可读事件的就绪:接收缓冲区中数据的大小大于低水位标记。(量化标准----通常默认为1个字节)
可写事件的就绪:发送缓冲区中剩余空间的大小大于低水位标记。(量化标准----通常默认为1个字节)
异常事件的就绪:描述符是否产生了某个异常。
③ 监控的调用返回,表示监控出错 / 有描述符就绪 / 监控等待超时了。
并且调用返回的时候,将事件监控的描述符集合中的未就绪描述符从集合中移除了 – (集合中仅仅保留就绪的描述符)。因为返回的时候修改了集合,因此下次监控的时候,就需要重新向集合中添加描述符。
④ 程序员轮询判断哪个描述符仍然还在哪个集合中,就确定了这个描述符是否就绪了某个事件,然后进行对应事件的操作即可。select不会直接返回给用户就绪的描述符直接操作,而是返回了就绪的描述符集合,因此需要程序员进行判断。
代码操作:
① 定义集合: struct fd_set — 成员只有一个数组 — 当作二进制位图使用 — 添加描述符就是将描述符的值对应比特位置。 因此select能够监控的描述符数量,取决于二进制位图的比特位有多少 — 而比特位有多少取决于宏 — _FD_SETSIZE,默认等于1024。
接口 | 含义 |
---|---|
void FD_ZERO(fd_set *set); | 初始化清空集合 |
void FD_SET(int fd, fd_set* set); | 将fd描述符添加到set集合中 |
② int select (int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout) ;
nfds:当前监控的集合中最大的描述符 + 1,减少遍历次数。
readfds / writefds / exceptfds:可读 / 可写 / 异常三种事件的描述符集合。
timeout:struct timeval{tv_sec; tv_usec;};时间结构体,通过这个时间决定select阻塞 / 非阻塞 / 限制超时的阻塞。(若timeout为NULL,则表示阻塞监控,直到描述符就绪,或者监控出错才会返回。 若timeout中的成员数据为0,则表示非阻塞监控,监控的时候若没有描述符就绪,则立即超时返回。 若timeout中成员数据不为0,则在指定时间内,没有就绪就超时返回)。
返回值:返回值大于0表示就绪的描述符个数;返回值等于0表示没有描述符就绪,超时返回;返回值小于0表示监控出错。
③ 调用返回,返回给程序员,就绪的描述符集合,程序员遍历判断哪个描述符还在哪个集合中,就是就绪了哪个事件。
int FD_ISSET(int fd, fd_set* set); – 判断fd描述符是否在集合中。
因为select返回时会修改集合,因此每次监控的时候都要重新添加描述符。
④ 若对描述符不想进行监控了,则从集合中移除描述符。
void FD_CLR(int fd, fd_set* set); – 从set集合中删除描述符fd。
//使用select对标准输入进行监控
#include
#include
#include
#include
#include
int main()
{
//1.定义指定事件的描述符集合
fd_set rfds;
while(1)
{
/*
select(maxfd + 1, 可读事件集合, 可写事件集合. 异常事件集合, 超时时间)
开始监控, 超时/有就绪则调用返回,返回的时候将集合中未就绪的描述符移除
超时:在tv指定的时间内都一直没有描述符就绪。
有就绪:有描述符就绪的指定的事件。
*/
//时间也得每次都重新赋值,因为select返回时会将监控描述符移除,
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
//初始化清空集合
FD_SERO(&rfds);
//将0号描述符添加到集合中
FD_SET(0, &rfds);
int ret = select(0+1, &rfds, NULL, NULL, &tv);
if(ret < 0)
{
perror("监控出错!\n");
return -1;
}
else if(ret == 0)
{
printf("超时退出!\n");
continue;
}
if(FD_ISSET(0, &rfds))
{
//判断描述符是否在集合中判断是否就绪了事件
printf("准备从标准输入读取数据:\n");
char buf[1024] = {0};
int ret = read(0, buf, 1023);
if(ret < 0)
{
perror("读取错误!\n");
//移除描述符从集合中
FD_CLR(0, &rfds);
return -1;
}
}
printf("读数据:[%s]\n", buf);
}
return 0;
}
#include
#include
#include
int main()
{
//1. 定义数组
struct pollfd poll_fd;
//2. 填充监控的描述信息
poll_fd,fd = 0;
poll_fd.events = POLLIN; // 标准输入事件
while(1)
{
//3. 开始监控,将就绪的事件填充到对应描述符的事件结构体的revents成员中
int ret = poll(&poll_fd, 1, 5000);
if(ret < 0)
{
perror("poll error");
continue;
}
else if(ret == 0)
{
printf("poll timeout!超时等待!\n");
continue;
}
//遍历数组,根据revents判断就绪了什么事件,进行相应的操作。
if(poll_fd.revents == POLLIN) //revents是就绪可读事件
{
char buf[1024] = {9};
read(0, buf, sizeof(buf) - 1);
printf("输入:%s", buf);
}
}
return 0;
}
epoll模型:是LInux下最好用的、性能最高的多路转接模型。
操作流程:
① 发起调用在内核中创建epoll句柄epoll_event结构体(这个结构体中包含很多信息,红黑树 + 双向链表)
② 发起调用对内核中的epoll_event结构添加/删除/修改所监控的描述符监控信息。
③ 发起调用开始监控,在内核中采用异步阻塞操作实现监控,等待超时/有描述符就绪了事件调用返回,返回给用户就绪描述符的事件结构信息。
④ 进程直接对就绪的事件结构体中的描述符成员进行操作即可。
接口认识:
① int epoll_create(int size) — 创建epoll句柄。
size:在Linux2.6.2之后被忽略,只要大于0即可。
返回值:文件描述符 — epoll的操作句柄。
② int epoll_ctl(int epfd, int cmd, int fd, struct epoll_event* ev);
epfd:epoll_create返回的操作句柄。
cmd:针对fd描述符的监控信息要进行的操作—添加/删除/修改 EPOLL_CTL_ADD / EPOLL_CTL_DEL / EPOLL_CTL_MOD
fd:要监控操作的描述符。
ev:fd描述符对应的事件结构体信息。
struct epoll_event {uint32_t events; .—对fd描述符要监控的事件(EPOLLIN / EPOLLOUT) union{int fd; void* ptr;} data; — 要填充的描述符信息};
一旦epoll开始监控,描述符若就绪了进程关心的事件,则就会给用户返回我们所添加的对应事件结构体信息,通过事件结构体信息中包含的描述符进行操作----因此第三个参数fd与结构体中的fd描述符通常是同一个描述符。
③ int epoll_wait(int epfd, strutct epoll_event* evs, int max_event, int timeout);
epfd:epoll操作句柄。
evs:struct epoll_event结构体数组的首地址,用于接收就绪描述巨幅对应的事件结构体信息。
max_event:本次监控想要获取的就绪事件的最大数量,不大于evs数组的节点个数,防止访问越界。
timeout:等待超时时间,单位:毫秒。
返回值:返回值大于0表示就绪的事件个数;返回值等于0表示等待超时;返回值小于0表示监控出错。
④ 进程遍历获取evs中就绪的事件结构体信息,针对其中的events就绪事件对data.fd进行相应操作。
//封装一个epoll,实现简单操作
#include
#include
#include
class Epoll
{
public:
Epoll()
:_epfd(-1)
{
//1.创建epoll句柄
_epfd = epoll_create(1);
if(_epfd < 0)
{
perror("epoll create error!");
eixt(-1);
}
}
bool Add(TcpSocket& sock)
{
//2.添加描述符监控事件信息
//(1)获取描述符
int fd = sock.GetFd();
//(2)定义描述符对应的事件结构体
//EPOLLIN -- 可读事件 EPOLLOUT -- 可写事件
struct epoll_event ev;
ev.events = EPOLLIN;
//ev.events = EPOLLINT | EPOLLET; //EPOLLET是边缘触发方式
ev.data.fd = fd;
//(3)添加到内核中
int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
if(ret < 0)
{
perror("epoll ctl add error!");
return false;
}
}
bool Del(TcpSocket& sock)
{
//3.从结构体中移除监控
int fd = sock.GetFd();
int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret < 0)
{
perror("epoll ctl del error!");
return false;
}
return true;
}
bool Wait(std::vector<TcpSocket>* list, int timeout)
{
//4.开始监控
//(1)开始监控
struct epoll_event evs[10];
int nfds = epoll_wair(_epfd, evs,, 10, timeout);
if(nfds < 0)
{
perror(""epoll wait error!);
return false;
}
else if(nfds == 0)
{
printf("epoll wait timeout,超时等待\n");
list->clear();
return true;
}
//(2)监控调用返回后,为每一个就绪的描述符组织TcpSocket对象
for(int i = 0; i < i < nfds; ++i)
{
if(evs[i].events & EPOLLIN) //判断是否为可读事件
{
//可读事件的操作
TcpSocket sock;
sock.SetFid(evs[1].data.fd);
//(3)将TcpSocket对象添加到list 中,进行返回
list->push_back(sock);
}
}
return true;
}
private:
int _epfd;
};
epoll中就绪事件的触发方式:(select和poll只有水平触发方式)(不写默认为水平触发方式)
① 水平触发方式:(EPOLLLT)
可读事件:接收缓冲区中数据大小大于低水位标记,就会触发可读事件。
可写事件:发送缓冲区中剩余空间大小大于低水位标记,就会触发可写事件。
低水位标记:基准衡量值,默认为1个字节。
② 边缘触发方式:(EPOLLET)
可读事件:只有新数据到来的时候,才会触发一次事件。
可写事件:发送缓冲区中剩余空间从无到有的时候才会触发一次事件。
边缘触发,因为触发方式的不同,因此要求进程中事件触发进行数据接收的时候,要求最好能够一次将所有数据全部读取。因为如果全部不读取,剩余数据不会触发第二次事件,只有新数据到来的时候才会触发。
如何保证读完缓冲区中所有数据----循环读取。然而循环读取能够保证读完缓冲区中的所有数据,但是在没有数据的时候就会造成阻塞。因此边缘触发方式中,描述符的操作都采用非阻塞操作。非阻塞操作的描述符操作在没有数据 / 超时的情况下会报错返回:EAGAIN(没有数据了) or EWOULDBLOCK(超时了)。
如何将描述符设置为非阻塞(对这个描述符所有操作都会改为非阻塞操作)
int fcntl(int fd, int cmd, … /arg/);
fd:指定的描述符
cmd:F_GETEL / F_SETFL – 获取 / 设置一个描述符的属性信息 – O_NONBLOCK – 非阻塞属性
arg:要设置的属性信息 / 获取的属性信息 F_GETFL使用的时候,arg被忽略,默认设置即可。
recv(fd, buf, len, flag); flag – MSG_DONTWAIT ---- 将本次接收操作设为非阻塞。(临时的,如果该操作本身就是非阻塞的,那么该设置就会被忽略)
//设置套接字为非阻塞
void SetNonBlock()
{
//获取原有数据,在原有数据的基础上增加非阻塞属性,设置回去
int flag = fcntl(_sockfd, F_GETFL, 0); // 获取原有属性
fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
}
边缘触发的作用:为了防止一些事件不断触发(接收数据后(按指定长度取数据,如按条取),缓冲区中留有半条,就会不断触发,这种情况要不然上层操作将半条数据读取出来,外部维护;要不然就使用边缘触发,等待新数据到来,数据完整了之后再触发事件。)
优点:
① 没有描述符监控数量的上限
② 监控信息只需要向内核中添加一次
③ 监控使用异步阻塞操作完后才能,性能不会随着描述符的增多而下降(因为异步阻塞:操作系统会把就绪的描述符放到双向链表中,进程只是每个一段时间判断链表是否为空来判断是否有描述符就绪。进程只判断链表是否为空,性能不会下降)
④ 直接向用户返回就绪的事件信息(包含描述符在内),进程直接可以针对描述符以及事件进行操作,不需要判断有没有就绪了。
缺点:
① 跨平台移植性差。