【在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版本稍加修改即可。

  1. 修改tcp_socket.hpp,新增非阻塞读和非阻塞写接口。
  2. 对于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_;
};

感谢各位大佬支持!!!

互三啦!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/59599.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机视觉 ---常见图像文件格式及其特点

常见的图像文件格式及其特点如下&#xff1a; JPEG&#xff08;Joint Photographic Experts Group&#xff09; 特点&#xff1a; 有损压缩&#xff1a;通过丢弃一些图像数据来实现高压缩比&#xff0c;能显著减小文件大小&#xff0c;适合用于存储照片等色彩丰富的图像。但过…

Vue3 动态获取 assets 文件夹图片

我真服了Vue3 这个老六了,一个简单图片src 赋值搞得那么复杂. //item.type 是我遍历类型的类型参数 <img alt"吐槽大会" :src"getAssetUrl(item.type)" /> 基于 Vue2 的Webpack 处理,还不错,可以用/ 这种绝对路径,可以接受,虽然多了个require很不爽…

响应“一机两用”政策 落实政务外网安全

在数字化时代&#xff0c;政务办公外网安全的重要性日益凸显&#xff0c;特别是在“一机两用”的背景下&#xff0c;即同一台终端既要处理政务内网的数据&#xff0c;又要访问互联网&#xff0c;这对网络安全提出了更高的要求。深信达SPN安全上网方案&#xff0c;即反向沙箱技术…

MySQL详解之DDL(数据定义语言)

引言 在关系型数据库的管理与操作中&#xff0c;DDL&#xff08;数据定义语言&#xff09;是一个至关重要的组成部分。它允许我们创建、修改和删除数据库的结构&#xff0c;包括数据库、表、视图等对象。对于新手来说&#xff0c;理解DDL的基本概念与常用命令&#xff0c;可以…

【MYSQL】数据库三大范式是什么?【最简单理解】

这些依赖都是按道理来说的&#xff0c;比如地址可以拆成国、省、市、区。 第一范式 数据表中拿到字段存储的数据不可再拆分&#xff0c;比如地址是国、省、市、区&#xff0c;必须拆封开&#xff0c;不能一股脑全写到一个字段中。 第二范式 数据表中在满足第一范式的情况下&…

Vue监视属性变化watch

在 Vue.js 中&#xff0c;watch 选项用于观察 Vue 实例的数据变化&#xff0c;并在数据变化时执行相应的回调函数。它特别适用于你需要在数据变化时执行异步或开销较大的操作的场景。watch 可以监听数据属性、计算属性的变化&#xff0c;也可以对深层嵌套的数据进行监听。 基本…

gitlab角色、权限

GitLab是一个基于Web的Git仓库管理工具&#xff0c;它提供了一套完整的角色和权限管理机制&#xff0c;以控制用户对项目和仓库的访问和操作权限。以下是GitLab中不同角色的基本权限概述&#xff1a; 访客&#xff08;Guest&#xff09;&#xff1a; 可以查看项目中的公开信息。…

基于机器学习电信号EMG训练分类模型控制仿生手控制系统(Matlab-Simulink实现)

基于机器学习电信号EMG训练分类模型控制仿生手控制系统&#xff08;Matlab-Simulink实现&#xff09; 引言 随着生物医学工程和机器学习技术的发展&#xff0c;仿生手控制系统的研发取得了显著进展。本文将介绍如何利用机器学习方法&#xff0c;通过肌电图&#xff08;Electr…

https://localhost/index 配置的nginx,一刷新就报404了

当你的Nginx配置导致页面刷新时报404错误时&#xff0c;通常是由于以下几个原因造成的&#xff1a; 静态文件路径配置错误&#xff1a;Nginx没有正确地指向静态文件的目录。前端路由问题&#xff1a;如果是SPA&#xff08;单页应用&#xff09;&#xff0c;刷新页面时Nginx没有…

计算机网络基础——针对实习面试

目录 计算机网络基础OSI七层模型TCP/IP四层模型为什么网络要分层&#xff1f;常见网络协议 计算机网络基础 OSI七层模型 开放系统互连参考模型&#xff08;Open Systems Interconnection Reference Model&#xff0c;简称OSI模型&#xff09;是一个概念性模型&#xff0c;用于…

leetcode100:相同的树

给你两棵二叉树的根节点 p 和 q &#xff0c;编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同&#xff0c;并且节点具有相同的值&#xff0c;则认为它们是相同的。 示例 1&#xff1a; 输入&#xff1a;p [1,2,3], q [1,2,3] 输出&#xff1a;true示例 2&…

将已有的MySQL8.0单机架构变成主从复制架构

过程: 把数据库做一个完全备份, 恢复到从节点上, 恢复后从备份的那个点开始往后复制,从而保证后续数据的一致性。 步骤: 修改 master 主节点 的配置&#xff08; server-id log-bin &#xff09;master 主节点 完全备份&#xff08; mysqldump &#xff09;master 主节点 创建…

如何在jupyter notebook切换python环境

目录 参考链接 首先确保conda已经正常安装 conda --version 或者conda -V 以下请将“myenv”替换成自己的命名&#xff01;&#xff01;&#xff01; 1-查看虚拟环境目录 conda env list 2-创建虚拟环境命令 conda create -n myenv 或者 conda create --name myenv 3-激活虚拟环…

Springboot 整合 itext 实现PDF文件合并,识别图片则转成PDF拼接

目录 前言一、引用依赖二、使用步骤1.Controller2.Service接口3.实现类三、请求接口及结果前言 本文实现 Springboot 整合 itext 实现PDF文件合并,图片转PDF拼接。 一、引用依赖 <dependency><groupId>com.itextpdf</groupId><artifactId>itext7-co…

在 Ubuntu 18.04 中搭建和测试 DNS 服务器

在 Ubuntu 18.04 中搭建和测试 DNS 服务器可以通过安装和配置 BIND&#xff08;Berkeley Internet Name Domain&#xff09;来实现。以下是详细的步骤&#xff1a; 1. 安装 BIND 打开终端并运行以下命令来安装 BIND&#xff1a; sudo apt update sudo apt install bind9 bin…

【嵌入式软件-STM32】OLED显示屏+调试方法

目录 一、调试方式 1&#xff09;串口调试 优势 弊端 2&#xff09;显示屏调试 优势 弊端 3&#xff09;Keil调试模式 4&#xff09;点灯调试法 5&#xff09;注释调试法 6&#xff09;对照法 二、OLED简介 OLED组件 OLED显示屏 0.96寸OLED模块 OLED外观和种类…

【知识科普】微内核架构与宏内核架构

微内核与宏内核 微内核一、微内核的定义二、微内核的特点三、微内核的优缺点四、微内核的应用场景操作系统 宏内核一、宏内核的定义二、宏内核的特点三、宏内核的优缺点四、宏内核的应用场景 微内核架构与宏内核架构简单比较微内核的优势宏内核的优势面向未来的架构 微内核 微…

Liebherr利勃海尔 EDI 需求分析

Liebherr 使用 EDI 技术来提高业务流程的效率、降低错误率、加快数据交换速度&#xff0c;并优化与供应商、客户和其他合作伙伴之间的业务沟通。通过 EDI&#xff0c;Liebherr 实现了与全球交易伙伴的自动化数据交换&#xff0c;提升了供应链管理和订单处理的透明度。 Liebher…

求字符 ‘a‘ 和 ‘b‘ 组成的,最大长度为n的字符串中字典序第 k 个字符串

求字符 ‘a’ 和 ‘b’ 组成的&#xff0c;最大长度为n的字符串中字典序第 k 个字符串 先来解释一下这个题目&#xff0c;假设最大长度为3&#xff0c;那么由字符a和b组成的字符串有&#xff1a; a, b, ab, aaa, aba...把这些字符串按照字典序排序: aaaaaaaabababaabbbbabaab…

再见 阿里巴巴EasyExcel替代品EasyExcel-Plus即将诞生

最近阿里发布公告通知&#xff0c;停止对EasyExcel 更新和维护&#xff0c;EasyExcel 是一款知名的 Java Excel 工具库&#xff0c;由阿里巴巴开源&#xff0c;作者是玉霄&#xff0c;在 GitHub 上有 30k stars、7.5k forks。 据了解&#xff0c;EasyExcel作者玉霄)去年已经从…