【在Linux世界中追寻伟大的One Piece】多路转接epoll(续)
目录
1 -> epoll的工作方式
1.1 -> 水平触发(Level Triggered)工作模式
1.2 -> 边缘触发(Edge Triggered)工作模式
2 -> 对比LT与ET
3 -> 理解ET模式和非阻塞文件描述符
4 -> epoll的使用场景
5 -> epoll示例
5.1 -> epoll服务器(LT模式)
5.2 -> epoll服务器(ET模式)
1 -> epoll的工作方式
epoll有2中工作方式:
- 水平触发(LT)
- 边缘触发(ET)
加入有这样一个例子:
- 已经把一个tcp_socket添加到epoll描述符。
- 这个时候socket的另一端被写入了2KB的数据。
- 调用epoll_wait,并且它会返回。说明它已经准备好读取操作。
- 然后调用read,只读取了1KB的数据。
- 继续调用epoll_wait……
1.1 -> 水平触发(Level Triggered)工作模式
epoll默认状态下就是LT工作模式。
- 当epoll检测到socket上事件就绪的时候,可以不立刻进行处理。或者只处理一部分。
- 如上面的例子,由于只读了1K的数据,缓冲区中还剩1K的数据,在第二次调用epoll_wait时,epoll_wait仍然会立刻返回并通知socket读事件就绪。
- 直到缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回。
- 支持阻塞读写和非阻塞读写。
1.2 -> 边缘触发(Edge Triggered)工作模式
如果我们在第一步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式。
- 当epoll检测到socket上事件就绪时,必须立刻处理。
- 如上面的例子,虽然只读了1K的数据,缓冲区还剩1K的数据,在第二次调用epoll_wait的时候,epoll_wait不会再返回了。
- 也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会。
- ET的性能比LT的性能更高(epoll_wait返回的次数少了很多)。Nginx默认采用ET模式使用epoll。
- 只支持非阻塞的读写。
select和poll其实也是工作在LT模式下。epoll既可以支持LT,也可以支持ET。
2 -> 对比LT与ET
LT是epoll的默认行为。
使用ET能够减少epoll触发的次数。但是代价就是强逼着程序员一次响应就绪过程中就把所有的数据都处理完。
相当于一个文件描述符就绪后,不会反复被提示就绪,看起来就比LT更高效一些。但是在LT情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被反复提示的话,其实性能也是一样的。
另一方面,ET的代码复杂程度更高了。
3 -> 理解ET模式和非阻塞文件描述符
使用ET模式的epoll,需要将文件描述设置为非阻塞。这个不是接口上的要求,而是“工程实践”上的要求。
假设一个场景:服务器接收到一个10k的请求,会向客户端返回一个应答数据。如果客户端收不到应答,不会发送第二个10K的请求。
如果服务端写的代码是阻塞式的read,并且一次只read1K数据的话(read不能保证一次就把所有的数据都读出来,参考man手册的说明,可能被信号打断),剩下的9K数据就会待在缓冲区中。
此时由于epoll是ET模式,并不会认为文件描述符读就绪。epoll_wait就不会再次返回。剩下的9K数据会一直在缓冲区中。直到下一次客户端再给服务器写数据。
但是问题来了。
- 服务器只读到1K个数据,要10K读完才会给客户端返回响应数据。
- 客户端要读到服务器的响应,才会发送下一个请求。
- 客户端发送了下一个请求,epoll_wait才会返回,才能去读缓冲区中剩余的数据。
所以,为了解决上述问题(阻塞read不一定能一下把完整的请求读完),于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来。
而如果是LT没这个问题。只要缓冲区中的数据没读完,就能够让epoll_wait返回文件描述符读就绪。
4 -> epoll的使用场景
epoll的高性能,是有一定的特定场景的。如果场景选择的不适宜,epoll的性能可能适得其反。
- 对于多连接,且多连接中只有一部分连接比较活跃时,比较适合使用epoll。
例如,典型的一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适合epoll。
如果只是系统内部,服务器和服务器之间进行通信,只有少数的几个连接,这种情况下用epoll就并不适合。具体要根据需求和场景特点来决定使用哪种IO模型。
5 -> epoll示例
5.1 -> epoll服务器(LT模式)
tcp_epoll_server.hpp
///
// 封装一个 Epoll 服务器, 只考虑读就绪的情况
///#pragma once
#include <vector>
#include <functional>
#include <iostream>
#include <sys/epoll.h>
#include "tcp_socket.hpp"typedef std::function<void(const std::string&, std::string*resp)> Handler;class Epoll
{
public:Epoll() {epoll_fd_ = epoll_create(10);}~Epoll() {close(epoll_fd_);}bool Add(const TcpSocket& sock) const {int fd = sock.GetFd();printf("[Epoll Add] fd = %d\n", fd);epoll_event ev;ev.data.fd = fd;ev.events = EPOLLIN;int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);if (ret < 0) {perror("epoll_ctl ADD");return false;}return true;}bool Del(const TcpSocket& sock) const {int fd = sock.GetFd();printf("[Epoll Del] fd = %d\n", fd);int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, NULL);if (ret < 0) {perror("epoll_ctl DEL");return false;}return true;}bool Wait(std::vector<TcpSocket>* output) const {output->clear();epoll_event events[1000];int nfds = epoll_wait(epoll_fd_, events, sizeof(events) / sizeof(events[0]), -1);if (nfds < 0) {perror("epoll_wait");return false;}// [注意!] 此处必须是循环到 nfds, 不能多循环for (int i = 0; i < nfds; ++i) {TcpSocket sock(events[i].data.fd);output->push_back(sock);}return true;}private:int epoll_fd_;
};class TcpEpollServer
{
public:TcpEpollServer(const std::string& ip, uint16_t port) : ip_(ip),port_(port) {}bool Start(Handler handler) {// 1. 创建 socketTcpSocket listen_sock;CHECK_RET(listen_sock.Socket());// 2. 绑定CHECK_RET(listen_sock.Bind(ip_, port_));// 3. 监听CHECK_RET(listen_sock.Listen(5));// 4. 创建 Epoll 对象, 并将 listen_sock 加入进去Epoll epoll;epoll.Add(listen_sock);// 5. 进入事件循环for (;;) {// 6. 进行 epoll_waitstd::vector<TcpSocket> output;if (!epoll.Wait(&output)) {continue;}// 7. 根据就绪的文件描述符的种类决定如何处理for (size_t i = 0; i < output.size(); ++i) {if (output[i].GetFd() == listen_sock.GetFd()) {// 如果是 listen_sock, 就调用 acceptTcpSocket new_sock;listen_sock.Accept(&new_sock);epoll.Add(new_sock);}else {// 如果是 new_sock, 就进行一次读写std::string req, resp;bool ret = output[i].Recv(&req);if (!ret) {// [注意!!] 需要把不用的 socket 关闭// 先后顺序别搞反. 不过在 epoll 删除的时候其实就已经关闭 socket 了epoll.Del(output[i]);output[i].Close();continue;}handler(req, &resp);output[i].Send(resp);} // end for} // end for (;;)}return true;}private:std::string ip_;uint16_t port_;
};
dict_server.cc只需要将server对象的类型改成TcpEpollServer即可。
5.2 -> epoll服务器(ET模式)
基于LT版本稍加修改即可。
- 修改tcp_socket.hpp,新增非阻塞读和非阻塞写接口。
- 对于accept返回的new_sock加上EPOLLET这样的选项。
注意:
此代码暂时未考虑listen_sock ET的情况。如果将listen_sock设为ET,则需要非阻塞轮询的方式accept。否则会导致同一时刻大量的客户端同时连接的时候,只能accept一次的问题。
tcp_socket.hpp
// 以下代码添加在 TcpSocket 类中
// 非阻塞 IO 接口bool SetNoBlock()
{int fl = fcntl(fd_, F_GETFL);if (fl < 0) {perror("fcntl F_GETFL");return false;}int ret = fcntl(fd_, F_SETFL, fl | O_NONBLOCK);if (ret < 0) {perror("fcntl F_SETFL");return false;}return true;
}bool RecvNoBlock(std::string* buf) const
{// 对于非阻塞 IO 读数据, 如果 TCP 接受缓冲区为空, 就会返回错误// 错误码为 EAGAIN 或者 EWOULDBLOCK, 这种情况也是意料之中, 需要重试// 如果当前读到的数据长度小于尝试读的缓冲区的长度, 就退出循环// 这种写法其实不算特别严谨(没有考虑粘包问题)buf->clear();char tmp[1024 * 10] = { 0 };for (;;) {ssize_t read_size = recv(fd_, tmp, sizeof(tmp) - 1, 0);if (read_size < 0) {if (errno == EWOULDBLOCK || errno == EAGAIN) {continue;}perror("recv");return false;}if (read_size == 0) {// 对端关闭, 返回 falsereturn false;}tmp[read_size] = '\0';*buf += tmp;if (read_size < (ssize_t)sizeof(tmp) - 1) {break;}}return true;
}bool SendNoBlock(const std::string& buf) const
{// 对于非阻塞 IO 的写入, 如果 TCP 的发送缓冲区已经满了, 就会出现出错的情况// 此时的错误号是 EAGAIN 或者 EWOULDBLOCK. 这种情况下不应放弃治疗// 而要进行重试ssize_t cur_pos = 0; // 记录当前写到的位置ssize_t left_size = buf.size();for (;;) {ssize_t write_size = send(fd_, buf.data() + cur_pos,left_size, 0);if (write_size < 0) {if (errno == EAGAIN || errno == EWOULDBLOCK) {// 重试写入continue;}return false;}cur_pos += write_size;left_size -= write_size;// 这个条件说明写完需要的数据了if (left_size <= 0) {break;}}return true;
}
tcp_epoll_server.hpp
///
// 封装一个 Epoll ET 服务器
// 修改点:
// 1. 对于 new sock, 加上 EPOLLET 标记
// 2. 修改 TcpSocket 支持非阻塞读写
// [注意!] listen_sock 如果设置成 ET, 就需要非阻塞调用 accept 了
///#pragma once#include <vector>
#include <functional>
#include <sys/epoll.h>
#include "tcp_socket.hpp"typedef std::function<void(const std::string&, std::string* resp)> Handler;class Epoll
{
public:Epoll() {epoll_fd_ = epoll_create(10);}~Epoll() {close(epoll_fd_);}bool Add(const TcpSocket& sock, bool epoll_et = false) const {int fd = sock.GetFd();printf("[Epoll Add] fd = %d\n", fd);epoll_event ev;ev.data.fd = fd;if (epoll_et) {ev.events = EPOLLIN | EPOLLET;}else {ev.events = EPOLLIN;}int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev);if (ret < 0) {perror("epoll_ctl ADD");return false;}return true;}bool Del(const TcpSocket& sock) const {int fd = sock.GetFd();printf("[Epoll Del] fd = %d\n", fd);int ret = epoll_ctl(epoll_fd_, EPOLL_CTL_DEL, fd, NULL);if (ret < 0) {perror("epoll_ctl DEL");return false;}return true;}bool Wait(std::vector<TcpSocket>* output) const {output->clear();epoll_event events[1000];int nfds = epoll_wait(epoll_fd_, events, sizeof(events) / sizeof(events[0]), -1);if (nfds < 0) {perror("epoll_wait");return false;}// [注意!] 此处必须是循环到 nfds, 不能多循环for (int i = 0; i < nfds; ++i) {TcpSocket sock(events[i].data.fd);output->push_back(sock);}return true;}private:int epoll_fd_;
};class TcpEpollServer
{
public:TcpEpollServer(const std::string& ip, uint16_t port) : ip_(ip),port_(port) {}bool Start(Handler handler) {// 1. 创建 socketTcpSocket listen_sock;CHECK_RET(listen_sock.Socket());// 2. 绑定CHECK_RET(listen_sock.Bind(ip_, port_));// 3. 监听CHECK_RET(listen_sock.Listen(5));// 4. 创建 Epoll 对象, 并将 listen_sock 加入进去Epoll epoll;epoll.Add(listen_sock);// 5. 进入事件循环for (;;) {// 6. 进行 epoll_waitstd::vector<TcpSocket> output;if (!epoll.Wait(&output)) {continue;}// 7. 根据就绪的文件描述符的种类决定如何处理for (size_t i = 0; i < output.size(); ++i) {if (output[i].GetFd() == listen_sock.GetFd()) {// 如果是 listen_sock, 就调用 acceptTcpSocket new_sock;listen_sock.Accept(&new_sock);epoll.Add(new_sock, true);}else {// 如果是 new_sock, 就进行一次读写std::string req, resp;bool ret = output[i].RecvNoBlock(&req);if (!ret) {// [注意!!] 需要把不用的 socket 关闭// 先后顺序别搞反. 不过在 epoll 删除的时候其实就已经关闭 socket 了epoll.Del(output[i]);output[i].Close();continue;}handler(req, &resp);output[i].SendNoBlock(resp);printf("[client %d] req: %s, resp: %s\n",output[i].GetFd(),req.c_str(), resp.c_str());} // end for} // end for (;;)}return true;}private:std::string ip_;uint16_t port_;
};
感谢各位大佬支持!!!
互三啦!!!
相关文章:

【在Linux世界中追寻伟大的One Piece】多路转接epoll(续)
目录 1 -> epoll的工作方式 1.1 -> 水平触发(Level Triggered)工作模式 1.2 -> 边缘触发(Edge Triggered)工作模式 2 -> 对比LT与ET 3 -> 理解ET模式和非阻塞文件描述符 4 -> epoll的使用场景 5 -> epoll示例 5.1 -> epoll服务器(LT模式) 5.2…...
【不写for循环】玩玩行列
利用numpy的并行操作可以比纯用Python的list快很多,不仅如此,代码往往精简得多。 So, 这篇来讲讲进阶的广播和花哨索引操作,少写几个for循环()。 目录 一个二维的例题 一个三维的例题 解法一 解法二 更难的三维例题…...
【Nginx】反向代理Https时相关参数:
在Nginx代理后台HTTPS服务时,有几个关键的参数需要配置,以确保代理服务器能够正确地与后端服务器进行通信。一些重要参数的介绍: proxy_ssl_server_name:这个参数用于指定是否在TLS握手时通过SNI(Server Name Indicati…...
第 17 章 - Go语言 上下文( Context )
在Go语言中,context包为跨API和进程边界传播截止时间、取消信号和其他请求范围值提供了一种方式。它主要应用于网络服务器和长时间运行的后台任务中,用于控制一组goroutine的生命周期。下面我们将详细介绍context的定义、使用场景、取消和超时机制&#…...

Android Framework AMS(16)进程管理
该系列文章总纲链接:专题总纲目录 Android Framework 总纲 本章关键点总结 & 说明: 说明:本章节主要解读AMS 进程方面的知识。关注思维导图中左上侧部分即可。 我们本章节主要是对Android进程管理相关知识有一个基本的了解。先来了解下L…...

STM32设计防丢防摔智能行李箱
目录 目录 前言 一、本设计主要实现哪些很“开门”功能? 二、电路设计原理图 1.电路图采用Altium Designer进行设计: 2.实物展示图片 三、程序源代码设计 四、获取资料内容 前言 随着科技的不断发展,嵌入式系统、物联网技术、智能设备…...

【异常解决】Linux shell报错:-bash: [: ==: 期待一元表达式 解决方法
博主介绍:✌全网粉丝21W,CSDN博客专家、Java领域优质创作者,掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…...

ML 系列: 第 23 节 — 离散概率分布 (多项式分布)
目录 一、说明 二、多项式分布公式 2.1 多项式分布的解释 2.2 示例 2.3 特殊情况:二项分布 2.4 期望值 (Mean) 2.5 方差 三、总结 3.1 python示例 一、说明 伯努利分布对这样一种情况进行建模:随机变量可以采用两个可能的值&#…...
Webpack 1.13.2 执行 shell 命令解决 打印时没有背景色和文字颜色的问题
这是因为 Webpack 1.13.2 不支持新的插件钩子 API。Webpack 1 的插件系统使用的是 plugin 方法,而不是 Webpack 4 中的 hooks。 在 Webpack 1 中,你可以使用以下代码来确保 sed 命令在打包完成后执行: const { exec } require(child_proce…...

C++构造函数详解
构造函数详解:C 中对象初始化与构造函数的使用 在 C 中,构造函数是一种特殊的成员函数,它在创建对象时自动调用,用来初始化对象的状态。构造函数帮助我们确保每个对象在被创建时就处于一个有效的状态,并且在不传递任何…...

POI实现根据PPTX模板渲染PPT
目录 1、前言 2、了解pptx文件结构 3、POI组件 3.1、引入依赖 3.2、常见的类 3.3、实现原理 3.4、关键代码片段 3.4.1、获取ppt实例 3.4.2、获取每页幻灯片 3.4.3、循环遍历幻灯片处理 3.4.3.1、文本 3.4.3.2、饼图 3.4.3.3、柱状图 3.4.3.4、表格 3.4.3.5、本地…...

【论文模型复现】深度学习、地质流体识别、交叉学科融合?什么情况,让我们来看看
文献:蓝茜茜,张逸伦,康志宏.基于深度学习的复杂储层流体性质测井识别——以车排子油田某井区为例[J].科学技术与工程,2020,20(29):11923-11930. 本文目录 一、前言二、文献阅读-基于深度学习的复杂储层流体性质测井识别2.1 摘要2.2 当前研究不足2.3 本文创新2.4 论文…...

树的直径计算:算法详解与实现
树的直径计算:算法详解与实现 1. 引言2. 算法概述3. 伪代码实现4. C语言实现5. 算法分析6. 结论在图论中,树的直径是一个关键概念,它表示树中任意两点间最长路径的长度。对于给定的树T=(V,E),其中V是顶点集,E是边集,树的直径定义为所有顶点对(u,v)之间最短路径的最大值。…...

conda创建 、查看、 激活、删除 python 虚拟环境
1、创建 python 虚拟环境 ,假设该环境命名为 “name”。 conda create -n name python3.11 2、查看 python 虚拟环境。 conda info -e 3、激活使用 python 虚拟环境。 conda activate name 4、删除 python 虚拟环境 conda remove -n name --all 助力快速掌握数据集…...

vs2022搭建opencv开发环境
1 下载OpenCV库 https://opencv.org/ 下载对应版本然后进行安装 将bin目录添加到系统环境变量opencv\build\x64\vc16\bin 复制该路径 打开高级设置添加环境变量 vs2022新建一个空项目 修改属性添加头文件路径和库路径 修改链接器,将OpenCV中lib库里的o…...

NVIDIA NIM 开发者指南:入门
NVIDIA NIM 开发者指南:入门 NVIDIA 开发者计划 想要了解有关 NIM 的更多信息?加入 NVIDIA 开发者计划,即可免费访问任何基础设施云、数据中心或个人工作站上最多 16 个 GPU 上的自托管 NVIDIA NIM 和微服务。 加入免费的 NVIDIA 开发者计…...

探索Python网络请求新纪元:httpx库的崛起
文章目录 **探索Python网络请求新纪元:httpx库的崛起**第一部分:背景介绍第二部分:httpx库是什么?第三部分:如何安装httpx库?第四部分:简单的库函数使用方法1. 发送GET请求2. 发送POST请求3. 超…...
学了Arcgis的水文分析——捕捉倾泻点,河流提取与河网分级,3D图层转要素失败的解决方法,测量学综合实习网站存着
ArcGIS水文分析实战教程(7)细说流域提取_汇流域栅格-CSDN博客 ArcGIS水文分析实战教程(6)河流提取与河网分级_arcgis的dem河流分级-CSDN博客 ArcGIS水文分析实战教程(5)细说流向与流量-CSDN博客 ArcGIS …...

QQ 小程序已发布,但无法被搜索的解决方案
前言 我的 QQ 小程序在 2024 年 8 月就已经审核通过,上架后却一直无法被搜索到。打开后,再在 QQ 上下拉查看 “最近使用”,发现他出现一下又马上消失。 上线是按正常流程走的,开发、备案、审核,没有任何违规…...

【C++】拷贝构造 和 赋值运算符重载
目录: 一、拷贝构造 (一)拷贝函数的特点 二、赋值运算符重载 (一)运算符重载 (二)赋值运算符重载 正文 一、拷贝构造 如果一个构造函数的第一个参数是自身类类型的引用,且任何…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...

自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...
Java线上CPU飙高问题排查全指南
一、引言 在Java应用的线上运行环境中,CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时,通常会导致应用响应缓慢,甚至服务不可用,严重影响用户体验和业务运行。因此,掌握一套科学有效的CPU飙高问题排查方法&…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
【Nginx】使用 Nginx+Lua 实现基于 IP 的访问频率限制
使用 NginxLua 实现基于 IP 的访问频率限制 在高并发场景下,限制某个 IP 的访问频率是非常重要的,可以有效防止恶意攻击或错误配置导致的服务宕机。以下是一个详细的实现方案,使用 Nginx 和 Lua 脚本结合 Redis 来实现基于 IP 的访问频率限制…...

基于PHP的连锁酒店管理系统
有需要请加文章底部Q哦 可远程调试 基于PHP的连锁酒店管理系统 一 介绍 连锁酒店管理系统基于原生PHP开发,数据库mysql,前端bootstrap。系统角色分为用户和管理员。 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销 2 个人中…...
在golang中如何将已安装的依赖降级处理,比如:将 go-ansible/v2@v2.2.0 更换为 go-ansible/@v1.1.7
在 Go 项目中降级 go-ansible 从 v2.2.0 到 v1.1.7 具体步骤: 第一步: 修改 go.mod 文件 // 原 v2 版本声明 require github.com/apenella/go-ansible/v2 v2.2.0 替换为: // 改为 v…...