一、IO的基本概念
1.IO的概念
I/O (input/output)也就是输入和输出,在冯诺依曼体系结构当中,将数据从输入设备拷贝到内存就叫作输入,将数据从内存拷贝到输出设备就叫作输出。

- 对文件进行的读写操作本质就是一种 IO,文件 IO 对应的外设就是磁盘。
- 对网络进行的读写操作本质也是一种 IO,网络 IO 对应的外设就是网卡。
2.IO的低效率问题
效率问题
IO最主要的问题就是效率极为低下。
以读取数据为例:
- 当底层缓冲区中没有数据 时,read/recv 就会阻塞等待
- 当底层缓冲区中有数据 时,read/recv 就会进行拷贝( read/recv 等一系列接口本质就是拷贝函数)
只要缓冲区中没有数据,read/recv 就会就会花费大量时间 进行阻塞等待,直到缓冲区中出现数据,然后进行拷贝,这就是一种低效的 IO 模式。
如何提升IO效率
**IO 的本质就是:等待(等待 IO 条件就绪) + 数据拷贝(当 IO 条件就绪后将数据拷贝到内存或外设)。**其中,"等待"时间通常远大于"拷贝"时间,尤其是在网络或磁盘IO中。
所以,高效IO的核心在于减少等待的时间
如何减少等待的时间呢?
这就与五种IO模型有关
二、五种IO模型
1.例子引入
下面我们来通过钓鱼的例子,将五种IO模型都串联起来介绍:
钓鱼 = 等 + 钓,我们要等🐟咬钩(等),再把🐟钓起来放进桶里(拷贝),如果等的时间短的话,那钓鱼的效率就高
核心比喻框架
-
钓鱼者 :代表应用程序/进程。
-
钓鱼 (等待鱼上钩 ):代表I/O操作(特别是"等待数据就绪"阶段)。
-
鱼 :代表需要读取或写入的数据。
-
河 :代表操作系统内核。
-
桶 :代表用户缓冲区(用于存放已拷贝的数据)。
五个人的不同钓鱼方式
1.张三 - 阻塞I/O
- 行为:用 1 根鱼竿,将鱼钩抛入水中后,就一直盯着浮标一动不动,直到有鱼上钩,就挥动鱼竿将鱼钓上来。
- 对应模型:发起I/O调用后,进程被**"阻塞"**,挂起等待,直到数据就绪并完成从内核到缓冲区的拷贝。
2.李四 - 非阻塞轮询I/O
- 行为:用 1 根鱼竿,将鱼钩抛入水中后,就可以去做其它事情了 ,然后定期观察浮标的动静,如果有鱼上钩就将鱼钓上来,否则就继续做其它事情。(非阻塞轮询式)
- 对应模型:发起I/O调用后,如果数据未就绪,内核立即返回一个错误,进程不会被挂起,可以继续执行 ,但需要不断轮询检查。
3.王五 - 信号驱动I/O
- 行为:用 1 根鱼竿,将鱼钩抛入水中后,在鱼竿顶部绑一个铃铛,就可以去做其它事情了 ,如果铃铛一响就知道有鱼上钩了,于是挥动鱼竿将鱼钓上来,否则就不管鱼竿。
- 对应模型:先开启信号驱动,然后进程可以继续执行 。当数据就绪时,内核会主动发送一个信号通知进程,进程再发起I/O操作进行数据拷贝。
4.赵六 - 多路复用(I/O多路转接)
- 行为:用 100 根鱼竿,将 100 个鱼钩抛入水中后,就定期观察这 100 个浮漂的动静,如果某个鱼竿有鱼上钩就挥动对应的鱼竿将鱼钓上来。
- 对应模型:使用 select、poll、epoll等系统调用,单个进程可以同时监视多个文件描述符。当其中任何一个就绪时,进程再进行处理。它高效的关键在于能用一次等待管理多个I/O。
5.田七 - 异步I/O
- 行为:田七是一个公司的领导,让自己的助理去钓鱼,当助理将鱼桶装满时再打电话告诉他。
- 对应模型:进程发起一个I/O请求后立即返回,内核会负责完成整个I/O操作(等待+拷贝),完成后通知进程"一切都已就绪,数据已在你的缓冲区里"。
关键问题
阻塞IO VS 非阻塞IO
阻塞IO与非阻塞IO的核心区别在于等待方式。
- **阻塞IO:**当IO条件不满足时(如数据未到达),调用会一直等待,直到条件满足才返回。
- **非阻塞IO:**当IO条件不满足时,立即返回错误或无数据状态,不等待。
因此,两者的主要区别是"是否主动等待",而非阻塞IO效率更高,因为它允许CPU执行其他任务。
钓鱼的效率
张三、李四、王五三个人的效率一样,赵六的效率最高
- 张三、李四、王五三个人的钓鱼的效率是一样的,因为他们每个人都是拿的一根鱼竿,鱼咬钩的概率是相等的,等待的时间相同
- 赵六拿了100根鱼竿,单位时间内鱼咬钩的概率更大,等待的时间更少
根据钓鱼的例子:
- 阻塞 IO、非阻塞 IO 和信号驱动 IO 不能提高 IO 的效率,因为没有减少 IO 中的等待时间,但非阻塞 IO 和信号驱动 IO 能提高整体做事的效率。
- 多路转接 IO 能够提高 IO 的效率,因为减少了 IO 中的等待时间
同步IO vs 异步IO
同步IO vs 异步IO 的核心区分标准是:线程/进程是否参与了"等"或"拷贝"阶段
- **同步IO:**用户进程参与IO的"等待"或"数据拷贝"阶段。例如阻塞、非阻塞、信号驱动、多路复用都属于同步IO。
- **异步IO:**用户进程只发起IO请求,后续的等待和数据拷贝由内核完成,完成后通知用户进程。
2.IO模型的工作流程
1.阻塞IO
阻塞 IO 是最常见的 IO 模型,所有的套接字,默认都是阻塞方式。
在内核将数据准备好之前,系统调用会一直等待。

2.非阻塞IO
即使内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码.
非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为**轮询,**这对 CPU 来说是较大的浪费,一般只有特定场景下才使用。

3.信号驱动IO
内核将数据准备好的时候,操作系统 使用 SIGIO 信号通知应用程序进行 IO 操作。
信号驱动 IO 是同步 IO 的一种。 因为当底层数据就绪时,当前进程或线程需要停下正在做的事情,转而进行数据的拷贝操作,当前进程或线程仍然需要参与 IO 过程。
4.IO多路转接
虽然从流程图上看起来和阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

5.异步IO
由内核在数据拷贝完成时,通知应用程序。(信号驱动IO是告诉应用程序何时可以开始拷贝数据)

3.小结
任何 IO 过程中,都包含两个步骤:等待 和拷贝。
在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让 IO 更高效,最核心的办法就是在单位时间内让等待的时间尽量少。
三、阻塞 IO
系统中大部分的接口都是阻塞式接口。
阻塞IO的例子:使用 read 函数从标准输入中读取数据
cpp
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
int main()
{
char buffer[1024];
while (true)
{
size_t size = read(0, buffer, sizeof(buffer) - 1);//阻塞式从标准输入读取数据
if (size < 0)
{
cerr << "read error" << endl;
break;
}
else
{
buffer[size] = 0;
cout << "echo#" << buffer << endl;
}
}
return 0;
}
**运行结果:**程序运行后,若不进行输入操作,底层数据不就绪, read 函数就会进行阻塞等待,该进程就会阻塞。

四、非阻塞 IO
1.设置非阻塞IO的方法
1.打开文件时,设置文件描述符fd为非阻塞模式
**在使用 open 函数打开文件时,如果传入 O_NONBLOCK 标志,则可以将返回的文件描述符 fd 设置为非阻塞模式。**此后,通过该 fd 进行读取或写入操作时,如果操作无法立即完成,调用不会阻塞等待,而是立即返回(例如返回错误或部分数据)。(普通文件、设备文件等)
cpp
int fd = open("/path/to/file", O_RDWR | O_NONBLOCK, 0644);
**在创建 socket 时,如果指定 SOCK_NONBLOCK 标志,则可以将返回的 socket 文件描述符 sockfd 设置为非阻塞模式。**此后,通过该 sockfd 进行读写操作时,如果操作无法立即完成,调用不会阻塞等待,而是立即返回。(网络通信)
cpp
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
调用open返回的文件描述符 fd 和调用socket返回的网络套接字 sockfd 本质都是文件描述符
2.网络通信时,单次的为数据发送或接收操作启用非阻塞模式
**在网络编程中,对于 send、recv、sendto、recvmsg 等特定 I/O 调用,可以使用 MSG_DONTWAIT 标志,从而临时地、仅针对本次调用启用非阻塞行为。**这意味着,即使网络套接字sockfd本身处于阻塞模式,本次调用也不会阻塞等待。
cpp
ssize_t n = recv(sockfd, buf, len, MSG_DONTWAIT);
在成功设置为非阻塞模式后,I/O 操作(如 read 或 write)的行为将分为以下两种情况:
-
如果操作无法立即完成,系统调用会立即返回 -1,并将错误码
errno设置为EAGAIN或EWOULDBLOCK。这并非真正的错误,而是预期的非阻塞行为,表示"资源暂时不可用,请稍后再试"。 -
如果操作确实遇到真正的错误(如无效文件描述符、权限问题等),系统调用同样返回 -1,但此时
errno会被设置为其他对应的错误码,以指示具体的错误原因。
3.打开文件后,设置文件描述符fd为非阻塞模式
这是最常用且通用的方法。对于已经打开的文件描述符fd,通过fcntl函数来设置文件描述符fd为非阻塞模式。
cpp
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl 函数根据传入的**cmd 参数值不同**,可执行多种不同的操作,并且其后续参数也随之变化。该函数主要提供以下五种功能:
-
复制文件描述符 (
cmd = F_DUPFD) -
获取/设置文件描述符标记 (
cmd = F_GETFD或F_SETFD) -
获取/设置文件状态标志 (
cmd = F_GETFL或F_SETFL) -
获取/设置异步 I/O 所有权 (
cmd = F_GETOWN或F_SETOWN) -
获取/设置记录锁 (
cmd = F_GETLK、F_SETLK或F_SETLKW)
在实际应用中,第三种功能(获取/设置文件状态标志) 最为常用,它允许我们动态修改一个已打开文件的属性,而无需重新打开文件。通过这一功能,可以方便地将文件描述符fd设置为非阻塞模式。
文件状态标志的核心操作
-
F_GETFL:获取 当前文件状态标志(例如O_RDONLY、O_NONBLOCK、O_APPEND等)。若要单独检查访问模式(只读、只写、读写),需要将返回值与掩码O_ACCMODE进行按位与操作。 -
F_SETFL:设置 文件状态标志。需要注意的是,并非所有标志均可修改,通常只能设置O_APPEND、O_NONBLOCK、O_ASYNC等。
修改文件描述符fd状态时,一般采用"读-改-写"的步骤:先调用 F_GETFL 获取当前fd状态标志,再修改所需位,最后用 F_SETFL 设置回去,以避免覆盖其他标志。
2.利用fcntl函数设置非阻塞
返回值说明
在非阻塞 I/O 中,对 read 等函数的返回值 n 进行处理是核心环节。根据返回值 n 的大小和错误码,可以判断当前操作的状态并采取相应措施:
1. 返回值
n > 0
- 含义 :成功读取到
n字节数据。2. 返回值
n == 0
含义 :到达文件末尾(End-of-File)。对于标准输入,这通常表示用户按下了**
Ctrl+D**(类 Unix 系统),输入流被关闭。处理:此时应跳出循环,结束读取操作。
3. 返回值
n < 0
含义 :操作未成功,需要根据
errno判断具体原因。错误码判断:
EAGAIN或EWOULDBLOCK(在绝大多数 Linux 系统上,这两个值等价**)**
表示当前没有数据可读,但并非错误,而是非阻塞模式下的正常情况:资源暂时不可用。
处理 :继续循环,尝试读取**(轮询)**。
EINTR
表示系统调用被信号中断。
处理 :立即重新读取,因为中断并不影响后续操作。
其他错误
例如无效文件描述符、权限问题等,属于真正的错误。
处理 :应根据错误类型记录日志、清理资源并退出循环。
代码示例
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
void SetNonBlock(int fd)// 将fd设置为非阻塞
{
int fl = fcntl(fd, F_GETFL);// 获取fd的状态标志
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 将O_NONBLOCK标志添加到fd的状态标志中
}
int main()
{
SetNonBlock(0);//将标准输入设置为非阻塞(0:标准输入,默认为阻塞模式)
char buffer[1024];
while (true)
{
ssize_t n = read(0, buffer, sizeof(buffer)-1);
// read的返回值n有三种情况:1. n>0 2. n<0 3. n=0
if (n > 0)// 读到了数据
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
else if (n < 0) // 读数据出错了/底层没有数据准备好
{
if(errno == EAGAIN || errno == EWOULDBLOCK) // 这两种错误码都表示底层没有数据准备好
{
std::cout << "数据没有准备好..." << std::endl;
sleep(2); // 等待一段时间后继续尝试读取数据
continue;
}
else if(errno == EINTR)// 读数据被信号中断了
{
continue;
}
else
{
// 其他错误
perror("read");
break;
}
}
else// n=0 读到了EOF,说明输入流被关闭了
{
break;
}
}
}
运行结果
通过ctrl+d关闭了输入流

五、多路转接IO
在单进程/单线程环境下,传统的阻塞 I/O 模型存在以下主要局限:
-
当进程阻塞在一个文件描述符上时,无法处理其他文件描述符的 I/O 事件。
-
若同时打开多个文件描述符(如多个网络连接),进程可能因某个描述符长期无数据而陷入永久阻塞。
-
若采用非阻塞轮询方式,则会导致 CPU 资源浪费。
I/O 多路复用技术正是为解决这些问题而设计的,其核心思想是:单个进程可以同时阻塞在多个文件描述符上,直到其中一个或多个描述符就绪(可读、可写或发生异常)。
Linux 提供了三种主流的 I/O 多路复用方案:
-
select:经典实现,具有较好的跨平台支持性。
-
poll:对 select 的改进,解决了文件描述符数量限制等问题。
-
epoll:Linux 特有的高性能方案,适合处理大量并发连接。
1.select
1.1 select的核心定位
select 是一个 I/O 多路复用 的等待与通知机制。它的核心任务是 "等" ------ 让应用进程能同时监控多个文件描述符(fd),并在其中任意一个或多个 fd 的 I/O 事件就绪时,通知上层应用。此时上层再调用函数进行I/O,就不再需要等待,直接拷贝即可。
1.2 select函数原型
cpp
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明
1.参数:
- nfds: 需要监视的文件描述符中,最大的文件描述符值 +1。
- readfds:输入输出型参数 ,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已就绪。(这个参数使用一次过后,需要进行重新设定)
- writefds:输入输出型参数 ,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已就绪。(这个参数使用一次过后,需要进行重新设定)
- exceptfds:输入输出型参数 ,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已就绪。(这个参数使用一次过后,需要进行重新设定)
- timeout:输入输出型参数 ,调用时由用户设置 select 的等待时间,返回时表示 timeout 的剩余时间。
2.fd_set 结构:
fd_set 本质是一个固定大小的 位图,用位图中对应的比特位来表示要监视的文件描述符。
在调用 select 函数之前,需要先通过 fd_set 结构定义对应的文件描述符集 ,并将需要监视的文件描述符添加到该集合中 。虽然添加过程本质上是对位图进行位操作,但系统提供了一组专门的宏接口:
cpp
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
使用此参数(以readfds为例):
- 输入的readfds:用户告诉内核哪些fd读事件需要监视
- 输出的readfds:内核要告诉用户哪些被监视的fd读事件已经就绪了
示例:

3. timeval 结构:
select 函数的最后一个参数 timeout 是一个指向 struct timeval 结构体的指针,用于指定等待的时间长度。
struct timeval 结构体:
cpp
struct timeval
{
time_t tv_sec; /* seconds 秒数*/
suseconds_t tv_usec; /* microseconds 微秒数*/
};
根据 timeout 参数的不同取值,select 可采用以下三种等待策略:
-
阻塞等待 :将
timeout设为nullptr,此时select会一直阻塞,直到有文件描述符就绪。 -
非阻塞轮询 :将
timeout设为{0, 0},此时无论被监视的文件描述符上的事件是否就绪,select会立即返回,实现非阻塞检查。 -
限时等待 :设置一个具体的时间值 (如
{5, 0}表示 5 秒),select会在该时间内阻塞等待:如果在指定的timeout时间内有文件描述符就绪,select函数会提前返回,此时timeout参数所指向的timeval结构体会被更新为能够等待的剩余时间;若超时则直接返回,返回值为0。(理解:timeout=timeout-(就绪时刻-开始等待时刻))
使用示例:


返回值说明
- n>0:函数调用成功,有n个文件描述符fd就绪。
- n=0:在当前等待时间内没有任何一个fd就绪,timeout 时间耗尽。
- n=-1:函数调用失败,同时错误码被设置。
select 调用失败时,错误码可能被设置为:
- EBADF:文件描述符为无效的或该文件已关闭。
- EINTR:此调用被信号所中断。
- EINVAL:参数 nfds 为负值。
- ENOMEM:核心内存不足。
只要有一个 fd 数据就绪或空间就绪,就可以进行返回了。
1.3 select的工作流程
- 进程初始化三个文件描述符集合(读、写、异常),指定需要监视的文件描述符
- 调用 select,进程阻塞(除非设置了超时)
- 内核监视所有指定的文件描述符,直到:某个描述符就绪(可读、可写或异常)或 超时时间到达
- 内核修改文件描述符集合,只保留就绪的描述符
- select 返回,进程通过FD_ISSET检查哪些描述符就绪并进行处理
1.4 select 版 TCP 服务器实现
下面我们实现一个基于 select 的 TCP 服务器,支持同时处理多个客户端连接。
整体设计思路
-
创建监听套接字
调用
socket()创建 TCP 套接字。 -
绑定地址与端口
调用
bind()将套接字绑定到指定 IP 和端口。 -
开始监听
调用
listen()将套接字设置为监听状态,等待客户端连接。 -
定义并初始化文件描述符数组
-
创建一个整型数组(如
_fd_array)用于保存所有需要监视的文件描述符。 -
将监听套接字加入数组,并记录当前最大文件描述符值(
maxfd)。
-
-
每次循环开始时重建读集合
-
定义
fd_set readfds,调用FD_ZERO清空。 -
遍历
_fd_array,将其中所有有效的文件描述符通过FD_SET添加到readfds中。 -
同时更新
maxfd为当前数组中最大描述符值。
-
-
调用 select 进行监视
-
调用
select(maxfd + 1, &readfds, nullptr, nullptr, timeout)。 -
若
timeout为nullptr,则阻塞直到有事件就绪;若设置为具体值,则限时等待;若为{0,0}则立即返回(非阻塞轮询)。
-
-
检查并处理就绪事件
-
监听套接字就绪
-
调用
accept接受新连接,获取新的已连接套接字connfd。 -
将
connfd添加到_fd_array中,并更新maxfd(若需要)。
-
-
已连接套接字就绪
调用
read读取数据。-
若返回值
< 0:需根据errno处理(如非阻塞模式下EAGAIN/EWOULDBLOCK可忽略,其他错误则关闭连接)。 -
若返回值
== 0:客户端关闭连接,调用close关闭该套接字,并从_fd_array中移除(可将其值设为 -1 或重新整理数组)。 -
若返回值
> 0:成功读取数据,打印或处理数据。
-
-
-
循环回到步骤 5,重复监视过程。
代码
- Socket.hpp:封装网络套接字socket接口
- SocketServer.hpp:封装select版服务器接口
- main.cc:主函数
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
class Sock
{
private:
const static int gbacklog = 10;//监听队列的长度
public:
Sock() {}
~Sock() {}
static int Socket()//创建套接字
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);//socket函数创建一个套接字,返回一个文件描述符,如果出错则返回-1
if (listensock < 0)
{
exit(2);
}
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));//打开端口复用
return listensock;
}
static void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")//绑定套接字到指定的IP地址和端口号
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(3);
}
}
static void Listen(int sock)//监听套接字,等待客户端连接请求
{
if (listen(sock, gbacklog) < 0)
{
exit(4);
}
}
static int Accept(int listensock, std::string *ip, uint16_t *port)//接受客户端连接请求
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicessock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicessock < 0)
{
return -1;
}
if (port)//获取客户端的端口号
*port = ntohs(src.sin_port);
if (ip)//获取客户端的IP地址
*ip = inet_ntoa(src.sin_addr);
return servicessock;
}
};
SocketServer.hpp
cpp
#pragma once
#include <iostream>
#include "Socket.hpp"
#include <sys/select.h>
#include <sys/time.h>
const uint16_t default_port = 8080; // 默认端口
const std::string default_ip = "0.0.0.0"; // 默认IP
const int fd_num_max = sizeof(fd_set) * 8; // 最大支持的文件描述符数
const int default_fd = -1; // 无效描述符标记
class SelectServer
{
private:
uint16_t port_; // 端口号
int listensock_; // 监听套接字
int fd_arry[fd_num_max]; // 存储所有需要监视的文件描述符
public:
SelectServer(const uint16_t port = default_port)
: port_(port)
{
// 初始化辅助数组
for (int i = 0; i < fd_num_max; ++i)
{
fd_arry[i] = default_fd;
}
}
// 初始化服务器
void Init()
{
listensock_ = Sock::Socket();
Sock::Bind(listensock_, port_);
Sock::Listen(listensock_);
std::cout << "Server initialized, listening on port " << port_ << std::endl;
}
// 启动服务器
void Start()
{
fd_arry[0] = listensock_; // 监听套接字放在数组首位
while (true)
{
fd_set rfds;
FD_ZERO(&rfds);
// 将所有有效的文件描述符添加到监视集合中,并找到当前最大的文件描述符
int maxfd = fd_arry[0];
for (int i = 0; i < fd_num_max; ++i)
{
if (fd_arry[i] == default_fd)
continue;
FD_SET(fd_arry[i], &rfds);
if (fd_arry[i] > maxfd)
{
maxfd = fd_arry[i];
}
}
// 调用select等待事件发生
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
if (n < 0)//select出错
{
std::cerr << "select error: " << strerror(errno) << std::endl;
continue;
}
else if (n == 0)//select超时
{
std::cout << "select timeout" << std::endl;
continue;
}
else//有事件发生
{
// 处理就绪事件
HandlerEvent(rfds);
}
}
}
private:
// 接受新连接
void Accept()
{
std::string clientip;
uint16_t clientport;
int sockfd = Sock::Accept(listensock_, &clientip, &clientport);
if (sockfd < 0)
return;
// 查找空位存放新连接
int i;
for (i = 1; i < fd_num_max; ++i)
{ // 从1开始,0号位是监听套接字
if (fd_arry[i] == default_fd)
break;
}
if (i < fd_num_max)//找到空位,存放新连接
{
fd_arry[i] = sockfd;
std::cout << "New connection: " << clientip << ":" << clientport
<< " (fd=" << sockfd << ")" << std::endl;
PrintOnlineFds();
}
else//没有空位,拒绝新连接
{
std::cerr << "Too many connections, closing new one" << std::endl;
close(sockfd);
}
}
// 处理客户端数据
void HandleClientData(int fd, int index)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "Received from fd " << fd << ": " << buffer << std::endl;
}
else if (n == 0)
{
std::cout << "Client fd " << fd << " disconnected" << std::endl;
close(fd);
fd_arry[index] = default_fd;
PrintOnlineFds();
}
else
{
std::cerr << "Read error on fd " << fd << ": " << strerror(errno) << std::endl;
close(fd);
fd_arry[index] = default_fd;
PrintOnlineFds();
}
}
// 处理就绪事件
void HandlerEvent(fd_set &rfds)
{
for (int i = 0; i < fd_num_max; ++i)
{
int fd = fd_arry[i];
if (fd == default_fd)
continue;
if (FD_ISSET(fd, &rfds))//判断fd是否就绪
{
// 监听套接字就绪:新连接
if (fd == listensock_)
{
Accept();
}
// 客户端套接字就绪:数据到达
else
{
HandleClientData(fd, i);
}
}
}
}
// 打印在线文件描述符
void PrintOnlineFds()
{
std::cout << "Online fds: ";
for (int i = 0; i < fd_num_max; ++i)
{
if (fd_arry[i] != default_fd)
{
std::cout << fd_arry[i] << " ";
}
}
std::cout << std::endl;
}
};
cpp
#include "SelectServer.hpp"
#include <memory>
int main(int argc, char *argv[]) {
if(argc!=2){
std::cerr<<"Usage: "<<argv[0]<<" <port>"<<std::endl;
return 1;
}
// 创建并启动服务器
std::unique_ptr<SelectServer> server(new SelectServer(std::stoi(argv[1])));
server->Init();
server->Start();
return 0;
}
关键点
参数输入输出特性
select 函数的 readfds、writefds、exceptfds 以及 timeout 参数均为输入输出型 参数------内核在返回时会修改它们,因此每次调用 select 前都必须重新设置这些参数。
最大文件描述符值
select 的第一个参数需传入所有被监视描述符中的最大值加1 (即 maxfd + 1)。由于描述符集合会动态变化(如新增或移除连接),每次重建集合时需重新计算 maxfd。
辅助数组的作用与维护
为了高效重建 fd_set,通常使用一个辅助数组 (如 fd_array)保存所有需要监视的描述符:
-
数组的第一个元素可固定为监听套接字,便于区分新连接事件与普通数据事件。
-
每次循环开始前,遍历该数组,将有效的描述符加入
readfds,并同步更新maxfd。 -
当客户端断开连接时,关闭对应套接字,并将数组中的位置重置为无效值(如
FD_NONE),同时重新计算maxfd。
事件处理机制
-
监听套接字就绪 :调用
accept接受新连接,并将返回的已连接套接字加入辅助数组。 -
客户端套接字就绪 :调用
read读取数据:-
若返回值
> 0:正常处理数据。 -
若返回值
== 0:表示对端关闭连接,需关闭该套接字并从辅助数组中移除。 -
若返回值
< 0:根据errno处理错误(如EAGAIN/EWOULDBLOCK可忽略,其他错误则关闭连接)。
-
多事件监视
若需要同时监视写事件,需额外定义 writefds 及其对应的辅助数组,并在每次循环前分别设置读集合和写集合。
错误处理
务必处理 read 返回 0 的情况(对端关闭),及时释放资源并更新辅助数组与 maxfd,避免资源泄漏或无效监视。
运行结果
单个客户端连接

多个客户端连接

1.5 select 的优缺点分析
优点
-
多文件描述符并发等待
select 允许单进程同时等待多个文件描述符,将"等"的时间重叠,显著提高 I/O 效率。它仅负责等待事件就绪,实际的 I/O 操作(如
accept、read、write)在就绪后执行,不会阻塞进程。 -
资源利用率高
在面对大量连接但仅有少量活跃连接时,select 能够避免为每个连接分配独立线程/进程,节省系统资源。
-
事件类型丰富
支持同时监视读、写和异常事件,满足多样化的 I/O 需求。
缺点
-
文件描述符数量受限
select 能监控的描述符数量由**
fd_set的位图大小** 决定,通常上限为 1024(sizeof(fd_set)*8),但是实际上进程自身可打开更多文件描述符(通过ulimit调整)。这也意味着除去监听套接字,服务器最多只能处理 1023 个客户端连接,难以应对高并发场景。 -
用户态/内核态拷贝开销大
每次调用 select 时,都需要将整个
fd_set集合从用户空间拷贝到内核空间;返回时,内核又将修改后的集合拷回用户空间。当监控的描述符数量很大时,这种频繁的内存拷贝会消耗大量 CPU 时间,成为性能瓶颈。 -
内核线性扫描所有描述符
select 在内核中采用线性遍历的方式检查每个描述符的状态,时间复杂度为 O(n)。即使只有少数描述符活跃,内核仍需扫描整个集合,导致效率随描述符数量增加而线性下降。
-
编程不便,维护复杂
-
每次调用 select 前都必须手动重建描述符集合(使用
FD_ZERO、FD_SET等宏),因为内核会修改传入的集合。 -
需要借助第三方数组(如
fd_array)保存所有待监控的描述符,并在每次循环中遍历该数组以重建集合、更新最大描述符值,增加了代码复杂度。 -
处理就绪事件时仍需遍历整个集合或依赖辅助数组,逻辑较为繁琐。
-
-
参数输入输出特性导致重复设置
readfds、writefds、exceptfds和timeout均为输入输出参数,每次返回后内容被改写,因此必须在下次调用前重新初始化,进一步增加了编程负担。
2.poll
正是由于 select的上述缺点,我们就需要使用另一种多路转接的方案------poll
1.1 poll的核心定位
poll函数的功能与select完全一致 :监视并等待多个文件描述符的状态变化,仅关注 I/O 过程中的 "等待" 阶段,一旦某个 fd 上的事件发生,poll 就会返回并通知应用程序。
1.2 从 select 缺陷到 poll 的诞生
select作为早期 I/O 多路复用技术,存在两个核心缺陷,这直接推动了poll函数的出现:
- **文件描述符(fd)数量上限:**select依赖fd_set位图结构(内核固定大小),默认上限通常为 1024(需修改内核参数才能调整),超过则报错。
- **参数输入输出耦合:**select的fd_set既是输入参数(指定要监视的 fd),也是输出参数(标记就绪的 fd)。每次调用select前,需重新初始化fd_set,导致用户态频繁遍历和拷贝,增加额外开销。
poll函数针对这两个缺陷做了改进:
- 突破 fd 数量上限(理论无限制,仅受系统资源约束);
- 分离输入与输出参数(通过pollfd结构体的events和revents字段),无需每次调用前重新设置监视 fd。
但需注意:poll与select的核心逻辑一致------ 均通过轮询检测 fd 状态,效率随 fd 数量增加而线性下降。
1.3 poll函数原型
cpp
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明
1.参数
- fds:一个 poll 函数监视的结构列表,每一个元素都包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合。
- nfds :表示 fds 数组的有效长度。(
nfds_t通常是unsigned int或unsigned long的别名) - timeout:表示 poll 函数的超时时间,单位是毫秒(ms)。
2. 参数 timeout 的取值
- -1:poll 调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
- 0:poll 调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,poll 检测后都会立即返回。
- >0 (特定的时间值):poll 调用后在指定的时间内进行阻塞等待,若被监视的文件描述符上没有事件就绪,则在该时间后 poll 进行超时返回。
3. pollfd 结构
cpp
struct pollfd {
int fd; // 待监视的文件描述符(如socket、管道、普通文件)
short events; // 输入参数:用户要监视的事件(位掩码)
short revents; // 输出参数:内核返回的就绪事件(位掩码,用户无需初始化)
};
- fd :特定的文件描述符,若设置为负值则忽略 events 字段并且 revents 字段返回 0。
- events:需要监视该文件描述符上的哪些事件。
- revents:poll 函数返回时告知用户该文件描述符上的哪些事件已经就绪。
events(输入) 和 revents(输出) 的取值:
以宏的方式表示事件,支持多种事件的组合(用|运算符)。

pollfd 结构的使用方法:
- 在调用 poll 函数之前,通过 |(或运算符) 将要监视的事件添加到events成员中
- 在 poll 函数返回后,通过 &(与运算符)检测revents成员中是否包含特定事件,以得知对应文件描述符的特定事件是否就绪
poll将输入参数和输出参数进行了分离,在调用poll前不再需要重新设置pollfd。
返回值
poll的返回值直接反映调用结果,需根据返回值做不同处理:
| 返回值 | 含义 | 后续操作 |
|---|---|---|
| -1 | 调用失败(如 fd 非法、内存不足) | 检查errno(如 EBADF、EINTR),处理错误 |
| 0 | 超时(无任何 fd 就绪) | 无需处理 fd,可重新调用poll |
| >0 | 就绪 fd 的数量(revents非 0 的结构体个数) |
遍历pollfd数组,处理revents非 0 的 fd |
1.4 poll的工作流程
1.调用前:用户填充 fds数组。
对每个关心的 fd,设置其 events字段(例如 POLLIN表示关心可读事件)。此时,fd和 events有效。这是"用户告诉内核":请帮我监视这个 fd上的这些事件。
2.调用 poll:内核开始监视,进程进入等待。
3.调用返回后:内核填充 revents字段。
检查每个 fd的 revents字段。如果某位被置位(如 revents & POLLIN为真),表示对应事件已就绪。此时,fd和 revents有效。这是"内核告诉用户":你让我监视的 fd上,这些事件已经就绪了,可以处理了!
1.5 poll 函数简单示例
何用poll监视一个文件是否可读,超时时间为 5 秒:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
int main() {
// 以只读模式打开文件(需确保test.txt存在)
int fd = open("test.txt", O_RDONLY);
if (fd < 0) {
perror("Failed to open file");
return EXIT_FAILURE;
}
// 初始化pollfd结构体(监视fd的可读事件)
struct pollfd fds[1];
fds[0].fd = fd;
fds[0].events = POLLIN; // 关注可读事件
fds[0].revents = 0; // 输出参数,可省略初始化(内核会覆盖)
int timeout = 5000; // 超时5秒
int ret = poll(fds, 1, timeout);
if (ret == -1) {
perror("poll failed");
close(fd);
return EXIT_FAILURE;
} else if (ret == 0) {
printf("No data within 5 seconds\n");
} else if (fds[0].revents & POLLIN) {
// 读取文件内容并打印
char buf[1024] = {0};
ssize_t bytes_read = read(fd, buf, sizeof(buf) - 1);
if (bytes_read > 0) {
printf("Read %zd bytes: %s\n", bytes_read, buf);
}
}
close(fd);
return EXIT_SUCCESS;
}
对于普通文件 (如通过 open 打开的磁盘文件),文件描述符总是处于"可读"状态。因此,即使文件为空, poll 也会认为 POLLIN 事件就绪,并立即返回,而不会等待超时。这与管道、套接字等设备不同,无数据时POLLIN 事件不会就绪,poll会阻塞等待超时。
1.6 poll 版 TCP 服务器实现
poll版 TCP 服务器的逻辑与select版类似,核心差异在于用pollfd数组管理 fd,而非fd_set位图。
整体设计思路
- 初始化监听 socket:创建、绑定、监听端口;
- 管理 pollfd 数组:将监听 socket 加入数组,设置监视事件(POLLIN);
- 循环调用 poll:阻塞等待 fd 就绪,处理超时、错误、就绪三种情况;
- 事件处理:
- 监听 socket 就绪:调用accept接受新连接,将新 socket 加入pollfd数组;
- 通信 socket 就绪:调用read读取客户端数据,处理断开连接或错误。
代码
- Sock.hpp:封装网络套接字socket接口
- PollServer.hpp:封装poll版服务器接口
- main.cc:主函数
Sock.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
class Sock
{
private:
const static int gbacklog = 10;//监听队列的长度
public:
Sock() {}
~Sock() {}
static int Socket()//创建套接字
{
int listensock = socket(AF_INET, SOCK_STREAM, 0);//socket函数创建一个套接字,返回一个文件描述符,如果出错则返回-1
if (listensock < 0)
{
exit(2);
}
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));//打开端口复用
return listensock;
}
static void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")//绑定套接字到指定的IP地址和端口号
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
exit(3);
}
}
static void Listen(int sock)//监听套接字,等待客户端连接请求
{
if (listen(sock, gbacklog) < 0)
{
exit(4);
}
}
static int Accept(int listensock, std::string *ip, uint16_t *port)//接受客户端连接请求
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicessock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicessock < 0)
{
return -1;
}
if (port)//获取客户端的端口号
*port = ntohs(src.sin_port);
if (ip)//获取客户端的IP地址
*ip = inet_ntoa(src.sin_addr);
return servicessock;
}
};
PollServer.hpp
cpp
#pragma once
#include <iostream>
#include "Socket.hpp"
#include <sys/select.h>
#include <sys/time.h>
#include <poll.h>
const uint16_t default_port = 8080; // 默认端口
const std::string default_ip = "0.0.0.0"; // 默认IP
const int fd_num_max = 64; // 最大文件描述符数量(可自定义扩容)
const int default_fd = -1; // 无效描述符标记
class PollServer
{
private:
uint16_t port_; // 端口号
int listensock_; // 监听套接字
struct pollfd fd_arry[fd_num_max]; // 存储所有需要监视的文件描述符
public:
PollServer(const uint16_t port = default_port)
: port_(port)
{
// 初始化辅助数组
for (int i = 0; i < fd_num_max; ++i)
{
fd_arry[i] = {default_fd, POLLIN, 0}; // 初始化为无效描述符,关注可读事件
}
}
// 初始化服务器
void Init()
{
listensock_ = Sock::Socket();
Sock::Bind(listensock_, port_);
Sock::Listen(listensock_);
std::cout << "Server initialized, listening on port " << port_ << std::endl;
}
// 启动服务器
void Start()
{
fd_arry[0].fd = listensock_; // 监听套接字放在数组首位
fd_arry[0].events = POLLIN; // 关注可读事件
fd_arry[0].revents = 0; // 初始化输出事件
while (true)
{
// 调用poll等待事件发生
int n = poll(fd_arry, fd_num_max, -1); // -1表示阻塞等待
if (n < 0)
{
std::cerr << "poll error: " << strerror(errno) << std::endl;
continue;
}
else if (n == 0)
{
std::cout << "poll timeout" << std::endl;
continue;
}
else
{
// 处理就绪事件
HandlerEvent(n);
}
}
}
private:
// 接受新连接
void Accept()
{
std::string clientip;
uint16_t clientport;
int sockfd = Sock::Accept(listensock_, &clientip, &clientport);
if (sockfd < 0)
return;
// 查找空位存放新连接
int i;
for (i = 1; i < fd_num_max; ++i)
{ // 从1开始,0号位是监听套接字
if (fd_arry[i].fd == default_fd)
break;
}
if (i < fd_num_max) // 找到空位,存放新连接
{
fd_arry[i].fd = sockfd;
fd_arry[i].events = POLLIN; // 关注可读事件
fd_arry[i].revents = 0; // 初始化输出事件
std::cout << "New connection: " << clientip << ":" << clientport
<< " (fd=" << sockfd << ")" << std::endl;
PrintOnlineFds();
}
else // 没有空位,拒绝新连接
{
std::cerr << "Too many connections, closing new one" << std::endl;
close(sockfd);
}
}
// 处理客户端数据
void HandleClientData(int fd, int index)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "Received from fd " << fd << ": " << buffer << std::endl;
}
else if (n == 0)
{
std::cout << "Client fd " << fd << " disconnected" << std::endl;
close(fd);
fd_arry[index].fd = default_fd;
PrintOnlineFds();
}
else
{
std::cerr << "Read error on fd " << fd << ": " << strerror(errno) << std::endl;
close(fd);
fd_arry[index].fd = default_fd;
PrintOnlineFds();
}
}
// 处理就绪事件
void HandlerEvent(int n)
{
for (int i = 0; i < fd_num_max; ++i)
{
int fd = fd_arry[i].fd;
if (fd == default_fd)
break; // 后续都是无效描述符,直接跳出循环
if (fd_arry[i].revents & POLLIN) // 判断fd是否就绪
{
// 监听套接字就绪:新连接
if (fd == listensock_)
{
Accept();
}
// 客户端套接字就绪:数据到达
else
{
HandleClientData(fd, i);
}
}
}
}
// 打印在线文件描述符
void PrintOnlineFds()
{
std::cout << "Online fds: ";
for (int i = 0; i < fd_num_max; ++i)
{
if (fd_arry[i].fd != default_fd)
{
std::cout << fd_arry[i].fd << " ";
}
}
std::cout << std::endl;
}
};
cpp
#include "PollServer.hpp"
#include <memory>
int main(int argc, char *argv[]) {
if(argc!=2){
std::cerr<<"Usage: "<<argv[0]<<" <port>"<<std::endl;
return 1;
}
// 创建并启动服务器
std::unique_ptr<PollServer> server(new PollServer(std::stoi(argv[1])));
server->Init();
server->Start();
return 0;
}
运行结果

1.7 poll 的优缺点
优点
1.无文件描述符数量限制
poll 使用 struct pollfd 数组管理监视的描述符,数组大小由用户自定义,理论上只受系统内存和进程可打开文件描述符上限的限制,彻底解决了 select 位图自身的大小限制。
2.输入输出参数分离,接口更易用
pollfd 结构中的 events(输入,用户设置关注的事件)和 revents(输出,内核返回实际发生的事件)相互独立,避免了 select 中每次调用前必须重新设置整个集合的繁琐操作,减少了编程错误。
3.效率相对 select 略有提升
poll 只需遍历数组中的有效元素,而非像 select 那样遍历到位图的最大描述符值,因此在描述符数值较大但数量不多时有一定优势。
4.异常事件的处理更加简洁
内核会自动在 revents 字段中标记异常状态(如 POLLERR 表示错误、POLLHUP 表示挂起等),无需像 select 那样单独设置异常集合进行监视。
缺点
-
用户态与内核态的数据拷贝开销大
每次调用 poll,都需要将整个
pollfd数组从用户空间拷贝到内核空间。当监控的描述符数量很大时,频繁的内存拷贝会显著影响性能。 -
内核仍需线性遍历所有描述符
与 select 类似,poll 在内核中同样需要遍历所有传入的描述符,检查其状态,时间复杂度为 O(n)。即使只有少量描述符活跃,内核也要扫描全部,导致性能随描述符数量增加而线性下降。
-
编程复杂度依然存在
尽管比 select 易用,但用户仍需手动维护
pollfd数组(如添加、删除、标记无效描述符),代码复杂度高于 epoll 等更高级的接口。
总结
poll 作为 select 的改进版,解决了"文件描述符数量限制"和"参数耦合"问题,但其轮询本质未变,仍适用于中低并发场景(如描述符数量在几千以内)。理解 poll 的设计有助于掌握 I/O 多路复用的演进脉络,并为学习 epoll 奠定基础。
3.epoll
3.1 初识epoll
**核心定位:**epoll 是一种基于多个 fd 的就绪事件通知机制,通过监控这些 fd 上的事件(如可读、可写),在事件就绪时通知应用程序,从而避免阻塞等待。这与 select 和 poll 的目标一致,但设计更高效。
历史背景:epoll 是在 Linux 内核 2.5.44 版本中引入的,按照 man 手册的说法,它是为处理大批量句柄(fd)而改进的 poll,旨在解决传统方法在高并发场景下的局限性。
**实现与使用:**epoll 的实现原理、接口使用与 select 和 poll 差别非常大。它通过 epoll_create、epoll_ctl 和 epoll_wait 三个核心系统调用实现,拥有更灵活的事件注册和触发机制,减少了遍历所有 fd 的开销,从而提升了性能。
3.2 epoll相关的系统调用
epoll 的功能通过三个核心系统调用实现,三者分工明确、协作紧密,共同构成了 "创建环境 - 注册事件 - 等待触发" 的完整流程。
epoll_create:搭建事件监控舞台
epoll_create 的核心作用是向内核申请一块专用资源,创建一个 epoll 实例(本质是内核中的eventpoll结构体),用于后续管理待监控的文件描述符(fd)
epoll_create
cppint epoll_create(int size);参数:
size:已废弃,只需要传大于0的数即可
返回值:
成功返回 epoll 描述符 epfd
失败返回-1,并设置错误码:
EMFILE:用户打开的 fd 数量达到上限。
ENFILE:系统全局打开的 fd 数量达到上限。
ENOMEM:内存不足,无法创建 epoll 实例。
注意: 当不再使用时,须调用 close 函数关闭 epoll 模型对应的文件描述符,当所有引用 epoll 实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源。
epoll_ctl:管理事件监控名单
poll_ctl是 epoll 的 "事件管理器",负责向 epoll 实例添加、修改或删除待监控的 fd 及其事件,是连接用户需求与内核监控的桥梁。
epoll_ctl
cppint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);参数
- **epfd:**epoll_create返回的 epoll 实例 epfd,标识要操作的目标 epoll 环境
- op: 操作类型:EPOLL_CTL_ADD: 添加 fd 到 epoll 监控;EPOLL_CTL_MOD: 修改已监控 fd 的事件;**EPOLL_CTL_DEL:**从 epoll 中删除 fd
- **fd:**待操作的目标 fd(如 socket、管道等)
- **event:**指向struct epoll_event的指针,描述 fd 的监控事件类型及关联数据
关键结构体 :epoll_event
struct epoll_event 结构中有两个成员:
- 第一个成员 events 表示监视的事件类型(输入)或就绪的事件(输出)(读事件/写事件......)
- 第二个成员 data 为联合体结构,一般选择使用该结构中的 fd,表示需要监听的文件描述符
cpp// 事件关联的数据(联合体,按需使用) typedef union epoll_data { void *ptr; // 指向用户自定义数据(如连接上下文) int fd; // 关联的fd(最常用,直接标识触发事件的fd) uint32_t u32; // 32位整数 uint64_t u64; // 64位整数 } epoll_data_t; // 事件结构体 struct epoll_event { uint32_t events; // 监控的事件类型(位掩码) epoll_data_t data; // 事件触发后返回的关联数据 };常用事件类型(events 参数):
- EPOLLIN:fd 可读(如 socket 收到数据)。
- EPOLLOUT:fd 可写(如 socket 发送缓冲区空闲)。
- EPOLLERR:fd 发生错误(无需主动设置,内核会自动通知)。
- EPOLLHUP:fd 被挂断(如客户端关闭连接,无需主动设置)。
- EPOLLET:边缘触发模式(ET,高效模式,需配合非阻塞 I/O)。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需继续监听该文件描述符 socket 的话,需重新将该文件描述符添加到 EPOLL 队列里。
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(表示应该有带外数据到来)。
返回值:
成功0,失败-1
epoll_wait:等待并获取就绪事件
**epoll_wait**是 epoll 的 "事件收集器",负责阻塞等待已注册的事件触发,并将就绪事件返回给用户空间,是 epoll 高效性的直接体现。
epoll_wait
cppint epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);参数
- epfd:目标 epoll 实例的 fd。
- events:用户预先分配的struct epoll_event数组,用于存储内核返回的多个就绪事件(非空指针)。
- maxevents:epoll_events数组的最大长度(即一次最多能处理的事件数,需与数组大小一致)。
- timeout:超时时间(毫秒):-1:无限阻塞,直到有事件触发;0:立即返回,不阻塞;- >0:等待指定毫秒后超时返回。
返回值与错误码
成功:返回就绪事件的数量(0 表示超时,无事件就绪)。
失败:返回 - 1,并设置errno:
- EBADF:epfd不是有效的 epoll 实例 fd。
- EINTR:系统调用被信号中断(需处理,避免程序异常退出)。
- EINVAL:maxevents≤0 或epfd无效。
3.3 epoll的底层原理
1.硬件基础:网络数据的接收流程
要理解 epoll,首先需要明确计算机如何接收网络数据,这是所有 I/O 操作的起点:
- **数据到达网卡:**网线传来的数据包被网卡接收,暂存到网卡缓冲区。
- **DMA 传输:**通过 DMA(直接内存访问)技术,数据从网卡缓冲区直接写入内存的内核缓冲区(无需 CPU 参与,减少 CPU 开销)。
- **触发中断:**数据写入完成后,网卡向 CPU 发送中断信号,告知 "有新数据待处理"。
2.中断机制:操作系统感知数据的关键
CPU 收到网卡中断信号后,会暂停当前任务,转而去处理中断(确保数据不丢失),流程如下:
- CPU 查找 "中断向量表",找到网卡中断对应的处理程序。
- 执行中断处理程序:将内存中的数据解析后,传递到对应协议栈(如 TCP/UDP)。
- 数据到达传输层后,被存入对应 socket 的接收缓冲区。
- 中断处理完成,CPU 恢复之前的任务。
这一机制确保了硬件事件能被及时响应,是 epoll "被动通知" 的基础。
3.回调机制:epoll 高效检测就绪事件的核心
epoll 最关键的优化是回调机制,彻底解决了 select/poll "遍历所有 fd 检测就绪" 的性能瓶颈:
- **注册回调:**当通过epoll_ctl添加 fd 时,内核会为该 fd 注册一个回调函数(ep_poll_callback)。
- **事件触发时自动回调:**当 fd 对应的事件就绪(如 socket 收到数据),内核会自动调用ep_poll_callback。
- **加入就绪链表:**回调函数将该 fd 对应的epitem(epoll 项)添加到就绪链表(rdllist)中。
整个过程无需遍历所有 fd,完全由内核自动完成,时间复杂度为 O (1)。
4.数据结构选择:红黑树与就绪链表的协同
epoll 使用 "红黑树 + 就绪链表" 的组合,兼顾了 "高效管理 fd" 和 "快速获取就绪事件" 的需求:
- 红黑树(rbr):存储所有待监控的 fd,支持 O (log n) 的增删改查,解决了 "大量 fd 管理" 的效率问题。相比 AVL 树,红黑树的旋转操作更少,在频繁修改场景下性能更稳定。
- 就绪链表(rdllist):暂存已就绪的 fd,epoll_wait直接从该链表获取结果,无需遍历红黑树,确保了 "获取就绪事件" 的 O (1) 时间复杂度。
当调用 epoll_create 函数时,Linux 内核会创建一个 eventpoll 结构体 ,即 epoll 模型,eventpoll 结构体中的成员rbr(红黑树)、rdlist(就绪链表) 也在这时被创建
cpp
struct eventpoll{
...
//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
struct rb_root rbr;
//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
struct list_head rdlist;
...
}
**在 epoll 的内部实现中,每个被监控的事件都对应一个 epitem 结构体。**该结构体包含以下关键成员,用于组织和管理事件:
-
rbn:作为红黑树的节点 ,用于将epitem挂载到内核维护的红黑树中,以便快速查找指定的文件描述符。 -
rdllink:作为双向链表的节点 ,用于将epitem链接到就绪队列(ready list)中,表示该描述符上有事件发生。 -
ffd:记录被监控的文件描述符值。 -
event:记录该文件描述符所关注的事件类型(如可读、可写等)。
cpp
struct epitem{
struct rb_node rbn; //红黑树节点
struct list_head rdllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
这也说明就绪链表中的节点并不是单独开辟空间创建的。
注意 :红黑树 是一种二叉搜索树,必须有键值 key ,文件描述符fd就是红黑树 key 值,在增/删/改监控事件时被使用
三个系统调用在内核中的主要动作
调用epoll_create时,内核会完成三件关键事:
- 分配并初始化struct eventpoll结构体(epoll 实例的核心数据结构)。
- 创建红黑树(rbr):用于高效存储和管理所有待监控的 fd,支持 O (log n) 的增删改查。
- 创建就绪链表(rdllist):用于暂存已就绪的事件,epoll_wait直接从该链表获取结果,避免遍历全部 fd。
调用epoll_ctl 时,以EPOLL_CTL_ADD(添加 fd)为例,内核会执行:
- 检查 fd 是否已在红黑树中(避免重复添加)。
- 若不存在,分配struct epitem结构体(存储 fd、事件、关联的 epoll 实例等信息)。
- 将epitem插入红黑树,并为 fd 注册内核回调函数(ep_poll_callback,用于事件就绪时触发)。
调用epoll_wait时:
1.检查就绪链表(rdllist)是否有数据:
- 若有数据:将就绪事件批量复制到用户空间的events数组(最多maxevents个)。
- 若无数据:根据timeout参数决定是否阻塞(阻塞时将进程挂到等待队列wq)。
2.若进程被唤醒(有事件就绪或超时):返回就绪事件数量,用户程序遍历events数组处理即可。

与 select/poll 的性能对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 数据结构 | 固定大小 fd_set | 动态数组 | 红黑树 + 就绪链表 |
| 事件检测方式 | 遍历所有 fd(O (n)) | 遍历所有 fd(O (n)) | 回调 + 就绪链表(O (1)) |
| 最大 fd 数量 | 受限(默认 1024) | 理论无限制(受内存限制) | 无限制(仅受系统资源限制) |
| 用户 / 内核数据复制 | 每次调用复制所有 fd | 每次调用复制所有 fd | 仅添加 / 删除时复制一次 |
| 适用场景 | 少量 fd、低并发 | 中等 fd、中并发 | 大量 fd、高并发 |
3.4 epoll的工作方式
epoll 是 Linux 下高效的 I/O 多路复用机制,支持两种工作模式:水平触发(LT,Level Triggered) 和 边缘触发(ET,Edge Triggered) 。这两种模式的核心区别在于事件通知的时机 和处理要求。
LT模式
-
默认模式:epoll 默认采用 LT 模式,与 select/poll 的行为一致。
-
通知规则 :只要文件描述符上还有未处理的事件(如读缓冲区仍有数据),每次调用
epoll_wait都会返回该事件,持续通知直到事件被完全处理。 -
数据处理 :可以分多次读取或写入,不必一次性完成。例如,读缓冲区有 2KB 数据,第一次只读 1KB,剩余 1KB 会在下一次
epoll_wait中再次触发通知。 -
文件描述符要求 :支持阻塞和非阻塞模式,编程简单,容错性高。
理解:
水平触发(LT)模式下,只要底层事件保持就绪状态,epoll 就会持续通知用户,如同高电平持续触发一样。

ET模式
-
需显式设置 :添加事件时使用
EPOLLET标志启用。 -
通知规则 :仅当文件描述符的状态发生变化时才通知一次(如从无数据变为有数据,或数据量增加)。若一次未处理完所有数据,后续不再重复通知,直到下次状态变化。
-
数据处理 :必须一次性 将本次就绪的数据全部读完(或写完)。通常需循环调用
read/write直到返回EAGAIN错误,以确认数据已耗尽。 -
文件描述符要求 :必须配合非阻塞模式使用,防止最后一次读取时因无数据而永久阻塞。
-
性能优势 :减少了
epoll_wait的返回次数,降低系统调用开销;同时强制及时清空缓冲区,使接收窗口更快更新,提升 TCP 吞吐量。
理解:
边缘触发(ET)模式下,epoll 仅在事件就绪状态发生变化的瞬间通知用户一次,类似于数字电路的上升沿触发。

LT VS ET
- LT是epoll的默认模式 ,只要文件描述符上还有未处理的事件(如读缓冲区仍有数据),每次调用
epoll_wait都会持续通知用户,直至事件被完全处理。所以,文件描述符可以是阻塞或非阻塞的 - ET模式需要显式设置
EPOLLET标志 。仅当事件状态发生变化时通知一次,之后即使数据未处理完也不再重复通知。这迫使程序员必须一次性将本轮数据全部读取或写入完毕。为避免阻塞,必须将文件描述符设置为非阻塞 ,并通过循环调用read/write直到返回EAGAIN,确保数据已耗尽。
在 epoll 的边缘触发(ET)模式下,必须将文件描述符设置为非阻塞 ,这并非接口的强制要求,而是工程实践中的必要选择。原因如下:
-
ET 模式仅在事件状态变化时通知一次,若未一次性读完所有数据,剩余数据将滞留缓冲区且不再触发通知,导致后续数据无法被处理。
-
使用阻塞式
read无法保证一次读完所有数据(例如可能被信号打断或受限于缓冲区大小),若仅读取部分数据,剩余数据将永久留在缓冲区,直到有新事件到来才能再次触发读取。 -
例如,服务器需读取完整的 10k 请求后才响应客户端,但若仅读取 1k 便阻塞等待,剩余 9k 数据不会被再次通知,服务器无法继续读取,也就无法发送响应。客户端因未收到响应而不会发送新请求,造成死锁。
-
解决方法是采用非阻塞 I/O,循环调用
read直至返回EAGAIN,确保一次性将所有就绪数据读出。 -
(需要循环调用read/write=》若上一次刚好把数据读完,这一次就读不到数据=》此时若文件描述符为阻塞模式,进程就会阻塞=》应设置文件描述符为非阻塞)
-
相比之下,水平触发(LT)模式则无此问题,因为只要缓冲区还有数据,
epoll_wait就会持续通知,允许分多次读取。
总结:LT 是"持续通知直到处理完",ET 是"状态变化时通知一次,必须立即处理完"。
ET模式的高效性
-
减少系统调用次数 :ET 仅在事件状态变化时通知一次,避免了水平触发(LT)模式下因未处理完数据而反复通知的开销 ,从而降低
epoll_wait的调用频率,减少用户态与内核态的切换成本。 -
提升网络吞吐量 :ET 强制程序尽快取走所有数据,使接收缓冲区迅速清空。这样一来,TCP 协议栈能及时向对端通告更大的接收窗口(接收窗口大小取决于缓冲区剩余空间),进而允许发送方扩大滑动窗口,一次发送更多数据,显著提高网络传输效率。(主要原因)
3.5 epoll 版 TCP 服务器实现(LT模式)
整体设计思路
- 初始化监听 socket:
socket创建 →bind绑定 IP/端口 →listen开始监听(并设置为非阻塞)。 - 初始化 epoll:创建 epoll 实例;将监听 fd 注册到 epoll,关注事件
EPOLLIN。 - 循环调用
epoll_wait:阻塞等待事件到来;处理三种结果:错误(如EINTR重试)、超时(本例 timeout=-1 基本不会)、就绪(返回 n 个事件)。 - 事件分发处理(遍历就绪事件数组):
- 监听 fd 就绪:循环
accept接受新连接;新连接设置非阻塞;把新 fd 加入 epoll,关注EPOLLIN。 - 通信 fd 就绪:循环
read尽量读完;读到 0 表示对端关闭;读出错(非EAGAIN)则关闭;关闭前先从 epoll DEL 再close。 - 异常事件:若出现
EPOLLERR/EPOLLHUP,优先关闭并清理该连接 fd。
- 监听 fd 就绪:循环
代码
-
main.cc- 作用:程序入口;解析端口参数,创建
EpollServer并调用Init()/Start()。 - 定位:只负责"启动流程",不承载网络细节。
- 作用:程序入口;解析端口参数,创建
-
EpollServer.hpp- 作用:TCP 服务器主逻辑(单线程、LT、非阻塞)。
- 核心流程:
Init():创建监听 socket(Socket()→Bind()→Listen()),并设为非阻塞Start():把监听 fd 加入 epoll,进入epoll_wait循环HandlerEvent():分发事件(listen fd →Accept();client fd →HandleClientData())CloseConnection():统一清理连接(从 epoll 删除 + close)
- 定位:把"事件循环骨架"和"连接生命周期"串起来。
-
Epoll.hpp- 作用:对 epoll 的封装(创建 epfd、增/删/改关心事件
AddEvent/DelEvent/ModEvent、等待事件就绪Wait)。 - 定位:把"系统调用细节"隔离,给上层提供稳定接口。
- 作用:对 epoll 的封装(创建 epfd、增/删/改关心事件
-
Socket.hpp- 作用:对 socket 常用操作的封装:
Socket/Bind/Listen/Accept,以及SetNonBlock(设置fd为非阻塞)。 - 定位:把"建连与 fd 属性设置"从服务器逻辑里抽离出来,减少
EpollServer的噪音。
- 作用:对 socket 常用操作的封装:
-
Log.hpp- 作用:简单日志系统(日志等级 + 时间 + pid + printf 格式化输出)。
- 定位:统一输出,方便观察事件循环行为。
-
Err.hpp- 作用:集中定义退出错误码(如
SOCKET_ERR/BIND_ERR/LISTEN_ERR/OPEN_ERR等)。 - 定位:让错误退出有明确语义。
- 作用:集中定义退出错误码(如
cpp
#include <memory>
#include <cstdlib>
#include "EpollServer.hpp"
int main(int argc, char *argv[]) {
if(argc!=2){
std::cerr<<"Usage: "<<argv[0]<<" <port>"<<std::endl;
return 1;
}
// 创建并启动服务器
std::unique_ptr<EpollServer> server(new EpollServer(std::stoi(argv[1])));
server->Init();
server->Start();
return 0;
}
EpollServer.hpp
cpp
#pragma once
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include "Socket.hpp"
#include "Epoll.hpp"
#include "Log.hpp"
#include "Err.hpp"
// 这是一个最小可读的 epoll TCP 服务器骨架(单线程、LT、非阻塞)。
// 阅读顺序建议:
// 1) Init(): 建立监听 socket(socket/bind/listen)并设置非阻塞
// 2) Start(): 把监听 fd 加入 epoll,并进入事件循环
// 3) HandlerEvent(): 事件分发:listen fd -> Accept;client fd -> HandleClientData
// 4) CloseConnection(): 统一清理连接资源
const uint16_t default_port = 8080; // 默认端口
const int fd_num_max = 64; // 每次 epoll_wait 取回的最大事件数量(不是系统 fd 上限)
class EpollServer
{
private:
uint16_t port_; // 端口号
int listensock_; // 监听套接字
Epoller epoller_; // epoll对象
struct epoll_event revs_[fd_num_max]; // 就绪事件数组
public:
EpollServer(const uint16_t port = default_port)
: port_(port), listensock_(-1)
{
epoller_.Create();
}
~EpollServer()
{
// epoller_ 在析构中会自动关闭 epoll fd;这里只需要收尾监听 fd。
if (listensock_ >= 0)
{
close(listensock_);
listensock_ = -1;
}
}
// 初始化:准备好监听 socket(但还没开始事件循环)
void Init()
{
listensock_ = Sock::Socket();
Sock::Bind(listensock_, port_);
Sock::Listen(listensock_);
// 事件循环依赖"非阻塞 I/O + epoll 通知",否则某次 accept/read 可能卡住整个循环。
Sock::SetNonBlock(listensock_);
logMessage(Info, "Server initialized, listening on port %u", port_);
}
// 启动:注册监听 fd,进入 epoll_wait 循环(核心调度点)
void Start()
{
// 对 listen fd 来说,EPOLLIN 表示"有新连接可 accept"。
epoller_.AddEvent(listensock_, EPOLLIN);
// timeout = -1:没有事件就睡眠,有事件就醒来处理。
int timeout = -1;
while (true)
{
// 等待事件发生,并把就绪事件填入 revs_
int n = epoller_.Wait(revs_, fd_num_max, timeout);
if (n < 0)
{
if (errno == EINTR)
continue;
logMessage(Error, "epoll_wait error, code: %d, errstring: %s", errno, strerror(errno));
continue;
}
else if (n == 0)
{
logMessage(Info, "epoll timeout");
continue;
}
else
{
// n 个就绪事件:逐个分发处理
Dispacher(n);
}
}
}
private:
// 统一关闭连接(并从 epoll 关注列表中移除)。
void CloseConnection(int fd)
{
if (fd < 0)
return;
epoller_.DelEvent(fd);
close(fd);
}
// 接受连接:把 listen fd 上的"新连接"变成可读写的 client fd,并注册到 epoll。
void Accept()
{
// 非阻塞 + 循环 accept:一直取到"暂时没新连接"为止。
while (true)
{
std::string clientip;
uint16_t clientport = 0;
int sockfd = Sock::Accept(listensock_, &clientip, &clientport);
if (sockfd < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
break;
if (errno == EINTR)
continue;
logMessage(Warning, "accept failed, errno=%d, err=%s", errno, strerror(errno));
break;
}
// 新连接同样设为非阻塞,避免后续读写阻塞事件循环。
Sock::SetNonBlock(sockfd);
logMessage(Info, "Accepted connection %s:%u, fd=%d", clientip.c_str(), clientport, sockfd);
epoller_.AddEvent(sockfd, EPOLLIN); // 添加客户端套接字到epoll
}
}
// 处理可读事件:把当前能读到的数据尽量读出来;读到对端关闭/发生错误则清理连接。
void HandleClientData(int fd)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
logMessage(Info, "Received from fd %d: %s", fd, buffer);
}
else if (n == 0)
{
logMessage(Info, "Client fd %d disconnected", fd);
CloseConnection(fd);
}
else
{
logMessage(Warning, "Read error on fd %d, errno=%d, err=%s", fd, errno, strerror(errno));
CloseConnection(fd);
}
}
// 事件分发:listen fd 与 client fd 的处理逻辑不同,这里做一个集中分发。
void Dispacher(int n)
{
for (int i = 0; i < n; ++i)
{
int fd = revs_[i].data.fd; // 获取就绪事件的文件描述符
uint32_t events = revs_[i].events; // 获取就绪事件的类型
// 错误/挂断:优先处理,避免在异常 fd 上继续读写。
if (events & (EPOLLERR | EPOLLHUP))
{
if (fd != listensock_)
CloseConnection(fd);
continue;
}
if (events & EPOLLIN)
{
// 监听套接字就绪:新连接
if (fd == listensock_)
{
Accept();
}
else // 客户端套接字就绪:数据到达
{
HandleClientData(fd);
}
}
}
}
};
Epoll.hpp
cpp
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <string>
#include <cstring>
#include <cerrno>
#include "Log.hpp"
#include "Err.hpp"
static const int defaultepfd = -1;
static const int gsize = 128;
class Epoller
{
public:
Epoller()
: epfd_(defaultepfd)
{
}
void Create()
{
epfd_ = epoll_create(gsize);
if (epfd_ < 0)
{
logMessage(Fatal, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));
exit(OPEN_ERR);
}
}
// Add/Del/Mod:把"我关心什么事件"同步到内核(epoll 实例)里。
// 添加事件
bool AddEvent(int sockfd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = sockfd;
int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, sockfd, &ev);
if (n < 0)
{
logMessage(Error, "epoll_ctl(ADD) error, fd=%d, events=0x%x, code=%d, errstring=%s",
sockfd, events, errno, strerror(errno));
return false;
}
return true;
}
// 删除事件
bool DelEvent(int sockfd)
{
if (epoll_ctl(epfd_, EPOLL_CTL_DEL, sockfd, nullptr) != 0)
{
logMessage(Warning, "epoll_ctl(DEL) failed, fd=%d, errno=%d, err=%s", sockfd, errno, strerror(errno));
return false;
}
return true;
}
// 修改事件
bool ModEvent(int sockfd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = sockfd; // 修改事件时需要重新设置fd
if (epoll_ctl(epfd_, EPOLL_CTL_MOD, sockfd, &ev) != 0)
{
logMessage(Warning, "epoll_ctl(MOD) failed, fd=%d, events=0x%x, errno=%d, err=%s",
sockfd, events, errno, strerror(errno));
return false;
}
return true;
}
// 等待事件就绪
int Wait(struct epoll_event *revs, int num, int timeout)
{
return epoll_wait(epfd_, revs, num, timeout);
}
// 获取epoll文件描述符
int epFd() const
{
return epfd_;
}
// 关闭epoll文件描述符
void Close()
{
if (epfd_ != defaultepfd)
close(epfd_);
epfd_ = defaultepfd;
}
~Epoller()
{
// RAII:Epoller 生命周期结束时自动释放内核资源(避免忘记 close)。
Close();
}
private:
int epfd_;
};
Sock.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <memory>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "Log.hpp"
#include "Err.hpp"
class Sock
{
private:
// 监听队列长度(用于 listen(backlog))
const static int gbacklog = 10;
public:
Sock() {}
~Sock() {}
// 将 fd 设置为非阻塞。
// 思路:事件循环线程不应该被某个 fd 的 I/O 阻塞;非阻塞 + epoll 才能让"有事就处理、没事就睡眠"成立。
static bool SetNonBlock(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0)
{
logMessage(Error, "fcntl(F_GETFL) failed, fd=%d, errno=%d, err=%s", fd, errno, strerror(errno));
return false;
}
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0)
{
logMessage(Error, "fcntl(F_SETFL) failed, fd=%d, errno=%d, err=%s", fd, errno, strerror(errno));
return false;
}
return true;
}
// 创建监听/通信套接字。
// 思路:把"更安全的默认项"尽量一次性设置好(CLOEXEC、端口复用),避免隐藏 bug。
static int Socket() // 创建套接字
{
int listensock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (listensock < 0)
{
logMessage(Fatal, "socket failed, errno=%d, err=%s", errno, strerror(errno));
exit(SOCKET_ERR);
}
int opt = 1;
setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));//打开端口复用
return listensock;
}
// 绑定套接字到指定的 IP 地址和端口号。
// 说明:ip 默认 0.0.0.0 代表监听所有网卡地址。
static void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0")
{
struct sockaddr_in local;
memset(&local, 0, sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &local.sin_addr);
if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
{
logMessage(Fatal, "bind failed, ip=%s, port=%u, errno=%d, err=%s", ip.c_str(), port, errno, strerror(errno));
exit(BIND_ERR);
}
}
// 监听套接字,进入被动监听状态(开始接收连接请求)。
static void Listen(int sock)
{
if (listen(sock, gbacklog) < 0)
{
logMessage(Fatal, "listen failed, errno=%d, err=%s", errno, strerror(errno));
exit(LISTEN_ERR);
}
}
// 接受客户端连接请求
static int Accept(int listensock, std::string *ip, uint16_t *port)
{
struct sockaddr_in src;
socklen_t len = sizeof(src);
int servicessock = accept(listensock, (struct sockaddr *)&src, &len);
if (servicessock < 0)
{
return -1;
}
if (port)//获取客户端的端口号
*port = ntohs(src.sin_port);
if (ip)//获取客户端的IP地址
*ip = inet_ntoa(src.sin_addr);
return servicessock;
}
};
Log.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <cstdarg>
#include <time.h>
// 日志系统是有等级的
enum
{
Debug=0,
Info,
Warning,
Error,
Fatal,
Uknown
};
static std::string toLevelString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Uknown";
}
}
static std::string getTime()
{
time_t curr=time(nullptr);
struct tm*tmp=localtime(&curr);
char buffer[128];
snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",tmp->tm_year+1900,tmp->tm_mon+1,tmp->tm_mday,
tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;
}
// 日志格式: 日志等级 时间 pid 消息体
// logLeft: 日志等级 时间 pid logRight: 消息体
// logMessage(Debug, "hello: %d, %s",12,s.c_str()) Debug, hello: 12, world
void logMessage(int level, const char*format,...)
{
char logLeft[1024];
std::string level_string=toLevelString(level);
std::string curr_time=getTime();
snprintf(logLeft,sizeof(logLeft),"[%s] [%s] [%d] ",level_string.c_str(),curr_time.c_str(),getpid());
char logRight[1024];
va_list p;
va_start(p,format);
vsnprintf(logRight,sizeof(logRight),format,p);
va_end(p);
printf("%s%s\n",logLeft,logRight);
}
Err.hpp
cpp
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
SET_ERR,
OPEN_ERR
};
运行结果

六、Reactor 模式
1.Reactor 模式定义
Reactor 反应器模式,也被称为分发者模式或通知者模式,是一种将就绪事件派发给对应服务处理程序的事件设计模式。
2.Reactor 模式的角色构成

3.Reactor 模式的工作流程
初始分发器(如事件循环框架)的工作流程如下:
-
注册事件处理器:具体事件处理器向初始分发器注册,声明自己关注哪些事件,并指定这些事件所关联的 Handle(如文件描述符或套接字)。
-
传递 Handle:初始分发器要求每个事件处理器提供其内部的 Handle,该 Handle 用于操作系统层面唯一标识该处理器所关心的 I/O 对象。
-
启动事件循环 :所有处理器注册完毕后,初始分发器启动事件循环。它将收集到的所有 Handle 合并,并交给底层的同步事件分离器(如
select、poll或epoll),由后者统一监听这些 Handle 上的事件。 -
等待事件就绪:当某个 Handle 变为就绪状态(如可读、可写)时,同步事件分离器立即通知初始分发器。
-
查找对应处理器:初始分发器以便利的 Handle 为键,在内部映射中快速找到与该 Handle 关联的事件处理器。
-
分发回调:最后,初始分发器调用该事件处理器中预先注册的回调方法,完成对事件的响应处理。
4.epoll ET 服务器(reactor模式)
4.1 设计思路
基于 epoll 的 ET 模式,实现一个典型的 Reactor 模式服务器:底层统一管理所有 fd 的事件(连接、读、写、异常),上层通过回调处理业务逻辑。
4.2 epoll ET 服务器的工作流程
读事件
- 监听套接字:
- 当监听套接字读事件就绪时,
TcpServer::handleNewConnection()被调用:一次性取完所有就绪 fd ,为每个新 fd 创建Connection,注册监视事件并设置ET模式。
- 当监听套接字读事件就绪时,
- 普通连接套接字:
- 当普通 fd 上读事件就绪时,
TcpServer::handleRead(conn)被调用:内部循环调用read,把本轮所有数据追加到对应fd的接收缓冲区中,直到对端关闭或数据全部读完。 - 读取完毕后:
- 若接收缓冲区非空,则进行业务处理(如打印、回显),然后清空对应 fd 的接收缓冲区。
- 若业务处理后有数据待发(对应fd的发送缓冲区非空),则打开对应 fd 的写事件关注。
- 当普通 fd 上读事件就绪时,
写事件
- 当 fd 上写事件就绪时,
TcpServer::handleWrite(conn)被调用:循环调用write,尽量把对应 fd 的发送缓冲区中的数据拷贝到内核发送缓冲区:- 写成功则从发送缓冲区中删掉掉已发送部分;
- 若发送缓冲区已全部写空,则关闭写事件关注,只保留读事件;
- 若因
EAGAIN/EWOULDBLOCK退出写循环,说明内核发送缓冲区暂时写满,保留写事件关注,等下一次可写再继续发送。
异常事件
- 无论是 epoll 返回的
EPOLLERR/EPOLLHUP,还是读写中出现致命错误(非EAGAIN/EWOULDBLOCK/EINTR),都会调用:TcpServer::handleError(conn)输出错误信息;TcpServer::handleClose(conn):reactor_->DelEvent(fd)从 epoll 模型中移除;- 从
connections_中删除该连接; Connection析构时关闭底层 fd 并打印关闭日志。
4.3 Reactor 模式五个角色的对应关系
句柄(Handle)
所有的文件描述符 fd:
- 监听 fd:
listenSocket_->fd() - 普通连接 fd:
Connection::fd()
同步事件分离器(Synchronous Event Demultiplexer)
类 Epoller:
AddEvent/ModEvent/DelEvent封装epoll_ctl;Wait封装epoll_wait,阻塞等待多个 fd 上的 I/O 事件发生。
初始分发器(Initiation Dispatcher)
TcpServer::start() 中等待事件就绪并处理每个就绪事件:
- 若有
EPOLLERR/EPOLLHUP-> 调用handleError和handleClose。 - 若有
EPOLLOUT-> 认为是写事件,调用handleWrite(connections_[fd]); - 若有
EPOLLIN/EPOLLPRI-> 认为是读事件,调用handleRead(connections_[fd]); - 若
fd == listenSocket_->fd()-> 认为是连接事件,调用handleNewConnection();
这样,TcpServer 负责"等待并根据 fd 和事件类型分发给具体处理函数",完整扮演了 Dispatcher 角色。
事件处理器(Event Handler)
TcpServer 中的一组成员函数:
handleNewConnection():处理连接到来;handleRead(Connection::Ptr):处理读事件;handleWrite(Connection::Ptr):处理写事件;handleError(Connection::Ptr)、handleClose(Connection::Ptr):处理错误和关闭。
具体事件处理器(Concrete Event Handler)
上述每个处理函数内部给出了具体的实现逻辑:
- 接受连接、设置非阻塞、注册事件;
- 读入数据、触发业务回调、决定是否打开写事件;
- 发送数据、根据发送情况开启或关闭写事件;
- 删除 epoll 事件、移除连接、关闭 fd 等。
4.4 Connection 结构说明
Connection 类的核心成员
int sockfd_:代表一个客户端连接的 fd。std::string inBuffer_:接收缓冲区,用于累积当前 fd 未处理完的输入数据。std::string outBuffer_:发送缓冲区,用于缓存待发送数据。
读缓冲区(_inBuffer)
Connection::Read() 中循环 read:
- 每次读取到的数据追加到
inBuffer_; - 遇到
EAGAIN/EWOULDBLOCK退出循环,说明本轮数据已全部读完; TcpServer::handleRead()中在读完后检查inBuffer_是否非空,若非空则交给messageCallback_处理。- 这样可以自然应对 TCP 中的半包/粘包:可以在
messageCallback_中实现协议切包逻辑(按分隔符或长度字段拆分inBuffer_),切出完整报文进行处理,剩余部分继续保留在inBuffer_中等待下次数据到来。
写缓冲区(_outBuffer)
应用层通过 Connection::Append(const std::string &data) 将待发送数据追加到 outBuffer_。
Connection::Write() 循环 write:
- 写成功则从
outBuffer_头部删除已发送的字节; - 若
outBuffer_清空,则通知服务器可以关闭写事件关注,只保留读事件; - 若因
EAGAIN/EWOULDBLOCK返回,说明内核发送缓冲区暂时写满了,保留写事件,等下一次可写时继续发送。
4.5 TcpServer 结构说明
管理所有连接
std::unordered_map<int, Connection::Ptr> connections_:
- key:fd
- value:对应的
Connection智能指针
通过connections_来管理连接:
- 当新连接到来时,创建
Connection并插入 - 当连接关闭或出错时,删除
Connection
注册与修改事件
在构造函数中:
- 创建监听 socket,绑定端口、监听;
- 将监听 fd 设为非阻塞;
- 通过
epoller_->AddEvent(listenSocket_->fd(), EPOLLIN | EPOLLET)注册监听套接字的读事件。
在读写处理函数中:
- 读到有待发送数据时,通过
epoller_->ModEvent(fd, EPOLLIN | EPOLLOUT | EPOLLET)打开发送事件 - 发送完所有数据后,通过
epoller_->ModEvent(fd, EPOLLIN | EPOLLET)关闭写事件,仅保留读事件
业务回调入口
setMessageCallback(MessageCallback cb)用于注册高层业务处理函数。- 在
handleRead中调用该回调,将Connection对象和 读缓冲区inBuffer_传给上层,由业务层决定如何解析数据、如何生成响应,并写入 写缓冲区outBuffer_。
4.6 运行流程总结
main中创建TcpServer,注册业务回调(例如打印 + 回显),然后调用server.start()。TcpServer构造时完成:创建监听 sock、绑定/监听、设为非阻塞并把注册监听 fd 。- 调用
TcpServer::start(),进入统一事件循环:- 使用
epoll_wait阻塞等待事件就绪; - 每有事件就绪,通过回调交给
TcpServer分发; TcpServer根据 fd 与事件类型调用handleNewConnection / handleRead / handleWrite / handleError / handleClose;- 这些处理函数内部操作
Connection的inBuffer_/outBuffer_、维护connections_、并修改 epoll 关注的事件集合。
- 使用
- 上层业务只需实现一个简洁的回调函数,通过
Connection::Append写入响应数据,即可在整个 Reactor 框架下完成高并发、非阻塞的 I/O 处理。
4.7 ET 模式 Reactor 的关键技术点
- **非阻塞 I/O 配合:**ET 模式必须与非阻塞 I/O 结合使用,确保能一次性读取 / 写入所有可用数据
- **事件处理完整性:**在 ET 模式下,每次事件触发都需要循环处理直到操作返回 EAGAIN/EWOULDBLOCK,告知程序 "当前没有可用数据" 或 "操作暂时无法完成"
- **连接管理:**使用哈希表存储所有活跃连接,便于根据文件描述符快速查找对应的连接对象
- **缓冲区设计:**每个连接维护独立的输入 / 输出缓冲区,解决 TCP 粘包和发送阻塞问题
- **边缘触发的新连接处理:**对于监听套接字,需要在 ET 模式下一次性接受所有待处理的新连接
- 读事件和写事件的处理逻辑的区别:
- 读事件 :通常需要持续关注(即始终注册
EPOLLIN事件)。当可读事件触发时,应用程序必须循环调用read直到返回EAGAIN,以确保一次性读取完所有到达的数据,避免漏读。 - 写事件 :则采用按需关注策略。仅在发送缓冲区中有待发送数据时,才临时注册
EPOLLOUT事件;当数据发送完毕或缓冲区满导致无法写入时,应立即取消写事件,防止因缓冲区一直可写而频繁触发无意义的空轮询,浪费 CPU 资源。
4.8 代码
- main.cc: TCP服务器的主程序,启动服务器并设置消息处理回调。
- TcpServer.hpp: TCP服务器类,管理监听套接字、连接和事件循环。
- Epoll.hpp: Epoll事件管理类,用于添加、修改、删除和等待I/O事件。
- Socket.hpp: 套接字类,处理套接字创建、绑定、监听和接受连接。
- Connection.hpp: 连接类,管理单个客户端连接的读写缓冲区和数据传输。
- nocopy.hpp: 禁止拷贝的基类,用于防止对象被拷贝。
main.cc
cpp
#include "TcpServer.hpp"
#include <iostream>
#include <string>
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
return 1;
}
// 创建服务器对象,监听指定端口
TcpServer server(std::stoi(argv[1]));
// 设置消息处理回调
server.setMessageCallback([](Connection::Ptr conn, std::string &data)
{
std::cout << "Received from " << conn->peerAddr() << ": " << data << std::endl;
// 简单回显
std::string response = "Server echo: " + data;
conn->Append(response); });
// 启动服务器
server.start();
return 0;
}
TcpServer.hpp
cpp
#pragma once
#include <unordered_map>
#include <memory>
#include <functional>
#include <iostream>
#include <cerrno>
#include "Socket.hpp"
#include "Epoll.hpp"
#include "Connection.hpp"
class TcpServer : public nocopy
{
public:
using ConnectionCallback = std::function<void(Connection::Ptr)>;
using MessageCallback = std::function<void(Connection::Ptr, std::string &)>;
// 构造函数,初始化服务器
TcpServer(uint16_t port) : port_(port), epoller_(new Epoller()),
listenSocket_(new Socket())
{
listenSocket_->Bind(port); // 绑定端口
listenSocket_->Listen(); // 开始监听
listenSocket_->SetNonBlock(listenSocket_->fd()); // 设置监听套接字为非阻塞
epoller_->AddEvent(listenSocket_->fd(), EPOLLIN | EPOLLET); // 注册监听套接字的读事件,使用ET模式
std::cout << "TcpServer started on port " << port << std::endl;
}
~TcpServer() = default;
// 启动服务器事件循环
void start()
{
while (true)
{
epoller_->resizeEvents(1024); // 调整事件数组大小,避免过小导致事件丢失
int nready = epoller_->Wait(1000); // 等待事件,设置超时时间为1000ms
for (int i = 0; i < nready; ++i)
{
epoll_event &ev = epoller_->getEvent(i);
int fd = ev.data.fd;
if (fd == listenSocket_->fd())
{
// 处理新连接
handleNewConnection();
}
else if (ev.events & (EPOLLIN | EPOLLPRI))
{
// 处理读事件
handleRead(connections_[fd]);
}
else if (ev.events & EPOLLOUT)
{
// 处理写事件
handleWrite(connections_[fd]);
}
else if (ev.events & (EPOLLERR | EPOLLHUP))
{
// 处理错误或断开事件
handleError(connections_[fd]);
handleClose(connections_[fd]);
}
}
}
}
// 设置回调函数
void setMessageCallback(MessageCallback cb) { messageCallback_ = cb; }
private:
// 处理新连接
void handleNewConnection()
{
std::string client_ip; // 获取客户端的IP地址
uint16_t client_port; // 获取客户端的端口号
while (true)
{
// 接受新连接,获取客户端信息
int clientfd = listenSocket_->Accept(&client_ip, &client_port);
if (clientfd < 0)
{
// 非阻塞模式下,accept返回-1且errno为EAGAIN或EWOULDBLOCK表示没有更多连接可接受
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
break;
}
perror("accept error");
return;
}
std::cout << "New connection from " << client_ip
<< ":" << client_port << std::endl;
listenSocket_->SetNonBlock(clientfd); // 设置新连接为非阻塞
auto conn = std::make_shared<Connection>(clientfd, client_ip, client_port); // 创建连接对象
connections_[conn->fd()] = conn; // 将连接对象添加到连接管理器中
epoller_->AddEvent(conn->fd(), EPOLLIN | EPOLLET); // 注册连接的读事件,使用ET模式
std::cout << "Connection established: " << conn->peerAddr() << std::endl;
}
}
// 处理读事件
void handleRead(Connection::Ptr conn)
{
ssize_t n = conn->Read(); // 读取数据到连接对象的输入缓冲区
// 调用消息回调函数,处理接收到的数据
if (messageCallback_ && !conn->inBuffer().empty())
{
messageCallback_(conn, conn->inBuffer());
}
conn->clearInBuffer();
// 如果有数据需要发送,注册写事件
if (!conn->outBuffer().empty())
{
epoller_->ModEvent(conn->fd(), EPOLLIN | EPOLLOUT | EPOLLET);
}
// 处理连接关闭
if (n == 0)
{
handleClose(conn);
}
// 处理读取错误
else if (n < 0)
{
// 非阻塞模式下,读返回-1且errno为EAGAIN或EWOULDBLOCK表示数据已读完
// 其他错误则表示连接异常,需要关闭连接
if (errno != EAGAIN && errno != EWOULDBLOCK)
{
handleError(conn);
handleClose(conn);
}
}
}
// 处理写事件
void handleWrite(Connection::Ptr conn)
{
ssize_t n = conn->Write(); // 将连接对象的输出缓冲区数据发送到客户端
// 如果输出缓冲区已空,说明数据发送完成,可以取消写事件监听
if (conn->outBuffer().empty())
{
epoller_->ModEvent(conn->fd(), EPOLLIN | EPOLLET);
}
// 处理连接关闭
if (n == 0)
{
handleClose(conn);
}
// 处理写入错误
else if (n < 0)
{
// 非阻塞模式下,写返回-1且errno为EAGAIN或EWOULDBLOCK表示发送缓冲区已满,稍后再试
// 其他错误则表示连接异常,需要关闭连接
if (errno != EAGAIN && errno != EWOULDBLOCK)
{
handleError(conn);
handleClose(conn);
}
}
}
// 处理连接关闭
void handleClose(Connection::Ptr conn)
{
std::cout << "Connection closed: " << conn->peerAddr() << std::endl;
epoller_->DelEvent(conn->fd());
connections_.erase(conn->fd());
}
// 处理错误
void handleError(Connection::Ptr conn)
{
std::cerr << "Error on connection: " << conn->peerAddr() << std::endl;
}
private:
uint16_t port_; // 服务器监听的端口
std::unique_ptr<Epoller> epoller_; // epoll对象,管理事件
std::unique_ptr<Socket> listenSocket_; // 监听套接字对象
std::unordered_map<int, Connection::Ptr> connections_; // 管理所有连接
MessageCallback messageCallback_; // 消息到达回调
};
Epoll.hpp
cpp
#pragma once
#include <vector>
#include <sys/epoll.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include "nocopy.hpp"
class Epoller : public nocopy
{
public:
Epoller() : epollfd_(epoll_create(1))
{
if (epollfd_ < 0)
{
perror("epoll create error");
exit(1);
}
}
~Epoller()
{
close(epollfd_);
}
// 添加事件
bool AddEvent(int fd, uint32_t events)
{
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
ev.data.fd = fd;
ev.events = events; // 支持ET模式: events | EPOLLET
return epoll_ctl(epollfd_, EPOLL_CTL_ADD, fd, &ev) == 0;
}
// 修改事件
bool ModEvent(int fd, uint32_t events)
{
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
ev.data.fd = fd;
ev.events = events;
return epoll_ctl(epollfd_, EPOLL_CTL_MOD, fd, &ev) == 0;
}
// 删除事件
bool DelEvent(int fd)
{
return epoll_ctl(epollfd_, EPOLL_CTL_DEL, fd, nullptr) == 0;
}
// 等待事件
int Wait(int timeout = -1)
{
return epoll_wait(epollfd_, events_.data(), events_.size(), timeout);
}
int Fd()
{
return epollfd_;
}
struct epoll_event &getEvent(size_t i)
{
return events_[i];
}
// 调整事件数组大小
void resizeEvents(size_t size)
{
if (events_.size() < size)
{
events_.resize(size);
}
}
private:
int epollfd_; // epoll文件描述符
std::vector<struct epoll_event> events_; // 事件数组,存储就绪事件
};
Socket.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <fcntl.h>
#include "nocopy.hpp"
enum
{
SocketErr = 2,
BindErr,
ListenErr
};
const int backlog = 10; // 监听队列长度
class Socket : public nocopy
{
public:
// 构造函数,创建套接字并设置选项
Socket()
{
listensock_ = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); // 创建非阻塞套接字
if (listensock_ < 0)
{
perror("socket create error");
exit(SocketErr);
}
int opt = 1;
setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 打开端口复用
}
~Socket()
{
if (listensock_ != -1)
{
close(listensock_);
}
}
// 绑定端口并开始监听
void Bind(uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
perror("bind error");
exit(BindErr);
}
}
// 开始监听
void Listen()
{
if (listen(listensock_, backlog) < 0)
{
perror("listen error");
exit(ListenErr);
}
}
// 接受新连接,获取客户端信息
int Accept(std::string *ip, uint16_t *port)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int servicessock = accept(listensock_, (struct sockaddr *)&client, &len);
if (servicessock < 0)
{
return -1;
}
if (port) // 获取客户端的端口号
*port = ntohs(client.sin_port);
if (ip) // 获取客户端的IP地址
*ip = inet_ntoa(client.sin_addr);
return servicessock;
}
int fd() const { return listensock_; }
// 设置非阻塞
void SetNonBlock(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
private:
int listensock_; // 监听套接字文件描述符
};
Connection.hpp
cpp
#pragma once
#include <memory>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <functional>
#include <cstring>
#include <unistd.h>
#include <iostream>
#include <arpa/inet.h>
#include <cerrno>
#include "nocopy.hpp"
class Connection : public std::enable_shared_from_this<Connection>, public nocopy
{
public:
typedef std::shared_ptr<Connection> Ptr; // 定义智能指针类型
using Callback = std::function<void(Connection::Ptr)>; // 定义回调函数类型
// 构造函数,初始化连接对象
Connection(int sockfd, std::string ip, uint16_t port)
: sockfd_(sockfd), peerAddr_(ip + ":" + std::to_string(port))
{
std::cout << "New connection from " << peerAddr_ << std::endl;
}
~Connection()
{
close(sockfd_);
std::cout << "Connection closed: " << peerAddr_ << std::endl;
}
// 获取连接的文件描述符
int fd() const { return sockfd_; }
// 获取客户端的IP地址和端口号
const std::string &peerAddr() const { return peerAddr_; }
// 读取数据
ssize_t Read()
{
char buf[1024];
ssize_t n;
while (true)
{
n = read(sockfd_, buf, sizeof(buf));
if (n > 0) // 数据读取成功,追加到输入缓冲区
{
inBuffer_.append(buf, n);
}
else if (n == 0) // 对端关闭:可能已读到部分数据,交给上层处理后再关闭
{
break;
}
else if (n == -1)
{
if (errno == EINTR) // 被信号中断,继续读取
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK) // 非阻塞模式下,读返回-1且errno为EAGAIN或EWOULDBLOCK表示数据已读完
{
break;
}
else
{ // 其他错误则表示连接异常,需要关闭连接
break;
}
}
}
return n;
}
// 发送数据
ssize_t Write()
{
ssize_t n;
while (true)
{
n = write(sockfd_, outBuffer_.data(), outBuffer_.size()); // 尝试发送输出缓冲区中的数据
if (n > 0) // 数据发送成功,从输出缓冲区删除已发送的数据
{
outBuffer_.erase(0, n);
if (outBuffer_.empty()) // 输出缓冲区已空,数据发送完成,可以取消写事件监听
{
break;
}
else // 继续发送剩余数据
{
continue;
}
}
else if (n == 0) // 对端关闭:可能已发送部分数据,交给上层处理后再关闭
{
break;
}
else if (n == -1)
{
if (errno == EINTR) // 被信号中断,继续发送
{
continue;
}
else if (errno == EAGAIN || errno == EWOULDBLOCK) // 非阻塞模式下,写返回-1且errno为EAGAIN或EWOULDBLOCK表示发送缓冲区已满,稍后再试
{
break;
}
else // 其他错误则表示连接异常,需要关闭连接
{
break;
}
}
}
return n;
}
// 缓冲区操作
std::string &inBuffer() { return inBuffer_; }
std::string &outBuffer() { return outBuffer_; }
void Append(const std::string &data) { outBuffer_.append(data); }
void clearInBuffer() { inBuffer_.clear(); }
void clearOutBuffer() { outBuffer_.clear(); }
private:
int sockfd_; // 连接的文件描述符
std::string peerAddr_; // 客户端地址字符串
std::string inBuffer_; // 输入缓冲区
std::string outBuffer_; // 输出缓冲区
};
nocopy.hpp
cpp
#pragma once
class nocopy
{
public:
nocopy() = default;
nocopy(const nocopy&) = delete;
nocopy& operator=(const nocopy&) = delete;
};
4.9 运行结果
