【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是依赖,下面的一行是命令。
这样的形式可以写很多,如果依赖也是目标的话,就会优先执行依赖是目标的语句,然后再最后执行目标(就跟拓扑那样)。
其他的就没有然后了,入门。。。绝不是我懒得学
关于最后两句加了&,是终端运行命令后交由后台运作的意思。这样就可以直接在一个终端里面运行着两个了,就是不方便观测过程了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战