13_epoll的 LT、ET-block socket 、ET-no-block socket

代码: 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_create
    • epoll_ctl
    • epoll_wait
  • 工作模式:

    • 水平 -> 默认
      • 只要满足条件就不停的通知
    • 边沿 -> 在事件中添加 EPOLLET
      • 只要满足条件只通知一次
        • 循环读数据
        • 设置为非阻塞模式 -> fd的属性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值