从零学会epoll的使用和原理
从零学会epoll的使用和原理
第一步:理解 select
/ poll
的缺陷
一、select 和 poll 是什么?
它们是 Linux 提供的 I/O 多路复用机制,可以让我们同时监听多个文件描述符(fd),比如 socket,来等待“是否有数据可以读/写”。
二、select 工作流程(伪代码)
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);select(maxfd + 1, &readfds, NULL, NULL, NULL);if (FD_ISSET(sockfd, &readfds)) {// 有数据可读
}
三、poll 工作流程(伪代码)
struct pollfd fds[1024];
poll(fds, nfds, timeout);
四、select 和 poll 的主要缺陷
缺陷点 | 描述 |
---|---|
1. fd 数量限制 | select 的 fd 上限是 1024(因为用的是 bitmap) |
2. 每次都要传入整个 fd 集合 | 调用时都需要将所有监听的 fd 从用户态拷贝到内核态 |
3. 事件通知不高效 | select/poll 会遍历所有 fd,查找哪一个就绪,O(n) 时间复杂度 |
4. 无状态 | 每次调用都要重新设置监听 fd 的集合,没法复用 |
5. 边缘触发支持差 | 不支持高效的边缘触发,只能是水平触发(LT) |
📌 总结一句话:
select/poll 太“啰嗦”和“笨重”,当连接数上千上万时,它们效率非常低,而 epoll 专为这种高并发场景优化。
select
示例代码(监听 stdin 和 socket)
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>int main() {int listen_fd = socket(AF_INET, SOCK_STREAM, 0);sockaddr_in addr{};addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = inet_addr("127.0.0.1");bind(listen_fd, (sockaddr*)&addr, sizeof(addr));listen(listen_fd, 5);std::cout << "Listening on 127.0.0.1:8888...\n";fd_set readfds;int max_fd = std::max(listen_fd, STDIN_FILENO); // 最大 fdwhile (true) {FD_ZERO(&readfds);FD_SET(STDIN_FILENO, &readfds); // 标准输入FD_SET(listen_fd, &readfds); // 监听 socketint ready = select(max_fd + 1, &readfds, nullptr, nullptr, nullptr);if (ready == -1) {perror("select");break;}if (FD_ISSET(STDIN_FILENO, &readfds)) {char buf[1024] = {};read(STDIN_FILENO, buf, sizeof(buf));std::cout << "[STDIN] 输入了: " << buf;}if (FD_ISSET(listen_fd, &readfds)) {sockaddr_in client{};socklen_t len = sizeof(client);int conn_fd = accept(listen_fd, (sockaddr*)&client, &len);std::cout << "[SOCKET] 新连接来自: " << inet_ntoa(client.sin_addr) << ":" << ntohs(client.sin_port) << "\n";close(conn_fd);}}close(listen_fd);return 0;
}
🔍 如何运行这个 demo:
- 编译:
g++ select_demo.cpp -o select_demo
- 运行:
./select_demo
- 打开另一个终端连接:
telnet 127.0.0.1 8888
- 或者在当前终端直接输入一些文字,它会 echo 出来。
📌 你能观察到什么?
- 每次
select()
都要重新设置fd_set
。 - 没有事件时程序就会阻塞在
select()
。 - 随着连接越来越多,你会发现效率会下降。
第二阶段:epoll
的基本使用
一、epoll 的工作流程(核心三步)
epoll 的使用核心是三步:
步骤 | 函数名 | 作用 |
---|---|---|
① 创建 epoll 对象 | epoll_create1() | 创建 epoll 文件描述符 |
② 注册事件 | epoll_ctl() | 将监听的 fd 注册到 epoll 实例 |
③ 等待事件 | epoll_wait() | 阻塞等待就绪事件,返回活跃的 fd |
二、epoll 的基本代码结构(伪代码)
int epfd = epoll_create1(0); // 创建 epoll 实例epoll_event ev;
ev.events = EPOLLIN; // 关注读事件
ev.data.fd = listen_fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev); // 添加 fd 到 epollepoll_event events[1024]; // 返回的事件数组
int n = epoll_wait(epfd, events, 1024, -1); // 阻塞直到有事件发生for (int i = 0; i < n; ++i) {if (events[i].data.fd == listen_fd) {accept(); // 新连接} else {read(); // 读数据}
}
三、epoll 示例:监听 stdin 和 socket(对比 select)
我们来把刚才 select 的例子,用 epoll 重写一遍。
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>int main() {int listen_fd = socket(AF_INET, SOCK_STREAM, 0);sockaddr_in addr{};addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = inet_addr("127.0.0.1");bind(listen_fd, (sockaddr*)&addr, sizeof(addr));listen(listen_fd, 5);std::cout << "Listening on 127.0.0.1:8888...\n";int epfd = epoll_create1(0); // 创建 epoll 对象epoll_event ev{};ev.events = EPOLLIN;ev.data.fd = listen_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev); // 添加监听 fdev.data.fd = STDIN_FILENO;epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); // 添加 stdinepoll_event events[1024];while (true) {int n = epoll_wait(epfd, events, 1024, -1);for (int i = 0; i < n; ++i) {int fd = events[i].data.fd;if (fd == listen_fd) {sockaddr_in client{};socklen_t len = sizeof(client);int conn_fd = accept(listen_fd, (sockaddr*)&client, &len);std::cout << "[SOCKET] 新连接来自: " << inet_ntoa(client.sin_addr) << ":" << ntohs(client.sin_port) << "\n";close(conn_fd);} else if (fd == STDIN_FILENO) {char buf[1024] = {};read(STDIN_FILENO, buf, sizeof(buf));std::cout << "[STDIN] 输入了: " << buf;}}}close(listen_fd);close(epfd);return 0;
}
四、为什么 epoll 更优秀?
比较项 | select/poll | epoll |
---|---|---|
fd 数量上限 | select: 1024,poll 无限制但效率低 | 理论上无上限,效率高 |
是否复用 fd 集合 | 否,每次都要传 | 是,注册一次后直接等待 |
是否遍历所有 fd | 是(每次都查) | 否(事件就绪才通知) |
通知机制 | 轮询 | 事件驱动 |
第三阶段:理解 ET(边缘触发)与非阻塞 I/O 的配合
这部分是 epoll 的精髓,也是它比 select 更高效的关键所在。我们先来搞清楚 LT vs ET,然后说非阻塞,再用代码加深理解。
一、什么是 LT 和 ET?
epoll 支持两种事件触发模式:
模式 | 名称 | 特点 | 默认值 |
---|---|---|---|
LT | Level Trigger(水平触发) | 只要条件满足(比如有数据),每次 epoll_wait 都会返回这个事件 | ✅ 默认 |
ET | Edge Trigger(边缘触发) | 只有状态从无到有变化时,才触发事件通知,只通知一次 | ❌ 不是默认,需要手动设置 |
二、简单对比:LT vs ET
假设 socket 缓冲区有数据:
- LT 模式:
epoll_wait
每次都会告诉你“有数据!”,直到你读完。
- ET 模式:
- 只在数据第一次到达时通知你一次,“之后不管了”。
- 如果你没一次性读完所有数据,就会“丢事件”,程序卡住。
三、为什么 ET 更快?
因为:
- 内核只在状态变化时通知,不会反复通知同一个事件;
- 节省系统调用时间(高并发下效果特别明显);
- 但是使用更复杂——必须配合非阻塞 I/O!
四、非阻塞 I/O 是什么?
默认情况下,socket 是阻塞的:
char buf[1024];
read(fd, buf, sizeof(buf)); // 如果没数据,会卡住等
非阻塞模式下,read 立刻返回:
fcntl(fd, F_SETFL, O_NONBLOCK); // 设置非阻塞
- 有数据就读
- 没数据就返回 -1,errno=EAGAIN / EWOULDBLOCK
五、ET + 非阻塞 I/O 的典型套路:
while (true) {ssize_t n = read(fd, buf, sizeof(buf));if (n == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) break; // 没数据可读else perror("read error");} else if (n == 0) {// 对方关闭连接break;} else {// 正常读取数据}
}
六、设置 ET 模式和非阻塞 socket 的完整代码片段
// 设置 fd 为非阻塞
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);// 注册 ET 模式
epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
七、ET 模式小结
问题 | 答案 |
---|---|
必须配合非阻塞? | ✅ 是的,不然容易卡死 |
要用循环读数据? | ✅ 是的,直到返回 EAGAIN |
什么时候触发? | 数据从没有 ➜ 有,才触发 |
好处? | 高性能,减少系统调用 |
风险? | 没处理好容易漏事件、阻塞 |
巩固下 ET 和非阻塞的相关细节
ET + 非阻塞 I/O 是 epoll 中最容易出 bug 的地方,
一、非阻塞 I/O 设置方式
我们经常对 socket 设置非阻塞,代码如下:
#include <fcntl.h>void set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL, 0);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
📌 重点解释:
O_NONBLOCK
是一个 flag,它告诉内核这个 fd 不允许阻塞。fcntl
拿到当前 flags,再 OR 一个非阻塞标志。
二、ET 模式触发行为再强化
我们看一个常见现象来理解 ET 和阻塞 read 的冲突:
❌ 错误代码(阻塞读配合 ET)
// 假设这是 ET 模式下的回调处理
char buf[1024];
read(fd, buf, sizeof(buf)); // 没读完就等死了!
如果缓冲区里数据不够 1024 字节,这个 read 会阻塞——
而 ET 只通知一次! 你程序就卡死在 read 上,永远等不到下一次触发。
三、正确姿势:循环读取直到 EAGAIN
char buf[1024];
while (true) {ssize_t n = read(fd, buf, sizeof(buf));if (n == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 没有数据了,结束循环break;} else {perror("read error");break;}} else if (n == 0) {// 对方关闭连接printf("Client closed connection\n");close(fd);break;} else {// 正常读取数据printf("Received: %.*s", (int)n, buf);}
}
📌 关键点总结:
行为 | 必须这么做的原因 |
---|---|
循环读取 | ET 只触发一次,要把所有数据读干净 |
判断 errno | 确认是“真的没数据”而不是其他错误 |
处理 n == 0 | 表示连接断开,必须 close(fd) |
四、添加客户端连接时也要设置非阻塞!
int connfd = accept(listen_fd, ...);
set_nonblocking(connfd); // 否则 read 可能阻塞
五、epoll 注册事件时需要设置 EPOLLET
epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
epoll + ET + 非阻塞 I/O 的最小完整 demo
场景功能说明:
- 使用 epoll 的 ET 模式监听所有 socket。
- 所有 fd 设置为非阻塞。
- 客户端发送数据,服务端完整读完并打印。
- 使用
read()
+EAGAIN
机制读取所有数据。
代码:epoll_et_server.cpp
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <vector>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>void set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}int main() {int listen_fd = socket(AF_INET, SOCK_STREAM, 0);sockaddr_in addr{};addr.sin_family = AF_INET;addr.sin_port = htons(8888);addr.sin_addr.s_addr = inet_addr("127.0.0.1");bind(listen_fd, (sockaddr*)&addr, sizeof(addr));listen(listen_fd, SOMAXCONN);set_nonblocking(listen_fd);int epfd = epoll_create1(0);epoll_event ev{};ev.events = EPOLLIN | EPOLLET; // 注意:ET 模式ev.data.fd = listen_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);std::vector<epoll_event> events(1024);std::cout << "Server listening on 127.0.0.1:8888\n";while (true) {int n = epoll_wait(epfd, events.data(), events.size(), -1);for (int i = 0; i < n; ++i) {int fd = events[i].data.fd;if (fd == listen_fd) {// 处理所有新连接while (true) {sockaddr_in client{};socklen_t len = sizeof(client);int conn_fd = accept(listen_fd, (sockaddr*)&client, &len);if (conn_fd == -1) break;std::cout << "[New Connection] "<< inet_ntoa(client.sin_addr) << ":" << ntohs(client.sin_port) << "\n";set_nonblocking(conn_fd);epoll_event cev{};cev.events = EPOLLIN | EPOLLET;cev.data.fd = conn_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &cev);}} else {// 处理已有连接的读事件while (true) {char buf[1024];ssize_t count = read(fd, buf, sizeof(buf));if (count == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 没数据可读了break;} else {perror("read");close(fd);break;}} else if (count == 0) {// 对方关闭连接std::cout << "[Disconnect] fd = " << fd << "\n";close(fd);break;} else {std::cout << "[Read] fd = " << fd << ", content: "<< std::string(buf, count);}}}}}close(listen_fd);close(epfd);return 0;
}
编译 & 运行方式
g++ epoll_et_server.cpp -o epoll_et_server
./epoll_et_server
打开另一个终端:
telnet 127.0.0.1 8888
# 或用 nc 测试:
# echo "hello" | nc 127.0.0.1 8888
提示:
- 你可以反复
telnet
连多个客户端; - 输入数据,服务端会完整打印;
- 断开连接也能被正常检测并关闭;
🔥 第四阶段:写一个真正的高并发服务器,支持长连接、广播、线程池等!
🧩 项目目标功能
- ✅ 基于
epoll
的 I/O 多路复用 - ✅ 使用 ET 模式 + 非阻塞 I/O
- ✅ 支持多个客户端的 长连接
- ✅ 实现 客户端广播 功能(群发消息)
- ✅ 使用 线程池 解耦 I/O 与业务处理
🚧 模块化开发路线图(按顺序完成)
🔹 第一步:网络通信模块(epoll
+ 非阻塞长连接管理)
- 接受新连接
- 使用
epoll
管理所有 socket - 使用
ET
模式读取客户端数据
🔹 第二步:线程池模块
- 预先创建线程池(工作线程 + 任务队列)
- 支持任务提交与消费
🔹 第三步:广播功能模块
- 所有客户端连接都被管理起来
- 某个客户端发消息 ➜ 广播给所有连接
🔹 第四步:服务端结构模块化 & 重构
- 将各部分封装为类(Server / ThreadPool / Connection 等)
- 保持逻辑清晰、模块解耦
我们现在从第一步开始:
📦 第一步:网络通信模块(只实现接收客户端连接 & 打印数据)
目标:
- 使用
epoll
+ET
+非阻塞
监听并读取每个连接的数据。 - 支持多个客户端长连接、持续交互。
// server.cpp
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <vector>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>void set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}int main() {int listen_fd = socket(AF_INET, SOCK_STREAM, 0);sockaddr_in server_addr{};server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888);server_addr.sin_addr.s_addr = INADDR_ANY;bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr));listen(listen_fd, SOMAXCONN);set_nonblocking(listen_fd);int epfd = epoll_create1(0);epoll_event ev{};ev.events = EPOLLIN | EPOLLET;ev.data.fd = listen_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);std::vector<epoll_event> events(1024);std::cout << "[INFO] Server started on port 8888" << std::endl;while (true) {int n = epoll_wait(epfd, events.data(), events.size(), -1);for (int i = 0; i < n; ++i) {int fd = events[i].data.fd;if (fd == listen_fd) {// 接收所有连接while (true) {sockaddr_in client{};socklen_t len = sizeof(client);int conn_fd = accept(listen_fd, (sockaddr*)&client, &len);if (conn_fd == -1) break;set_nonblocking(conn_fd);epoll_event conn_ev{};conn_ev.events = EPOLLIN | EPOLLET;conn_ev.data.fd = conn_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &conn_ev);std::cout << "[CONNECT] New client: "<< inet_ntoa(client.sin_addr) << ":"<< ntohs(client.sin_port) << std::endl;}} else {// 读取数据while (true) {char buf[1024];ssize_t count = read(fd, buf, sizeof(buf));if (count == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) break;perror("read error");close(fd);break;} else if (count == 0) {std::cout << "[DISCONNECT] fd: " << fd << std::endl;close(fd);break;} else {std::cout << "[DATA] fd " << fd << ": "<< std::string(buf, count);}}}}}close(epfd);close(listen_fd);return 0;
}
🔧 编译:
g++ server.cpp -o server
🚀 运行:
./server
然后另开几个终端:
telnet 127.0.0.1 8888
# 或用 nc 测试
# echo "hello" | nc 127.0.0.1 8888
🔹 第二步:线程池模块
// thread_pool.h
#pragma once
#include <vector>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <atomic>class ThreadPool {
public:using Task = std::function<void()>;explicit ThreadPool(size_t thread_count = 4): stop_flag(false) {for (size_t i = 0; i < thread_count; ++i) {workers.emplace_back([this]() {while (true) {Task task;{std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [this]() { return stop_flag || !tasks.empty(); });if (stop_flag && tasks.empty()) return;task = std::move(tasks.front());tasks.pop();}task();}});}}~ThreadPool() {{std::lock_guard<std::mutex> lock(mtx);stop_flag = true;}cv.notify_all();for (auto& t : workers) {if (t.joinable()) t.join();}}void enqueue(Task task) {{std::lock_guard<std::mutex> lock(mtx);tasks.push(std::move(task));}cv.notify_one();}private:std::vector<std::thread> workers;std::queue<Task> tasks;std::mutex mtx;std::condition_variable cv;std::atomic<bool> stop_flag;
};
测试一下线程池
好的!我们来写一个简单的测试程序来验证这个 ThreadPool
是否正常工作。
示例:test_thread_pool.cpp
#include "thread_pool.h"
#include <iostream>
#include <chrono>void test_task(int id) {std::cout << "[TASK START] Task " << id << " in thread "<< std::this_thread::get_id() << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500));std::cout << "[TASK END] Task " << id << " done\n";
}int main() {ThreadPool pool(4); // 启动4个线程for (int i = 0; i < 10; ++i) {pool.enqueue([i]() { test_task(i); });}std::this_thread::sleep_for(std::chrono::seconds(3));std::cout << "[MAIN] Done\n";return 0;
}
🔧 编译:
g++ test_thread_pool.cpp -o test_thread_pool -std=c++11 -pthread
(注意加上 -pthread
)
🚀 运行:
./test_thread_pool
🔹 第三步:广播功能模块
// server.cpp (含线程池和广播结构)
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <vector>
#include <unordered_set>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "thread_pool.h"void set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}int main() {int listen_fd = socket(AF_INET, SOCK_STREAM, 0);sockaddr_in server_addr{};server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8888);server_addr.sin_addr.s_addr = INADDR_ANY;bind(listen_fd, (sockaddr*)&server_addr, sizeof(server_addr));listen(listen_fd, SOMAXCONN);set_nonblocking(listen_fd);int epfd = epoll_create1(0);epoll_event ev{};ev.events = EPOLLIN | EPOLLET;ev.data.fd = listen_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);std::vector<epoll_event> events(1024);std::unordered_set<int> clients;ThreadPool pool(4);std::cout << "[INFO] Server started on port 8888" << std::endl;while (true) {int n = epoll_wait(epfd, events.data(), events.size(), -1);for (int i = 0; i < n; ++i) {int fd = events[i].data.fd;if (fd == listen_fd) {while (true) {sockaddr_in client{};socklen_t len = sizeof(client);int conn_fd = accept(listen_fd, (sockaddr*)&client, &len);if (conn_fd == -1) break;set_nonblocking(conn_fd);epoll_event conn_ev{};conn_ev.events = EPOLLIN | EPOLLET;conn_ev.data.fd = conn_fd;epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &conn_ev);clients.insert(conn_fd);std::cout << "[CONNECT] "<< inet_ntoa(client.sin_addr) << ":"<< ntohs(client.sin_port)<< " (fd: " << conn_fd << ")" << std::endl;}} else {while (true) {char buf[1024];ssize_t count = read(fd, buf, sizeof(buf));if (count == -1) {if (errno == EAGAIN || errno == EWOULDBLOCK) break;perror("read error");close(fd);clients.erase(fd);break;} else if (count == 0) {std::cout << "[DISCONNECT] fd: " << fd << std::endl;close(fd);clients.erase(fd);break;} else {std::string msg(buf, count);// 异步处理(广播)pool.enqueue([fd, msg, &clients]() {std::string broadcast = "[Client " + std::to_string(fd) + "] " + msg;for (int client_fd : clients) {if (client_fd != fd) {send(client_fd, broadcast.c_str(), broadcast.size(), 0);}}std::cout << "[BROADCAST from " << fd << "]: " << msg;});}}}}}close(epfd);close(listen_fd);return 0;
}
完成第三步 ✅:将线程池整合进服务器,并构建了广播功能的基础框架。
🧩 你现在拥有的功能:
- 支持多个客户端 长连接
- 客户端发送消息 ➜ 异步投递到线程池中处理
- 消息被广播给所有其他客户端
- 采用
epoll + ET + 非阻塞 I/O
,高效并发
🚀 快速测试建议:
- 编译:
g++ server.cpp -o server -std=c++11 -pthread
- 运行服务端:
./server
- 用多个终端模拟客户端:
telnet 127.0.0.1 8888
# 或者 nc:
nc 127.0.0.1 8888
客户端 A 发消息 ➜ B、C 会收到。
模块封装重构(Server / Connection / ThreadPool 分离)
Server
// server.h
#pragma once
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <memory>
#include "thread_pool.h"
#include "connection.h"class Server {
public:Server(int port = 8888);~Server();void run();private:void set_nonblocking(int fd);void handle_new_connection();void handle_client_event(int fd);int listen_fd_;int epoll_fd_;ThreadPool pool_;std::unordered_map<int, std::shared_ptr<Connection>> connections_;std::unordered_set<int> client_fds_;
};
// server.cpp
#include "server.h"
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <cstring>Server::Server(int port) : pool_(4) {listen_fd_ = socket(AF_INET, SOCK_STREAM, 0);sockaddr_in server_addr{};server_addr.sin_family = AF_INET;server_addr.sin_port = htons(port);server_addr.sin_addr.s_addr = INADDR_ANY;bind(listen_fd_, (sockaddr*)&server_addr, sizeof(server_addr));listen(listen_fd_, SOMAXCONN);set_nonblocking(listen_fd_);epoll_fd_ = epoll_create1(0);epoll_event ev{};ev.events = EPOLLIN | EPOLLET;ev.data.fd = listen_fd_;epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, listen_fd_, &ev);std::cout << "[INFO] Server started on port " << port << std::endl;
}Server::~Server() {close(epoll_fd_);close(listen_fd_);
}void Server::set_nonblocking(int fd) {int flags = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}void Server::run() {std::vector<epoll_event> events(1024);while (true) {int n = epoll_wait(epoll_fd_, events.data(), events.size(), -1);for (int i = 0; i < n; ++i) {int fd = events[i].data.fd;if (fd == listen_fd_) {handle_new_connection();} else {handle_client_event(fd);}}}
}void Server::handle_new_connection() {while (true) {sockaddr_in client{};socklen_t len = sizeof(client);int conn_fd = accept(listen_fd_, (sockaddr*)&client, &len);if (conn_fd == -1) break;set_nonblocking(conn_fd);epoll_event ev{};ev.events = EPOLLIN | EPOLLET;ev.data.fd = conn_fd;epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, conn_fd, &ev);auto conn = std::make_shared<Connection>(conn_fd);connections_[conn_fd] = conn;client_fds_.insert(conn_fd);std::cout << "[CONNECT] fd: " << conn_fd << std::endl;}
}void Server::handle_client_event(int fd) {auto conn = connections_[fd];bool closed = false;std::string msg = conn->read_data(closed);if (closed) {std::cout << "[DISCONNECT] fd: " << fd << std::endl;close(fd);epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, nullptr);connections_.erase(fd);client_fds_.erase(fd);} else {pool_.enqueue([fd, msg, this]() {std::string broadcast = "[Client " + std::to_string(fd) + "] " + msg;for (int client_fd : client_fds_) {if (client_fd != fd && connections_.count(client_fd)) {connections_[client_fd]->send_data(broadcast);}}std::cout << "[BROADCAST from " << fd << "]: " << msg;});}
}
我们现在完成了服务端模块的完整封装 ✅:
✅ 模块结构现状:
文件名 | 说明 |
---|---|
connection.h | 封装客户端连接的读写逻辑 |
thread_pool.h | 通用线程池,用于异步任务调度 |
server.h/cpp | Server 类封装 epoll + 连接管理 + 线程池 |
📦 使用方式(主程序 main.cpp
):
#include "server.h"int main() {Server server(8888);server.run();return 0;
}
编译方式:
g++ main.cpp server.cpp -o server -std=c++11 -pthread
C++ 项目结构模板
🗂 项目结构建议
epoll_server/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── server.cpp
│ ├── server.h
│ ├── connection.h
│ └── thread_pool.h
├── build/ ← 编译输出目录(CMake 会自动创建)
└── README.md
📄 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(epoll_server)set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pthread")# 包含头文件目录
include_directories(${PROJECT_SOURCE_DIR}/src)# 源文件
file(GLOB SOURCES src/*.cpp)# 可执行文件
add_executable(server ${SOURCES})
📄 示例 src/main.cpp
#include "server.h"int main() {Server server(8888);server.run();return 0;
}
🧪 构建和运行步骤:
cd epoll_server
mkdir -p build && cd build
cmake ..
make
./server
相关文章:
从零学会epoll的使用和原理
从零学会epoll的使用和原理 第一步:理解 select / poll 的缺陷 一、select 和 poll 是什么? 它们是 Linux 提供的 I/O 多路复用机制,可以让我们同时监听多个文件描述符(fd),比如 socket,来等…...
XHTMLConverter把docx转换html报java.lang.NullPointerException异常
一.报错 1.报错信息 org.apache.poi.xwpf.converter.core.XWPFConverterException: java.lang.NullPointerExceptionat org.apache.poi.xwpf.converter.xhtml.XHTMLConverter.convert(XHTMLConverter.java:77)at org.apache.poi.xwpf.converter.xhtml.XHTMLConverter.doConve…...
教育科技质检的三重挑战 质检LIMS系统在教育技术研发的应用
在教育技术研发领域,实验室作为产品验证的核心环节,其质检效率与数据安全性直接关乎企业的创新竞争力。LIMS(实验室信息管理系统)作为贯穿检测全流程的数字化中枢,正在成为教育科技企业的"质量守护者"。本文…...
MySQL最左前缀原则深度解析:优化索引设计的核心法则
一、什么是最左前缀原则? 最左前缀原则(Leftmost Prefix Principle) 指在使用复合索引(Composite Index)时,MySQL会按照索引定义的列顺序,从左到右匹配查询条件。只有连续且从最左侧开始的列组…...
多模态大语言模型arxiv论文略读(三十五)
On the Out-Of-Distribution Generalization of Multimodal Large Language Models ➡️ 论文标题:On the Out-Of-Distribution Generalization of Multimodal Large Language Models ➡️ 论文作者:Xingxuan Zhang, Jiansheng Li, Wenjing Chu, Junjia…...
Linux 安装pm2并全局可用
前言 本文基于:操作系统 CentOS Stream 8 使用工具:Xshell8、Xftp8 服务器基础环境: node - 请查看 Linux安装node并全局可用 所需服务器基础环境,请根据提示进行下载、安装。 1.安装依赖 npm install pm2 -g2.配置全局软链…...
39.剖析无处不在的数据结构
数据结构是计算机中组织和存储数据的特定方式,它的目的是方便且高效地对数据进行访问和修改。数据结构表述了数据之间的关系,以及操作数据的一系列方法。数据又是程序的基本单元,因此无论是哪种语言、哪种领域,都离不开数据结构&a…...
基于 Vue 的Tiptap 富文本编辑器使用指南
目录 🧰 技术栈 📦 所需依赖 📁 文件结构 🧱 编辑器组件实现(components/Editor.vue) ✨ 常用操作指令 🧠 小贴士 🧩 Tiptap 扩展功能使用说明(含快捷键与命令&am…...
【音视频】AAC-ADTS分析
AAC-ADTS 格式分析 AAC⾳频格式:Advanced Audio Coding(⾼级⾳频解码),是⼀种由MPEG-4标准定义的有损⾳频压缩格式,由Fraunhofer发展,Dolby, Sony和AT&T是主 要的贡献者。 ADIF:Audio Data Interchange Format ⾳…...
vue中将elementUI和echarts转成pdf文件
若要将包含 ElementUI 组件数据和多个 ECharts 图表的数据转换为 PDF 文档,可结合 html2canvas、jspdf 以及 dom-to-image 来实现。其中,html2canvas 和 dom-to-image 可将 ECharts 图表转换为图片,jspdf 则用于生成 PDF 文档。对于 ElementU…...
基于 Electron、Vue3 和 TypeScript 的辅助创作工具全链路开发方案:涵盖画布系统到数据持久化的完整实现
基于 Electron、Vue3 和 TypeScript 的辅助创作工具全链路开发方案:涵盖画布系统到数据持久化的完整实现 引言 在数字内容创作领域,高效的辅助工具是连接创意与实现的关键桥梁。创作者需要一款集可视化画布、节点关系管理、数据持久化于一体的专业工具&…...
本地部署DeepSeek-R1模型接入PyCharm
以下是DeepSeek-R1本地部署及接入PyCharm的详细步骤指南,整合了视频内容及官方文档核心要点: 一、本地部署DeepSeek-R1模型 1. 安装Ollama框架 下载安装包 访问Ollama官网(https://ollama.com/download)或通过视频提供的百度云盘链接下载对应系统的安装包。Windows用户…...
基于LightGBM-TPE算法对交通事故严重程度的分析与可视化
基于LightGBM-TPE算法对交通事故严重程度的分析与可视化 原文: Analysis and visualization of accidents severity based on LightGBM-TPE 1. 引言部分 文章开篇强调了道路交通事故作为意外死亡的主要原因,引起了多学科领域的关注。分析事故严重性特…...
音视频小白系统入门课-3
本系列笔记为博主学习李超老师课程的课堂笔记,仅供参阅 往期课程笔记传送门: 音视频小白系统入门笔记-0音视频小白系统入门笔记-1音视频小白系统入门笔记-2 视频: 由一组图像组成:像素、分辨率、RGB 8888(24位) 、RGBA(32位)为…...
考研系列-计算机网络-第五章、传输层
一、传输层提供的服务 1.重点知识...
将Ubuntu系统中已有的Python环境迁移到Anaconda的虚拟环境中
需求:关于如何将Ubuntu系统中已有的Python环境迁移到Anaconda的虚拟环境test2里,而且他们提到用requirements.txt 安装一直报错,所以想尝试直接拷贝的方法。 可以尝试通过直接拷贝移植的方式迁移Python环境到Anaconda虚拟环境,但…...
AI 数字短视频数字人源码开发:多维赋能短视频生态革新
在短视频行业深度发展的进程中,AI 数字短视频数字人源码开发凭借其独特的技术优势,从多个维度为行业生态带来了革命性的变化,重塑短视频创作、传播与应用的格局。 数据驱动,实现内容精准化创作 AI 数字短视频数字人源码开发能够深…...
ffmpeg 硬解码相关知识
一:FFMPEG 支持的硬解方式:如下都是了解知识 DXVA2 - windows DXVA2 硬件加速技术解析 一、核心特性与适用场景 技术定义:DXVA2(DirectX Video Acceleration 2)是微软推出的基于 DirectX 的硬件加速标准…...
Ubuntu数据连接访问崩溃问题
目录 一、分析问题 1、崩溃问题本地调试gdb调试: 二、解决问题 1. 停止 MySQL 服务 2. 卸载 MySQL 相关包 3. 删除 MySQL 数据目录 4. 清理依赖和缓存 5.重新安装mysql数据库 6.创建程序需要的数据库 三、验证 1、动态库更新了 2、头文件更新了 3、重新…...
边缘计算全透视:架构、应用与未来图景
边缘计算全透视:架构、应用与未来图景 一、产生背景二、本质三、特点(一)位置靠近数据源(二)分布式架构(三)实时性要求高 四、关键技术(一)硬件技术(二&#…...
迅为iTOP-RK3576开发板/核心板6TOPS超强算力NPU适用于ARM PC、边缘计算、个人移动互联网设备及其他多媒体产品
迅为iTOP-3576开发板采用瑞芯微RK3576高性能、低功耗的应用处理芯片,集成了4个Cortex-A72和4个Cortex-A53核心,以及独立的NEON协处理器。它适用于ARM PC、边缘计算、个人移动互联网设备及其他多媒体产品。 支持INT4/INT8/INT16/FP16/BF16/TF32混合运算&a…...
前沿分享|技术雷达202504月刊精华
本期雷达 ###技术部分 7. GraphRAG 试验 在上次关于 检索增强生成(RAG)的更新中,我们已经介绍了GraphRAG。它最初在微软的文章中被描述为一个两步的流程: (1)对文档进行分块,并使用基于大语言…...
[创业之路-380]:企业法务 - 企业经营中,企业为什么会虚开増值税发票?哪些是虚开増值税发票的行为?示例?风险?
一、动机与风险 1、企业虚开增值税发票的动机 利益驱动 骗抵税款:通过虚开发票虚增进项税额,减少应纳税额,降低税负。公司套取国家的利益。非法套现:虚构交易开具发票,将资金从公司账户转移至个人账户,用…...
嵌入式:ARM公司发展史与核心技术演进
一、发展历程:从Acorn到全球算力基石 1. 起源(1978-1990) 1978年:奥地利物理学家Hermann Hauser与工程师Chris Curry创立剑桥处理器公司(CPU Ltd.),后更名为**艾康电脑(Acor…...
ubuntu的各种工具配置
1.nfs:虚拟机桥接模式下,开发板和虚拟机保持在同一网段下,开发板不要直连电脑 挂载命令:mount -v -t nfs 192.168.110.154:/home/lhj /mnt -o nolock (1) 安装 NFS 服务器 sudo apt update sudo apt install nfs-kernel-server -y…...
Go 剥离 HTML 标签的三把「瑞士军刀」——从正则到 Bluemonday
1 为什么要「剥皮」? 安全:去掉潜在的 <script onload…> 等恶意标签,防止存储型 XSS。可读性:日志、消息队列、搜索索引里往往只需要纯文本。一致性:不同富文本编辑器生成的 HTML 五花八门,统一成「…...
【Java面试笔记:基础】6.动态代理是基于什么原理?
1. 反射机制 定义:反射是 Java 语言提供的一种基础功能,允许程序在运行时自省(introspect),直接操作类或对象。功能: 获取类定义、属性和方法。调用方法或构造对象。运行时修改类定义。 应用场景ÿ…...
docker容器中uv的使用
文章目录 TL;DRuv简介uv管理项目依赖step 1step 2WindowsLinux/Mac step 3依赖包恢复 在Docker容器中使用uv TL;DR 本文记录uv在docker容器中使用注意点, uv简介 uv是用rust编写的一个python包管理器,特点是速度快,且功能强大,目标是替代p…...
分部积分选取u、v的核心是什么?
分部积分选取u、v的核心是什么?是反对幂指三吗? 不全是,其实核心是:v要比u更容易积分,也就是更容易求得原函数,来看一道例题:...
Android Studio调试中的坑二
下载新的Android studio Meerkat后,打开发现始终无法更新对应的SDK,连Android 15的SDK也无法在SDK Manger中显示出来,但是Meerkat必须要使用新版本SDK。 Android studio下载地址 命令行工具 | Android Studio | Android Developers 解决…...
【Redis】缓存三剑客问题实践(上)
本篇对缓存三剑客问题进行介绍和解决方案说明,下篇将进行实践,有需要的同学可以跳转下篇查看实践篇:(待发布) 缓存三剑客是什么? 缓存三剑客指的是在分布式系统下使用缓存技术最常见的三类典型问题。它们分…...
2025年4月22日(平滑)
在学术和工程语境中,表达“平滑”需根据具体含义选择术语。以下是专业场景下的精准翻译及用法解析: 1. 数学/信号处理中的「平滑」(消除噪声) Smooth (verb/noun/adjective) “Apply a Gaussian filter to smooth the noisy signa…...
给vue-admin-template菜单栏 sidebar-item 添加消息提示
<el-badge :value"200" :max"99" class"item"><el-button size"small">评论</el-button> </el-badge> <!-- 在 SidebarItem.vue 中 --> <template><div v-if"!item.hidden" class&q…...
C++(初阶)(十二)——stack和queue
十二,stack和queue 十二,stack和queueStackQueuepriority_queue 简单使用模拟实现deque Stack 函数说明stack()构造空栈empty()判断栈是否为空size()返回栈的有效元素个数top()返会栈顶元素的引用push()将所给元素val压入栈中pop()将栈的尾部元素弹出 …...
数据采集:AI 发展的基石与驱动力
人工智能(AI)无疑是最具变革性的技术力量之一,正以惊人的速度重塑着各行各业的格局。从智能语音助手到自动驾驶汽车,从精准的医疗诊断到个性化的推荐系统,AI 的广泛应用已深刻融入人们的日常生活与工作的各个层面。而在…...
Kubernetes Docker 部署达梦8数据库
Kubernetes & Docker 部署达梦8数据库 一、达梦镜像获取 目前达梦官方暂未在公共镜像仓库提供Docker镜像,需通过达梦官网联系获取官方镜像包。 二、Kubernetes部署方案 部署配置文件示例 apiVersion: apps/v1 kind: Deployment metadata:labels:app: dm8na…...
宏碁笔记本电脑怎样开启/关闭触摸板
使用快捷键:大多数宏碁笔记本可以使用 “FnF7” 或 “FnF8” 组合键来开启或关闭触摸板,部分型号可能是 “FnF2”“FnF9” 等。如果不确定,可以查看键盘上的功能键图标,一般有触摸板图案的按键就是触摸板的快捷键。通过设备管理器…...
计算机组成与体系结构:缓存(Cache)
目录 为什么需要 Cache? 🧱 Cache 的分层设计 🔹 Level 1 Cache(L1 Cache)一级缓存 🔹 Level 2 Cache(L2 Cache)二级缓存 🔹 Level 3 Cache(L3 Cache&am…...
【VS Code】打开远程服务器Docker项目或文件夹
1、配置SSH连接 在VS Code中,按CtrlShiftP打开命令面板。 输入并选择Remote-SSH: Connect to Host...。 输入远程服务器的SSH地址(例如userhostname或userip_address)。 如果这是您第一次连接到该主机,VS Code可能会要求您配置…...
docker 常见命令
指定服务名查看日志 docker-compose logs -f doc-cleaning docker inspect id 启动所有服务 在docker-compose目录下 docker-compose up -d docker-compose down会删除容器和网络 docker compose stop redis rabbitmq docker compose stop可以快速停止服务,方…...
C#抽象类和虚方法的作用是什么?
抽象类 (abstract class): 不能直接实例化,只能被继承。 用来定义一套基础框架和规范,强制子类必须实现某些方法(抽象方法)。 可用来封装一些共通的逻辑,减少代码重复。 虚方法 (virtual): …...
redis数据类型-基数统计HyperLogLog
redis数据类型-基数统计HyperLogLog 文档 redis单机安装redis常用的五种数据类型redis数据类型-位图bitmap 说明 官网操作命令指南页面:https://redis.io/docs/latest/commands/?nameget&groupstringHyperLogLog介绍页面:https://redis.io/docs…...
音视频学习 - MP3格式
环境 JDK 13 IDEA Build #IC-243.26053.27, built on March 16, 2025 Demo MP3Parser MP3 MP3全称为MPEG Audio Layer 3,它是一种高效的计算机音频编码方案,它以较大的压缩比将音频文件转换成较小的扩展名为.mp3的文件,基本保持源文件的音…...
Oracle--PL/SQL编程
前言:本博客仅作记录学习使用,部分图片出自网络,如有侵犯您的权益,请联系删除 PL/SQL(Procedural Language/SQL)是Oracle数据库中的一种过程化编程语言,构建于SQL之上,允许编写包含S…...
【愚公系列】《Python网络爬虫从入门到精通》063-项目实战电商数据侦探(主窗体的数据展示)
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! …...
DAPP(去中心化应用程序)开发全解析:构建去中心化应用的流程
去中心化应用(DApp)凭借其透明性、抗审查性和用户数据主权,正重塑金融、游戏、社交等领域。本文基于2025年最新开发实践,系统梳理DApp从需求规划到部署运维的全流程,并融入经济模型设计、安全加固等核心要点࿰…...
Spark与Hadoop之间有什么样的对比和联系
一、什么是Spark Spark 是一个快速、通用且可扩展的大数据处理框架,最初由加州大学伯克利分校的AMPLab于2009年开发,并于2010年开源。它在2013年成为Apache软件基金会的顶级项目,是大数据领域的重要工具之一。 Spark 的优势在于其速度和灵活…...
spark和Hadoop之间的对比和联系
Spark 诞生主要是为了解决 Hadoop MapReduce 在迭代计算以及交互式数据处理时面临的性能瓶颈问题。 一,spark的框架 Hadoop MR 框架 从数据源获取数据,经过分析计算后,将结果输出到指定位置,核心是一次计算,不适合迭…...
LeetCode 第 262 题全解析:从 SQL 到 Swift 的数据分析实战
文章目录 摘要描述题解答案(SQL)Swift 题解代码分析代码示例(可运行 Demo)示例测试及结果时间复杂度分析空间复杂度分析总结未来展望 摘要 在实际业务中,打车平台要监控行程的取消率,及时识别服务质量的问…...
“融合Python与机器学习的多光谱遥感技术:数据处理、智能分类及跨领域应用”
随着遥感技术的快速发展,多光谱数据凭借其多波段信息获取能力,成为地质、农业及环境监测等领域的重要工具。相较于高光谱数据,Landsat、哨兵-2号等免费中分辨率卫星数据具有长时间序列、广覆盖的优势,而无人机平台的兴起进一步补充…...