@bit::Shadow
✧(≖ ◡ ≖✿
目录
LT && ET
1.LT(Level trigered)水平触发 设计模式
2.ET(Edge trigered)边缘触发 设计模式
为什么epoll_wait 后若不Accept,底层就会一直通知服务端呢?
ET模式与O_NONBLOCK的配合
🔗Epoll_ET_IO示例(千行代码)
ET模式原理
设置非阻塞模式
先明确前提
举例read/recv
举例send/write
为什么不可以通过循环来处理非阻塞?而是要设置fd为非阻塞?
ET与紧急指针的关联
多路转接的发送问题
认识One Thread One Loop(OTOL设计模式)
LT && ET
LT ET 是两种内核中的事件通知上层的机制。
1.LT(Level trigered)水平触发 设计模式
只要底层有报文,就会一直通知上层的机制。
若上层仅读取一部分数据,那么LT设计模式下底层仍然会一直通知。
2.ET(Edge trigered)边缘触发 设计模式
底层数据从无到有 / 从有到多才会通知上层。
若上层仅读取了一部分数据(未读取完全)就不读了,后来也没有(到达接受缓冲区的)新增数据那么底层就不会通知上层。
为什么epoll_wait 后若不Accept,底层就会一直通知服务端呢?
答:对于一个监听listenfd来讲,“可读”的含义是:内核中已完成链接队列里存在等待被Accept的链接(可见wait的底层是先检验Accept信号)。(注意:epoll_wait返回后代表已经将资源拷贝了,从就绪队列内拿取过了)
ET模式与O_NONBLOCK的配合
ET强制要求必须一次性读完(接收的信息)
ET设置
_listenfd

sockfd

使用

O_NONBLOCK
非阻塞下设置以“信号”代替读取完毕的标识

🔗Epoll_ET_IO示例(千行代码)
高度解耦化、可复用性、健壮性、鲁棒性极好
1.EPOLLET fcntl() EPOLLOUT使用 2.重点理解继承、多态机制、分层结构。 3.回指指针_Rptr、handler_t的使用 4._connection的map结构

ET模式原理
设置非阻塞模式
void SetNoBlock(int fd)
{
int fl = fcntl (fd, F_GETFL);
if(fl < 0)
{
LOG (LogLevel::ERROR) << "NoBlock中fd获取失败";
return;
}
int ret = fcntl (fd, F_SETFL, fl | O_NONBLOCK);
if(ret < 0)
{
LOG (LogLevel::ERROR) << "NoBlock 设置失败";
return;
}
// LOG (LogLevel::DEBUG) << "设置NoBlock成功fd: " << fd;
}
非阻塞的(O_NONBLOCK)fd在非阻塞下,当读取fd内无数据时,不会再阻塞等待了而是会直接返回。
在非阻塞模式下,读和写遇到"暂时无法完成"时,行为高度对称:立即返回 -1,errno = EAGAIN / EWOULDBLOCK,不阻塞。下面分别展开。
先明确前提
-
fd 设置了 O_NONBLOCK。
-
这两个场景都是"当前无法立即完成,但以后可能可以",属于暂时性的,不是真错误。
-
EAGAIN 和 EWOULDBLOCK 在 Linux 上是同一个值,可互换。
举例read/recv
ET + 非阻塞 read/recv时
| n > 0 | —— | 读到了 n 字节(可能短读) | 继续读 |
| n == 0 | —— | 对端关闭(EOF) | 关闭连接,退出循环 |
| n == -1 | EAGAIN/EWOULDBLOCK | 数据读空了(非阻塞) | 退出循环(这次边沿处理完) |
| n == -1 | EINTR | 被信号打断 | continue 重试 |
| n == -1 | 其他 | 真错误 | 关闭连接,退出 |
使用read/recv
while (true) {
//或者read
ssize_t n = read(fd, buf, sizeof(buf), 0);
if (n > 0) {
// 1. 读到数据,处理,继续读
// 注意:短读也要继续,不能因为 n < sizeof(buf) 就退出
}
else if (n == 0) {
// 2. 对端关闭(EOF)
// 关闭连接,退出
break;
}
else { // n == -1
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 3. 数据读空了 → 这次边沿处理完毕
break;
}
if (errno == EINTR) {
// 4. 被信号打断 → 重试
continue;
}
// 5. 真错误 → 关闭连接
break;
}
}
举例send/write
| n > 0 | —— | 写入了 n 字节(可能部分写,n < len) | 继续写剩余部分 |
| n == 0 | —— | 几乎不会出现(见下方说明) | 视情况,一般不当成功 |
| n == -1 | EAGAIN/EWOULDBLOCK | 发送缓冲区满(非阻塞) | 退出循环,注册 EPOLLOUT 等下次可写 |
| n == -1 | EINTR | 被信号打断 | continue 重试 |
| n == -1 | EPIPE | 对端已关闭(写端) | 关闭连接,退出 |
| n == -1 | 其他 | 真错误 | 关闭连接,退出 |
关键区分:EAGAIN(缓冲区满,稍后再写)、EPIPE(对端关闭)、其他错误,三者语义完全不同。
为什么没有 n == 0 作为"关闭":与 read 不同,send/write 返回 0 不代表对端关闭。len > 0 时正常情况不会返回 0;只有 len == 0(空发送)时才返回 0,那只是"发了 0 字节"。判断对端关闭要看 EPIPE 或 ECONNRESET。
while (还有数据要发) { // 例如 off < len
// 或者 send(fd, buf + off, len – off, 0)
ssize_t n = write(fd, buf + off, len – off);
if (n > 0) {
// 1. 写入了 n 字节,更新偏移,继续写剩余
// 注意:部分写(n < 剩余量)也要继续,不能直接当完成
off += n;
}
else if (n == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 2. 发送缓冲区满 → 这次能写的写完了
// 把剩余数据存到应用层发送缓冲区,
// 注册 EPOLLOUT,等下次可写再继续
RegisterWriteEvent(fd);
break;
}
if (errno == EINTR) {
// 3. 被信号打断 → 重试
continue;
}
if (errno == EPIPE || errno == ECONNRESET) {
// 4. 对端已关闭 → 关闭连接
CloseConnection(fd);
break;
}
// 5. 真错误 → 关闭连接
CloseConnection(fd);
break;
}
else { // n == 0
// 只有 len == 0 时才会到这;正常发送不应出现
break;
}
}
为什么不可以通过循环来处理非阻塞?而是要设置fd为非阻塞?
每次读100若来2000第21次读取则会阻塞。
ET与紧急指针的关联
ET模式在于约束程序员的行为与催促底层尽量/尽快读取接受缓冲区内的数据,并在网络层面优化了滑动窗口的大小。(PSH报头(报文)催促)
具体体现在EAGIN信号
多路转接的发送问题
接受缓冲区(不是fd的读事件)默认是不就绪的(因为在任何链接到来前没有内容),发送缓冲区(不是fd的写事件)默认是就绪的(是否 有接收空间 决定)。
1.因为内核中读事件默认是不就绪的,所以我们常设(来嵌入内核)。而写事件是需要按需设置的!!
//客户端
class Channel : public Connection
{
public:
Channel (int fd) : _sockfd (fd)
{
SetNoBlock (fd);
//常设
EvtSet (EPOLLIN | EPOLLET);
FdSet (fd);
}
~Channel ()
{
}
private:
int _sockfd; // 客户端fd
// 输入 输出缓冲区
std::string _inbuf;
std::string _otbuf;
};
2.写事件如何按需设置?/ 如何正确处理发送?
前提:对于“写”是系统面向系统的过程中应用层是难以干涉的!(所以我们使用负反馈来作为写的信号)
答:直接发送!!!若发送缓冲区被写满,再把写事件设置为就绪关心状态。(交由服务端的不断循环的下一轮wait接收并处理)
// 发送
void Sender()
{
while(true)
{
//. . .
}/*send_while*/
// 理应发送完毕
// 1.满了 EPOLLOUT 2.正常
if(!_otbuf.empty())//没有发送完全
{
RptrGet ()->EnableReadWrite (_sockfd, true, true);//读写事件就绪
}
else
{
RptrGet ()->EnableReadWrite (_sockfd, true, false);
}
}
认识One Thread One Loop(OTOL设计模式)
“一个执行流,一个循环”
将Reactor改为多线程/多进程放在Reactor内的fd层面进行并发就会导致颗粒度大的情况。但是如果我们采用OTOL模式即在Reactor层进行并发管理的话,fd就会被安全、均衡地派发到各个线程/进程内。
One Thread One Loop(单线程单事件循环) 加餐
感谢支持,长期连载 欢迎关注 



