Linux 网络编程:select、poll 与 epoll 深度解析 —— 从基础到高并发实战
一、IO 多路复用:解决并发 IO 的核心技术
在网络编程中,当需要同时处理大量客户端连接时,传统阻塞式 IO 会导致程序卡在单个操作上,造成资源浪费。IO 多路复用技术允许单线程监听多个文件描述符(FD),当任意 FD 就绪(可读 / 可写 / 异常)时,程序能立即响应,是高效处理并发的关键。
Linux 提供了三种主流实现:select
、poll
和 epoll
,其中 epoll
是高并发场景的首选方案。
二、select:经典多路复用接口(适用于小规模并发)
1. 核心原理与数据结构
(1)核心设计思想
select 是 Linux 早期实现的 IO 多路复用接口,通过 位掩码集合 监听多个文件描述符(FD)的可读、可写或异常事件。其核心是将用户空间的 FD 集合复制到内核空间,由内核检测哪些 FD 就绪,最后将就绪状态返回给用户空间。
(2)数据结构:fd_set
位掩码
- 本质:一个固定大小的位掩码(数组),每一位对应一个 FD。
- 默认限制:受限于系统宏
FD_SETSIZE
(通常为 1024),即最多监听 1024 个 FD(FD 范围:0~1023)。 - 操作函数:
#include <sys/select.h> void FD_ZERO(fd_set *set); // 清空集合(所有位设为 0) void FD_SET(int fd, fd_set *set); // 将 FD 添加到集合(对应位设为 1) void FD_CLR(int fd, fd_set *set); // 将 FD 从集合移除(对应位设为 0) int FD_ISSET(int fd, fd_set *set); // 检查 FD 是否在集合中(对应位是否为 1)
(3)核心函数:select
#include <sys/select.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数 | 解释 |
---|---|
maxfd | 监听的最大 FD 值 + 1(例如 FD 为 3、5,则 maxfd=6 ),确保覆盖所有监听的 FD。 |
readfds | 可读事件集合(监听哪些 FD 有数据可读)。 |
writefds | 可写事件集合(监听哪些 FD 可无阻塞写入,较少使用)。 |
exceptfds | 异常事件集合(如带外数据,通常设为 NULL )。 |
timeout | 超时时间: - NULL :永久阻塞,直到任意 FD 就绪- {0, 0} :立即返回- {tv_sec, tv_usec} :指定超时时间(秒 + 微秒) |
返回值 | 就绪 FD 数量;0 表示超时;-1 表示错误(如被信号中断,errno 查看具体原因)。 |
(4)触发模式:水平触发(LT, Level Triggered)
- 核心逻辑:只要 FD 的事件条件满足(如数据可读),就会持续触发事件,直到数据被处理。
- 示例场景:客户端发送 10KB 数据,
select
会多次触发EPOLLIN
事件,直到数据被完全读取。
2. 使用步骤(监听多个客户端:从初始化到事件处理)
步骤 1:初始化事件集合
fd_set read_fds;
FD_ZERO(&read_fds); // 清空集合(必须第一步,避免脏数据)
FD_SET(server_fd, &read_fds); // 添加服务器监听 FD(如 socket 描述符)
- 关键点:服务器启动时,先将监听套接字(
server_fd
)加入readfds
,用于检测新客户端连接。
步骤 2:计算 maxfd
int maxfd = server_fd; // 初始时只有服务器 FD
// 若有客户端 FD(如 client_fd=5),则更新为 maxfd = client_fd
- 为什么 + 1?:
select
函数需要检测从 0 到maxfd
的所有 FD,因此传入参数为maxfd + 1
。
步骤 3:等待事件就绪(阻塞或超时)
struct timeval timeout = {2, 0}; // 2 秒超时(2 秒内无事件则返回)
int ready_count = select(maxfd + 1, &read_fds, NULL, NULL, &timeout);
- 三种状态:
ready_count > 0
:有ready_count
个 FD 就绪。ready_count == 0
:超时,无事件发生(可用于定时轮询任务)。ready_count == -1
:错误(如EINTR
表示被信号中断,需重新调用)。
步骤 4:遍历检查就绪 FD(线性扫描)
for (int fd = 0; fd <= maxfd; fd++) { if (FD_ISSET(fd, &read_fds)) { // 检查 FD 是否在就绪集合中 if (fd == server_fd) { // 处理新客户端连接(accept) int client_fd = accept(server_fd, ...); FD_SET(client_fd, &read_fds); // 将新客户端 FD 添加到下次监听集合 maxfd = (client_fd > maxfd) ? client_fd : maxfd; // 更新 maxfd } else { // 处理客户端数据(recv) char buf[1024]; ssize_t recv_len = recv(fd, buf, sizeof(buf), 0); if (recv_len == 0) { // 客户端关闭连接,移除 FD FD_CLR(fd, &read_fds); close(fd); } } }
}
- 核心缺陷:无论是否就绪,都需从
0
到maxfd
逐个检查(时间复杂度 O (n)),FD 越多性能越差。
3. 完整示例:select 实现简易 TCP 服务器
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h> #define PORT 8080
#define MAX_FD 1024 // 受限于 FD_SETSIZE int main() { // 1. 创建服务器套接字 int server_fd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(PORT), .sin_addr.s_addr = INADDR_ANY}; bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)); listen(server_fd, 5); fd_set read_fds; int maxfd = server_fd; while (1) { FD_ZERO(&read_fds); FD_SET(server_fd, &read_fds); // 2. 添加所有客户端 FD 到集合(假设客户端 FD 存储在数组 clients[] 中) for (int i = 0; i < MAX_FD; i++) { int client_fd = clients[i]; if (client_fd > 0) FD_SET(client_fd, &read_fds); } // 3. 等待事件(永久阻塞) int ready = select(maxfd + 1, &read_fds, NULL, NULL, NULL); if (ready < 0) { perror("select"); continue; } // 4. 处理就绪 FD for (int fd = 0; fd <= maxfd; fd++) { if (FD_ISSET(fd, &read_fds)) { if (fd == server_fd) { // 处理新连接 int client_fd = accept(server_fd, NULL, NULL); clients[client_idx++] = client_fd; // 假设 clients 是全局数组 maxfd = (client_fd > maxfd) ? client_fd : maxfd; } else { // 处理数据接收 char buf[1024]; if (recv(fd, buf, sizeof(buf), 0) <= 0) { close(fd); FD_CLR(fd, &read_fds); // 从集合中移除失效 FD } else { send(fd, buf, strlen(buf), 0); // 简单回显 } } } } } close(server_fd); return 0;
}
4. 优缺点对比与适用场景
(1)优点:入门友好,跨平台
- 跨平台支持:Windows(
select
)和 Linux 均支持,适合需要跨平台的轻量级程序(如简单代理工具)。 - 接口简单:仅需操作
fd_set
集合,适合新手快速入门多路复用概念。 - 小规模场景适用:当 FD 数量较少(如 < 100)时,开发成本低,无需复杂配置。
(2)缺点:性能瓶颈明显
- FD 数量限制:受
FD_SETSIZE
限制(默认 1024),无法处理大规模并发(如万级连接)。 - 内核拷贝开销:每次调用
select
都需将整个fd_set
从用户空间拷贝到内核空间,FD 越多开销越大。 - 线性扫描效率低:通过
FD_ISSET
逐个检查 FD,时间复杂度为 O (n),高并发时 CPU 占用率飙升。 - 集合重置麻烦:内核会修改
fd_set
集合(移除未就绪的 FD),每次调用前需重新调用FD_ZERO/FD_SET
重置。
(3)适用场景
- 小规模并发:如聊天工具(客户端数量 < 100)、简单日志服务器。
- 跨平台开发:需要同时支持 Windows 和 Linux 时,
select
是唯一选择。 - 学习阶段:作为理解 IO 多路复用的入门接口,帮助掌握事件驱动基本思想。
5. 错误处理与最佳实践
(1)常见错误码处理
EINTR
:select
被信号中断(如SIGINT
),可忽略并重新调用。EBADF
:集合中包含无效 FD(如已关闭的 FD),需在FD_ISSET
前检查 FD 有效性。
(2)优化技巧
- 预分配 FD 数组:用数组存储所有监听的 FD,避免遍历时检查无效 FD(如 FD=0 可能是标准输入)。
- 限制超时时间:避免永久阻塞(
timeout=NULL
),可设置短超时(如 1 秒),期间穿插其他任务(如定时心跳)。
(3)新手常见问题
- Q:为什么每次调用 select 前要重置 fd_set?
A:内核会修改fd_set
集合,移除未就绪的 FD,因此下次调用前需重新添加所有监听的 FD。 - Q:如何监听可写事件?
A:将目标 FD 添加到writefds
集合,检测是否可无阻塞写入(如发送缓冲区未满)。
6. 总结:select 的 “利” 与 “弊”
select 作为经典多路复用接口,是理解 IO 并发的重要起点,但其设计缺陷使其在高并发场景中逐渐被淘汰。对于新手,掌握 select 的核心在于理解位掩码集合的操作和水平触发机制,为后续学习 poll 和 epoll 打下基础。在下一节中,我们将对比 poll 接口,了解其如何改进 select 的 FD 数量限制问题。
三、poll:改进的多路复用接口(适用于中规模并发)
1. 核心原理与数据结构
1.1 核心原理
poll
是 Linux 系统中用于实现 I/O 多路复用的系统调用,它改进了 select
存在的一些问题。poll
的核心原理是通过一个 struct pollfd
数组来管理多个文件描述符(FD),并允许内核监听这些文件描述符上的特定事件。当其中任何一个文件描述符上的指定事件发生时,poll
函数会返回,通知程序哪些文件描述符已经就绪。
1.2 数据结构:struct pollfd
poll
使用 struct pollfd
数组来动态管理文件描述符,这个数组没有固定的大小限制,仅受系统资源的约束。以下是 struct pollfd
的定义:
struct pollfd {int fd; // 文件描述符(-1 表示忽略)short events; // 监听事件(如 POLLIN 可读)short revents; // 就绪事件(内核填充)
};
fd
:要监听的文件描述符。如果设置为 -1,则表示忽略该条目,poll
函数不会对其进行检查。events
:指定要监听的事件类型。可以使用按位或(|
)运算符组合多个事件。常见的事件类型如下:POLLIN
:文件描述符有普通数据可读。例如,对于一个套接字,当有新的数据到达接收缓冲区时,就会触发POLLIN
事件。POLLOUT
:文件描述符可写,即发送缓冲区有空间可以写入数据。比如在网络编程中,当套接字的发送缓冲区有空闲空间时,就会触发POLLOUT
事件。POLLERR
:文件描述符发生错误。这可能是由于网络连接中断、文件损坏等原因导致的。POLLHUP
:文件描述符被挂起。例如,在 TCP 连接中,当对方关闭连接时,就会触发POLLHUP
事件。POLLNVAL
:文件描述符无效。可能是因为文件描述符没有被正确打开或者已经被关闭。POLLPRI
:文件描述符有紧急数据可读。在网络编程中,紧急数据通常用于带外数据传输,例如 TCP 的紧急指针机制。当有紧急数据到达时,会触发POLLPRI
事件。
revents
:由内核填充的实际发生的事件。程序在poll
函数返回后,可以检查这个字段来确定哪些事件已经发生。
1.3 核心函数:poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 参数解释:
fds
:指向struct pollfd
数组的指针,该数组包含了要监听的文件描述符及其相关事件。nfds
:数组中有效元素的数量,即要监听的文件描述符的数量。与select
不同,poll
不需要计算最大的文件描述符加 1。timeout
:超时时间,以毫秒为单位。其取值有以下几种情况:-1
:表示永久阻塞,直到有文件描述符上的事件发生。0
:表示立即返回,无论是否有事件发生。- 大于 0 的值:表示等待指定的毫秒数,如果在这段时间内没有事件发生,则
poll
函数返回 0。
- 返回值:
- 大于 0:表示有
n
个文件描述符上的事件发生。 - 0:表示超时,在指定的时间内没有文件描述符上的事件发生。
- -1:表示发生错误,错误信息存储在
errno
中。
- 大于 0:表示有
2. 使用步骤(监听多个客户端)
2.1 初始化 pollfd
数组
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define MAX_FDS 1024int main() {// 创建服务器套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");return 1;}// 绑定地址和端口struct sockaddr_in server_addr = {0};server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(8080);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(server_fd);return 1;}// 监听连接if (listen(server_fd, 5) == -1) {perror("listen");close(server_fd);return 1;}// 初始化 pollfd 数组struct pollfd fds[MAX_FDS];fds[0].fd = server_fd;fds[0].events = POLLIN;int nfds = 1;// 后续代码...
}
- 解释:
- 首先创建了一个服务器套接字,并将其绑定到指定的地址和端口,然后开始监听连接。
- 接着定义了一个
struct pollfd
数组fds
,大小为MAX_FDS
。 - 将服务器套接字的文件描述符赋值给
fds[0].fd
,并设置要监听的事件为POLLIN
(即有新的连接请求可读)。 nfds
表示当前fds
数组中有效元素的数量,初始值为 1,因为只有服务器套接字被添加到了数组中。
2.2 等待事件就绪
// ... 前面的代码 ...while (1) {int ready = poll(fds, nfds, 2000); // 2000 毫秒超时if (ready == -1) {perror("poll");break;} else if (ready == 0) {printf("Timeout, no events occurred.\n");continue;}// 后续代码...}// ... 后面的代码 ...
- 解释:
- 使用
poll
函数等待事件发生,设置超时时间为 2000 毫秒。 - 如果
poll
函数返回 -1,表示发生错误,使用perror
输出错误信息并跳出循环。 - 如果返回 0,表示超时,在 2000 毫秒内没有文件描述符上的事件发生,打印提示信息并继续下一次循环。
- 如果返回值大于 0,表示有文件描述符上的事件发生,继续后续的处理。
- 使用
2.3 遍历检查就绪事件
// ... 前面的代码 ...for (int i = 0; i < nfds; i++) {if (fds[i].fd == -1 || !(fds[i].revents & POLLIN)) continue;if (fds[i].fd == server_fd) {// 处理新的连接请求struct sockaddr_in client_addr = {0};socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}// 将新的客户端套接字添加到 pollfd 数组中if (nfds < MAX_FDS) {fds[nfds].fd = client_fd;fds[nfds].events = POLLIN | POLLPRI;nfds++;} else {printf("Too many clients, rejecting new connection.\n");close(client_fd);}} else {// 处理客户端数据char buffer[1024];ssize_t bytes_read;if (fds[i].revents & POLLPRI) {// 处理紧急数据bytes_read = recv(fds[i].fd, buffer, sizeof(buffer), MSG_OOB);if (bytes_read == -1) {perror("recv urgent data");} else {buffer[bytes_read] = '\0';printf("Received urgent data from client: %s\n", buffer);}}if (fds[i].revents & POLLIN) {bytes_read = recv(fds[i].fd, buffer, sizeof(buffer), 0);if (bytes_read == -1) {perror("recv");close(fds[i].fd);fds[i].fd = -1; // 标记为忽略} else if (bytes_read == 0) {// 客户端关闭连接close(fds[i].fd);fds[i].fd = -1; // 标记为忽略} else {// 处理接收到的数据buffer[bytes_read] = '\0';printf("Received from client: %s\n", buffer);// 回显数据给客户端send(fds[i].fd, buffer, bytes_read, 0);}}}}// ... 后面的代码 ...
- 解释:
- 遍历
fds
数组,对于每个元素,首先检查fds[i].fd
是否为 -1,如果是,则表示该条目被忽略,跳过本次循环。 - 然后检查
fds[i].revents
是否包含POLLIN
事件,如果不包含,则表示该文件描述符上没有可读事件发生,跳过本次循环。 - 如果
fds[i].fd
等于服务器套接字的文件描述符,表示有新的连接请求,使用accept
函数接受连接,并将新的客户端套接字添加到fds
数组中,同时更新nfds
的值。这里监听客户端套接字的POLLIN
和POLLPRI
事件。 - 如果
fds[i].fd
不等于服务器套接字的文件描述符,表示有客户端数据可读或有紧急数据到达。- 当
fds[i].revents
包含POLLPRI
事件时,使用recv
函数并设置MSG_OOB
标志来接收紧急数据。 - 当
fds[i].revents
包含POLLIN
事件时,使用recv
函数接收普通数据。- 如果
recv
函数返回 -1,表示发生错误,关闭该客户端套接字,并将fds[i].fd
设置为 -1 以标记为忽略。 - 如果
recv
函数返回 0,表示客户端关闭了连接,同样关闭该客户端套接字,并将fds[i].fd
设置为 -1。 - 如果
recv
函数返回值大于 0,表示成功接收到数据,将数据打印出来,并使用send
函数将数据回显给客户端。
- 如果
- 当
- 遍历
3. 优缺点
3.1 优点
- 无 FD 数量硬编码限制:与
select
不同,poll
没有FD_SETSIZE
这样的硬编码限制,仅受系统资源的约束。这意味着可以处理更多的文件描述符,适用于中规模并发的场景。 - 事件类型更清晰:
poll
使用POLLIN
、POLLOUT
、POLLERR
、POLLHUP
、POLLNVAL
、POLLPRI
等明确的事件类型,比select
使用的fd_set
位掩码更易于理解和使用。开发者可以更方便地指定要监听的事件类型,并且在处理事件时也更加直观。
3.2 缺点
- 仍需线性扫描就绪 FD:
poll
函数返回后,程序需要遍历struct pollfd
数组来检查哪些文件描述符上的事件已经发生。这个过程的时间复杂度为 O (n),其中 n 是要监听的文件描述符的数量。当文件描述符数量较多时,线性扫描会消耗较多的 CPU 时间,影响性能。 - 每次调用需复制
pollfd
数组到内核:与select
类似,poll
函数在每次调用时都需要将struct pollfd
数组从用户空间复制到内核空间。虽然poll
在性能上优于select
,但这种复制操作仍然会带来一定的开销,尤其是在处理大量文件描述符时。
4. 拓展:错误处理和性能优化建议
4.1 错误处理
在使用 poll
函数时,需要对可能出现的错误进行处理。常见的错误情况包括:
EBADF
:fds
数组中包含无效的文件描述符。在使用poll
之前,应该确保所有的文件描述符都是有效的。EFAULT
:fds
数组的指针无效,可能是因为指针指向了无效的内存地址。EINTR
:poll
函数被信号中断。在这种情况下,可以重新调用poll
函数继续等待事件。
以下是一个简单的错误处理示例:
int ready = poll(fds, nfds, 2000);if (ready == -1) {switch (errno) {case EBADF:printf("Invalid file descriptor in fds array.\n");break;case EFAULT:printf("Invalid pointer to fds array.\n");break;case EINTR:printf("Poll was interrupted by a signal, retrying...\n");continue;default:perror("poll");break;}}
4.2 性能优化建议
- 合理设置超时时间:根据具体的应用场景,合理设置
poll
函数的超时时间。如果设置的超时时间过长,可能会导致程序在没有事件发生时长时间阻塞;如果设置的超时时间过短,可能会导致poll
函数频繁返回,增加系统开销。 - 动态管理
pollfd
数组:在实际应用中,可能会有新的文件描述符需要添加到poll
监听列表中,或者有一些文件描述符不再需要监听。可以动态地管理struct pollfd
数组,避免不必要的文件描述符被监听,从而减少线性扫描的时间。 - 结合多线程或多进程:对于高并发的场景,可以结合多线程或多进程来处理
poll
函数返回的就绪事件。每个线程或进程可以负责处理一部分文件描述符,从而提高程序的并发处理能力。
通过以上的学习,你应该对 poll
函数有了更深入的理解,并且能够使用它来实现一个简单的多客户端服务器。在实际应用中,可以根据具体的需求和场景,选择合适的 I/O 多路复用机制。
四、epoll:Linux 高并发终极方案(适用于万级以上连接)
1. 核心原理与数据结构
1.1 核心原理
epoll
是 Linux 内核为处理大批量文件描述符而作了改进的 I/O
多路复用技术。其核心原理是使用一个事件表来记录所有关注的文件描述符及其事件,当有事件发生时,内核会将这些就绪的事件通知给用户空间。epoll
通过红黑树和链表这两种数据结构来高效地管理文件描述符和就绪事件,避免了像 select
和 poll
那样的线性扫描和大量的数据拷贝,从而在高并发场景下表现出卓越的性能。
1.2 数据结构
红黑树
红黑树是一种自平衡的二叉搜索树,epoll
使用红黑树来管理所有监听的文件描述符(FD)。红黑树的特点是插入、删除和查找操作的时间复杂度都是 O(logN),其中 N 是树中节点的数量。在 epoll
中,红黑树的每个节点代表一个被监听的文件描述符,通过红黑树可以快速地添加、删除和修改监听的文件描述符。
链表
链表用于存储就绪事件。当有文件描述符上的事件就绪时,内核会将这些事件添加到链表中。epoll_wait
函数直接从这个链表中获取就绪事件,而不需要像 select
和 poll
那样扫描所有的文件描述符,从而大大提高了效率。
1.3 核心函数
#include <sys/epoll.h>
int epoll_create(int size); // 创建 epoll 实例(size 为预估 FD 数)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
-
epoll_create
函数- 功能:创建一个
epoll
实例,并返回一个文件描述符(epfd
),用于后续的epoll
操作。 - 参数:
size
:该参数在 Linux 2.6.8 之后被忽略,但必须为正数。它最初用于告诉内核需要监听的文件描述符的大致数量。
- 返回值:成功时返回一个非负的文件描述符,失败时返回 -1,并设置
errno
。
- 功能:创建一个
-
epoll_ctl
函数- 功能:用于控制
epoll
实例中的文件描述符,包括添加、修改和删除监听的文件描述符及其事件。 - 参数:
epfd
:epoll
实例的文件描述符,由epoll_create
函数返回。op
:操作类型,有以下三种取值:EPOLL_CTL_ADD
:将指定的文件描述符添加到epoll
实例中,并监听指定的事件。EPOLL_CTL_MOD
:修改已经添加到epoll
实例中的文件描述符的监听事件。EPOLL_CTL_DEL
:从epoll
实例中删除指定的文件描述符。
fd
:要操作的文件描述符。event
:指向struct epoll_event
结构体的指针,用于指定要监听的事件类型和关联的数据。
- 返回值:成功时返回 0,失败时返回 -1,并设置
errno
。
- 功能:用于控制
-
epoll_wait
函数- 功能:等待
epoll
实例中监听的文件描述符上的事件发生。当有事件发生时,将就绪的事件信息复制到events
数组中。 - 参数:
epfd
:epoll
实例的文件描述符。events
:指向struct epoll_event
数组的指针,用于存储就绪的事件信息。maxevents
:events
数组的最大元素个数,即最多可以返回的就绪事件数量。timeout
:超时时间,以毫秒为单位。取值如下:-1
:表示永久阻塞,直到有事件发生。0
:表示立即返回,无论是否有事件发生。- 大于 0 的值:表示等待指定的毫秒数,如果在这段时间内没有事件发生,则返回 0。
- 返回值:返回就绪的事件数量,即
events
数组中有效的元素个数。如果发生错误,返回 -1,并设置errno
。
- 功能:等待
1.4 struct epoll_event
结构体
struct epoll_event {uint32_t events; // 事件类型(如 EPOLLIN/EPOLLET)epoll_data_t data; // 存储 FD 或自定义数据
};
-
events
:表示要监听的事件类型或已经发生的事件类型。常见的事件类型有:EPOLLIN
:文件描述符有数据可读。EPOLLOUT
:文件描述符可写。EPOLLERR
:文件描述符发生错误。EPOLLHUP
:文件描述符被挂起。EPOLLET
:设置为边缘触发模式(默认是水平触发模式)。
-
data
:epoll_data_t
是一个联合体,用于存储与文件描述符关联的数据。常见的用法是存储文件描述符本身,也可以存储自定义的指针或整数值。
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;
2. 触发模式(关键特性)
2.1 水平触发(LT,默认模式)
特点
水平触发(Level Triggered,LT)是 epoll
的默认触发模式。在这种模式下,只要文件描述符上的事件条件满足(例如,有数据可读),就会持续触发相应的事件。也就是说,如果一次没有将数据完全读取完,epoll
会再次触发 EPOLLIN
事件,直到数据被完全读取。这种模式适合处理低速 I/O
操作,因为它允许程序有足够的时间来处理数据。
代码示例
#include <stdio.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>#define BUFFER_SIZE 1024int main() {struct epoll_event event;int fd = STDIN_FILENO; // 标准输入文件描述符// 创建 epoll 实例int epfd = epoll_create(1);if (epfd == -1) {perror("epoll_create");return 1;}// 设置监听事件为水平触发,数据可读时触发event.events = EPOLLIN;event.data.fd = fd;// 将文件描述符添加到 epoll 实例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {perror("epoll_ctl");close(epfd);return 1;}struct epoll_event events[1];char buf[BUFFER_SIZE];while (1) {// 等待事件发生int ready = epoll_wait(epfd, events, 1, -1);if (ready == -1) {perror("epoll_wait");break;}if (events[0].events & EPOLLIN) {// 处理数据while (recv(fd, buf, sizeof(buf), 0) > 0) {// 这里可以添加具体的数据处理逻辑printf("Received data: %s\n", buf);memset(buf, 0, sizeof(buf));}}}close(epfd);return 0;
}
2.2 边缘触发(ET,高性能模式)
特点
边缘触发(Edge Triggered,ET)是一种高性能的触发模式。在这种模式下,只有当文件描述符上的事件状态发生变化(例如,有新的数据到达)时,才会触发一次事件。也就是说,一旦事件被触发,程序必须一次性将所有的数据读取完,否则后续即使还有数据,也不会再次触发事件。因此,在边缘触发模式下,文件描述符必须设置为非阻塞模式,以确保能够一次性读取完所有数据。
使用条件
使用边缘触发模式时,文件描述符必须设置为非阻塞模式。可以使用 fcntl
函数来设置文件描述符的属性。
#include <fcntl.h>// 设置文件描述符为非阻塞模式
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) {perror("fcntl");return 1;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl");return 1;
}
代码示例
#include <stdio.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>#define BUFFER_SIZE 1024int main() {struct epoll_event event;int fd = STDIN_FILENO; // 标准输入文件描述符// 设置文件描述符为非阻塞模式int flags = fcntl(fd, F_GETFL, 0);if (flags == -1) {perror("fcntl");return 1;}if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl");return 1;}// 创建 epoll 实例int epfd = epoll_create(1);if (epfd == -1) {perror("epoll_create");return 1;}// 设置监听事件为边缘触发,数据可读时触发event.events = EPOLLIN | EPOLLET;event.data.fd = fd;// 将文件描述符添加到 epoll 实例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event) == -1) {perror("epoll_ctl");close(epfd);return 1;}struct epoll_event events[1];char buf[BUFFER_SIZE];while (1) {// 等待事件发生int ready = epoll_wait(epfd, events, 1, -1);if (ready == -1) {perror("epoll_wait");break;}if (events[0].events & EPOLLIN) {// 处理数据while (1) {ssize_t len = recv(fd, buf, sizeof(buf), 0);if (len == -1 && errno != EAGAIN) {perror("recv");break;} else if (len == -1 && errno == EAGAIN) {// 无数据时退出break;} else if (len == 0) {// 对方关闭连接break;} else {// 处理接收到的数据printf("Received data: %s\n", buf);memset(buf, 0, sizeof(buf));}}}}close(epfd);return 0;
}
3. 使用步骤(高并发服务器)
3.1 创建 epoll 实例
#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>#define MAX_EVENTS 1024int main() {// 创建服务器套接字int server_fd = socket(AF_INET, SOCK_STREAM, 0);if (server_fd == -1) {perror("socket");return 1;}// 绑定地址和端口struct sockaddr_in server_addr = {0};server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(8080);if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {perror("bind");close(server_fd);return 1;}// 监听连接if (listen(server_fd, 5) == -1) {perror("listen");close(server_fd);return 1;}// 创建 epoll 实例int epfd = epoll_create(1024);if (epfd == -1) {perror("epoll_create");close(server_fd);return 1;}// 后续代码...
}
- 解释:首先创建一个服务器套接字,并将其绑定到指定的地址和端口,然后开始监听连接。接着使用
epoll_create
函数创建一个epoll
实例,返回的文件描述符epfd
用于后续的epoll
操作。
3.2 添加监听事件
struct epoll_event event;event.events = EPOLLIN;event.data.fd = server_fd;// 将服务器套接字添加到 epoll 实例中if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &event) == -1) {perror("epoll_ctl");close(epfd);close(server_fd);return 1;}// 后续代码...
- 解释:创建一个
struct epoll_event
结构体变量event
,设置要监听的事件为EPOLLIN
(即有新的连接请求可读),并将服务器套接字的文件描述符存储在event.data.fd
中。然后使用epoll_ctl
函数将服务器套接字添加到epoll
实例中进行监听。
3.3 等待就绪事件
struct epoll_event events[MAX_EVENTS];while (1) {// 等待事件发生,永久阻塞int ready = epoll_wait(epfd, events, MAX_EVENTS, -1);if (ready == -1) {perror("epoll_wait");break;}// 后续代码...}close(epfd);close(server_fd);return 0;
}
- 解释:定义一个
struct epoll_event
数组events
,用于存储就绪的事件信息。使用epoll_wait
函数等待事件发生,设置超时时间为 -1,表示永久阻塞,直到有事件发生。当有事件发生时,epoll_wait
函数返回就绪的事件数量。
3.4 处理就绪事件
for (int i = 0; i < ready; i++) {int fd = events[i].data.fd;if (fd == server_fd) {// 处理新连接struct sockaddr_in client_addr = {0};socklen_t client_addr_len = sizeof(client_addr);int client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len);if (client_fd == -1) {perror("accept");continue;}// 设置客户端套接字为非阻塞模式int flags = fcntl(client_fd, F_GETFL, 0);if (flags == -1) {perror("fcntl");close(client_fd);continue;}if (fcntl(client_fd, F_SETFL, flags | O_NONBLOCK) == -1) {perror("fcntl");close(client_fd);continue;}// 将客户端套接字添加到 epoll 实例中struct epoll_event client_event;client_event.events = EPOLLIN | EPOLLET; // 边缘触发模式client_event.data.fd = client_fd;if (epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &client_event) == -1) {perror("epoll_ctl");close(client_fd);}} else {// 处理客户端数据char buffer[1024];while (1) {ssize_t len = recv(fd, buffer, sizeof(buffer), 0);if (len == -1 && errno != EAGAIN) {perror("recv");close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);break;} else if (len == -1 && errno == EAGAIN) {// 无数据时退出break;} else if (len == 0) {// 客户端关闭连接close(fd);epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);break;} else {// 处理接收到的数据buffer[len] = '\0';printf("Received from client: %s\n", buffer);// 回显数据给客户端send(fd, buffer, len, 0);}}}}
- 解释:遍历
events
数组,对于每个就绪的事件,首先获取其关联的文件描述符fd
。如果fd
等于服务器套接字的文件描述符,表示有新的连接请求,使用accept
函数接受连接,并将新的客户端套接字设置为非阻塞模式,然后将其添加到epoll
实例中进行监听。如果fd
不等于服务器套接字的文件描述符,表示有客户端数据可读,使用recv
函数接收数据,并根据接收结果进行相应的处理。
4. 优缺点
4.1 优点
- 高性能:
epoll
使用红黑树和链表来管理文件描述符和就绪事件,就绪事件通过链表直接返回,时间复杂度接近 O(1)。相比select
和poll
的线性扫描,epoll
在处理大量文件描述符时具有明显的性能优势。 - 无 FD 限制:
epoll
仅受系统最大打开文件数的限制,默认情况下,系统最大打开文件数为 1024,但可以通过ulimit -n
命令进行调整。因此,epoll
可以处理万级以上的连接。 - 灵活触发模式:
epoll
支持水平触发(LT)和边缘触发(ET)两种模式,开发者可以根据不同的应用场景选择合适的触发模式。边缘触发模式适合处理高速网络I/O
,可以减少事件触发的次数,提高效率。
4.2 缺点
- 仅 Linux 支持:
epoll
是 Linux 特有的接口,在 Windows 等其他操作系统上没有对应的实现。如果需要开发跨平台的网络应用程序,需要使用其他的I/O
多路复用技术。 - ET 模式需手动处理非阻塞 IO:边缘触发模式要求文件描述符必须设置为非阻塞模式,并且需要手动处理非阻塞
I/O
操作,这增加了代码的复杂度。如果处理不当,可能会导致数据丢失或程序出现异常。
5. 拓展:错误处理和性能优化
5.1 错误处理
在使用 epoll
函数时,需要对可能出现的错误进行处理。常见的错误情况包括:
EFAULT
:events
数组指针无效,可能是因为指针指向了无效的内存地址。EINTR
:epoll_wait
函数被信号中断。在这种情况下,可以重新调用epoll_wait
函数继续等待事件。EBADF
:epfd
不是一个有效的epoll
实例文件描述符。EINVAL
:epfd
不是一个epoll
实例文件描述符,或者maxevents
小于等于 0。
以下是一个简单的错误处理示例:
int ready = epoll_wait(epfd, events, MAX_EVENTS, -1);if (ready == -1) {switch (errno) {case EFAULT:printf("Invalid events array pointer.\n");break;case EINTR:printf("epoll_wait was interrupted by a signal, retrying...\n");continue;case EBADF:printf("Invalid epoll instance file descriptor.\n");break;case EINVAL:printf("Invalid epoll instance or maxevents value.\n");break;default:perror("epoll_wait");break;}}
5.2 性能优化
- 合理设置
maxevents
:maxevents
参数表示events
数组的最大元素个数,即最多可以返回的就绪事件数量。应该根据实际情况合理设置这个参数,避免设置过大或过小。如果设置过小,可能需要多次调用epoll_wait
函数才能处理完所有的就绪事件;如果设置过大,会浪费内存空间。 - 批量处理事件:在处理就绪事件时,可以采用批量处理的方式,减少系统调用的次数。例如,可以将多个客户端的请求合并处理,提高处理效率。
- 使用线程池:对于高并发的场景,可以使用线程池来处理就绪事件。每个线程负责处理一部分客户端的请求,避免单个线程处理过多的请求导致性能下降。
通过以上的学习,你应该对 epoll
有了更深入的理解,并且能够使用它来实现一个高并发的服务器。在实际应用中,可以根据具体的需求和场景,充分发挥 epoll
的优势,提高网络应用程序的性能。
五、三者核心对比表
特性 | select | poll | epoll |
---|---|---|---|
数据结构 | fd_set (位掩码,固定大小) | pollfd 数组(动态) | 红黑树(管理 FD)+ 链表(就绪事件) |
FD 限制 | FD_SETSIZE(默认 1024) | 受限于系统 ulimit -n | 理论无限制(仅受内存影响) |
内核操作 | 每次复制 FD 集合到内核 | 每次复制 pollfd 数组到内核 | 仅首次添加 FD 到内核(增量更新) |
就绪通知 | 水平触发(LT),线性扫描所有 FD | 水平触发(LT),扫描就绪 FD | 支持 LT/ET,直接返回就绪列表 |
时间复杂度 | O(n) | O(n) | O (1)(仅处理就绪事件) |
内存拷贝 | 高(每次 select 全量拷贝) | 中(每次 poll 全量拷贝) | 低(仅 epoll_ctl 时增量拷贝) |
跨平台 | 支持(Windows/Linux) | 仅 Linux/UNIX 支持 | 仅 Linux 支持 |
典型场景 | 小规模并发(FD < 1024) | 中规模并发(FD 中等数量) | 高并发(FD 万级以上) |
六、如何选择?场景化决策指南
1. 小规模并发(FD < 100,跨平台需求)
- 选 select:接口简单,无需复杂配置,适合入门学习或轻量级应用(如简单代理工具)。
2. 中规模并发(100 ≤ FD ≤ 1000,Linux 平台)
- 选 poll:突破
FD_SETSIZE
限制,事件类型更清晰,适合中等并发场景(如中小型服务器)。
3. 高并发(FD > 1000,Linux 平台)
- 选 epoll:
- LT 模式:代码简单,适合低速 IO 或对实时性要求不高的场景(如日志服务器)。
- ET 模式:搭配非阻塞 IO,适合高速网络 IO(如 Web 服务器、即时通讯系统),需注意一次性读取所有数据。
4. 性能优化建议
- epoll 最佳实践:
- 对高频读写的 FD 使用 ET 模式,减少事件触发次数。
- 设置
EPOLLONESHOT
避免重复处理同一事件(适合状态机模型)。 - 调整系统参数:
ulimit -n 65535
提高最大打开文件数,优化内核 TCP 缓冲区。
七、总结:从基础到高阶的技术演进
- select:入门级多路复用,适合小规模、跨平台场景。
- poll:Linux 平台中规模并发的过渡方案,解决 FD 数量限制。
- epoll:Linux 高并发的终极选择,通过红黑树和事件链表实现高效事件管理,是 Nginx、Redis 等高性能框架的底层核心。
掌握这三种机制的原理与适用场景,能帮助开发者在不同项目中选择最优方案,从基础网络编程逐步进阶到高并发系统设计。实际开发中,建议优先使用 epoll
(Linux 平台),并结合非阻塞 IO 和线程池技术,打造高性能网络应用。
相关文章:
Linux 网络编程:select、poll 与 epoll 深度解析 —— 从基础到高并发实战
一、IO 多路复用:解决并发 IO 的核心技术 在网络编程中,当需要同时处理大量客户端连接时,传统阻塞式 IO 会导致程序卡在单个操作上,造成资源浪费。IO 多路复用技术允许单线程监听多个文件描述符(FD)&#…...
在统信UOS1060上安装Fail2Ban并通过邮件发送通知
在统信UOS1060上安装Fail2Ban并通过邮件发送通知 Fail2Ban 是一个开源的防止暴力攻击的软件,可以有效保护您的服务器免受频繁的登录失败攻击。本文将指导您如何在统信UOS 1060上安装Fail2Ban,并在IP被封禁后通过邮件发送通知。 步骤 1:查看…...
PyTorch 分布式 DistributedDataParallel (DDP)
在之前的讨论(或者如果你直接跳到这里)中,我们了解了 torch.nn.DataParallel (DP) 作为 PyTorch 多 GPU 训练的入门选项。它简单易用,但其固有的主 GPU 瓶颈、GIL 限制和低效的通信模式,往往让它在实际应用中难以充分发…...
精益数据分析(14/126):基于数据洞察优化产品与运营
精益数据分析(14/126):基于数据洞察优化产品与运营 在创业和数据分析的道路上,我们都在不断摸索前行。我一直希望能和大家共同学习、共同进步,所以今天继续为大家解读《精益数据分析》。这次我们将深入探讨HighScore …...
flutter 插件收集
2025年 1月10号Flutter插件手机 声音转文字 speech_to_text | Flutter package 文字转声音 flutter_tts | Flutter package 堆栈信息 stack_trace | Dart package 跳转到app设置里面 app_settings | Flutter package 轻松的动画 animations | Flutter package 日志打印 t…...
WPF特性分析
文章目录 WPF特性全面分析与性能优化指南引言WPF核心特性1. 声明式UI与XAML2. 硬件加速渲染3. 数据绑定与MVVM4. 样式与模板5. 动画系统 WPF与其他框架比较WPF vs. WinFormsWPF vs. UWPWPF vs. MAUI WPF性能优化最佳实践1. 内存管理优化2. UI虚拟化3. 使用冻结对象4. 减少视觉树…...
3.1goweb框架gin下
Gin 框架有内置的模板引擎,它允许你将数据和 HTML 模板结合,动态生成网页内容。 模板引擎基础使用 单模板文件示例 以下是一个简单的使用单个 HTML 模板文件的示例,展示了如何在 Gin 中渲染模板: package mainimport ("g…...
【全解析】深入理解 JavaScript JSON 数据解析
一、JSON 概述 1. 概念 JSON 全称为 JavaScript Object Notation,是一种轻量级的数据交换格式。它是 JavaScript 中用于描述对象数据的语法的扩展。不过并不限于与 JavaScript 一起使用。它采用完全独立于语言的文本格式,这些特性使 JSON 成为理想的数…...
影刀RPA怎么和AI结合,制作自动采集小红书爆款文章+自动用AI改写标题、内容+用AI文生图生成发文图片+自动在小红书上发布文章
环境: 影刀5.26.24 Win10专业版 doubao deepseek r1 wps 问题描述: 影刀RPA怎么和AI结合,制作自动采集小红书爆款文章+自动用AI改写标题、内容+用AI文生图生成发文图片+自动在小红书上发布文章,最后上传到飞书备份 解决方案: 1.主要流程如下: 全局变量设置(关键…...
懒人一键搭建符号执行环境V5K3
0.背景 在写完上一篇文章后发现,其实V5k3的组合也可以使用。Verilator v5.x 系列版本完全支持本项目的编译与仿真。 不同于 v3 版本,Verilator v5 引入了更严格的访问控制机制:要从 Verilator 生成的 C 仿真模型中访问内部信号或变量&#x…...
Java队列(Queue)核心操作与最佳实践:深入解析与面试指南
文章目录 概述一、Java队列核心实现类对比1. LinkedList2. ArrayDeque3. PriorityQueue 二、核心操作API与时间复杂度三、经典使用场景与最佳实践场景1:BFS层序遍历(树/图)场景2:滑动窗口最大值(单调队列) …...
Android 中实现图片翻转动画(卡片翻转效果)
1、简述 通过 ObjectAnimator 和 AnimatorSet 可以实现图片的翻转动画,并在翻转过程中切换图片,同时避免图片被镜像。 ObjectAnimator 是 Android 动画框架中的一个类,用于对对象的属性进行动画效果处理。它通过改变对象的属性值来实现动画效果,非常适合实现复杂的动画,如…...
智能电网第1期 | 工业交换机在变电站自动化系统中的作用
随着智能电网建设的加速推进,变电站自动化系统对通信网络的实时性、可靠性和安全性提出了更高要求。在变电站智能化改造过程中,传统网络架构面临诸多挑战: 多协议兼容难题:继电保护、测控装置等设备通信协议多样,难以统…...
01.浏览器自动化webdriver源码分析之启动函数
日后,网络爬虫也好,数据采集也好,自动化必然是主流。因此,笔者未雨绸缪,在此研究各类自动化源码,希望能够赶上时代,做出一套实用的自动化框架。 这里先研究传统的webdriver中转来进行浏览器自动…...
day35图像处理OpenCV
文章目录 一、图像预处理17 直方图均衡化17.1绘制直方图17.2直方图均衡化1. 自适应直方图均衡化2. 对比度受限的自适应直方图均衡化3. 示例 19 模板匹配 一、图像预处理 17 直方图均衡化 直方图:反映图像像素分布的统计图,横坐标就是图像像素的取值&…...
精益数据分析(15/126):解锁数据分析关键方法,驱动业务增长
精益数据分析(15/126):解锁数据分析关键方法,驱动业务增长 在创业与数据分析的征程中,我们都在努力探寻成功的密码。今天,我依旧带着和大家共同进步的初衷,深入解读《精益数据分析》的相关内容…...
JETBRAINS USER AGREEMENT【2025.4.16】更新用户许可协议
JETBRAIN旗下的各产品更新用户许可协议: 大致跟漂亮国出口管制政策有关,以下是详细内容: JETBRAINS USER AGREEMENT Version 2.0, effective as of April 16, 2025 THIS IS A LEGAL AGREEMENT. BY CLICKING ON THE "I AGREE" (OR…...
【数字图像处理】立体视觉基础(1)
成像 成像过程:三维空间坐标到二维图像坐标的变换 相机矩阵:建立三维到二维的投影关系 相机的使用步骤(模型-视图变换): (1)视图变换 (2)模型变换 (3&…...
通过AI工具或模型创建PPT的不同方式详解,结合 Assistants API、DALL·E 3 等工具的功能对比及表格总结
以下是通过AI工具或模型创建PPT的不同方式详解,结合 Assistants API、DALLE 3 等工具的功能对比及表格总结: 1. 主要实现方式详解 1.1 基于文本生成PPT 工具示例:Microsoft PowerPoint Copilot、Google Workspace(AI-powered D…...
weibo_har鸿蒙微博分享,单例二次封装,鸿蒙微博,微博登录
weibo_har鸿蒙微博分享,单例二次封装,鸿蒙微博 HarmonyOS 5.0.3 Beta2 SDK,原样包含OpenHarmony SDK Ohos_sdk_public 5.0.3.131 (API Version 15 Beta2) 🏆简介 zyl/weibo_har是微博封装使用,支持原生core使用 &a…...
C++ Lambda表达式复习
C Lambda表达式 (C Lambda Expressions: Beginner to Advanced) Lambda表达式是C11引入的一种轻量级匿名函数语法,支持闭包捕获,可以简化代码逻辑,特别是在函数式编程、回调函数和STL算法场景中尤为常用。本文将从基础语法到高级应用&#x…...
鸿蒙NEXT开发权限工具类(申请授权相关)(ArkTs)
import abilityAccessCtrl, { Permissions } from ohos.abilityAccessCtrl; import { bundleManager, common, PermissionRequestResult } from kit.AbilityKit; import { BusinessError } from ohos.base; import { ToastUtil } from ./ToastUtil;/*** 权限工具类(…...
1000 QPS 下 MySQL 性能瓶颈解决方案
当 MySQL 在 1000 QPS 时出现性能瓶颈,需从索引优化、查询逻辑调整、服务器配置调优、架构扩展等多维度综合解决,具体策略如下: 一、索引优化 补充缺失索引 通过慢查询日志定位高频低效 SQL,使用 EXPLAIN 分…...
【MySQL】MySQL 表的增删改查(CRUD)—— 下篇(内含聚合查询、group by和having子句、联合查询、插入查询结果)
目录 1. 插入查询结果 2 聚合查询 (行与行之间运算) count 计算查询结果的行数 sum 求和 avg 求平均值 max 最大值 min 最小值 【小结】 3. group by 子句 分组 where 条件 having 条件 4. 联合查询(多表查询) 内连接…...
简化K8S部署流程:通过Apisix实现蓝绿发布策略详解(上)
本次主题主要目的是为大家讲解蓝绿发布,但是发现文档和内容太长了,对此将文档拆分成了两部分,视频拆分成了好几部分,这样大家刷起来没疲劳感。 第一部分《apisix argorollout 实现蓝绿发布I-使用apisix发布应用》,主要…...
FLV 与 MP4 格式深度剖析:结构、原理
1 FLV格式分析 1.1 定义 FLV(Flash Video)是Adobe公司推出的⼀种流媒体格式,由于其封装后的⾳视频⽂件体积⼩、封装简单等特点,⾮常适合于互联⽹上使⽤。⽬前主流的视频⽹站基本都⽀持FLV。采⽤FLV格式封装的⽂件后缀为.flv FLV封装格式是由⼀个**⽂件…...
k8s的yaml文件里的volume跟volumeMount的区别
volume 是 Pod 级别的资源,用于定义存储卷。它是一个独立于容器的存储资源,可以被一个或多个容器共享使用。volume 的定义位于 Pod 的 spec.volumes 部分。 特点 独立性:volume 是 Pod 的一部分,而不是容器的一部分。它独立于容…...
Git常用操作命令
配置 Git git config --global user.name "Your Name": 设置用户名。git config --global user.email "your_emailexample.com": 设置用户邮箱。 初始化和克隆仓库 git init: 初始化一个新的 Git 仓库。git clone [URL]: 克隆一个远程仓库到本地。 git cl…...
09.传输层协议 ——— TCP协议
文章目录 TCP协议 谈谈可靠性TCP协议格式 序号与确认序号窗口大小六个标志位 确认应答机制(ACK)超时重传机制连接管理机制 三次握手四次挥手 流量控制滑动窗口拥塞控制延迟应答捎带应答面向字节流粘包问题TCP异常情况TCP小结基于TCP的应用层协议 TCP协…...
NineData 与飞书深度集成,企业级数据管理审批流程全面自动化
NineData 正式推出与飞书审批系统的深度集成功能,企业用户在 NineData 平台发起的审批工单,将自动推送至审批人的飞书中,审批人可以直接在飞书进行审批并通过/拒绝。该功能实现跨系统协作,带来巨大的审批效率提升,为各…...
WebRTC服务器Coturn服务器中的通信协议
1、概述 作为WebRTC服务器,coturn通信协议主要是STUN和TURN协议 STUN&TURN协议头部都是20个字节,用 Message Type来区分不同的协议 |------2------|------2------|------------4------------|------------------------12-------------------------|-----------…...
4.19除自身以外数组的乘积
我自己的思路,想用双指针, 一个从左边left开始乘,一个从右边right开始乘,如果left,或者right遇到了目标索引i(也就是我们要跨过去的当前元素),那么直接让对应的指针加一,当前元素不参与累积的计算ÿ…...
Anaconda3使用conda进行包管理
一、基础包管理操作 安装包 使用 conda install <包名> 安装指定包,支持多包批量安装和版本指定: conda install numpy # 安装单个包 conda install numpy scipy pandas # 批量安装多个包 conda install numpy1.21 # 指定版本 conda instal…...
媒体关注:联易融聚焦AI+业务,重塑供应链金融生态
近日,供应链金融科技龙头企业联易融科技集团(以下简称“联易融”)发布的公告显示,截至2024年末,公司现金储备达51亿元,同比上一年增加2亿元。公司称,公司经营性现金流保持健康,现金储…...
安装 Conda 环境
安装 Conda 环境:快速指南 什么是 Conda? Conda 是一个开源的跨平台包管理器和环境管理系统,支持 Python、R、Julia 等语言。它广泛用于数据科学和机器学习领域,能够轻松创建、管理和切换开发环境。 安装步骤 1. 安装 Anaconda…...
Qt Creator 创建 Qt Quick Application一些问题
一、Qt Creator 创建 Qt Quick Application 时无法选择 MSVC 编译器(即使已安装 Qt 5.15.2 和 MSVC2019) 1、打开 Qt Creator 的编译器设置 工具 (Tools) → 选项 (Options) → Kits → 编译器 (Compilers) 检查是否存在 Microsoft Visual C++ Compiler (x86_amd64) 或类似条…...
Spark-Streaming核心编程
以下是今天所学的知识点与代码测试: Spark-Streaming DStream实操 案例一:WordCount案例 需求:使用 netcat 工具向 9999 端口不断的发送数据,通过 SparkStreaming 读取端口数据并统计不同单词出现的次数 实验步骤:…...
深度剖析神经网络:从基础原理到面试要点(二)
引言 在人工智能蓬勃发展的今天,神经网络作为其核心技术之一,广泛应用于图像识别、自然语言处理、语音识别等众多领域。深入理解神经网络的数学模型和结构,对于掌握人工智能技术至关重要。本文将对神经网络的关键知识点进行详细解析…...
c#操作excel
说明 vs2022开发,调用excel 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using Sy…...
MQTTX + MCP:MQTT 客户端秒变物联网 Agent
引言:MQTTX 与 MCP 的融合 作为最受欢迎的 MQTT 客户端工具,MQTTX 在 1.12.0 beta 版本中集成了模型上下文协议(MCP)到 Copilot AI 功能中,显著提升了服务能力。这一融合让 MQTTX 转变为 MCP Host(也就是发…...
GSAP 动画引擎实战:打造丝滑动效交互组件库
目录 一、前言二、项目初始化三、核心动效组件实战1. 元素淡入组件:FadeIn.vue2. 列表级联动画:SlideList.vue3. 滚动触发 Reveal 动画:ScrollReveal.vue4. 拖拽盒子组件:DraggableBox.vue5. 打字机效果组件:Typewrite…...
[OpenGL] Lambertian材质漫反射BRDF方程的解释与推导
一、简介 本文简单的介绍了 Physical Based Rendering, PBR 中的 Lambertian 材质漫反射BRDF公式 f r l a m b e r t i a n c d i f f π fr_{lambertian}\frac{c_{diff}}{\pi} frlambertianπcdiff的推导。 二、漫反射项 根据 渲染方程: L o ( v ) ∫ …...
网易云音乐如何修改缓存地址到D盘
你可以通过创建 符号链接(Symbolic Link) 将网易云音乐的缓存目录转移到D盘,无需修改软件设置。以下是具体步骤: 操作步骤 关闭网易云音乐 确保程序完全退出(任务栏右下角无残留进程)。 备份并移动原缓存文…...
react使用01
React.cloneElement(element,props,…children) 这个是React的官方API,,主要用于克隆并修改React元素,, 本质: 复制一个已有的React元素,并允许你修改他的props element : 必须是一个有效的element元素p…...
yooAsset打包后材质丢失
以安卓为目标平台打出的AssetBundle包(尤其是YooAsset打出的),在Window下Unity编辑器以HostPlayMode运行,有时显示会丢失部分材质。 这是因为安卓目标的AssetBundle包适合OpenglES,而window下Unity编辑器模式是Dx11&a…...
Codeforces Round 1019 (Div. 2)
A. Common Multiple 找不同的数字 #include<iostream> #include<vector> #include<algorithm> using namespace std; int main() {int t; cin >> t;while (t--) {int n;cin >> n;vector<int> a(n);for (int i 0; i < n; i)cin >&…...
【Spring Boot】MyBatis多表查询的操作:注解和XML实现SQL语句
1.准备工作 1.1创建数据库 (1)创建数据库: CREATE DATABASE mybatis_test DEFAULT CHARACTER SET utf8mb4;(2)使用数据库 -- 使⽤数据数据 USE mybatis_test;1.2 创建用户表和实体类 创建用户表 -- 创建表[⽤⼾表…...
Docker离线安装与配置指南
Docker离线安装与配置指南 离线安装步骤 1. 下载离线安装包 官方下载地址: https://download.docker.com/linux/static/stable/x86_64/注意:国内用户若无法访问,可能需要使用科学上网工具。本文档以Docker 20.10.23版本为例。 2. 安装与部…...
N8N 官方 MCP 节点实战指南:AI 驱动下的多工具协同应用场景全解析
在低代码自动化领域,N8N 凭借其强大的节点扩展能力和灵活的工作流编排,成为企业构建复杂自动化流程的首选工具。随着 AI Agent 技术的兴起,通过 MCP(Multi-Tool Coordination Protocol)实现 AI 与外部工具的协同调用&a…...
v-html 显示富文本内容
返回数据格式: 只有图片名称 显示不出完整路径 解决方法:在接收数据后手动给img格式的拼接vite.config中的服务器地址 页面: <el-button click"">获取信息<el-button><!-- 弹出层 --> <el-dialog v-model&…...