epoll接口

B站就业班视频代码搬运 p54

但是我跟老师的代码还是有点区别。。老师那里居然ev复用。。那么数组里那些结构体都用不上??

注意,本篇不是epoll反应堆。

I/O多路复用一共有select , poll ,epoll等模型,但是真正的高并发的话是epoll。原因:selcet 和poll都是把监控的文件描述符从应用程序拷贝到内核,然后挨个询问再把有事件发生的拷贝到程序内存,总之消耗很大,最好不好超过1024。所以掌握epoll更好。

当epoll采用默认的水平触发模式的时候,可以认为是一个更快速的Poll .

epoll有简单版本和复杂版本,复杂的叫做epoll反应堆模型,需要你自定义一个结构体,其中的内容你自己写,但是必须至少包括 1个文件描述符、1个回调函数、1个参数。哈哈这部分我还没看懂,所以今天搬运一个简单的epoll服务器代码啦

详细内容参考这位博主的文章

详细参考

1.基本函数介绍

1.1 创建epoll红黑树

int epoll_create(int size);

参数:必须填写一个大于0的数。

返回值

成功: 一个非零的 文件描述符,是红黑树的 树根节点。

失败:-1 并设置errno

1.2 添加监控的文件描述符(上树、修改、删除)

int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event);

epfd就是上面那个create 函数的返回值。

op的值:

EPOLL_CTL_ADD

将一个文件描述符添加到epoll树上

EPOLL_CTL_MOD

改变树上某个文件描述符的一些属性设定。

EPOLL_CTL_DEL

将某个制定的文件描述符从列表中删除,此时第四个参数可以写NULL 但是注意有BUG (自己man epoll_ctl 自己看一下BUG吧我没看)

fd: 你要处理的文件描述符。

struct epoll_event *event :肯定是这个名叫event的地址啦!那么event的数据结构是啥?它是一种结构体,名字叫做 epoll_event .

typedef union epoll_data {

void *ptr;

int fd;

uint32_t u32;

uint64_t u64;

} epoll_data_t;

这个void*万能指针在后面的 epoll反应堆模型里有很大用处,当然这里没啥用 这里用fd就行了

struct epoll_event {

uint32_t events; /* Epoll events */

epoll_data_t data; /* User data variable */

};

1.3 回收

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epfd: 红黑树的树根

struct epoll_event *event :这里不是单个event的地址了 而是数组的首地址,这个数组每个成员都是结构体epoll_event

maxevents: 一般是1024 反正必须大于0

timeout: 你自己规定的超时时间,单位是毫秒

-1 永远不算超时,一直等待、阻塞

0 立即返回

返回值:

成功: 在 I/O读写中,已经就绪的文件描述符的个数, 如果一个都没有就返回0

失败: -1 and errno is set appropriately.

2.使用epoll的高并发服务器

客户端的代码,前面一篇文章去拷贝就行了

cpp 复制代码
//epoll模型
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <sys/epoll.h>

int main() {
    int sfd = socket(AF_INET, SOCK_STREAM, 0); 
    //设置端口复用
    int opt =1;
    setsockopt ( sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
   
    //定义服务器地址的 结构体,利用本机任意ip,端口为8888
    struct sockaddr_in servad;
    bzero(&servad, sizeof(servad));
    servad.sin_family =AF_INET;
    servad.sin_port =htons(8888);
    servad.sin_addr.s_addr = htonl (INADDR_ANY);
 
    int ret = bind(sfd, (struct sockaddr*)&servad, sizeof( servad));
 
    //监听描述符sfd 设置完成
    listen(sfd,128);  
 
    //创建一颗 epoll红黑树
    int epfd = epoll_create(1024); 
    if (epfd<0){
	perror ("epoll create fail\n");
	return -1;
    } 
    //将监听描述符添加到树上
    struct epoll_event sev;
    sev.events = EPOLLIN;
    sev.data.fd = sfd;
    epoll_ctl (epfd, EPOLL_CTL_ADD,sfd,&sev);
    struct epoll_event evarray[1024];
    int nready =0; 
    while (1) {
        //超时时间设置-1,所以进程会无限阻塞在这一步,除非有了变化才往下走
        nready = epoll_wait (epfd,evarray,1024,-1);
        if (nready < 0) {
	    if (errno == EINTR){
	       continue;
	    }
	    break;
	}
	for (int i =0;i<nready;i++){
	   int sockfd =evarray[i].data.fd;
            //第一种情况,有客户端的新请求
	    if (sockfd == sfd) {
	        int newfd = accept (sfd,NULL, NULL);
		//将新的通信描述符newfd上树
		evarray[i].events =EPOLLIN;
      		evarray[i].data.fd = newfd;
		epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &(evarray[i]));
		continue;
	    }
           //第二种情况,眼前的描述符,有可读事件发生 
            char buf[256];
            //读数据
            memset(buf,0x00,sizeof(buf));
            int n =read (sockfd, buf, sizeof(buf));
	    //int n = recv (sockfd, buf, sizeof(buf), 0);
            if (n <=0){ 
                printf("这里是服务器端, read error or client close\n"); 
                close (sockfd);
		//sockfd 下树这里第四个参数可是NULL
		epoll_ctl (epfd, EPOLL_CTL_DEL, sockfd, NULL);
		continue;
	    }
            else {
                printf ("这里是服务器端n =%d ,读到的是%s \n", n,buf);
                //将小写字母都转化wie大写
                for (int j =0; j<n; j++) { 
                    buf[j] =toupper (buf[j]); 
                }    
                //发送数据
                write (sockfd, buf, n);
            }
        }
    }
close (sfd);
close (epfd);
return 0;
}

3. 如何测试你的epoll是ET (边缘触发) 还是LT (水平触发):

默认都是水平触发,只要缓冲区有字符,就一直读,写100个,读限制为30字符,那就读3次。如何判断出来的呢?

3.1 判断epoll是LT的代码:

把上面 n =read ( XXX 最后一个参数改成2 ) 让它每次只能读俩字符。然后你再客户端中,敲10个字符。这时你会发现,服务器分6次向你返回了所有字符的大写(最后又一个空回车)

说明默认是LT模式

3.2 ET 模式怎么设置

XXXX.events = EPOLLIN | EPOLLET; /*边沿触发 */

**read函数 最后一个参数改成2 ,**再把通信描述符设置成ET模式,

//将新的通信描述符 newfd上树

evarray[i].events =EPOLLIN | EPOLLET;

你发送10个字符之后,服务器只返回俩字符;你再敲回车,又返回俩字符;直到读完为止。

3.3 ET模式下,就想一口气读完所有数据

while 1 加上去看似很简单

cpp 复制代码
//现在是一口气读完所有数据ETmoshi 
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <sys/epoll.h>

int main() {
    int sfd = socket(AF_INET, SOCK_STREAM, 0); 
    //设置端口复用
    int opt =1;
    setsockopt ( sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(int));
   
    //定义服务器地址的 结构体,利用本机任意ip,端口为8888
    struct sockaddr_in servad;
    bzero(&servad, sizeof(servad));
    servad.sin_family =AF_INET;
    servad.sin_port =htons(8888);
    servad.sin_addr.s_addr = htonl (INADDR_ANY);
 
    int ret = bind(sfd, (struct sockaddr*)&servad, sizeof( servad));
 
    //监听描述符sfd 设置完成
    listen(sfd,128);  
 
    //创建一颗 epoll红黑树
    int epfd = epoll_create(1024); 
    if (epfd<0){
	perror ("epoll create fail\n");
	return -1;
    } 
    //将监听描述符添加到树上
    struct epoll_event sev;
    sev.events = EPOLLIN;
    sev.data.fd = sfd;
    epoll_ctl (epfd, EPOLL_CTL_ADD,sfd,&sev);
    struct epoll_event evarray[1024];
    int nready =0; 
    while (1) {
        //超时时间设置-1,所以进程会无限阻塞在这一步,除非有了变化才往下走
        nready = epoll_wait (epfd,evarray,1024,-1);
        if (nready < 0) {
	    if (errno == EINTR){
	       continue;
	    }
	    break;
	}
	for (int i =0;i<nready;i++){
	   int sockfd =evarray[i].data.fd;
            //第一种情况,有客户端的新请求
	    if (sockfd == sfd) {
	        int newfd = accept (sfd,NULL, NULL);
		//将新的通信描述符newfd上树
		evarray[i].events =EPOLLIN | EPOLLET;
      		evarray[i].data.fd = newfd;
		epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &(evarray[i]));
		continue;
	    }
           //第二种情况,眼前的描述符,有可读事件发生 
            char buf[256];
	    while (1) {
                //读数据
                memset(buf,0x00,sizeof(buf));
                int n =read (sockfd, buf, 2);
	        //int n = recv (sockfd, buf, sizeof(buf), 0);
                if (n <=0){ 
                    printf("这里是服务器端, read error or client close\n"); 
                    close (sockfd);
		    epoll_ctl (epfd, EPOLL_CTL_DEL, sockfd, NULL);
		    break;
	        }
                else {
                    printf ("这里是服务器端n =%d ,读到的是%s \n", n,buf);
                    //将小写字母都转化wie大写
                    for (int j =0; j<n; j++) { 
                        buf[j] =toupper (buf[j]); 
                    }    
                    //发送数据
                    write (sockfd, buf, n);
                }
            }
	}
    }
close (sfd);
close (epfd);
return 0;
}

但是此时会出现一个BUG 第一个客户端连接了以后,其他客户端的连接,服务器似乎不管了。。只有眼前这一个退出了才行。。

原因:read函数返回值小于0的时候,你关闭了文件描述符。。就算你不写关闭文件描述符的代码,read 或者recv函数一直在那里等待着。

3.3 如何解决新问题( ET模式)

将传递信息的文件描述符(通信描述符)设置为非阻塞(未完待续)

相关推荐
娅娅梨7 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
yaosheng_VALVE8 分钟前
稀硫酸介质中 V 型球阀的材质选择与选型要点-耀圣
运维·spring cloud·自动化·intellij-idea·材质·1024程序员节
兵哥工控11 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
我爱工作&工作love我19 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
娃娃丢没有坏心思1 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h1 小时前
探索 C++20:C++ 的新纪元
c++·c++20
看山还是山,看水还是。1 小时前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
扣得君1 小时前
C++20 Coroutine Echo Server
运维·服务器·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20
白-胖-子1 小时前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-统计数字
开发语言·c++·算法·蓝桥杯·等考·13级