目录
代码: https://github.com/WHaoL/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
代码: https://gitee.com/liangwenhao/study/tree/master/00_06_Linux_SystemCode_and_SocketCode
1. epoll
内核检测epoll传递的fd集合, 是以红黑树的形式遍历的
内核创建一块共享内存: 内核和用户区共享
如果内存1G, epoll就支持10万连接
1.1 epoll的使用
1.1.1epoll的函数
#include <sys/epoll.h>
// 1、创建epoll模型 -> 得到的是epoll树的根
int epoll_create(int size);
- 参数: size: 已经被废弃了, 没有实际意义, 指定大于0的数就可以
- 返回值: epoll树的根节点
typedef union epoll_data
{
void *ptr;
int fd;
//fd:常用的一个变量, 对应epoll_ctl()函数的第三个参数fd(同一个)
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event
{
uint32_t events; /* Epoll events */
- EPOLLIN: 读事件 -> 检测fd的读缓冲区
- EPOLLOUT: 写事件 -> 检测fd的写缓冲区, 检测写缓冲区是不是可用(是不是满了)
epoll_data_t data; /* User data variable */
};
//2、在epoll树上进行节点的操作 -> 添加, 删除, 修改
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
- epfd: epoll_create()函数的返回值, 指定操作的是哪个epoll树
- op: 要对epoll树进行的操作
EPOLL_CTL_ADD: 添加节点
EPOLL_CTL_MOD: 修改节点的属性
EPOLL_CTL_DEL: 删除节点
- fd: 要挂到epoll树上用于检测的文件描述符
- event: 指定要检测的文件描述符的事件(read-EPOLLIN/write-EPOLLOUT)
// 3、使用epoll委托内核检测挂到epoll树上的节点的状态
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数:
- epfd: epoll_create()函数的返回值, 指定操作的是那个epoll树
- events: 结构体数组的地址, 存储实际发送变化的fd的状态 -> 传出
- maxevents: 修饰第二个参数, 描述数组的实际大小
- timeout: epoll_wait阻塞时长, 单位: 毫秒
- >0: 阻塞对应的毫秒数
- -1: 一直阻塞
- =0: 不阻塞
返回值: 树上的节点有多少fd状态发生变化
1.1.2代码
// 伪代码
int main()
{
//1. 创建套接字 -> 监听
int lfd = socket();
// 2. 绑定
bind();
// 3. 监听
listen();
// 4. 使用epoll
// 4.1 创建epoll模型
int epfd = epoll_create(100);
// 4.2 将唯一的这个监听的fd挂到树上, 检测读事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); // 添加节点操作完成
// 4.4 开始检测
struct epoll_event evs[1024];
int lenevs=sizeof(evs)/sizeof(evs[0])
while(1)
{
// 持续检测
int ret = epoll_wait(epfd, evs, lenevs, -1);
for(int i=0; i<ret; ++i)
{
int fd = evs[i].data.fd;
// 监听
if(fd == lfd)
{
int cfd = accept();
// 新的cfd挂到树上
ev.events = EPOLLIN;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); // 添加节点操作完成
}
// 通信
else
{
int num = read();
if(num == 0)
{
// client disconnect
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
write();
}
}
}
}
client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <errno.h>
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == lfd)
{
perror("socket");
exit(0);
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverAddr.sin_addr.s_addr);
int ret = connect(lfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (ret == -1)
{
perror("accept");
exit(0);
}
while (1)
{
char buf[1024] = {0};
fgets(buf, sizeof(buf), stdin);
write(lfd, buf, strlen(buf) + 1);
read(lfd, buf, sizeof(buf));
printf("server data: %s\n", buf);
sleep(1);
}
close(lfd);
return 0;
}
server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == lfd)
{
perror("socket");
exit(0);
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverAddr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (-1 == ret)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 5);
if (-1 == ret)
{
perror("listen");
exit(0);
}
int epfd = epoll_create(1);
if (-1 == epfd)
{
perror("epoll_create");
exit(0);
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[128];
int lenevs = sizeof(evs) / sizeof(evs[0]);
while (1)
{
int num = epoll_wait(epfd, evs, lenevs, -1);
for (int i = 0; i < num; ++i)
{
int curfd = evs[i].data.fd;
if (lfd == curfd)
{
struct sockaddr_in clientAddr;
int lencli = sizeof(clientAddr);
int cfd = accept(lfd, (struct sockaddr *)&clientAddr, (socklen_t*)&lencli);
ev.events = EPOLLIN;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else
{
char buf[1024] = {0};
int num = read(curfd, buf, sizeof(buf));
if (num > 0)
{
printf("client data: %s\n", buf);
write(curfd, buf, num);
}
else if (num == 0)
{
printf("客户端已经关闭连接...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else
{
perror("read");
exit(0);
}
}
}
}
close(lfd);
return 0;
}
1.2 epoll 的工作模式
1.2.1LT模式
LT模式:水平触发模式, 这是epoll()默认的工作模式
特点:
1.委托内核检测读操作
--> 检测读缓存区是不是有数据
--> 只要读缓存区有数据,就通知我们
1.1.没数据->有数据,epoll通知(解除阻塞返回)
1.2.有数据,读走一部分,剩下一部分, epoll通知(解除阻塞返回)
1.3.有数据->全部读出,没数据, epoll不通知(阻塞)
2.委托内核检测写操作
--> 检测写缓冲区是不是可用(不可用:已经满了)
--> 只要写缓冲区不满就一直通知)
2.1.没有满,epoll通知(解除阻塞返回)-> 检测一次通知一次
2.2.满了,epoll不通知(阻塞)
2.3.满->不满(发送出去一部分),epoll通知(解除阻塞返回)->检测一次通知一次
水平模式总结:
只要读缓存区有数据,就通知我们
只要写缓冲区可用,就通知我们
epoll通知:解除阻塞直接返回
概念:LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
clinet.c
同上
server.c
/*
* 测试epoll()的LT模式
* 因为epoll()默认情况下就是LT模式。
* 所以源代码不需要修改,只需添加printf()
* 即可测试
* */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == lfd)
{
perror("socket");
exit(0);
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverAddr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (-1 == ret)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 5);
if (-1 == ret)
{
perror("listen");
exit(0);
}
int epfd = epoll_create(1);
if (-1 == epfd)
{
perror("epoll_create");
exit(0);
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[128];
int lenevs = sizeof(evs) / sizeof(evs[0]);
int NNN = 1;
while (1)
{
int num = epoll_wait(epfd, evs, lenevs, -1);
//测试epoll的LT模式
//epoll每返回一次,下面的输出语句就被调用一次
printf("epoll 返回第:%d次\n", NNN++);
for (int i = 0; i < num; ++i)
{
int curfd = evs[i].data.fd;
if (lfd == curfd)
{
struct sockaddr_in clientAddr;
int lencli = sizeof(clientAddr);
int cfd = accept(lfd, (struct sockaddr *)&clientAddr, (socklen_t *)&lencli);
ev.events = EPOLLIN;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else
{
char buf[5] = {0};
int num = read(curfd, buf, sizeof(buf));
if (num > 0)
{
printf("client data: %s\n", buf);
write(curfd, buf, num);
}
else if (num == 0)
{
printf("客户端已经关闭连接...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else
{
perror("read");
exit(0);
}
}
}
}
close(lfd);
return 0;
}
[root@lwh testcpp]# ./ser
epoll 返回第:1次
epoll 返回第:2次
client data: aksdh
epoll 返回第:3次
client data: gjkah
epoll 返回第:4次
client data: djfgh
epoll 返回第:5次
client data: ajkdf
epoll 返回第:6次
client data: hgkj
epoll 返回第:7次
client data:
epoll 返回第:8次
客户端已经关闭连接...
^C
[root@lwh testcpp]#
[root@lwh testcpp]# ./cli
aksdhgjkahdjfghajkdfhgkj
server data: aksdh
^C
[root@lwh testcpp]#
1.2.2ET模式
ET模式:(效率最高) 边沿触发模式 -> 需要设置
特点:
1.读操作: (检测到数据变化, 只通知一次, 不管缓冲区是否读完)
1.1.从没数据 -> 有数据, epoll通知(解除阻塞返回)
1.2.读数据, 读了一部分,剩下一部分,epoll不通知(阻塞)->读不完也不会继续通知
1.3.有数据->全部读出,没数据,epoll不通知(阻塞)
2.写操作:
- 写缓冲区没有满 -> epoll通知(解除阻塞返回) -> 一次
- 满了, epoll不通知(阻塞)
- 满 -> 不满(发送出去一部分) , epoll通知( 解除阻塞返回) -> 一次
--> epoll的边沿非阻塞模式下,效率是最高的
概念:
'ET(edge-triggered)是高速工作方式,只支持no-block socket'。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。'但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)'。
'ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高'。epoll工作在ET模式的时候,'必须使用非阻塞套接口',以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
1.2.3如何设置epoll的边沿模式
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;//设置边沿模式:使用边沿模式检测读缓冲区
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
client.c 同上
servic.c ET&&阻塞
/*
* 测试epoll()的ET模式
* */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == lfd)
{
perror("socket");
exit(0);
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverAddr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (-1 == ret)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 5);
if (-1 == ret)
{
perror("listen");
exit(0);
}
int epfd = epoll_create(1);
if (-1 == epfd)
{
perror("epoll_create");
exit(0);
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[128];
int lenevs = sizeof(evs) / sizeof(evs[0]);
int NNN = 1;
while (1)
{
int num = epoll_wait(epfd, evs, lenevs, -1);
//测试epoll的LT模式
//epoll每返回一次,下面的输出语句就被调用一次
printf("epoll 返回第:%d次\n", NNN++);
for (int i = 0; i < num; ++i)
{
int curfd = evs[i].data.fd;
if (lfd == curfd)
{
struct sockaddr_in clientAddr;
int lencli = sizeof(clientAddr);
int cfd = accept(lfd, (struct sockaddr *)&clientAddr, (socklen_t *)&lencli);
ev.events = EPOLLIN | EPOLLET;//改为了ET模式!!!
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else
{
char buf[5] = {0};
int num = read(curfd, buf, sizeof(buf));
if (num > 0)
{
printf("client data: %s\n", buf);
write(curfd, buf, num);
}
else if (num == 0)
{
printf("客户端已经关闭连接...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else
{
perror("read");
exit(0);
}
}
}
}
close(lfd);
return 0;
}
[root@lwh testcpp]# ./ser
epoll 返回第:1次
epoll 返回第:2次
client data: 12345
epoll 返回第:3次
client data: 67890
epoll 返回第:4次
client data:
epoll 返回第:5次
client data:
epoll 返回第:6次
客户端已经关闭连接...
^C
[root@lwh testcpp]#
[root@lwh testcpp]# ./cli
1234567890
server data: 12345
server data: 67890
server data:
server data:
^C
[root@lwh testcpp]#
1.2.4边沿模式下-通信的fd必须设置为非阻塞
// 边沿模式,非阻塞情况下:服务器和客户端通信的部分代码
else
{
char buf[512];
int len;
//
while((len= recv(curfd, buf, sizeof(buf), 0)) > 0)
{
printf("recv buf: %s", buf);
send(curfd, buf, strlen(buf)+1, 0);
}
if(len == 0)
{
printf("client disconnect ...\n");
// 将curfd从epoll树删除
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else
{
perror("recv");
exit(0);
}
}
#上边代码的问题:
当客户端和服务器没有断开连接的时候:
接收数据使用的函数 read() / recv() 默认是阻塞的(缓冲区没数据的时候阻塞), 当前
通过 while 循环读数据的时候, 当数据被读完了进程会阻塞在 recv() / read() 函数上
#解决方案: 设置fd为非阻塞
// 1. 得到文件描述符的默认flag属性
int flag = fcntl(fd, F_GETFL);
// 2. 在默认属性基础上添加非阻塞属性 -> O_NONBLOCK
flag |= O_NONBLOCK;
// 3. 将新 的属性设置给文件描述符
fcntl(fd, F_SETFL, flag);
1.没断开连接时,当读缓冲区内没数据可读取时
1.1.不管fd阻塞还是非阻塞,read都不返回0
1.2.fd阻塞,此时进程会阻塞在read这儿,无法处理其他操作,
等到有数据时,才解除阻塞
1.3.fd非阻塞,进程不阻塞,可以去处理进程内的其他操作
2.当断开时,不管fd阻塞非阻塞,read都返回0
client.c 同上
servic.c ET&&非阻塞
/*
* 测试epoll()的LT模式
* 因为epoll()默认情况下就是LT模式。
* 所以源代码不需要修改,只需添加printf()
* 即可测试
* */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main()
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == lfd)
{
perror("socket");
exit(0);
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.184.134", &serverAddr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
if (-1 == ret)
{
perror("bind");
exit(0);
}
ret = listen(lfd, 5);
if (-1 == ret)
{
perror("listen");
exit(0);
}
int epfd = epoll_create(1);
if (-1 == epfd)
{
perror("epoll_create");
exit(0);
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event evs[128];
int lenevs = sizeof(evs) / sizeof(evs[0]);
int NNN = 1;
while (1)
{
int num = epoll_wait(epfd, evs, lenevs, -1);
//测试epoll的LT模式
//epoll每返回一次,下面的输出语句就被调用一次
printf("epoll 返回第:%d次\n", NNN++);
for (int i = 0; i < num; ++i)
{
int curfd = evs[i].data.fd;
if (lfd == curfd)
{
struct sockaddr_in clientAddr;
int lencli = sizeof(clientAddr);
int cfd = accept(lfd, (struct sockaddr *)&clientAddr, (socklen_t *)&lencli);
int flag = fcntl(curfd, F_GETFL); //将cfd设置为非阻塞
flag |= O_NONBLOCK;
fcntl(curfd, F_SETFL, flag);
ev.events = EPOLLIN | EPOLLET; //ET模式!!!
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}
else
{
char buf[5] = {0};
int num = 0;
while ((num = read(curfd, buf, sizeof(buf))) > 0)
{
write(STDOUT_FILENO, buf, num); //1. 显示到自己的终端上
write(curfd, buf, num); //2. 回发给client
}
if (num == 0)
{
printf("客户端已经关闭连接...\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
printf("数据已经读完...\n");
}
else
{
perror("read");
exit(0);
}
}
}
}
}
close(lfd);
return 0;
}
[root@lwh testcpp]# ./ser
epoll 返回第:1次
epoll 返回第:2次
kkkkkkkkkkkkkkkkkkkkkkkkkllllllllllllllllllllllfffffffffffffffffdddddddddddddd
read: Connection reset by peer
[root@lwh testcpp]#
[root@lwh testcpp]# ./cli
kkkkkkkkkkkkkkkkkkkkkkkkkllllllllllllllllllllllfffffffffffffffffdddddddddddddd
server data: kkkkkkkkkkkkkkkkkkkkkkkkkllllllllllllllllllllllfffffffffffffffffdddddddddddddd
^C
[root@lwh testcpp]#
“Connection reset by peer”表示当前服务器接受到了通信对端发送的TCP RST信号,即通信对端已经关闭了连接,通过RST信号希望接收方关闭连接。
参看:https://www.cnblogs.com/toSeeMyDream/p/9890024.html
2. epoll总结
-
是什么?
- epoll -> IO转接模型
-
干什么?
- 使用epoll委托内核, 检测一系列的有效的fd 对应的读写缓冲区状态
- 读: 有没有数据到达
- 写: 是不是可写
- 提高了程序的执行效率
- 使用epoll委托内核, 检测一系列的有效的fd 对应的读写缓冲区状态
-
怎么使用?
- epoll_create
- epoll_ctl
- epoll_wait
-
工作模式:
- 水平 -> 默认
- 只要满足条件就不停的通知
- 边沿 -> 在事件中添加 EPOLLET
- 只要满足条件只通知一次
- 循环读数据
- 设置为非阻塞模式 -> fd的属性
- 只要满足条件只通知一次
- 水平 -> 默认