目录
1. epoll是当今世界上性能最高的多路转接IO模型。不支持跨平台,只能在Linux操作系统使用
2. epoll接口函数
2.1 创建epoll操作句柄
int epoll_create(int size);
size:目前的size没有实际含义,epoll底层采用扩容的方式。注意:size不要传入小于0的数字
返回值:epoll的操作句柄
epoll_create创建的epoll操作句柄,本质上是在内核当中创建struct eventpoll{…}结构体
程序员通过操作句柄就可以找到eventpoll结构体,从而实现操作(添加、删除、等待)
2.2 操作epoll的接口
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
将文件描述符(fd)加入到事件结构当中,让epoll开始监控,如果监控到了,就会把这个产生的事件结构拷贝到双向链表当中,程序员可以调用epoll_wait函数,从双向链表当中拿到这个产生的事件
epfd:epoll操作句柄
op:告诉epoll_ctl函数做什么操作
EPOLL_CTL_ADD:向epoll当中添加想要监控的文件描述符对应的事件结构
EPOLL_CTL_MOD:修改某个文件描述符对应的事件结构
EPOLL_CTL_DEL:从epoll当中删除某个文件描述符对应的事件结构
fd:要操作的文件描述符
event:epoll的事件结构结构体
返回值:成功则返回0 ,失败返回-1,错误原因存于 errno 中
struct epoll_event
{
unit32_t events; //Epoll events
epoll_data_t data; //User data variable
};
events:告知epoll当中该文件描述符关心什么事件
EPOLLIN:可读事件
EPOLLOUT:可写事件
data:这个变量的数据是给用户查看的,对于epoll的监控,并没有任何作用
typedef union epoll_data
{
void *ptr;
int fd; //两者共用一块内存
}epoll_data_t;
ptr:用户在监控之前,可以给ptr当中保存数据,以供监控成功之后继续查看。
fd:保存监控的文件描述符,这个也是在监控成功之后,用户查看fd字段,就知道当前的结构体属于哪一个文件描述符,从而进行操作。
使用原则:使用了fd,那就不要使用ptr。如果使用了ptr,在ptr保存的内存当中一定要让用户知道该结构体属于哪一个文件描述符。
2.3 epoll等待接口
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:epoll操作句柄
events:拷贝就绪的事件结构数组,出参,从epoll的双向链表当中拷贝就绪的事件结构到用户准备的事件结构数组当中
maxevents:最大可以拷贝多少个事件结构
timeout:>0:带有超时时间的监控
==0 :非阻塞监控
< 0 :阻塞监控
返回值: 0表示超时; -1表示错误; 大于0表示返回的就绪的事件结构个数(需要处理的事件个数)。
3. epoll工作原理
4. epoll阻塞监控代码
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
int main()
{
//1. 创建epoll操作句柄
int epfd = epoll_create(5); //里面的数字随便给,但是不能小于0
{
if(epfd < 0)
{
perror("epoll_create");
return 0;
}
}
//2. 操作epoll,添加0号文件描述符对应的事件结构到epoll当中
//操作之前,需要创建epoll_event结构体
struct epoll_event ee;
ee.events = EPOLLIN; //关心多种事件 ,则用按位或的方式EPOLLIN | EPOLLOUT
ee.data.fd = 0;
//当监控成功之后,从双向链表当中将事件结构拷贝到用户空间的事件结构数组之后
//程序员可以通过该结构当中的fd,知道该结构属于哪一个文件描述符
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
//3. epoll等待
struct epoll_event arr[10];
epoll_wait(epfd, arr, 10, -1); // -1 表示非阻塞
char buf[1024] = {0};
read(arr[0].data.fd, buf, sizeof(buf) - 1);
printf("buf is : %s", buf);
return 0;
}
5. epoll非阻塞监控代码
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
int main()
{
int epfd = epoll_create(6);
if(epfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
int count = 0;
while(1)
{
struct epoll_event arr[10];
int ret = epoll_wait(epfd, arr, 10, 0); // 0 是非阻塞
if(ret < 0)
{
perror("epoll_wait");
return 0;
}
else if(ret == 0)
{
printf("epoll timeout : %d\n", count++);
sleep(1);
continue;
}
char buf[1024] = {0};
read(arr[0].data.fd, buf, sizeof(buf) - 1);
printf("buf is:%s", buf);
}
return 0;
}
6. epoll超时时间监控代码
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
int main()
{
int epfd = epoll_create(6);
if(epfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
int count = 0;
while(1)
{
struct epoll_event arr[10];
int ret = epoll_wait(epfd, arr, 10, 1);
if(ret < 0)
{
perror("epoll_wait");
return 0;
}
else if(ret == 0)
{
printf("epoll timeout : %d\n", count++);
sleep(1);
continue;
}
char buf[1024] = {0};
read(arr[0].data.fd, buf, sizeof(buf) - 1);
printf("buf:%s", buf);
}
return 0;
}
7. epoll的工作模式:LT和ET
7.1 LT和ET的概念
LT:水平触发。当事件就绪之后,需要一直通知程序员,直到程序员将该就绪的事件处理之后,就不会为了该事件进行通知。
ET:边缘触发。当事件就绪之后,只会通知程序员一次,不关心程序员是否处理该事件,直到有新的数据到来之后,才会再次触发通知
epoll的默认工作方式是LT。如果想要ET工作模式,只需要在epoll_event结构体的events变量当中按位或上EPOLLET。
7.2 epoll - LT代码
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
int main()
{
int epfd = epoll_create(6);
if(epfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
int count = 0;
while(1)
{
struct epoll_event arr[10];
int ret = epoll_wait(epfd, arr, 10, 0);
if(ret < 0)
{
perror("epoll_wait");
return 0;
}
else if(ret == 0)
{
continue;
}
char buf[2] = {0};
read(arr[0].data.fd, buf, sizeof(buf) - 1);
printf("buf is:%s\n", buf);
printf("%d\n", count++);
}
return 0;
}
7.3 epoll - ET
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
int main()
{
int epfd = epoll_create(6);
if(epfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN | EPOLLET;
ee.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
int count = 0;
while(1)
{
struct epoll_event arr[10];
int ret = epoll_wait(epfd, arr, 10, 0);
if(ret < 0)
{
perror("epoll_wait");
return 0;
}
else if(ret == 0)
{
continue;
}
char buf[2] = {0};
read(arr[0].data.fd, buf, sizeof(buf) - 1);
printf("buf is:%s\n", buf);
printf("%d\n",count++);
}
return 0;
}
ET模式只会通知一次,流程变得高效了,但是与之带来的是,需要将就绪事件产生的数据全部处理完,如果没有处理完,那就只能等下一次新的数据到来的时候,epoll来通知了。
问题:ET代码中如何一次性将数据读取完?
解决:想到的就是循环读
#include <stdio.h>
#include <unistd.h>
#include <sys/epoll.h>
int main()
{
int epfd = epoll_create(6);
if(epfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN | EPOLLET;
ee.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
while(1)
{
struct epoll_event arr[10];
int ret = epoll_wait(epfd, arr, 10, 0);
if(ret < 0)
{
perror("epoll_wait");
return 0;
}
else if(ret == 0)
{
continue;
}
while(1)
{
char buf[2] = {0};
read(arr[0].data.fd, buf, sizeof(buf) - 1);
printf("buf: %s ", buf);
}
}
return 0;
}
循环读导致的问题就是虽然可以一次性读完,但是会阻塞在read函数,而不是epoll_wait函数
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string>
#include <sys/epoll.h>
using namespace std;
int main()
{
//将0号文件描述符改为非阻塞
int flag = fcntl(0, F_GETFL); //获取0号文件描述符的属性
fcntl(0, F_SETFL, flag | O_NONBLOCK); //设置0号文件描述符的属性
int epfd = epoll_create(6);
if(epfd < 0)
{
perror("epoll_create");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &ee);
while(1)
{
struct epoll_event arr[10];
int ret = epoll_wait(epfd, arr, 10, 0);
if(ret < 0)
{
perror("epoll_wait");
return 0;
}
else if(ret == 0)
{
continue;
}
string str;
while(1)
{
char buf[2] = {0};
int ret = read(arr[0].data.fd, buf, sizeof(buf) - 1);
if(ret < 0)
{
//EAGAIN和EWOULDBLOCK是linux环境下的两个错误码,在非阻塞IO中经常会碰到
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
break;
}
}
//之前只是打印输出,并没有将读到的数据保存起来
str.append(buf);//将buf当中的数据存入到str中
}
printf("str:%s\n", str.c_str());
}
return 0;
}
EAGAIN和EWOULDBLOCK是linux环境下的两个错误码,在非阻塞IO中经常会碰到
这个错误产生的情况:
(1)尝试在一个设置了非阻塞模式的对象上执行阻塞操作,重试这个操作可能会阻塞直到其他条件让它可读、可写或者其他操作。
(2)对某些操作来说,资源短暂不可用。例如fork函数可能返回这个错误(当没有足够的资源能够创建一个进程时),可以采取的操作是休息一段时间然后再继续操作。
8. epoll_LT + tcp代码
服务端代码
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <string.h>
// 1.实现单线程的TCP服务端
// 2.在单线程TCP服务端的基础上,添加epoll代码,让epoll监控侦听套接字和新连接的套接字
// 侦听套接字只有一个,新连接的套接字有多个
// 侦听套接字(读事件):一旦读事件就绪,表示当前有新连接,三次握手建立连接了,调用accept函数处理读事件
// 新连接的套接字:一旦读事件就绪了,表示当前客户端给服务端发送消息了,调用recv函数处理读事件
int main()
{
//建立侦听套接字,只有服务端有侦听套接字
int listen_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if(listen_sockfd < 0)
{
perror("socket:");
return 0;
}
//绑定地址信息
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(45678);
addr.sin_addr.s_addr = inet_addr("0.0.0.0");
int ret = bind(listen_sockfd, (struct sockaddr*)&addr, sizeof(addr));
{
if(ret < 0)
{
perror("bind:");
return 0;
}
}
//监听
ret = listen(listen_sockfd, 5);
if(ret < 0)
{
perror("listen:");
return 0;
}
// 1.创建epoll操作句柄
// 2.添加侦听套接字,让epoll监控
// 3.监控到侦听套接字的读事件之后,调用accept函数处理读事件(接受新连接)
// 区分到底是新连接套接字还是侦听套接字,分别处理
// 对于新连接的套接字还需要添加到epoll当中进行监控
// 如果是新连接套接字的读事件产生,则接收数据
int epfd = epoll_create(6);
if(epfd < 0)
{
perror("epoll_create:");
return 0;
}
struct epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = listen_sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sockfd, &ee); //将listen_sockfd加入到事件结构当中,让epoll开始监控,如果监控到了,就会把这个产生的事件结构拷贝到双向链表当中
while(1)
{
struct epoll_event arr[10];
//程序员可以调用epoll_wait函数,从双向链表当中拿到这个产生的事件
int ret = epoll_wait(epfd, arr, 10, -1); //返回的是就绪的事件结构的个数,将返回的事件结构个数放入到arr数组当中
if(ret < 0) //监控失败
{
perror("epoll_wait:");
continue;
}
else if(ret == 0) //超时
{
continue;
}
//能执行到这里就说明epoll监控到的文件描述符,有对应的就绪事件产生
//对上面的就绪事件开始遍历
for(int i = 0; i < ret; i++) //ret是个数,说明arr数组有ret个元素,遍历arr数组
{
if(arr[i].data.fd == listen_sockfd) //如果是侦听套接字
{
int newsockfd = accept(listen_sockfd, NULL, NULL);//对端的地址信息结构和长度不需要先可以设为NULL
if(newsockfd < 0) //接收失败
{
perror("accept:");
continue;
}
//接收成功
//将接收成功的新套接字放入到epoll当中进行监控
struct epoll_event new_ee;
new_ee.events = EPOLLIN;
new_ee.data.fd = newsockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, newsockfd, &new_ee);
}
else //arr[i].data.fd == newsockfd;
{
int newsockfd = arr[i].data.fd;
char buf[1024] = {0};
ssize_t recv_size = recv(newsockfd, buf, sizeof(buf), 0);//0表示阻塞接收
if(recv_size < 0)
{
perror("recv_size:");
continue;
}
else if(recv_size == 0)
{
printf("peer shut down connect\n");
//对端关闭连接,自己在epoll当中要把相应的事件结构删除
epoll_ctl(epfd, EPOLL_CTL_DEL, newsockfd, NULL);
//自己关闭连接
close(newsockfd);
continue;
}
printf("buf:%s\n", buf);
//发送
memset(buf, '\0', sizeof(buf)); //初始化
strcpy(buf, "hello, i am server~");
send(newsockfd, buf, strlen(buf), 0);
}
}
}
epoll_ctl(epfd, EPOLL_CTL_DEL, listen_sockfd, NULL);//将事件从epoll中移除
close(listen_sockfd);
return 0;
}
客户端代码
include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
//创建套接
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //地址域信息,套接字类型,套接字类型使用的协议
if(sockfd < 0) //socket返回的是套接字描述符,即文件描述符。文件描述符不能小于0
{
perror("socket");
return 0;
}
//客户端不需要绑定地址信息
//首先需要创建协议所用的地址信息结构体
struct sockaddr_in addr; //IPV4使用的是 struct sockaddr_in 这个结构体
addr.sin_family = AF_INET; //地址域信息
addr.sin_port = htons(45678); //端口,要把这个端口转换成为网络字节序
addr.sin_addr.s_addr = inet_addr("82.157.94.99"); //IP地址转换为unit32_t,再转换为网络字节序
int ret = connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));//connect接收的是服务端的地址信息结构
if(ret < 0)
{
perror("connect");
return 0;
//continue; //如果连接失败也可以继续连接
}
while(1)
{
//发送
char buf[1024] = "i am client1";
int send_size = send(sockfd, buf, strlen(buf), 0);
if(send_size <= 0)
{
perror("send");
continue;
}
else {
printf("发送的字节数是:%zu\n", send_size); //ssize_t 用 %zu 输出
}
//接收
memset(buf, '\0', sizeof(buf)); //接收之前先初始化buf,否则会覆盖初始化之前buf里的内容。比如接收到123,就会覆盖i a,变成123m client111
ssize_t recv_size = recv(sockfd, buf, sizeof(buf) - 1, 0);
//发送和接收最后一位参数都是标志位,0表示阻塞发送或接收
if(recv_size < 0)
{
perror("recv");
continue;
}
else if(recv_size == 0)
{
printf("peer close connect\n");
close(sockfd);
continue;
}
printf("%s\n", buf);
sleep(1); //每隔一秒发一次
}
close(sockfd);
return 0;
}
可以开多个客户端来验证: