【Linux C++】IO多路复用模型:epoll | makefile入门

日期:2025.2.14

学习内容:

  • IO多路复用模型

  • makefile入门

个人总结:

epoll模型

回忆一下之前我们写过一个多进程的服务端的内容,当时是提到用fork函数来不断创造出进程,父进程用于accept,子进程用于解决,但是这样做很浪费资源,一个进程的资源利用率并不高,事实上,一个进程可以同时处理数量较多的客户端连接,通常处理上千个客户端是可行的。那为了可以一个进程解决多个客户端,便有了IO多路复用模型。

先上代码:


    // 创建 epoll 实例
    epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1");
        close(listen_fd);
        return 1;
    }

    // 将监听套接字添加到 epoll 实例中
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) == -1) {
        perror("epoll_ctl: listen_fd");
        close(listen_fd);
        close(epoll_fd);
        return 1;
    }

    printf("Server started, listening on port %d...\n", PORT);

    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            continue;
        }

        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == listen_fd) {
                // 处理新的连接
                struct sockaddr_in client_addr;
                socklen_t client_addr_len = sizeof(client_addr);
                int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
                if (client_fd == -1) {
                    perror("accept");
                    continue;
                }

                // 将新的客户端套接字添加到 epoll 实例中
                ev.events = EPOLLIN | EPOLLET;  // 使用边缘触发模式
                ev.data.fd = client_fd;
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
                    perror("epoll_ctl: client_fd");
                    close(client_fd);
                }

                printf("New connection from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
            }
            else {
                // 处理客户端数据
                char buffer[BUFFER_SIZE];
                memset(buffer, 0, sizeof buffer);
                ssize_t n = recv(events[i].data.fd, buffer, BUFFER_SIZE, 0);
                if (n <= 0) {
                    // 客户端关闭连接
                    close(events[i].data.fd);
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                    printf("Client disconnected\n");
                }
                else {


                    // 回显数据给客户端
                    if (send(events[i].data.fd, "ok", 2, 0) == -1) {
                        perror("send");
                        close(events[i].data.fd);
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                        printf("Error sending data to client\n");
                    }
                    printf("recv: %s\n", buffer);


                }
            }
        }
    }

    // 关闭监听套接字和 epoll 实例
    close(listen_fd);
    close(epoll_fd);

    return 0;
}

epoll_fd = epoll_create1(0);:创建一个epoll实例

//这里是创建一个单例,将监听描述符加入到epoll_fd中。
ev.events = EPOLLIN;
ev.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev)

用epoll_ctl函数,第二个参数填宏,有EPOLL_CTL_ADD,EPOLL_CTL_DEL。

当宏是EPOLL_CTL_ADD的时候,第三个参数填目标文件描述符,第四个填指向epoll_event结构体的指针。

用到DEL宏的时候下面再讲。

epoll_wait(epoll_fd, events, MAX_EVENTS, -1);

这个函数是等到集合里的有更新的描述符之前会等待下去。

第一个参数是之前创建的epoll实例,第二个是epoll_event类型的数组,第三个是数组大小,第四个是等待时间,-1代表着一直等待下去。

int nfds=epoll_wait(...)用一个变量记录返回值,然后直接

for (int i = 0; i < nfds; i++) {...
	events[i].data.fd == listen_fd//用这个方式来遍历和判断描述符类型

然后这段代码是代表将新的客户端加入到epollfd里:

		ev.events = EPOLLIN | EPOLLET;  // 使用边缘触发模式
        ev.data.fd = client_fd;
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) {
        	perror("epoll_ctl: client_fd");
            close(client_fd);
        }

水平模式和边缘模式

这两个简单来说就是,当缓冲区的数据有的时候,在水平模式下就会一直提醒这个描述符是更新的。但是在边缘模式下是不一样,边缘模式下是只有当描述符的状态出现了变化之后才会提示更新。也就是说如果我们没有一次性的将缓冲区的数据读取完的话,边缘模式也不会在之后提醒,从而读取之后的数据。

而在水平模式下就算我们没有一次性的读取完数据,但是只要缓冲区的数据还有,那么水平模式就会继续提醒应用程序。

这里再补充一句:在边缘模式下如果描述符所对应的缓冲区的状态出现了更新,那么同样也会提醒应用程序。

小ques:

由于这种模式的特点,所以我们往往会用while1循环+边缘模式来代替水平模式,能够提高我们的性能,这是因为:

ET 模式结合 while(1) 循环可以让应用程序更精准地处理事件。应用程序可以在一次事件通知后,通过循环一次性处理完所有相关的数据,避免了多次处理同一事件的复杂性,使得事件处理更加清晰和高效

当有大量数据到达时,应用程序可以在一次事件通知后,通过循环不断读取数据,直到没有更多数据可读,避免了多次等待通知的开销。

阻塞、非阻塞IO函数

之前有学习到一些类如recv函数,像这样的函数当我们没有及时接收到消息的时候,当前这个进程便会阻塞,直到满足函数的要求才会进行下去。但是在IO多路复用模式当中如果我们还用这种方式的话显然是不可以的。因为我们当前一个进程会用来处理很多个客户端。所以这里我们就需要将连接客户端的描述符转化成非阻塞状态,从而在调用那些阻塞IO函数的时候可以变成非阻塞状态。

下面这个函数就可以将描述符变成非阻塞状态

int SetNonblocking(int fd) {
    int fl;
    if ((fl = fcntl(fd, F_GETFL, 0)) == -1) {
        fl = 0;
    }
    return fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

makefile:

这里关于makefile懒得讲2333,baidu一下就有了。就随便写写记录而已。。。

之前大概看过一眼这个,没咋写过,忘完了。这次上手写了写,但是都是很简单的东西。

为什么又看了这个?是因为tm在写上次的实现文件传输功能的时候debug,我关于编译和运行的内容浪费了太多时间,于是就来看一看如何简单化:

# 定义默认目标,输入 `make` 时会执行这个目标
all: demo1 demo2 run

# 生成 demo1 的规则
demo1: 001.cpp
	g++ 001.cpp -o demo1

# 生成 demo2 的规则
demo2: 002.cpp
	g++ 002.cpp -o demo2

# 运行可执行文件的规则
run: demo1 demo2
	./demo2 5005 ./data2 &
	./demo1 192.168.11.132 5005 a.txt 57 &

这样命令行直接make就完成了,真方便😀嘻嘻

Makefile主要就是目标,依赖和命令。

hello: hello.c
    gcc hello.c -o hello

这里面 hello是目标,hello.c是依赖,下面的一行是命令。

这样的形式可以写很多,如果依赖也是目标的话,就会优先执行依赖是目标的语句,然后再最后执行目标(就跟拓扑那样)。

其他的就没有然后了,入门。。。绝不是我懒得学

关于最后两句加了&,是终端运行命令后交由后台运作的意思。这样就可以直接在一个终端里面运行着两个了,就是不方便观测过程了。

posted @   AdviseDY  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示