操作系统
文件IO
IO模型

同步IO:(都会在过程2:read阻塞)
- 阻塞IO
- 非阻塞IO
- 基于非阻塞IO的IO多路复用
异步IO:
IO操作的两个关键步骤
- 等待数据就绪:等待网络数据包到达、磁盘数据读取到内核缓冲区等。这个过程是耗时的。
- 数据拷贝:将已就绪的数据从内核缓冲区拷贝到应用程序指定的用户空间内存。这个操作本身是同步的。
I/O 分为两个过程:
- 数据准备的过程
- 数据从内核空间拷贝到用户进程缓冲区的过程
阻塞 I/O 会阻塞在「过程 1 」和「过程 2」,而非阻塞 I/O 和基于非阻塞 I/O 的多路复用只会阻塞在「过程 2」,所以这三个都可以认为是同步 I/O。
异步 I/O 则不同,「过程 1 」和「过程 2 」都不会阻塞。
阻塞IO
非阻塞IO
- 特点:
- 避免线程挂起:在等待数据时,线程可以去做其他工作。
- CPU浪费:轮询过程会消耗大量的CPU时间,效率低下。
- 关键点:在数据就绪阶段不阻塞,但需要主动轮询;在数据拷贝阶段仍然是阻塞的。
IO多路复用
IO多路复用整个流程要比阻塞 IO 要复杂,似乎也更浪费性能。但 I/O 多路复用接口最大的优势在于,用户可以在一个线程内同时处理多个 socket 的 IO 请求。用户可以注册多个 socket,然后不断地调用 I/O 多路复用接口读取被激活的 socket,即可达到在同一个线程内同时处理多个 IO 请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
特点:
- 高效:一个线程可以管理成千上万的连接,极大地减少了线程数量和对系统资源的消耗。
- 可扩展性强:特别适合处理高并发连接,但每个连接本身不是特别活跃的场景(如聊天服务器、Web服务器)。
- 本质上仍是阻塞:
select/epoll_wait 这个调用本身是阻塞的,但它阻塞在“多个事件”上,而不是单个IO操作上。
关键点:它将“阻塞等待”这个步骤从“具体的IO操作”提升到了一个“事件通知器”。应用程序先阻塞在select/epoll上,当有IO就绪后,再对就绪的IO进行实际的读写操作(读写时可能还是会阻塞)。
select、poll、epoll
IO多路复用:IO 多路复用是一种高效的 IO 模型,它允许单个线程同时监听多个文件描述符(FD),并在某个 FD 可读、可写或出现异常时得到通知。这样可以避免无效的等待,充分利用 CPU 资源。
监听FD以及通知的方式 有多种实现,常见的有:select、poll、epoll
epoll 相比于 poll 和 select ,能支持更大的并发连接数、性能更高
select 模式
- 内核空间存储FD的结构是固定大小为1024的数组,因此最大并发连接数受限于数组大小
- 当某个FD就绪时,从内核空间将所有FD拷贝到用户空间,用户空间需要遍历数组,找出哪些FD就绪
poll模式
- 内核空间存储FD的结构是链表,因此理论上没有最大连接数限制
- 某个FD就绪时,从内核空间将所有FD拷贝到用户空间,用户空间需要遍历链表,找出哪些FD就绪,当监听的FD越多 (即链表越长),遍历耗时增加,影响并发性能
epoll模式
- 内核空间存储FD的结构是红黑树,增删改查性能稳定
- 当通知有数据就绪可读/可写时,从内核空间只将就绪的FD拷贝到用户空间,减少了内核空间和用户空间之间的数据拷贝,且用户空间无需再遍历所有FD,直接处理返回的就绪的FD
三种 IO 多路复用模型对比(select/poll/epoll):
| 特性 |
select |
poll |
epoll(Linux 特有) |
| 事件存储结构 |
fd_set(固定大小) |
pollfd 数组(动态) |
内核事件表(红黑树) |
| 最大监控 fd 限制 |
有(FD_SETSIZE=1024) |
无(仅受系统资源限制) |
无 |
| 触发模式 |
仅水平触发(LT) |
仅水平触发(LT) |
水平触发(LT)/ 边缘触发(ET) |
| 事件遍历效率 |
O (n)(遍历所有 fd) |
O (n)(遍历所有 pollfd) |
O (1)(仅遍历就绪事件) |
| 内存拷贝 |
每次调用拷贝 fd_set |
每次调用拷贝 pollfd 数组 |
仅初始化时拷贝(共享内存) |
| 适用场景 |
小规模连接(<1024) |
中等规模连接 |
高并发大规模连接(如服务器) |
阻塞IO、非阻塞IO、IO多路复用区别总结
| 特性 |
阻塞IO |
非阻塞IO |
IO多路复用 |
| 核心机制 |
调用后一直等待,直到IO完成 |
调用立即返回,需要轮询检查状态 |
调用**select/epoll阻塞,监听多个**IO状态 |
| 线程状态(数据就绪阶段) |
阻塞,线程挂起 |
非阻塞,线程可执行其他任务 |
阻塞,但阻塞在事件集合上,而非单个IO |
| CPU利用率 |
低(线程在等待时不消耗CPU) |
高(由于不断轮询) |
高(仅用一个线程管理大量连接,效率高) |
| 编程复杂度 |
简单 |
中等(需要处理轮询和返回码) |
复杂(需要管理事件和描述符集合) |
| 适用场景 |
连接数少、并发低的场景 |
需要后台处理任务的特定场景 |
高并发、连接数多但流量小的场景(如Web服务器) |
| 一个线程能处理的连接数 |
1 |
1(但可以交替处理) |
成百上千 |
- 阻塞与非阻塞:主要区别在于进程是否在IO操作期间被挂起。阻塞IO会阻塞进程,而非阻塞IO立即返回,允许进程继续执行。
- IO多路复用与非阻塞:IO多路复用允许进程同时监视多个IO操作,而非阻塞IO通常需要进程主动轮询单个操作。IO多路复用更适用于高并发场景,而非阻塞IO可能因轮询导致CPU开销。
- 效率与复杂度:阻塞IO简单但效率低;非阻塞IO灵活但需要轮询;IO多路复用高效但编程复杂。在实际应用中,IO多路复用常与非阻塞IO结合使用(如epoll与非阻塞描述符),以进一步提升性能。
IO多路复用与异步IO的区别
- IO多路复用:它告诉你什么时候可以开始进行一个不会阻塞的IO操作(即数据就绪了)。真正的IO读写操作(数据拷贝)还是由应用程序自己同步调用的,这个调用过程本身可能还是会阻塞(尽管时间很短)。
- 异步IO(AIO):它告诉你IO操作已经全部完成。从数据等待到数据拷贝的所有工作都由内核完成,内核完成后才通知你。应用程序在整个过程中都没有被阻塞。
IO多路复用-epoll
在 Linux 中使用 epoll 进行 I/O 多路复用时,核心依赖三个函数和一个关键结构体。这些组件共同实现了高效的事件监控机制。
一、核心结构体:struct epoll_event,用于描述监控的事件类型和关联的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include <sys/epoll.h>
struct epoll_event { uint32_t events; epoll_data_t data; };
typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
|
events 字段:指定监控的事件类型,常用取值:
EPOLLIN:文件描述符可读(如 socket 有数据到达、管道有数据、定时器到期)。
EPOLLOUT:文件描述符可写(如 socket 可发送数据)。
EPOLLERR:文件描述符发生错误(无需主动设置,内核会自动触发)。
EPOLLHUP:文件描述符被挂断(如 peer 关闭连接,无需主动设置)。
EPOLLET:边缘触发模式(Edge Triggered),仅在事件状态变化时通知一次。
EPOLLONESHOT:一次性事件,事件触发后自动从监控列表中移除,需重新添加才会再次监控。
EPOLLRDHUP:流式 socket 半关闭(对方关闭写端)。
data 字段:用于事件触发时标识对应的资源(如哪个 socket 有数据),通常存储 fd(文件描述符)或指向自定义结构体的 ptr。
二、核心函数解析
epoll_create1:创建 epoll 实例
1
| int epoll_create1(int flags);
|
作用:创建一个 epoll 实例(内核维护的事件表),返回该实例的文件描述符(epfd),后续所有操作都通过该 epfd 进行。
参数flags控制创建行为,常用值:
EPOLL_CLOEXEC:设置 FD_CLOEXEC 标志,进程执行 exec 时自动关闭该 epfd,避免资源泄漏。
返回值
失败:返回 -1,并设置 errno(如 ENFILE 表示系统文件描述符耗尽)。
注意:早期版本有 epoll_create(size_t size),但 size 参数已被忽略,推荐使用 epoll_create1。
epoll_ctl:控制 epoll 实例的事件(添加 / 修改 / 删除)
1
| int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
|
- 作用:操作
epoll 实例(epfd)中的事件,包括添加、修改、删除对目标文件描述符(fd)的监控。
参数:
epfd:epoll_create1 返回的实例文件描述符。
op:操作类型:
EPOLL_CTL_ADD:向 epfd 中添加对 fd 的监控,需指定 event 结构体。
EPOLL_CTL_MOD:修改 epfd 中已监控的 fd 的事件(如从监控可读改为监控可写)。
EPOLL_CTL_DEL:从 epfd 中删除对 fd 的监控,此时 event 可设为 NULL。
fd:需要监控的目标文件描述符(如 socket、timerfd 等)。
event:struct epoll_event 指针,指定监控的事件类型(events)和关联数据(data)。
返回值
- 成功:返回
0。
- 失败:返回
-1,并设置 errno(如 EINVAL 表示 epfd 无效,ENOENT 表示 fd 未被监控)。
注意
fd 必须是非阻塞的(尤其是在边缘触发模式下),否则可能导致事件处理时阻塞。
同一 fd 可被添加到多个 epoll 实例中。
epoll_wait:等待事件触发
1
| int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
|
三、epoll 工作流程示例
创建 epoll 实例:
1 2
| int epfd = epoll_create1(EPOLL_CLOEXEC); if (epfd == -1) { perror("epoll_create1"); exit(1); }
|
添加监控对象(如一个 socket):
1 2 3 4 5 6 7 8
| struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.fd = sockfd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) { perror("epoll_ctl add"); close(epfd); exit(1); }
|
循环等待并处理事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| struct epoll_event events[10]; while (1) { int n = epoll_wait(epfd, events, 10, -1); if (n == -1) { if (errno == EINTR) continue; perror("epoll_wait"); break; }
for (int i = 0; i < n; i++) { if (events[i].events & EPOLLIN) { handle_read(events[i].data.fd); } if (events[i].events & EPOLLOUT) { handle_write(events[i].data.fd); } } }
|
清理资源:
四、关键特性总结
利用 epoll 可以构建高效的事件驱动程序(如服务器、定时器系统等)。
Socket编程
在网络编程中,socket 是核心接口,用于实现不同主机或同一主机不同进程间的通信。基于 TCP 协议的 socket 编程流程最为典型,涉及一系列函数的协作。以下按服务器端和客户端的通信流程,详细解读核心函数的作用、参数及使用场景。
一、基础概念与通用函数
- socket 描述符:所有 socket 操作通过一个整数(
int 类型)标识,类似文件描述符,用于唯一标识一个通信端点。
- 字节序转换:网络中数据传输采用大端字节序(网络字节序),而主机可能是大端或小端,需通过函数转换(避免因字节序差异导致数据错误)。
- 字节序转换函数
用于主机字节序与网络字节序的转换,确保数据在网络中正确传输:
1 2 3 4 5 6 7 8 9 10
| #include <arpa/inet.h>
uint16_t htons(uint16_t hostshort);
uint32_t htonl(uint32_t hostlong);
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
|
- 示例:端口号(16 位)需用
htons() 转换,IP 地址(32 位)需用 htonl() 转换。
- IP 地址转换函数
用于字符串形式的 IP 地址(如 "192.168.1.1")与整数形式(32 位网络字节序)的转换:
1 2 3 4 5 6 7 8 9
| #include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
|
- 参数:
af 为协议族(AF_INET 表示 IPv4);src 为输入(字符串或整数);dst 为输出缓冲区。
- 示例:
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr) 将字符串 IP 转换为整数。
二、服务器端核心函数(TCP 流程)
服务器端的流程为:创建 socket → 绑定地址和端口 → 监听连接 → 接受连接 → 收发数据 → 关闭连接。
socket():创建套接字
1 2 3
| #include <sys/socket.h>
int socket(int domain, int type, int protocol);
|
- 作用:创建一个 socket 描述符,用于后续通信。
- 参数:
domain:协议族(地址族),指定通信的地址类型:
AF_INET:IPv4 协议(最常用);
AF_INET6:IPv6 协议;
AF_UNIX:本地进程间通信(Unix 域套接字)。
type:套接字类型,指定通信方式:
SOCK_STREAM:流式套接字(TCP 协议,可靠、面向连接);
SOCK_DGRAM:数据报套接字(UDP 协议,不可靠、无连接)。
protocol:具体协议,通常设为 0(表示根据前两个参数自动选择默认协议,如 SOCK_STREAM 对应 IPPROTO_TCP)。
- 返回值:成功返回非负 socket 描述符;失败返回
-1(需检查 errno)。
bind():绑定地址和端口
1 2 3
| #include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
作用:将 socket 描述符与特定的 IP 地址和端口号绑定(服务器端必须调用,客户端可选)。
参数:
返回值:成功返回 0;失败返回 -1(常见错误:端口被占用、权限不足)。
注意:服务器通常绑定 INADDR_ANY(表示监听所有本地网卡的 IP 地址),端口号需大于 1024(避免与系统端口冲突)。
listen():监听连接
1 2 3
| #include <sys/socket.h>
int listen(int sockfd, int backlog);
|
- 作用:将主动套接字(
socket() 创建的默认类型)转换为被动套接字,使其能接收客户端的连接请求(仅 TCP 服务器需要)。
- 参数:
sockfd:已绑定的 socket 描述符。
backlog:未完成连接队列(三次握手未完成)的最大长度(超过则新连接被拒绝,具体值受系统限制)。
- 返回值:成功返回
0;失败返回 -1。
accept():接受连接
1 2 3
| #include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
|
- 作用:从已完成连接队列中取出一个连接,创建一个新的 socket 描述符用于与该客户端通信(原
sockfd 继续监听新连接)。
- 参数:
sockfd:监听用的 socket 描述符(listen() 处理过的)。
addr:输出参数,用于存储客户端的 IP 地址和端口(可设为 NULL 表示不关心)。
addrlen:输入输出参数,传入 addr 缓冲区的长度,返回实际存储的地址长度(需用指针)。
- 返回值:成功返回新的 socket 描述符(用于与客户端通信);失败返回
-1(若设置为非阻塞,无连接时返回 -1 且 errno=EAGAIN)。
- 注意:默认是阻塞函数,直到有新连接到来才返回。
三、客户端核心函数(TCP 流程)
客户端的流程为:创建 socket → 连接服务器 → 收发数据 → 关闭连接。
connect():连接服务器
1 2 3
| #include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
- 作用:主动向服务器发起连接(TCP 三次握手在此过程中完成)。
- 参数:
sockfd:客户端的 socket 描述符(socket() 创建)。
addr:指向服务器地址结构的指针(需填充服务器的 IP 和端口,网络字节序)。
addrlen:服务器地址结构的长度。
- 返回值:成功返回
0(三次握手完成);失败返回 -1(如服务器未启动、网络不可达)。
- 注意:默认是阻塞函数,直到连接建立或失败才返回。
四、数据传输函数(TCP 通用)
服务器和客户端通过以下函数收发数据(基于已建立的连接)。
send():发送数据
1 2 3
| #include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
|
- 作用:通过已连接的 socket 发送数据。
- 参数:
sockfd:accept() 或 connect() 返回的已连接描述符。
buf:待发送数据的缓冲区。
len:待发送数据的长度(字节数)。
flags:发送标志,通常设为 0(特殊需求可设 MSG_NOSIGNAL 避免连接断开时产生信号)。
- 返回值:成功返回实际发送的字节数(可能小于
len,需循环发送);失败返回 -1。
recv():接收数据
1 2 3
| #include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
|
- 作用:通过已连接的 socket 接收数据。
- 参数:
sockfd:已连接的 socket 描述符。
buf:接收数据的缓冲区。
len:缓冲区的最大长度(避免溢出)。
flags:接收标志,通常设为 0。
- 返回值:
- 成功:返回实际接收的字节数(
>0);
- 对方关闭连接:返回
0;
- 失败:返回
-1(非阻塞模式下无数据时 errno=EAGAIN)。
五、连接关闭函数
close():关闭 socket
1 2 3
| #include <unistd.h>
int close(int fd);
|
- 作用:关闭 socket 描述符,释放资源(默认会终止连接)。
- 注意:
close() 会关闭读写两个方向,且可能立即终止未发送完的数据。
shutdown():优雅关闭连接(可选)
1 2 3
| #include <sys/socket.h>
int shutdown(int sockfd, int how);
|
- 作用:更灵活地关闭连接(可单独关闭读或写方向),适合需要先发送完数据再关闭的场景。
- 参数:
how:关闭方式:
SHUT_RD:关闭读方向(不再接收数据);
SHUT_WR:关闭写方向(不再发送数据,已发送的数据会继续传输);
SHUT_RDWR:同时关闭读写(等价于 close() 的连接关闭效果)。
- 返回值:成功返回
0;失败返回 -1。
六、UDP 相关函数(补充)
UDP 是无连接协议,无需 listen()、accept()、connect(),直接通过以下函数收发数据:
sendto():发送数据报
1 2
| ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
|
- 作用:向指定地址(
dest_addr)发送 UDP 数据报(需指定目标 IP 和端口)。
recvfrom():接收数据报
1 2
| ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
|
- 作用:接收 UDP 数据报,并通过
src_addr 获取发送方的地址。
七、总结:TCP 编程流程与函数对应关系
| 阶段 |
服务器端 |
客户端 |
| 创建套接字 |
socket() |
socket() |
| 绑定地址 |
bind() |
(可选,通常由系统自动分配) |
| 准备连接 |
listen() |
- |
| 建立连接 |
accept()(阻塞等待) |
connect()(发起连接) |
| 数据传输 |
send() / recv() |
send() / recv() |
| 关闭连接 |
close() / shutdown() |
close() / shutdown() |
这些函数是 socket 编程的基础,掌握它们的参数和返回值处理,就能实现基本的网络通信功能。实际开发中,还需结合错误处理、I/O 多路复用(如 epoll)等机制,应对高并发场景。
编程实现
1.阻塞IO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h>
const int PORT = 8080; const int BUFFER_SIZE = 1024;
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "Failed to create socket" << std::endl; return 1; }
int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { std::cerr << "Failed to set socket options" << std::endl; close(server_fd); return 1; }
sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { std::cerr << "Bind failed" << std::endl; close(server_fd); return 1; }
if (listen(server_fd, 3) < 0) { std::cerr << "Listen failed" << std::endl; close(server_fd); return 1; }
std::cout << "阻塞IO服务器启动,监听端口 " << PORT << std::endl; std::cout << "使用 telnet localhost 8080 进行测试" << std::endl;
while (true) { socklen_t addrlen = sizeof(address); int new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen); if (new_socket < 0) { std::cerr << "Accept failed" << std::endl; continue; }
std::cout << "新连接: " << inet_ntoa(address.sin_addr) << ":" << ntohs(address.sin_port) << std::endl;
char buffer[BUFFER_SIZE] = {0}; ssize_t valread = read(new_socket, buffer, BUFFER_SIZE); if (valread < 0) { std::cerr << "Read failed" << std::endl; close(new_socket); continue; }
std::cout << "收到数据: " << buffer << std::endl;
const char *response = "消息已收到"; send(new_socket, response, strlen(response), 0); std::cout << "发送响应: " << response << std::endl;
close(new_socket); std::cout << "连接已关闭" << std::endl; }
close(server_fd); return 0; }
|
2-1.非阻塞IO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <chrono> #include <thread>
const int PORT = 8080; const int BUFFER_SIZE = 1024;
void set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { std::cerr << "fcntl F_GETFL failed" << std::endl; return; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { std::cerr << "fcntl F_SETFL failed" << std::endl; } }
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "Failed to create socket" << std::endl; return 1; }
int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { std::cerr << "Failed to set socket options" << std::endl; close(server_fd); return 1; }
set_non_blocking(server_fd);
sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { std::cerr << "Bind failed" << std::endl; close(server_fd); return 1; }
if (listen(server_fd, 3) < 0) { std::cerr << "Listen failed" << std::endl; close(server_fd); return 1; }
std::cout << "非阻塞IO服务器启动,监听端口 " << PORT << std::endl; std::cout << "使用 telnet localhost 8080 进行测试" << std::endl;
int client_socket = -1; char buffer[BUFFER_SIZE] = {0};
while (true) { if (client_socket == -1) { socklen_t addrlen = sizeof(address); int new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen); if (new_socket < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Accept failed" << std::endl; } } else { std::cout << "新连接: " << inet_ntoa(address.sin_addr) << ":" << ntohs(address.sin_port) << std::endl; set_non_blocking(new_socket); client_socket = new_socket; } }
if (client_socket != -1) { ssize_t valread = read(client_socket, buffer, BUFFER_SIZE); if (valread < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Read failed" << std::endl; close(client_socket); client_socket = -1; memset(buffer, 0, BUFFER_SIZE); } } else if (valread == 0) { std::cout << "连接已关闭" << std::endl; close(client_socket); client_socket = -1; memset(buffer, 0, BUFFER_SIZE); } else { std::cout << "收到数据: " << buffer << std::endl; const char *response = "消息已收到"; send(client_socket, response, strlen(response), 0); std::cout << "发送响应: " << response << std::endl; close(client_socket); client_socket = -1; memset(buffer, 0, BUFFER_SIZE); } } static int count = 0; if (++count % 10 == 0) { std::cout << "服务器正在处理其他任务..." << std::endl; } std::this_thread::sleep_for(std::chrono::milliseconds(100)); }
close(server_fd); return 0; }
|
2-2.非阻塞IO(处理多个连接)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <chrono> #include <thread> #include <vector>
const int PORT = 8080; const int BUFFER_SIZE = 1024; const int MAX_CLIENTS = 10;
struct Client { int sockfd; char buffer[BUFFER_SIZE]; sockaddr_in addr; };
void set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { std::cerr << "fcntl F_GETFL failed (fd: " << fd << ")" << std::endl; return; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { std::cerr << "fcntl F_SETFL failed (fd: " << fd << ")" << std::endl; } }
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "Failed to create server socket" << std::endl; return 1; }
int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { std::cerr << "Failed to set socket options" << std::endl; close(server_fd); return 1; }
set_non_blocking(server_fd);
sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { std::cerr << "Bind failed (port: " << PORT << ")" << std::endl; close(server_fd); return 1; }
if (listen(server_fd, 5) < 0) { std::cerr << "Listen failed" << std::endl; close(server_fd); return 1; }
std::cout << "非阻塞IO服务器启动(支持多连接),监听端口 " << PORT << std::endl; std::cout << "提示:打开多个终端,用 telnet localhost 8080 测试多连接" << std::endl;
std::vector<Client> clients;
while (true) { sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int new_client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_addr_len); if (new_client_fd > 0) { if (clients.size() >= MAX_CLIENTS) { std::cerr << "客户端数量已达上限(" << MAX_CLIENTS << "),拒绝新连接" << std::endl; close(new_client_fd); continue; }
Client new_client; new_client.sockfd = new_client_fd; new_client.addr = client_addr; memset(new_client.buffer, 0, BUFFER_SIZE);
set_non_blocking(new_client_fd);
clients.push_back(new_client); std::cout << "新连接:" << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << "(当前连接数:" << clients.size() << ")" << std::endl; } else if (new_client_fd < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Accept failed (errno: " << errno << ")" << std::endl; }
auto it = clients.begin(); while (it != clients.end()) { Client &client = *it; bool need_remove = false;
ssize_t read_len = read(client.sockfd, client.buffer, BUFFER_SIZE - 1); if (read_len > 0) { client.buffer[read_len] = '\0'; std::cout << "收到来自 " << inet_ntoa(client.addr.sin_addr) << ":" << ntohs(client.addr.sin_port) << " 的数据:" << client.buffer << std::endl;
const char *response = "服务器已收到消息:"; send(client.sockfd, response, strlen(response), 0); send(client.sockfd, client.buffer, strlen(client.buffer), 0); send(client.sockfd, "\n", 1, 0);
std::cout << "已向 " << inet_ntoa(client.addr.sin_addr) << ":" << ntohs(client.addr.sin_port) << " 发送响应" << std::endl;
memset(client.buffer, 0, BUFFER_SIZE); } else if (read_len == 0) { std::cout << "连接关闭:" << inet_ntoa(client.addr.sin_addr) << ":" << ntohs(client.addr.sin_port) << "(当前连接数:" << clients.size() - 1 << ")" << std::endl; close(client.sockfd); need_remove = true; } else { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Read failed from " << inet_ntoa(client.addr.sin_addr) << ":" << ntohs(client.addr.sin_port) << " (errno: " << errno << ")" << std::endl; close(client.sockfd); need_remove = true; } }
if (need_remove) { it = clients.erase(it); } else { ++it; } }
static int task_count = 0; if (++task_count % 20 == 0) { std::cout << "服务器空闲中,处理其他任务...(当前连接数:" << clients.size() << ")" << std::endl; }
std::this_thread::sleep_for(std::chrono::milliseconds(50)); }
close(server_fd); return 0; }
|
关键修改点与说明
1.用vector<Client>管理多客户端(核心)
原代码用单个int client_socket存储客户端,只能处理 1 个连接。修改后:
- 定义
Client结构体:包含客户端sockfd、独立缓冲区(避免多客户端数据混乱)、地址信息(用于日志)。
- 用
std::vector<Client> clients存储所有活跃客户端,支持动态添加 / 删除。
2.非阻塞accept的调整
原代码仅在client_socket == -1时尝试accept(只能处理 1 个连接)。修改后:
- 每次循环都调用
accept:非阻塞accept即使无新连接也会立即返回(错误码EAGAIN/EWOULDBLOCK),不阻塞主线程。
- 增加连接数限制(
MAX_CLIENTS):避免客户端过多导致资源耗尽。
3.遍历处理所有客户端的 IO 事件
原代码仅处理单个客户端的read。修改后:
- 用迭代器遍历
clients列表,逐个处理每个客户端的read操作。
- 正确处理的
read三种结果:
read_len > 0:读取数据并发送响应,用独立缓冲区避免数据冲突。
read_len == 0:客户端关闭连接,关闭sockfd并从列表中删除。
read_len < 0:区分 “暂时无数据”(EAGAIN/EWOULDBLOCK)和真正错误,错误时清理资源。
4.迭代器安全删除客户端
遍历vector时删除元素需注意迭代器有效性:
- 用
it = clients.erase(it):erase会返回下一个有效迭代器,避免 “悬空迭代器” 导致崩溃。
- 无删除时用
++it移动到下一个元素。
5.平衡 CPU 占用与响应速度
非阻塞 IO 的本质是 “轮询”,会持续消耗 CPU。通过std::this_thread::sleep_for(50ms)控制轮询频率:
- 休眠时间越长,CPU 占用越低,但客户端数据的响应延迟越高。
- 休眠时间越短,响应越快,但 CPU 占用越高(极端情况会 100% 占用)。
- 实际项目中,非阻塞 IO 的轮询方案仅适合连接数极少的场景(如 10 个以内),大量连接需用
epoll等 IO 多路复用机制。
非阻塞 IO 多连接方案的局限性:
- CPU 浪费:即使所有客户端都空闲,轮询仍会持续执行(需靠
sleep缓解)。
- 效率低下:连接数越多,遍历轮询的开销越大(如 1000 个连接需循环 1000 次
read)。
- 响应延迟:
sleep时间会导致数据响应延迟(如 50ms 休眠,最坏延迟 50ms)。
因此,生产环境的高并发场景(如 100 + 连接)必须用epoll(Linux)、kqueue(macOS)等 IO 多路复用机制,通过内核通知事件替代主动轮询,实现高效的多连接处理。
3-1.IO多路复用-epoll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <sys/epoll.h> #include <vector> #include <errno.h>
const int PORT = 8080; const int BUFFER_SIZE = 1024; const int MAX_EVENTS = 10;
void set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { std::cerr << "fcntl F_GETFL failed" << std::endl; return; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { std::cerr << "fcntl F_SETFL failed" << std::endl; } }
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "Failed to create socket" << std::endl; return 1; }
int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { std::cerr << "Failed to set socket options" << std::endl; close(server_fd); return 1; }
set_non_blocking(server_fd);
sockaddr_in address; address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { std::cerr << "Bind failed" << std::endl; close(server_fd); return 1; }
if (listen(server_fd, 3) < 0) { std::cerr << "Listen failed" << std::endl; close(server_fd); return 1; }
int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { std::cerr << "epoll_create1 failed" << std::endl; close(server_fd); return 1; }
struct epoll_event event; event.events = EPOLLIN; event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) { std::cerr << "epoll_ctl: server_fd" << std::endl; close(server_fd); close(epoll_fd); return 1; }
std::cout << "IO多路复用服务器启动,监听端口 " << PORT << std::endl; std::cout << "使用 telnet localhost 8080 进行测试" << std::endl;
std::vector<struct epoll_event> events(MAX_EVENTS);
while (true) { int nfds = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1); if (nfds == -1) { std::cerr << "epoll_wait failed" << std::endl; break; }
for (int i = 0; i < nfds; ++i) { if (events[i].data.fd == server_fd) { socklen_t addrlen = sizeof(address); int new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen); if (new_socket == -1) { std::cerr << "Accept failed" << std::endl; continue; }
std::cout << "新连接: " << inet_ntoa(address.sin_addr) << ":" << ntohs(address.sin_port) << std::endl;
set_non_blocking(new_socket);
event.events = EPOLLIN | EPOLLET; event.data.fd = new_socket; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event) == -1) { std::cerr << "epoll_ctl: new_socket" << std::endl; close(new_socket); continue; } } else { int fd = events[i].data.fd; char buffer[BUFFER_SIZE] = {0}; ssize_t total_read = 0; bool is_closed = false;
while (true) { ssize_t valread = read(fd, buffer + total_read, BUFFER_SIZE - 1 - total_read);
if (valread > 0) { total_read += valread; if (total_read >= BUFFER_SIZE - 1) { buffer[total_read] = '\0'; std::cout << "收到部分数据: " << buffer << std::endl; total_read = 0; memset(buffer, 0, BUFFER_SIZE); } } else if (valread == 0) { std::cout << "连接已关闭" << std::endl; is_closed = true; break; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { if (total_read > 0) { buffer[total_read] = '\0'; std::cout << "收到完整数据: " << buffer << std::endl; const char *response = "消息已收到"; send(fd, response, strlen(response), 0); std::cout << "发送响应: " << response << std::endl; } break; } else { std::cerr << "Read failed" << std::endl; is_closed = true; break; } } }
if (is_closed) { close(fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL); } } } }
close(server_fd); close(epoll_fd); return 0; }
|
3-2.IO多路复用-select
- 基于 文件描述符集(fd_set) 监控事件,支持读、写、异常三类事件。
- 每次调用
select 前需重新初始化文件描述符集(内核会修改集合,清空未就绪的 fd)。
- 存在最大监控 fd 限制(默认
FD_SETSIZE=1024),需跟踪当前最大 fd 以优化遍历效率。
- 仅支持 水平触发(LT),无需循环读取(未读完的数据下次会再次通知)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <sys/select.h> #include <errno.h>
const int PORT = 8080; const int BUFFER_SIZE = 1024; const int MAX_CLIENTS = 1024;
void set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { std::cerr << "fcntl F_GETFL failed (fd: " << fd << ")" << std::endl; return; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { std::cerr << "fcntl F_SETFL failed (fd: " << fd << ")" << std::endl; } }
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "Failed to create socket" << std::endl; return 1; }
int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { std::cerr << "Failed to set socket options" << std::endl; close(server_fd); return 1; }
set_non_blocking(server_fd);
sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { std::cerr << "Bind failed" << std::endl; close(server_fd); return 1; }
if (listen(server_fd, 5) < 0) { std::cerr << "Listen failed" << std::endl; close(server_fd); return 1; }
std::cout << "select 多路复用服务器启动,监听端口 " << PORT << std::endl; std::cout << "使用 telnet localhost 8080 进行测试" << std::endl;
fd_set read_fds; int max_fd = server_fd; int client_fds[MAX_CLIENTS] = {0};
while (true) { FD_ZERO(&read_fds); FD_SET(server_fd, &read_fds);
for (int i = 0; i < MAX_CLIENTS; ++i) { int client_fd = client_fds[i]; if (client_fd > 0) { FD_SET(client_fd, &read_fds); if (client_fd > max_fd) { max_fd = client_fd; } } }
int ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL); if (ret == -1) { if (errno == EINTR) { continue; } std::cerr << "select failed" << std::endl; break; } else if (ret == 0) { continue; }
if (FD_ISSET(server_fd, &read_fds)) { sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int new_client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len); if (new_client_fd == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Accept failed" << std::endl; } continue; }
std::cout << "新连接: " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
set_non_blocking(new_client_fd);
int i; for (i = 0; i < MAX_CLIENTS; ++i) { if (client_fds[i] == 0) { client_fds[i] = new_client_fd; break; } } if (i == MAX_CLIENTS) { std::cerr << "客户端数量已达上限(" << MAX_CLIENTS << "),拒绝新连接" << std::endl; close(new_client_fd); } }
for (int i = 0; i < MAX_CLIENTS; ++i) { int client_fd = client_fds[i]; if (client_fd == 0 || !FD_ISSET(client_fd, &read_fds)) { continue; }
char buffer[BUFFER_SIZE] = {0}; ssize_t valread = read(client_fd, buffer, BUFFER_SIZE - 1);
if (valread > 0) { std::cout << "收到数据(fd: " << client_fd << "): " << buffer << std::endl; const char* response = "消息已收到\n"; send(client_fd, response, strlen(response), 0); std::cout << "发送响应(fd: " << client_fd << "): " << response; } else if (valread == 0) { std::cout << "连接关闭(fd: " << client_fd << ")" << std::endl; close(client_fd); client_fds[i] = 0; } else { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Read failed(fd: " << client_fd << ")" << std::endl; close(client_fd); client_fds[i] = 0; } } } }
for (int i = 0; i < MAX_CLIENTS; ++i) { if (client_fds[i] > 0) { close(client_fds[i]); } } close(server_fd); return 0; }
|
3-3.IO多路复用-poll
- 基于
struct pollfd 数组 监控事件,每个元素包含 fd(监控的文件描述符)和 events(关注的事件)。
- 无需重新初始化数组(仅需修改对应 fd 的
events),内核通过 revents 返回就绪事件。
- 无最大 fd 限制(仅受系统资源限制),通过动态数组(如
vector)管理更灵活。
- 仅支持 水平触发(LT),逻辑与 select 类似,但代码更简洁(无需跟踪 max_fd)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <sys/poll.h> #include <vector> #include <errno.h>
const int PORT = 8080; const int BUFFER_SIZE = 1024; const int MAX_EVENTS = 100;
void set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { std::cerr << "fcntl F_GETFL failed (fd: " << fd << ")" << std::endl; return; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { std::cerr << "fcntl F_SETFL failed (fd: " << fd << ")" << std::endl; } }
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "Failed to create socket" << std::endl; return 1; }
int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { std::cerr << "Failed to set socket options" << std::endl; close(server_fd); return 1; }
set_non_blocking(server_fd);
sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { std::cerr << "Bind failed" << std::endl; close(server_fd); return 1; }
if (listen(server_fd, 5) < 0) { std::cerr << "Listen failed" << std::endl; close(server_fd); return 1; }
std::cout << "poll 多路复用服务器启动,监听端口 " << PORT << std::endl; std::cout << "使用 telnet localhost 8080 进行测试" << std::endl;
std::vector<struct pollfd> fds; struct pollfd server_pollfd = { .fd = server_fd, .events = POLLIN, .revents = 0 }; fds.push_back(server_pollfd);
while (true) { int ret = poll(fds.data(), fds.size(), -1); if (ret == -1) { if (errno == EINTR) { continue; } std::cerr << "poll failed" << std::endl; break; } else if (ret == 0) { continue; }
for (size_t i = 0; i < fds.size(); ++i) { struct pollfd& curr_pollfd = fds[i];
if (!(curr_pollfd.revents & POLLIN)) { continue; }
if (curr_pollfd.fd == server_fd) { sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int new_client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len); if (new_client_fd == -1) { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Accept failed" << std::endl; } continue; }
std::cout << "新连接: " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
set_non_blocking(new_client_fd);
struct pollfd client_pollfd = { .fd = new_client_fd, .events = POLLIN, .revents = 0 }; fds.push_back(client_pollfd); std::cout << "当前监控的 fd 数量: " << fds.size() << std::endl; } else { int client_fd = curr_pollfd.fd; char buffer[BUFFER_SIZE] = {0}; ssize_t valread = read(client_fd, buffer, BUFFER_SIZE - 1);
if (valread > 0) { std::cout << "收到数据(fd: " << client_fd << "): " << buffer << std::endl; const char* response = "消息已收到\n"; send(client_fd, response, strlen(response), 0); std::cout << "发送响应(fd: " << client_fd << "): " << response; } else if (valread == 0) { std::cout << "连接关闭(fd: " << client_fd << ")" << std::endl; close(client_fd); fds[i] = fds.back(); fds.pop_back(); i--; } else { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Read failed(fd: " << client_fd << ")" << std::endl; close(client_fd); fds[i] = fds.back(); fds.pop_back(); i--; } } }
curr_pollfd.revents = 0; } }
for (auto& pollfd : fds) { close(pollfd.fd); } return 0; }
|
水平触发和边缘触发
服务器 socket(监听连接)使用的是水平触发(LT),而客户端 socket(处理数据)使用的是边缘触发(ET)。两种触发模式的区分通过 epoll_event 结构体的 events 字段明确设置,具体分析如下:
核心判断依据:EPOLLET 标志的使用
epoll 的触发模式由 struct epoll_event 中的 events 字段决定:
- 水平触发(LT,Level Triggered):默认模式,无需设置额外标志(
events 中不含 EPOLLET)。当文件描述符处于就绪状态时,epoll_wait 会持续通知(直到事件被处理完毕)。
- 边缘触发(ET,Edge Triggered):需显式设置
EPOLLET 标志(events 中包含 EPOLLET)。当文件描述符状态从 “未就绪” 变为 “就绪” 时,epoll_wait 仅通知一次(无论事件是否处理完毕)。
1.服务器 socket(server_fd):水平触发(LT)
服务器 socket 用于监听新连接,代码中添加到 epoll 时的设置为:
1 2 3 4 5
| struct epoll_event event; event.events = EPOLLIN; event.data.fd = server_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
|
events 字段仅包含 EPOLLIN(表示监控 “可读事件”,即有新连接到来),不含 EPOLLET,因此是水平触发模式。
- 水平触发的特点:若有未处理的新连接(如
accept 未调用),epoll_wait 会持续返回该事件,确保新连接不会被遗漏。
2.客户端 socket(new_socket):边缘触发(ET)
客户端 socket 用于与客户端通信,代码中添加到 epoll 时的设置为:
1 2 3 4
| event.events = EPOLLIN | EPOLLET; event.data.fd = new_socket; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event);
|
events 字段包含 EPOLLET 标志(与 EPOLLIN 结合),因此是边缘触发模式。
- 边缘触发的特点:客户端发送数据时,
epoll_wait 仅在 “数据刚到达” 时通知一次(状态从 “无数据” 变为 “有数据”),若一次未读完所有数据,后续不会再通知(需手动循环读取直到 EAGAIN 错误)。
这段代码中,epoll 对不同类型的 socket 使用了不同的触发模式:
- 服务器监听 socket(
server_fd):水平触发(LT),通过 event.events = EPOLLIN 实现,确保新连接不会被遗漏。
- 客户端通信 socket(
new_socket):边缘触发(ET),通过 event.events = EPOLLIN | EPOLLET 实现,减少不必要的事件通知,提高效率(需配合非阻塞 IO 确保数据读取完整)。
这种混合模式在实际开发中常见:监听连接用 LT 保证可靠性,处理数据用 ET 提高效率。
| 触发模式 |
关键规则 |
依赖条件 |
| 水平触发(LT) |
只要文件描述符有未处理的就绪数据(如未读完),epoll_wait 会持续通知该事件 |
可使用阻塞 / 非阻塞 IO(推荐非阻塞) |
| 边缘触发(ET) |
仅在文件描述符状态从 “未就绪” 变为 “就绪” 时通知一次,后续数据需主动循环读取 |
必须使用非阻塞 IO(否则会阻塞) |
完整代码实现(含 LT 和 ET 对比)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
| #include <iostream> #include <cstring> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <fcntl.h> #include <sys/epoll.h> #include <vector> #include <errno.h>
const int PORT = 8080; const int BUFFER_SIZE = 1024; const int MAX_EVENTS = 10;
#define TRIGGER_MODE 0
void set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) { std::cerr << "fcntl F_GETFL failed (fd: " << fd << ")" << std::endl; return; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { std::cerr << "fcntl F_SETFL failed (fd: " << fd << ")" << std::endl; } }
void read_data_lt(int client_fd, const sockaddr_in& client_addr) { char buffer[BUFFER_SIZE] = {0}; ssize_t read_len = read(client_fd, buffer, BUFFER_SIZE - 1); if (read_len > 0) { buffer[read_len] = '\0'; std::cout << "[LT模式] 收到来自 " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << " 的数据:" << buffer << "(本次读取 " << read_len << " 字节)" << std::endl; const char* response = "[LT模式] 消息已收到\n"; send(client_fd, response, strlen(response), 0); } else if (read_len == 0) { std::cout << "[LT模式] 连接关闭:" << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl; close(client_fd); } else { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "[LT模式] Read失败(fd: " << client_fd << ",errno: " << errno << ")" << std::endl; close(client_fd); } } }
void read_data_et(int client_fd, const sockaddr_in& client_addr) { char buffer[BUFFER_SIZE] = {0}; ssize_t total_read = 0; while (true) { ssize_t read_len = read(client_fd, buffer + total_read, BUFFER_SIZE - 1 - total_read); if (read_len > 0) { total_read += read_len; if (total_read >= BUFFER_SIZE - 1) { buffer[total_read] = '\0'; std::cout << "[ET模式] 缓冲区已满,当前数据:" << buffer << std::endl; total_read = 0; memset(buffer, 0, BUFFER_SIZE); } } else if (read_len == 0) { if (total_read > 0) { buffer[total_read] = '\0'; std::cout << "[ET模式] 连接关闭,剩余数据:" << buffer << std::endl; } std::cout << "[ET模式] 连接关闭:" << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl; close(client_fd); break; } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { if (total_read > 0) { buffer[total_read] = '\0'; std::cout << "[ET模式] 本次事件读取完成,总字节数:" << total_read << ",数据:" << buffer << std::endl; const char* response = "[ET模式] 消息已收到\n"; send(client_fd, response, strlen(response), 0); } break; } else { std::cerr << "[ET模式] Read失败(fd: " << client_fd << ",errno: " << errno << ")" << std::endl; close(client_fd); break; } } } }
int main() { int server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { std::cerr << "创建socket失败" << std::endl; return 1; }
int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) { std::cerr << "设置socket选项失败" << std::endl; close(server_fd); return 1; }
sockaddr_in server_addr; server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { std::cerr << "绑定端口失败" << std::endl; close(server_fd); return 1; }
if (listen(server_fd, 5) < 0) { std::cerr << "监听失败" << std::endl; close(server_fd); return 1; }
int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { std::cerr << "创建epoll实例失败" << std::endl; close(server_fd); return 1; }
struct epoll_event server_event; server_event.events = EPOLLIN; server_event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &server_event) == -1) { std::cerr << "添加server_fd到epoll失败" << std::endl; close(server_fd); close(epoll_fd); return 1; }
const char* mode_str = (TRIGGER_MODE == 0) ? "水平触发(LT)" : "边缘触发(ET)"; std::cout << "epoll服务器启动,模式:" << mode_str << ",监听端口:" << PORT << std::endl; std::cout << "测试方式:telnet localhost 8080,发送长数据(如超过1024字节)观察差异" << std::endl;
std::vector<struct epoll_event> events(MAX_EVENTS); while (true) { int nfds = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1); if (nfds == -1) { std::cerr << "epoll_wait失败" << std::endl; break; }
for (int i = 0; i < nfds; ++i) { int curr_fd = events[i].data.fd;
if (curr_fd == server_fd) { sockaddr_in client_addr; 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) { std::cerr << "接受连接失败" << std::endl; continue; }
std::cout << "新连接:" << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port) << std::endl;
set_non_blocking(client_fd);
struct epoll_event client_event; client_event.data.fd = client_fd; if (TRIGGER_MODE == 0) { client_event.events = EPOLLIN; } else { client_event.events = EPOLLIN | EPOLLET; }
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_event) == -1) { std::cerr << "添加client_fd到epoll失败" << std::endl; close(client_fd); continue; }
static std::vector<sockaddr_in> client_addrs; client_addrs.push_back(client_addr); } else { static std::vector<sockaddr_in> client_addrs; sockaddr_in client_addr = client_addrs[0];
if (TRIGGER_MODE == 0) { read_data_lt(curr_fd, client_addr); } else { read_data_et(curr_fd, client_addr); }
if (TRIGGER_MODE == 0) { close(curr_fd); epoll_ctl(epoll_fd, EPOLL_CTL_DEL, curr_fd, NULL); } } } }
close(server_fd); close(epoll_fd); return 0; }
|
关键代码解析(LT 与 ET 的核心差异)
- 事件注册时的模式区分
通过 epoll_event.events 是否包含 EPOLLET 标志区分模式:
1 2 3 4 5
| client_event.events = EPOLLIN;
client_event.events = EPOLLIN | EPOLLET;
|
- 数据读取逻辑的差异(核心)
(1)水平触发(LT)的读取逻辑(read_data_lt)
- 无需循环读取:只要有数据可读,
epoll_wait 会持续通知,因此读一次即可(未读完的数据下次仍会触发事件)。
- 错误处理简单:仅需处理真正的错误(如连接关闭),
EAGAIN 可忽略(LT 模式下较少出现,因未读完会持续通知)。
- 示例中读取后关闭连接(简化处理),实际项目可保留连接复用。
(2)边缘触发(ET)的读取逻辑(read_data_et)
- 必须循环读取:
epoll 仅通知一次状态变化,需通过 while 循环调用 read,直到返回 EAGAIN(无更多数据),否则会丢失后续数据。
- 依赖非阻塞 IO:若 socket 为阻塞模式,
read 会在无数据时阻塞,导致线程卡住,因此 ET 模式强制要求非阻塞 IO。
- 总字节数统计:需记录本次事件中读取的总数据(避免分多次读取导致数据碎片化)。
测试步骤
编译代码(支持 C++11 及以上):
1
| g++ epoll_lt_et.cpp -o epoll_lt_et -std=c++11
|
切换模式:修改代码中 #define TRIGGER_MODE 0(LT)或 1(ET)。
启动服务器:
客户端连接(用 telnet 或 netcat):
发送长数据(如超过 1024 字节,例如连续输入 “1234567890” 100 次),观察服务器日志。
预期差异
| 触发模式 |
日志表现(长数据场景) |
| LT |
分多次打印数据(每次读取约 1024 字节),epoll_wait 每次有未读完数据都会通知。 |
| ET |
一次打印所有数据(循环读取直到 EAGAIN),epoll_wait 仅通知一次,后续无数据不通知。 |
- LT 模式:简单易用,适合新手,无需担心数据丢失(未读完会持续通知),但频繁通知可能降低效率。
- ET 模式:效率高(仅通知一次),但需严格遵循 “非阻塞 IO + 循环读取”,否则会丢失数据,适合高并发场景。
实际开发中,ET 模式更常用(如 Nginx、Redis),但需注意数据读取的完整性;LT 模式适合简单场景或对效率要求不高的业务。
阻塞IO和非阻塞IO代码区别
阻塞 IO 和非阻塞 IO 的核心区别在于IO 操作是否会阻塞线程执行,以及线程在等待 IO 就绪时能否处理其他任务。这一区别在两段代码中有明确体现,具体分析如下:
一、核心区别总结
| 特性 |
阻塞 IO |
非阻塞 IO |
| IO 操作是否阻塞线程 |
会阻塞:IO 未就绪时,线程挂起等待 |
不阻塞:IO 未就绪时,函数立即返回 |
| 线程利用率 |
低:等待 IO 时无法执行其他任务 |
高:等待 IO 时可处理其他任务 |
| 实现关键 |
依赖函数默认的阻塞行为 |
通过 fcntl 设置 O_NONBLOCK 标志 |
| 错误处理 |
仅返回实际错误(如连接失败) |
需处理 “暂时无法完成” 的特殊错误(EAGAIN) |
二、代码中体现的具体差异
- 是否设置非阻塞模式(核心区别)
非阻塞 IO 代码:明确通过 set_non_blocking 函数设置套接字为非阻塞模式:
1 2 3 4
| void set_non_blocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); }
|
该操作会改变套接字的行为:所有 IO 操作(accept、read 等)不再阻塞线程,而是立即返回。
阻塞 IO 代码:未进行任何非阻塞设置,套接字默认处于阻塞模式,所有 IO 操作会阻塞线程直到完成。
accept 操作的行为差异
阻塞 IO 代码:accept 函数在没有新连接时会一直阻塞线程,直到有客户端连接到来:
1 2
| int new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen);
|
这意味着服务器在处理完一个连接前,无法接受其他连接(单线程下完全串行)。
非阻塞 IO 代码:accept 在没有新连接时会**立即返回 -1**,并通过 errno 设为 EAGAIN 或 EWOULDBLOCK 表示 “暂时无连接”,线程可以继续执行:
1 2 3 4 5 6 7
| int new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen); if (new_socket < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { std::cerr << "Accept failed" << std::endl; } }
|
read 操作的行为差异
- 线程在等待 IO 时的行为
阻塞 IO 代码:线程在 accept 或 read 阻塞期间完全闲置,无法执行任何其他任务。例如,在等待客户端连接时,服务器不能打印日志、处理其他逻辑等。
非阻塞 IO 代码:线程在 IO 未就绪时(如无新连接、无数据)可以继续执行其他任务。代码中明确体现了这一点:
1 2 3 4 5
| static int count = 0; if (++count % 10 == 0) { std::cout << "服务器正在处理其他任务..." << std::endl; }
|
即使没有连接或数据,服务器也能周期性输出信息,说明线程未被阻塞。
- 连接处理模式
- 阻塞 IO 代码:采用 “处理完一个再处理下一个” 的串行模式。例如,必须等当前客户端发送数据、服务器响应并关闭连接后,才能通过
accept 接受新连接。
- 非阻塞 IO 代码:采用 “轮询检查” 模式,循环中不断检查是否有新连接或新数据,理论上可以同时处理多个连接(示例中简化为单连接,但框架支持扩展)。
三、总结
两段代码的核心差异在于IO 操作是否阻塞线程:
- 阻塞 IO 依赖函数默认的阻塞行为,线程在等待 IO 时完全闲置,实现简单但效率低,适合连接少、逻辑简单的场景。
- 非阻塞 IO 通过
O_NONBLOCK 标志避免线程阻塞,需处理特殊错误码,线程在等待 IO 时可执行其他任务,适合高并发或需要同时处理多任务的场景。
非阻塞 IO 的代码通过 fcntl 设置非阻塞标志、处理 EAGAIN 错误、以及在循环中执行其他任务,清晰体现了其与阻塞 IO 的区别。
非阻塞IO和IO多路复用代码区别
非阻塞 IO 和 IO 多路复用(此处以 epoll 实现为例)的核心区别在于如何检测 IO 事件就绪,以及对多连接的处理效率。两者虽都基于非阻塞 IO 操作,但在事件监控机制上有本质差异,具体体现在以下方面及对应代码实现中:
一、核心区别总结
| 特性 |
非阻塞 IO |
IO 多路复用(epoll) |
| 事件检测方式 |
程序主动轮询(循环检查每个 IO 对象) |
内核通知(通过 epoll 机制等待事件触发) |
| 资源消耗 |
高(轮询过程浪费 CPU) |
低(无事件时阻塞,仅在事件发生时处理) |
| 多连接处理能力 |
弱(需手动遍历所有连接,效率低) |
强(内核直接返回就绪的连接,无需遍历全部) |
| 关键依赖 |
非阻塞 IO 操作(O_NONBLOCK)+ 轮询逻辑 |
非阻塞 IO 操作 + epoll 系列函数(内核支持) |
二、代码中体现的具体差异
- 事件检测机制(最核心区别)
非阻塞 IO 代码:采用主动轮询方式检测 IO 事件。程序在无限循环中不断检查是否有新连接(accept)和数据(read),即使没有事件发生也会反复执行检查逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| while (true) { if (client_socket == -1) { int new_socket = accept(server_fd, ...); }
if (client_socket != -1) { ssize_t valread = read(client_socket, ...); }
std::this_thread::sleep_for(std::chrono::milliseconds(100)); }
|
这种方式的本质是 “猜”—— 程序无法知道事件何时发生,只能通过反复询问(轮询)确认,必然浪费 CPU 资源。
IO 多路复用代码:采用内核通知方式检测 IO 事件。程序通过 epoll 机制将需要监控的文件描述符(如服务器 socket、客户端 socket)交给内核,由内核负责监控事件。当事件发生时,内核主动通知程序,程序只需处理就绪的事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| int epoll_fd = epoll_create1(0);
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);
while (true) { int nfds = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1); for (int i = 0; i < nfds; ++i) { if (events[i].data.fd == server_fd) { } else { } } }
|
这种方式的本质是 “等通知”—— 程序无需主动询问,由内核高效监控并通知,几乎不浪费 CPU 资源。
- 多连接处理能力
非阻塞 IO 代码:难以高效处理多个连接。代码中通过 client_socket 变量只能记录一个客户端连接,即使扩展为数组存储多个连接,也需要在循环中逐个轮询检查每个连接(例如遍历数组调用 read),随着连接数增加,效率急剧下降:
1 2
| int client_socket = -1;
|
IO 多路复用代码:天然支持高效处理多个连接。所有客户端连接的 socket 都会被添加到 epoll 监控(通过 epoll_ctl(EPOLL_CTL_ADD)),内核会跟踪每个连接的状态。事件发生时,epoll_wait 直接返回所有就绪的连接,程序无需遍历全部连接,只需处理内核返回的就绪列表:
1 2 3
| event.data.fd = new_socket; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event);
|
这种方式下,即使有上千个连接,只要大部分处于空闲状态,程序也能高效处理(仅处理就绪的少数连接)。
- 线程阻塞行为
非阻塞 IO 代码:线程在轮询间隙 “空转” 或强制休眠。为避免 CPU 被 100% 占用,代码中加入了 sleep_for(100ms) 强制休眠,但这会导致事件响应延迟(最长 100ms)。若不休眠,线程会无意义地反复执行轮询逻辑,浪费 CPU:
1 2
| std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
IO 多路复用代码:线程在无事件时阻塞休眠(由内核挂起)。epoll_wait 函数在无事件发生时会阻塞线程,此时线程不消耗 CPU 资源;当事件发生时,内核会唤醒线程立即处理,既无资源浪费,也无响应延迟:
1 2
| int nfds = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1);
|
- 关键函数依赖
三、总结
两段代码虽都使用了非阻塞 IO 操作(通过 set_non_blocking 设置 O_NONBLOCK),但核心区别在于事件检测方式:
- 非阻塞 IO 通过主动轮询检查事件,需要手动遍历所有连接,效率低且浪费 CPU,适合连接极少的场景;
- IO 多路复用通过内核通知获取事件,由内核筛选就绪连接,无需轮询,效率高,适合高并发场景。
代码中最直观的差异是:非阻塞 IO 使用循环 + 休眠轮询,而 IO 多路复用使用 epoll 系列函数实现事件驱动处理。