操作系统:IO模型

操作系统

文件IO

IO模型

同步IO:(都会在过程2:read阻塞)

  • 阻塞IO
  • 非阻塞IO
  • 基于非阻塞IO的IO多路复用

异步IO:

  • 异步IO(aio_read)
IO操作的两个关键步骤
  1. 等待数据就绪:等待网络数据包到达、磁盘数据读取到内核缓冲区等。这个过程是耗时的。
  2. 数据拷贝:将已就绪的数据从内核缓冲区拷贝到应用程序指定的用户空间内存。这个操作本身是同步的。

I/O 分为两个过程:

  1. 数据准备的过程
  2. 数据从内核空间拷贝到用户进程缓冲区的过程

阻塞 I/O 会阻塞在「过程 1 」和「过程 2」,而非阻塞 I/O 和基于非阻塞 I/O 的多路复用只会阻塞在「过程 2」,所以这三个都可以认为是同步 I/O。
异步 I/O 则不同,「过程 1 」和「过程 2 」都不会阻塞。

阻塞IO
  • 特点

    • 简单直观:编程模型非常简单。
    • 资源浪费:在等待期间,线程被完全占用。如果有大量并发,系统资源(CPU内存)消耗巨大。
  • 关键点:在数据就绪数据拷贝这两个阶段,线程都是被阻塞的。

非阻塞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; // 用户数据(用于事件触发时标识资源)
};

// data 是一个联合体,可存储文件描述符或指针
typedef union epoll_data {
void *ptr; // 指向用户自定义数据
int fd; // 文件描述符(常用)
uint32_t u32; // 32位整数
uint64_t u64; // 64位整数
} epoll_data_t;
  • events 字段:指定监控的事件类型,常用取值:
    • EPOLLIN:文件描述符可读(如 socket 有数据到达、管道有数据、定时器到期)。
    • EPOLLOUT:文件描述符可写(如 socket 可发送数据)。
    • EPOLLERR:文件描述符发生错误(无需主动设置,内核会自动触发)。
    • EPOLLHUP:文件描述符被挂断(如 peer 关闭连接,无需主动设置)。
    • EPOLLET:边缘触发模式(Edge Triggered),仅在事件状态变化时通知一次。
    • EPOLLONESHOT:一次性事件,事件触发后自动从监控列表中移除,需重新添加才会再次监控。
    • EPOLLRDHUP:流式 socket 半关闭(对方关闭写端)。
  • data 字段:用于事件触发时标识对应的资源(如哪个 socket 有数据),通常存储 fd(文件描述符)或指向自定义结构体的 ptr

二、核心函数解析

  1. epoll_create1:创建 epoll 实例
1
int epoll_create1(int flags);
  • 作用:创建一个 epoll 实例(内核维护的事件表),返回该实例的文件描述符(epfd),后续所有操作都通过该 epfd 进行。

  • 参数flags控制创建行为,常用值:

    • 0:默认行为。
  • EPOLL_CLOEXEC:设置 FD_CLOEXEC 标志,进程执行 exec 时自动关闭该 epfd,避免资源泄漏。

  • 返回值

    • 成功:返回非负整数(epfd)。
  • 失败:返回 -1,并设置 errno(如 ENFILE 表示系统文件描述符耗尽)。

  • 注意:早期版本有 epoll_create(size_t size),但 size 参数已被忽略,推荐使用 epoll_create1

  1. epoll_ctl:控制 epoll 实例的事件(添加 / 修改 / 删除)
1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 作用:操作 epoll 实例(epfd)中的事件,包括添加、修改、删除对目标文件描述符(fd)的监控。

参数:

  • epfdepoll_create1 返回的实例文件描述符。

  • op:操作类型:

    • EPOLL_CTL_ADD:向 epfd 中添加对 fd 的监控,需指定 event 结构体。

    • EPOLL_CTL_MOD:修改 epfd 中已监控的 fd 的事件(如从监控可读改为监控可写)。

    • EPOLL_CTL_DEL:从 epfd 中删除对 fd 的监控,此时 event 可设为 NULL

  • fd:需要监控的目标文件描述符(如 socket、timerfd 等)。

  • eventstruct epoll_event 指针,指定监控的事件类型(events)和关联数据(data)。

  • 返回值

    • 成功:返回 0
    • 失败:返回 -1,并设置 errno(如 EINVAL 表示 epfd 无效,ENOENT 表示 fd 未被监控)。
  • 注意

    • fd 必须是非阻塞的(尤其是在边缘触发模式下),否则可能导致事件处理时阻塞。
  • 同一 fd 可被添加到多个 epoll 实例中。

  1. epoll_wait:等待事件触发
1
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
  • 作用:阻塞等待 epfd 中监控的事件触发,返回触发的事件列表。

  • 参数:

    • epfdepoll 实例的文件描述符。

    • events:输出参数,指向一个 struct epoll_event 数组,用于存储触发的事件。

    • maxeventsevents 数组的最大长度(必须大于 0)。

    • timeout:超时时间(毫秒):

      • -1:无限等待,直到有事件触发。
      • 0:立即返回,无论是否有事件(非阻塞模式)。
      • 正数:等待指定毫秒数后返回,若超时则返回 0
  • 返回值:

    • 成功:返回触发的事件数量(n0 ≤ n ≤ maxevents)。
    • 失败:返回 -1,并设置 errno(如 EINTR 表示被信号中断,可重试)。
  • 注意:

    • 函数返回后,events 数组的前 n 个元素即为触发的事件,需遍历处理。
  • 水平触发(LT)模式下,若事件未处理完毕(如数据未读完),下次调用仍会触发;边缘触发(ET)模式下,仅在状态变化时触发一次,需一次性处理完所有数据。

三、epoll 工作流程示例

  1. 创建 epoll 实例

    1
    2
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    if (epfd == -1) { perror("epoll_create1"); exit(1); }
  2. 添加监控对象(如一个 socket):

    1
    2
    3
    4
    5
    6
    7
    8
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET; // 边缘触发,监控可读事件
    ev.data.fd = sockfd; // 关联 socket 的文件描述符
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
    perror("epoll_ctl add");
    close(epfd);
    exit(1);
    }
  3. 循环等待并处理事件

    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];  // 最多处理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) {
    // 处理可读事件(如读取 socket 数据)
    handle_read(events[i].data.fd);
    }
    if (events[i].events & EPOLLOUT) {
    // 处理可写事件(如发送数据)
    handle_write(events[i].data.fd);
    }
    }
    }
  4. 清理资源

    1
    close(epfd);  // 关闭 epoll 实例,自动移除所有监控的 fd

四、关键特性总结

  • 高效性epoll 采用内核事件表,避免了 select/poll 的轮询遍历,适用于高并发场景(监控大量文件描述符)。

  • 两种触发模式

    • 水平触发(LT):默认模式,事件未处理完会持续通知,易用性高。
    • 边缘触发(ET):仅在状态变化时通知一次,需配合非阻塞 I/O 使用,效率更高。
  • 事件驱动:通过 epoll_wait 统一等待所有事件(I/O、定时器等),适合单线程处理多任务。

利用 epoll 可以构建高效的事件驱动程序(如服务器、定时器系统等)。

Socket编程

在网络编程中,socket 是核心接口,用于实现不同主机或同一主机不同进程间的通信。基于 TCP 协议的 socket 编程流程最为典型,涉及一系列函数的协作。以下按服务器端客户端的通信流程,详细解读核心函数的作用、参数及使用场景。

一、基础概念与通用函数

  • socket 描述符:所有 socket 操作通过一个整数(int 类型)标识,类似文件描述符,用于唯一标识一个通信端点。
  • 字节序转换:网络中数据传输采用大端字节序(网络字节序),而主机可能是大端或小端,需通过函数转换(避免因字节序差异导致数据错误)。
  1. 字节序转换函数

用于主机字节序与网络字节序的转换,确保数据在网络中正确传输:

1
2
3
4
5
6
7
8
9
10
#include <arpa/inet.h>

// 将主机字节序的短整数(16位)转换为网络字节序
uint16_t htons(uint16_t hostshort);
// 将主机字节序的长整数(32位)转换为网络字节序
uint32_t htonl(uint32_t hostlong);
// 将网络字节序的短整数转换为主机字节序
uint16_t ntohs(uint16_t netshort);
// 将网络字节序的长整数转换为主机字节序
uint32_t ntohl(uint32_t netlong);
  • 示例:端口号(16 位)需用 htons() 转换,IP 地址(32 位)需用 htonl() 转换。
  1. IP 地址转换函数

用于字符串形式的 IP 地址(如 "192.168.1.1")与整数形式(32 位网络字节序)的转换:

1
2
3
4
5
6
7
8
9
#include <arpa/inet.h>

// 将点分十进制字符串(如"192.168.1.1")转换为网络字节序的32位整数
in_addr_t inet_addr(const char *cp);

// 功能更全面的转换函数(支持 IPv4 和 IPv6)
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 → 绑定地址和端口 → 监听连接 → 接受连接 → 收发数据 → 关闭连接

  1. 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)。
  1. bind():绑定地址和端口
1
2
3
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用:将 socket 描述符与特定的 IP 地址和端口号绑定(服务器端必须调用,客户端可选)。

  • 参数:

    • sockfdsocket() 返回的描述符。

    • addr:指向地址结构的指针,需根据协议族填充(以 IPv4 为例):

      1
      2
      3
      4
      5
      6
      struct sockaddr_in {
      sa_family_t sin_family; // 协议族(必须为 AF_INET)
      uint16_t sin_port; // 端口号(网络字节序,需用 htons() 转换)
      struct in_addr sin_addr; // IP 地址(网络字节序,INADDR_ANY 表示绑定所有本地地址)
      unsigned char sin_zero[8]; // 填充字段,需设为 0
      };
    • addrlen:地址结构的长度(sizeof(struct sockaddr_in))。

  • 返回值:成功返回 0;失败返回 -1(常见错误:端口被占用、权限不足)。

  • 注意:服务器通常绑定 INADDR_ANY(表示监听所有本地网卡的 IP 地址),端口号需大于 1024(避免与系统端口冲突)。

  1. listen():监听连接
1
2
3
#include <sys/socket.h>

int listen(int sockfd, int backlog);
  • 作用:将主动套接字(socket() 创建的默认类型)转换为被动套接字,使其能接收客户端的连接请求(仅 TCP 服务器需要)。
  • 参数:
    • sockfd:已绑定的 socket 描述符。
    • backlog:未完成连接队列(三次握手未完成)的最大长度(超过则新连接被拒绝,具体值受系统限制)。
  • 返回值:成功返回 0;失败返回 -1
  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(若设置为非阻塞,无连接时返回 -1errno=EAGAIN)。
  • 注意:默认是阻塞函数,直到有新连接到来才返回。

三、客户端核心函数(TCP 流程)

客户端的流程为:创建 socket → 连接服务器 → 收发数据 → 关闭连接

  1. 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 通用)

服务器和客户端通过以下函数收发数据(基于已建立的连接)。

  1. send():发送数据
1
2
3
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • 作用:通过已连接的 socket 发送数据。
  • 参数:
    • sockfdaccept()connect() 返回的已连接描述符。
    • buf:待发送数据的缓冲区。
    • len:待发送数据的长度(字节数)。
    • flags:发送标志,通常设为 0(特殊需求可设 MSG_NOSIGNAL 避免连接断开时产生信号)。
  • 返回值:成功返回实际发送的字节数(可能小于 len,需循环发送);失败返回 -1
  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)。

五、连接关闭函数

  1. close():关闭 socket
1
2
3
#include <unistd.h>

int close(int fd);
  • 作用:关闭 socket 描述符,释放资源(默认会终止连接)。
  • 注意close() 会关闭读写两个方向,且可能立即终止未发送完的数据。
  1. 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(),直接通过以下函数收发数据:

  1. 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 和端口)。
  1. 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() {
// 创建socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return 1;
}

// 设置socket选项,允许端口重用
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;
}

// 绑定socket到端口
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;
}

// 关闭服务器socket(实际不会执行到这里)
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() {
// 创建socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return 1;
}

// 设置socket选项,允许端口重用
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);

// 绑定socket到端口
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) {
// 如果是EAGAIN或EWOULDBLOCK,说明当前没有新连接
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) {
// 如果是EAGAIN或EWOULDBLOCK,说明当前没有数据可读
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;
}

// 短暂休眠,避免CPU占用过高
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

// 关闭服务器socket(实际不会执行到这里)
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; // 限制最大客户端数量(可根据需求调整)

// 定义客户端结构体:存储socket和独立缓冲区(避免多客户端数据冲突)
struct Client {
int sockfd; // 客户端socket描述符
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() {
// 1. 创建服务器socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create server socket" << std::endl;
return 1;
}

// 2. 设置端口重用(避免重启时端口占用)
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;
}

// 3. 设置服务器socket为非阻塞模式
set_non_blocking(server_fd);

// 4. 绑定地址和端口
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;
}

// 5. 开始监听连接(backlog=5表示未完成连接队列大小)
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;

// 6. 用vector管理所有客户端连接(存储Client结构体)
std::vector<Client> clients;

while (true) {
// -------------------------- 阶段1:接受新连接 --------------------------
sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);

// 非阻塞accept:无论是否有新连接,立即返回
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); // 客户端socket也需非阻塞

// 将新客户端加入管理列表
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) {
// 排除"暂时无新连接"的错误,处理真正的accept失败
std::cerr << "Accept failed (errno: " << errno << ")" << std::endl;
}

// -------------------------- 阶段2:处理所有客户端的IO事件 --------------------------
// 遍历客户端列表(用迭代器便于删除无效连接)
auto it = clients.begin();
while (it != clients.end()) {
Client &client = *it; // 当前客户端引用
bool need_remove = false; // 标记是否需要删除该客户端

// 非阻塞read:读取客户端数据
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;

// 发送响应(非阻塞send,实际项目需处理"发送不完整"的情况)
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) {
// 读取到0:客户端主动关闭连接
std::cout << "连接关闭:" << inet_ntoa(client.addr.sin_addr)
<< ":" << ntohs(client.addr.sin_port)
<< "(当前连接数:" << clients.size() - 1 << ")" << std::endl;
close(client.sockfd); // 关闭socket
need_remove = true; // 标记为需删除
} else {
// read失败:区分"暂时无数据"和真正错误
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;
}
// 若为EAGAIN/EWOULDBLOCK:暂时无数据,不处理
}

// 删除无效客户端(迭代器需正确更新,避免悬空)
if (need_remove) {
it = clients.erase(it); // erase返回下一个有效迭代器
} else {
++it; // 无删除则移动到下一个客户端
}
}

// -------------------------- 阶段3:模拟其他业务逻辑 --------------------------
static int task_count = 0;
if (++task_count % 20 == 0) { // 每20次循环(约2秒)打印一次
std::cout << "服务器空闲中,处理其他任务...(当前连接数:" << clients.size() << ")" << std::endl;
}

// -------------------------- 阶段4:控制轮询频率,避免CPU占用过高 --------------------------
// 非阻塞IO的核心问题:轮询会消耗CPU,sleep可降低占用,但会影响响应速度
std::this_thread::sleep_for(std::chrono::milliseconds(50)); // 50ms休眠(可调整)
}

// 理论上不会执行到这里(循环无限),但仍需释放资源
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() {
// 创建socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return 1;
}

// 设置socket选项,允许端口重用
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);

// 绑定socket到端口
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;
}

// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
std::cerr << "epoll_create1 failed" << std::endl;
close(server_fd);
return 1;
}

// 将服务器socket添加到epoll监控
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) {
// 等待事件发生,-1表示无限等待
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) {
// 服务器socket有新连接
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);

// 将新连接添加到epoll监控
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 {
// 客户端socket有数据(修改后)
int fd = events[i].data.fd;
char buffer[BUFFER_SIZE] = {0};
ssize_t total_read = 0; // 累计读取的总字节数
bool is_closed = false; // 标记连接是否关闭

// 核心:ET模式必须循环read,直到返回EAGAIN/EWOULDBLOCK
while (true) {
ssize_t valread = read(fd, buffer + total_read, BUFFER_SIZE - 1 - total_read);
// 留1字节存'\0',避免字符串乱码

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; // 退出循环,等待下一次ET通知
} 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> // select 头文件
#include <errno.h>

const int PORT = 8080;
const int BUFFER_SIZE = 1024;
const int MAX_CLIENTS = 1024; // 受 FD_SETSIZE 限制,默认最大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() {
// 1. 创建服务器 socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return 1;
}

// 2. 设置端口重用(避免重启时端口占用)
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;
}

// 3. 设置服务器 socket 为非阻塞(避免 accept 阻塞)
set_non_blocking(server_fd);

// 4. 绑定地址和端口
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;
}

// 5. 监听连接(backlog=5,未完成连接队列大小)
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;

// 6. 初始化 select 相关变量
fd_set read_fds; // 监控可读事件的文件描述符集
int max_fd = server_fd; // 跟踪当前最大 fd(优化 select 遍历效率)
int client_fds[MAX_CLIENTS] = {0}; // 存储所有客户端 fd(初始化为0,0为标准输入,不使用)

while (true) {
// 关键:每次 select 前重新初始化 read_fds(内核会修改集合)
FD_ZERO(&read_fds); // 清空集合
FD_SET(server_fd, &read_fds); // 将服务器 fd 加入集合(监控新连接)

// 将所有活跃客户端 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);
// 更新最大 fd(确保 select 能覆盖所有监控的 fd)
if (client_fd > max_fd) {
max_fd = client_fd;
}
}
}

// 7. 调用 select 等待事件(-1 表示无限阻塞,仅监控可读事件)
int ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
if (ret == -1) {
// 处理被信号中断的情况(如 Ctrl+C 触发 SIGINT)
if (errno == EINTR) {
continue;
}
std::cerr << "select failed" << std::endl;
break;
} else if (ret == 0) {
// 超时(本示例无超时,不会进入)
continue;
}

// 8. 处理就绪事件
// 情况1:服务器 fd 就绪(有新连接)
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) {
// 非阻塞 accept 无新连接时返回 EAGAIN,忽略
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;

// 设置客户端 fd 为非阻塞(避免单个连接阻塞整个服务器)
set_non_blocking(new_client_fd);

// 将新客户端 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);
}
}

// 情况2:客户端 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; // 跳过空闲或未就绪的 fd
}

// 读取客户端数据(水平触发,无需循环读取)
char buffer[BUFFER_SIZE] = {0};
ssize_t valread = read(client_fd, buffer, BUFFER_SIZE - 1); // 留1字节存 '\0'

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 {
// 读取错误(非 EAGAIN 为真正错误)
if (errno != EAGAIN && errno != EWOULDBLOCK) {
std::cerr << "Read failed(fd: " << client_fd << ")" << std::endl;
close(client_fd);
client_fds[i] = 0;
}
}
}
}

// 9. 清理资源(关闭所有客户端 fd 和服务器 fd)
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> // poll 头文件
#include <vector> // 动态存储 pollfd 数组
#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() {
// 1. 创建服务器 socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "Failed to create socket" << std::endl;
return 1;
}

// 2. 设置端口重用
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;
}

// 3. 设置服务器 socket 为非阻塞
set_non_blocking(server_fd);

// 4. 绑定地址和端口
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;
}

// 5. 监听连接
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;

// 6. 初始化 pollfd 数组(动态管理,初始仅包含服务器 fd)
std::vector<struct pollfd> fds;
struct pollfd server_pollfd = {
.fd = server_fd, // 服务器 fd
.events = POLLIN, // 关注可读事件(新连接)
.revents = 0 // 就绪事件(由内核填充)
};
fds.push_back(server_pollfd);

while (true) {
// 7. 调用 poll 等待事件(-1 表示无限阻塞,监控所有 fds)
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; // 超时(无超时设置,不会进入)
}

// 8. 处理就绪事件(遍历所有 pollfd,检查 revents)
for (size_t i = 0; i < fds.size(); ++i) {
struct pollfd& curr_pollfd = fds[i];

// 跳过未就绪的 fd
if (!(curr_pollfd.revents & POLLIN)) {
continue;
}

// 情况1:服务器 fd 就绪(新连接)
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;

// 设置客户端 fd 为非阻塞
set_non_blocking(new_client_fd);

// 将新客户端 fd 加入 pollfd 数组
struct pollfd client_pollfd = {
.fd = new_client_fd,
.events = POLLIN, // 关注可读事件(数据)
.revents = 0
};
fds.push_back(client_pollfd);
std::cout << "当前监控的 fd 数量: " << fds.size() << std::endl;
}
// 情况2:客户端 fd 就绪(数据可读)
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) {
// 客户端关闭连接:从 pollfd 数组中删除,关闭 fd
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;
}
}

// 9. 清理资源
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
// 将服务器socket添加到epoll监控
struct epoll_event event;
event.events = EPOLLIN; // 仅设置EPOLLIN(可读事件),无EPOLLET标志
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
// 将新连接(客户端socket)添加到epoll监控
event.events = EPOLLIN | EPOLLET; // 同时设置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;

// 选择触发模式:0=水平触发(LT),1=边缘触发(ET)
#define TRIGGER_MODE 0 // 可切换为1测试ET模式

// 设置文件描述符为非阻塞模式(ET必须依赖非阻塞IO)
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;
}
}

// 水平触发(LT)的数据读取逻辑
void read_data_lt(int client_fd, const sockaddr_in& client_addr) {
char buffer[BUFFER_SIZE] = {0};
// LT模式:无需循环读取,读一次即可(未读完下次epoll仍会通知)
ssize_t read_len = read(client_fd, buffer, BUFFER_SIZE - 1); // 留1字节存'\0'

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 {
// 错误处理(LT模式下read失败通常是连接异常)
if (errno != EAGAIN && errno != EWOULDBLOCK) {
std::cerr << "[LT模式] Read失败(fd: " << client_fd << ",errno: " << errno << ")" << std::endl;
close(client_fd);
}
}
}

// 边缘触发(ET)的数据读取逻辑
void read_data_et(int client_fd, const sockaddr_in& client_addr) {
char buffer[BUFFER_SIZE] = {0};
ssize_t total_read = 0; // 记录本次事件中读取的总字节数

// ET模式:必须循环读取,直到read返回EAGAIN(无更多数据)
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() {
// 1. 创建服务器socket
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
std::cerr << "创建socket失败" << std::endl;
return 1;
}

// 2. 设置端口重用
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;
}

// 3. 绑定地址和端口
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;
}

// 4. 监听连接(backlog=5)
if (listen(server_fd, 5) < 0) {
std::cerr << "监听失败" << std::endl;
close(server_fd);
return 1;
}

// 5. 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
std::cerr << "创建epoll实例失败" << std::endl;
close(server_fd);
return 1;
}

// 6. 服务器socket添加到epoll(默认LT模式,监听新连接)
struct epoll_event server_event;
server_event.events = EPOLLIN; // 无EPOLLET,LT模式
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;

// 7. 事件循环
std::vector<struct epoll_event> events(MAX_EVENTS);
while (true) {
// 等待事件(-1表示无限阻塞)
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;

// 情况1:服务器socket有新连接
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;

// 客户端socket必须设置为非阻塞(ET模式强制,LT模式推荐)
set_non_blocking(client_fd);

// 根据模式设置epoll事件
struct epoll_event client_event;
client_event.data.fd = client_fd;
if (TRIGGER_MODE == 0) {
// LT模式:仅设置EPOLLIN(无EPOLLET)
client_event.events = EPOLLIN;
} else {
// ET模式:设置EPOLLIN | EPOLLET
client_event.events = EPOLLIN | EPOLLET;
}

// 将客户端socket添加到epoll
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);
}
// 情况2:客户端socket有数据可读
else {
// 查找客户端地址(简化处理,实际项目需将fd与addr绑定存储)
static std::vector<sockaddr_in> client_addrs;
sockaddr_in client_addr = client_addrs[0]; // 仅为演示,实际需映射fd与addr

// 根据模式调用不同的读取函数
if (TRIGGER_MODE == 0) {
read_data_lt(curr_fd, client_addr);
} else {
read_data_et(curr_fd, client_addr);
}

// LT模式读取后可保留连接(示例中简化为关闭,实际可复用)
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 的核心差异)

  1. 事件注册时的模式区分

通过 epoll_event.events 是否包含 EPOLLET 标志区分模式:

1
2
3
4
5
// LT模式:无EPOLLET
client_event.events = EPOLLIN;

// ET模式:有EPOLLET
client_event.events = EPOLLIN | EPOLLET;
  1. 数据读取逻辑的差异(核心)

(1)水平触发(LT)的读取逻辑(read_data_lt

  • 无需循环读取:只要有数据可读,epoll_wait 会持续通知,因此读一次即可(未读完的数据下次仍会触发事件)。
  • 错误处理简单:仅需处理真正的错误(如连接关闭),EAGAIN 可忽略(LT 模式下较少出现,因未读完会持续通知)。
  • 示例中读取后关闭连接(简化处理),实际项目可保留连接复用。

(2)边缘触发(ET)的读取逻辑(read_data_et

  • 必须循环读取epoll 仅通知一次状态变化,需通过 while 循环调用 read,直到返回 EAGAIN(无更多数据),否则会丢失后续数据。
  • 依赖非阻塞 IO:若 socket 为阻塞模式,read 会在无数据时阻塞,导致线程卡住,因此 ET 模式强制要求非阻塞 IO
  • 总字节数统计:需记录本次事件中读取的总数据(避免分多次读取导致数据碎片化)。

测试步骤

  1. 编译代码(支持 C++11 及以上):

    1
    g++ epoll_lt_et.cpp -o epoll_lt_et -std=c++11
  2. 切换模式:修改代码中 #define TRIGGER_MODE 0(LT)或 1(ET)。

  3. 启动服务器:

    1
    ./epoll_lt_et
  4. 客户端连接(用 telnet 或 netcat):

    1
    telnet localhost 8080
  5. 发送长数据(如超过 1024 字节,例如连续输入 “1234567890” 100 次),观察服务器日志。

  6. 预期差异

触发模式 日志表现(长数据场景)
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

二、代码中体现的具体差异

  1. 是否设置非阻塞模式(核心区别)
  • 非阻塞 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); // 添加O_NONBLOCK标志
    }

    该操作会改变套接字的行为:所有 IO 操作(acceptread 等)不再阻塞线程,而是立即返回。

  • 阻塞 IO 代码:未进行任何非阻塞设置,套接字默认处于阻塞模式,所有 IO 操作会阻塞线程直到完成。

  1. accept 操作的行为差异
  • 阻塞 IO 代码accept 函数在没有新连接时会一直阻塞线程,直到有客户端连接到来:

    1
    2
    int new_socket = accept(server_fd, (struct sockaddr *)&address, &addrlen);
    // 若无新连接,线程会卡在这一行,无法执行后续代码

    这意味着服务器在处理完一个连接前,无法接受其他连接(单线程下完全串行)。

  • 非阻塞 IO 代码accept 在没有新连接时会**立即返回 -1**,并通过 errno 设为 EAGAINEWOULDBLOCK 表示 “暂时无连接”,线程可以继续执行:

    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;
    }
    }
  1. read 操作的行为差异
  • 阻塞 IO 代码read 函数在没有数据可读时会阻塞线程,直到客户端发送数据或连接关闭:

    1
    2
    ssize_t valread = read(new_socket, buffer, BUFFER_SIZE);
    // 若无数据,线程会卡在这一行,无法做其他事
  • 非阻塞 IO 代码read 在没有数据可读时会**立即返回 -1**,并通过 errno 设为 EAGAINEWOULDBLOCK 表示 “暂时无数据”,线程可以继续执行:

    1
    2
    3
    4
    5
    6
    7
    ssize_t valread = read(client_socket, buffer, BUFFER_SIZE);
    if (valread < 0) {
    // 仅处理真正的错误,忽略“暂时无数据”的情况
    if (errno != EAGAIN && errno != EWOULDBLOCK) {
    std::cerr << "Read failed" << std::endl;
    }
    }
  1. 线程在等待 IO 时的行为
  • 阻塞 IO 代码:线程在 acceptread 阻塞期间完全闲置,无法执行任何其他任务。例如,在等待客户端连接时,服务器不能打印日志、处理其他逻辑等。

  • 非阻塞 IO 代码:线程在 IO 未就绪时(如无新连接、无数据)可以继续执行其他任务。代码中明确体现了这一点:

    1
    2
    3
    4
    5
    // 模拟服务器处理其他任务
    static int count = 0;
    if (++count % 10 == 0) {
    std::cout << "服务器正在处理其他任务..." << std::endl;
    }

    即使没有连接或数据,服务器也能周期性输出信息,说明线程未被阻塞。

  1. 连接处理模式
  • 阻塞 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 系列函数(内核支持)

二、代码中体现的具体差异

  1. 事件检测机制(最核心区别)
  • 非阻塞 IO 代码:采用主动轮询方式检测 IO 事件。程序在无限循环中不断检查是否有新连接(accept)和数据(read),即使没有事件发生也会反复执行检查逻辑:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    while (true) {
    // 1. 轮询检查是否有新连接
    if (client_socket == -1) {
    int new_socket = accept(server_fd, ...); // 非阻塞accept
    // 处理连接(无论是否有新连接,都会执行这部分代码)
    }

    // 2. 轮询检查是否有数据可读
    if (client_socket != -1) {
    ssize_t valread = read(client_socket, ...); // 非阻塞read
    // 处理数据(无论是否有数据,都会执行这部分代码)
    }

    // 3. 为减少CPU占用,强制休眠(轮询的典型优化)
    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
    // 创建epoll实例(内核维护的事件表)
    int epoll_fd = epoll_create1(0);
    // 将服务器socket添加到epoll监控
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event);

    while (true) {
    // 阻塞等待内核通知事件(无事件时线程休眠,不消耗CPU)
    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 资源。

  1. 多连接处理能力
  • 非阻塞 IO 代码:难以高效处理多个连接。代码中通过 client_socket 变量只能记录一个客户端连接,即使扩展为数组存储多个连接,也需要在循环中逐个轮询检查每个连接(例如遍历数组调用 read),随着连接数增加,效率急剧下降:

    1
    2
    // 代码中仅支持单个客户端连接
    int client_socket = -1; // 只能记录一个客户端
  • IO 多路复用代码:天然支持高效处理多个连接。所有客户端连接的 socket 都会被添加到 epoll 监控(通过 epoll_ctl(EPOLL_CTL_ADD)),内核会跟踪每个连接的状态。事件发生时,epoll_wait 直接返回所有就绪的连接,程序无需遍历全部连接,只需处理内核返回的就绪列表:

    1
    2
    3
    // 新连接建立后,立即添加到epoll监控
    event.data.fd = new_socket;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event); // 监控新客户端

    这种方式下,即使有上千个连接,只要大部分处于空闲状态,程序也能高效处理(仅处理就绪的少数连接)。

  1. 线程阻塞行为
  • 非阻塞 IO 代码:线程在轮询间隙 “空转” 或强制休眠。为避免 CPU 被 100% 占用,代码中加入了 sleep_for(100ms) 强制休眠,但这会导致事件响应延迟(最长 100ms)。若不休眠,线程会无意义地反复执行轮询逻辑,浪费 CPU:

    1
    2
    // 短暂休眠,避免CPU占用过高(轮询的无奈之举)
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  • IO 多路复用代码:线程在无事件时阻塞休眠(由内核挂起)。epoll_wait 函数在无事件发生时会阻塞线程,此时线程不消耗 CPU 资源;当事件发生时,内核会唤醒线程立即处理,既无资源浪费,也无响应延迟:

    1
    2
    // -1表示无限阻塞,直到有事件发生(无CPU浪费)
    int nfds = epoll_wait(epoll_fd, events.data(), MAX_EVENTS, -1);
  1. 关键函数依赖
  • 非阻塞 IO 代码:仅依赖非阻塞模式设置(fcntl)和基础 IO 函数(acceptread),无额外内核机制依赖:

    1
    2
    3
    4
    // 核心是设置O_NONBLOCK标志
    void set_non_blocking(int fd) {
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    }
  • IO 多路复用代码:依赖 epoll 系列函数(内核提供的多路复用机制),包括 epoll_create1(创建监控实例)、epoll_ctl(添加 / 移除监控对象)、epoll_wait(等待事件):

    1
    2
    3
    int epoll_fd = epoll_create1(0);  // 创建epoll实例
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event); // 添加监控
    int nfds = epoll_wait(epoll_fd, ...); // 等待事件

三、总结

两段代码虽都使用了非阻塞 IO 操作(通过 set_non_blocking 设置 O_NONBLOCK),但核心区别在于事件检测方式

  • 非阻塞 IO 通过主动轮询检查事件,需要手动遍历所有连接,效率低且浪费 CPU,适合连接极少的场景;
  • IO 多路复用通过内核通知获取事件,由内核筛选就绪连接,无需轮询,效率高,适合高并发场景。

代码中最直观的差异是:非阻塞 IO 使用循环 + 休眠轮询,而 IO 多路复用使用 epoll 系列函数实现事件驱动处理。


操作系统:IO模型
http://surourou8.github.io/2025/10/06/操作系统:IO模型/
作者
Su Rourou
发布于
2025年10月6日
许可协议