高级IO---五种IO模型多路转接之Select

文章目录

  • 五种IO模型
    • 1、阻塞IO
    • 2、非阻塞IO
    • 3、信号驱动IO
    • 4、多路转接IO
    • 5、异步IO
    • 总结IO
  • 同步与异步
  • 阻塞与非阻塞
  • 设置非阻塞
    • 利用fcntl接口实现一个设置非阻塞的函数
  • 多路转接之Select
    • select函数原型
      • fd_set结构
      • 返回值
    • socket就绪条件
      • 读就绪
      • 写就绪
    • select的特点
    • select使用示例
      • Util.hpp(工具类,将用到的函数放在该类中)
      • Server.hpp(实现服务器)
      • log.hpp(日志类)
      • Server.cc
      • 效果演示

五种IO模型

1、阻塞IO

在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。

也就是说,在数据准备好之前,系统调用只会静静的等待着数据的到来并不会去干其他的事情

就好比去钓鱼,将鱼饵丢进水里后,啥也不干就静静的看着鱼饵随时准备鱼上钩

2、非阻塞IO

如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用。

就好比去钓鱼,将鱼饵丢进水里后,不会一直去盯着鱼饵,一边干着其他事看看书啥的偶尔看看鱼饵

3、信号驱动IO

内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。

这就好比在鱼竿上系上一个铃铛,然后去干别的事,当鱼上钩时拉动鱼线就会使铃铛摇晃发出声音提醒鱼上钩了

4、多路转接IO

最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

这就好比拿着多条鱼竿去钓鱼,全部丢进水里然后巡视所有鱼竿,一有鱼上钩就拉动对应的鱼竿

5、异步IO

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

这就好比老板想吃鱼,不用自己去钓,让手下去钓鱼调到了之后交给他

总结IO

任何IO过程中都包含两个步骤:第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。所以让IO更高效最核心的办法就是让等待的时间尽量减少

所以综上而言,多把钓竿同时等待,鱼上钩的概率就越大上钩的时间也就越快,所以多路转接IO效率高

同步与异步

同步和异步关注的是消息通信机制

  1. 同步:就是在发出一个调用时,在没有得到结果之前,该调用不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果;
  2. 异步:正好相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

注意这里的同步通信和进程之间的同步是完全不想干的概念

阻塞与非阻塞

  1. 阻塞:调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回
  2. 非阻塞:在不能立刻得到结果之前,该调用不会阻塞当前线程。

设置非阻塞

需要利用系统调用 ---- fcntl

#include <unistd.h>
#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );

参数一为:需要设置的文件描述符

参数二为:想要让fcntl实现的功能

  1. 复制一个现有的描述符(cmd = F_DUPFD) .
  2. 获得/设置文件描述符标记(cmd = F_GETFD或F_SETFD).
  3. 获得/设置文件状态标记(cmd = F_GETFL或F_SETFL).
  4. 获得/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN).
  5. 获得/设置记录锁(cmd = F_GETLK,F_SETLK或F_SETLKW)

其中设置非阻塞为第三个功能。

后面的为追加参数

利用fcntl接口实现一个设置非阻塞的函数

void SetNoBlock(int fd) {// 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)int fl = fcntl(fd, F_GETFL);if (fl < 0) {perror("fcntl");return;}// 再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

多路转接之Select

系统提供select函数来实现多路复用输入/输出模型

select系统调用是用来让程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数原型

 #include <sys/select.h>/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
  1. 参数nfds是需要监视的最大的文件描述符值+1
  2. rdset、wrset、exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合
  3. 参数timeout为结构timeval,用来设置select()的等待时间,如果在指定的时间段里没有事件发生, select将超时返回

fd_set结构

typedef struct{/* XPG4.2 requires this member name.  Otherwise avoid the namefrom the global namespace.  */
#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif} fd_set;

其实这个结构就是一个整数数组, 更严格的说,是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.

提供了一组操作fd_set的接口, 来比较方便的操作位图

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的全部位

例如取fd_set为1个字节,为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd

返回值

  1. 执行成功则返回文件描述词状态已改变的个数
  2. 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
  3. 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测。

socket就绪条件

读就绪

  1. socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
  2. socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  3. 监听的socket上有新的连接请求;
  4. socket上有未处理的错误

写就绪

  1. socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
  2. socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
  3. socket使用非阻塞connect连接成功或失败之后;
  4. socket上有未读取的错误;

select的特点

  1. 可监控的文件描述符个数取决与sizeof(fd_set)的值。每bit表示一个文件描述符
  2. 将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,
    1. 一是用于再select 返回后, array作为源数据和fd_set进行FD_ISSET判断。
    2. 二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数

select的缺点:

  1. 每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
  2. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  3. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  4. select支持的文件描述符数量太小

select使用示例

这里以只关心读事件为例,写事件同理

Util.hpp(工具类,将用到的函数放在该类中)

#pragma once#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <sys/socket.h>
#include <functional>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include <cstring>
#include <vector>using namespace std;#define INITPORT 8000
#define FDNUM 1024
#define DEFAULTFD -1// 打印函数调试
void Print(const vector<int> &fdv)
{cout << "fd list: ";for (int i = 0; i < FDNUM; i++){if (fdv[i] != DEFAULTFD)cout << fdv[i] << " ";}cout << endl;
}class Util
{
public:static void Recv(vector<int> &fdv, int sock, int i){// 读取// 读取失败就关闭sock并且修改集合组里的数据char buff[1024];ssize_t s = recv(sock, buff, sizeof(buff) - 1, 0);if (s > 0){buff[s] = 0;cout << "client: " << buff << endl;LogMessage(NORMAL, "client: %s", buff);}else if (s == 0){close(sock);fdv[i] = DEFAULTFD;LogMessage(NORMAL, "client quit");return;}else{close(sock);fdv[i] = DEFAULTFD;LogMessage(ERROR, "client quit: %s", strerror(errno));return;}// 写回数据// 这里不考虑写事件string response = buff;write(sock, response.c_str(), response.size());LogMessage(DEBUG, "Recver end");}// 将通信sock添加进集合组static void AddSock(vector<int> &fdv, int listensock){// listensock读事件就绪string clientip;uint16_t clientport;int sock = Util::GetSock(listensock, &clientip, &clientport);if (sock < 0)return;else{LogMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 遍历数组,要考虑满的情况// 遇到为-1的位置插入新的sockint i = 0;for (; i < FDNUM; ++i)if (fdv[i] == DEFAULTFD)break;if (i == FDNUM){LogMessage(WARNING, "server if full, please wait");close(sock);}elsefdv[i] = sock;}Print(fdv);LogMessage(DEBUG, "Accepter out");}// 获取新连接创建通信sockstatic int GetSock(int listensock, string *clientip, uint16_t *clientport){struct sockaddr_in peer;memset(&peer, 0, sizeof(peer));socklen_t len = sizeof(peer);int sock = accept(listensock, (struct sockaddr *)&peer, &len);if (sock < 0)LogMessage(ERROR, "accept socket error, next");else{LogMessage(NORMAL, "accept socket %d success", sock);cout << "sock: " << sock << endl;*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}// 设置监听套接字为监听状态static void setListen(int listensock){if (listen(listensock, 5) < 0){LogMessage(FATAL, "listen socket error!");exit(3);}LogMessage(NORMAL, "listen socket success");}// 绑定网络信息static void bindSock(int port, int listensock){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){LogMessage(FATAL, "bind socket error!");exit(2);}LogMessage(NORMAL, "bind sock success");}// 创建监听套接字static void createSock(int *listensock){*listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){LogMessage(FATAL, "create socket error!");exit(1);}LogMessage(NORMAL, "create socket success");// 设置进程可以立即重启int opt = 1;setsockopt(*listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));}// 设置非阻塞static void SetNonBlock(int fd){int f = fcntl(fd, F_GETFL);if (f < 0){cerr << "fcntl" << endl;return;}fcntl(fd, F_SETFL, f | O_NONBLOCK);}
};

Server.hpp(实现服务器)

#pragma once#include <iostream>
#include "Util.hpp"
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>using namespace std;class Server
{
public:Server(const uint16_t port = INITPORT): _port(port), _listensock(-1){}void HandlerEvent(fd_set &rfds){int i = 0;for (auto e : fdv){// 过滤掉非法的fdif (e == DEFAULTFD)continue;if (FD_ISSET(e, &rfds) && e == _listensock) // 判断listensock在不在就绪的集合中Util::AddSock(fdv, _listensock);else if(FD_ISSET(e, &rfds)) // 如果为其他的文件描述符则读取数据Util::Recv(fdv, e, i);else{}++i;}}void Init(){// 创建监听套接字Util::createSock(&_listensock);// 绑定网络信息Util::bindSock(_port, _listensock);// 设置监听套接字为监听状态Util::setListen(_listensock);fdv.resize(FDNUM, DEFAULTFD);fdv[0] = _listensock;}void start(){while (1){fd_set rfds;// 清除描述词组set的全部位FD_ZERO(&rfds);// 记录下最大的文件描述符int max = fdv[0];// 遍历数组,将合法的fd插入到事件集中// 并记录最大的fd为调用select接口做准备for (auto e : fdv){if (e == DEFAULTFD)continue;// 设置描述词组set中相关fd的位FD_SET(e, &rfds);if (e > max)max = e;}// 设置等待时间结构struct timeval timeout = {1, 0};int n = select(max + 1, &rfds, nullptr, nullptr, &timeout);switch (n){case 0:cout << "timeout...." << endl;LogMessage(NORMAL, "timeout....");break;case -1:printf("select error, code: %d, err string: %s", errno, strerror(errno));LogMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));break;default:// 有事件就绪cout << "event readly" << endl;LogMessage(NORMAL, "event readly");// 处理事件HandlerEvent(rfds);break;}}}~Server(){if (_listensock < 0)close(_listensock);}private:int _listensock;uint16_t _port;vector<int> fdv;
};

log.hpp(日志类)

#pragma once#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>using namespace std;#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4const char *to_levelstr(int level)
{switch (level){case DEBUG:return "DEBUG";case NORMAL:return "NORMAL";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return nullptr;}
}void LogMessage(int level, const char *format, ...)
{
#define NUM 1024char logpre[NUM];snprintf(logpre, sizeof(logpre), "[%s][%ld][%d]", to_levelstr(level), (long int)time(nullptr), getpid());char line[NUM];// 可变参数va_list arg;va_start(arg, format);vsnprintf(line, sizeof(line), format, arg);// 保存至文件FILE* log = fopen("log.txt", "a");FILE* err = fopen("log.error", "a");if(log && err){FILE *curr = nullptr;if(level == DEBUG || level == NORMAL || level == WARNING) curr = log;if(level == ERROR || level == FATAL) curr = err;if(curr) fprintf(curr, "%s%s\n", logpre, line);fclose(log);fclose(err);}
}

Server.cc

#include "Server.hpp"
#include <memory>// 输出命令错误函数
void Usage(string proc)
{cout << "Usage:\n\t" << proc << " local_port\n\n";
}int main(int argc, char *argv[])
{// 启动服务端不需要指定IPif (argc != 2){Usage(argv[0]);exit(1);}uint16_t port = atoi(argv[1]);unique_ptr<Server> sptr(new Server(port));sptr->Init();sptr->start();return 0;
}

效果演示

image-20230907205551697

首先由客户端连接,listen套接字就绪,建立连接

然后客户端发送数据,负责通信的套接字就绪,读取数据后再发回去

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

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

相关文章

转载: 又拍云【PrismCDN 】低延时的P2P HLS直播技术实践

低延时的P2P HLS直播技术实践本文是第二部分《PrismCDN 网络的架构解析,以及低延迟、低成本的奥秘》低延时的P2P HLS直播技术实践 [首页 > Open Talk NO.41 | 2018 音视频技术沙龙深圳站 > 低延时 WebP2P 直播技术实践https://opentalk-blog.b0.upaiyun.com/prod/2018-0…

TSINGSEE青犀AI视频分析/边缘计算/AI算法·人脸识别功能——多场景高效运用

旭帆科技AI智能分析网关可提供海量算法供应&#xff0c;涵盖目标监测、分析、抓拍、动作分析、AI识别等&#xff0c;可应用于各行各业的视觉场景中。同时针对小众化场景可快速定制AI算法&#xff0c;主动适配大厂近百款芯片&#xff0c;打通云/边/端灵活部署&#xff0c;算法一…

【Linux】高级IO --- Reactor网络IO设计模式

人其实很难抵制诱惑&#xff0c;人只能远离诱惑&#xff0c;所以千万不要高看自己的定力。 文章目录 一、LT和ET模式1.理解LT和ET的工作原理2.通过代码来观察LT和ET工作模式的不同3.ET模式高效的原因&#xff08;fd必须是非阻塞的&#xff09;4.LT和ET模式使用时的读取方式 二…

Java实践-物联网loT入门-MQTT传输协议

前言 MQTT是一个极其轻量级的发布/订阅消息传输协议,适用于网络带宽较低的场合. 它通过一个代理服务器&#xff08;broker&#xff09;&#xff0c;任何一个客户端&#xff08;client&#xff09;都可以订阅或者发布某个主题的消息&#xff0c;然后订阅了该主题的客户端则会收…

华硕ROG2/ROG5/ROG6/ROG7Pro强解锁L锁-快速实现root权限-支持Zenfone9/8/7

2023年9月新增解锁BL适配&#xff08;需要联系技术远程操作&#xff09;&#xff1a; 新增支持华硕ROG5/5S/5Pro机型强制解锁BL&#xff0c;并且支持OTA在线更新功能 新增支持华硕ROG6/6Pro机型强制解锁BL&#xff0c;并且支持OTA在线更新功能 新增支持华硕ROG7/7Pro机型强制解…

kuiper安装

1:使用docker方式安装 docker pull lfedge/ekuiper:latest docker run -p 9081:9081 -d --name kuiper -e MQTT_SOURCE__DEFAULT__SERVERtcp://127.0.0.1:1883 lfedge/ekuiper:latest这样就安装好了&#xff0c;但是操作只能通过命令完成&#xff0c;如果想要通过页面来操作&…

1065 A+B and C (64bit)

题&#xff1a;点我 题目大意&#xff1a; 这题虽然看着像签到&#xff0c;然鹅签不过去。 因为我最初写的沙雕代码是&#xff1a; #include<iostream> #include<cstdio> using namespace std; int main(void) {int t;scanf("%d", &t);for (int i …

Java后端开发面试题——JVM虚拟机篇

目录 什么是程序计数器&#xff1f; 你能给我详细的介绍Java堆吗? 什么是虚拟机栈 1. 垃圾回收是否涉及栈内存&#xff1f; 2. 栈内存分配越大越好吗&#xff1f; 3. 方法内的局部变量是否线程安全&#xff1f; 4.什么情况下会导致栈内存溢出&#xff1f; 5.堆栈的区别…

React Hook之useContext

1. 什么是useContext React官方解释&#xff1a;useContext 是一个 React Hook&#xff0c;可以让你读取和订阅组件中的 context&#xff08;React官方文档地址&#xff09;。 通俗的讲&#xff0c;useContext的作用就是&#xff1a;实现组件间的状态共享&#xff0c;主要应用场…

RFID溯源驱动汽车座椅制造的智能时代

在今天的快速发展的制造业中&#xff0c;信息化和智能化已经成为不可或缺的部分。信息化和智能化能够极大地提高生产效率、减少浪费&#xff0c;降低成本&#xff0c;提升产品的质量。汽车座椅产线信息化和智能化是汽车座椅产线升级的重要方向&#xff0c;RFID技术方案在汽车座…

【Flask】from flask_sqlalchemy import SQLAlchemy报错

【可能出现的情况】 1、未安装 Flask-SQLAlchemy&#xff1a; 在使用 flask_sqlalchemy 之前&#xff0c;你需要确保已经通过 pip 安装了 Flask-SQLAlchemy。可以通过以下命令安装它&#xff1a; pip install Flask-SQLAlchemy 2、包名大小写问题&#xff1a; Python 是区分大…

VGG 07

一、发展 1989年&#xff0c;Yann LeCun提出了一种用反向传导进行更新的卷积神经网络&#xff0c;称为LeNet。 1998年&#xff0c;Yann LeCun提出了一种用反向传导进行更新的卷积神经网络&#xff0c;称为LeNet-5 AlexNet是2012年ISLVRC 2012&#xff08;ImageNet Large Sca…

l8-d8 TCP并发实现

一、TCP多进程并发 1.地址快速重用 先退出服务端&#xff0c;后退出客户端&#xff0c;则服务端会出现以下错误&#xff1a; 地址仍在使用中 解决方法&#xff1a; /*地址快速重用*/ int flag1,len sizeof (int); if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &a…

yum安装mysql5.7散记

## 数据源安装 $ yum -y install wget $ wget http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm $ yum localinstall mysql57-community-release-el7-8.noarch.rpm $ yum repolist enabled | grep "mysql.*-community.*" $ yum install mysql-…

宇凡微YE09合封芯片,集成高性能32位mcu和2.4G芯片

合封芯片是指将主控芯片和外部器件合并封装的芯片&#xff0c;能大幅降低开发成本、采购成本、减少pcb面积等等。宇凡微YE09合封芯片&#xff0c;将技术领域推向新的高度。这款高度创新性的芯片融合了32位MCU和2.4G芯片&#xff0c;为各种应用场景提供卓越的功能和性能。 32位M…

谈一谈冷门的C语言爬虫

目录 C语言写爬虫是可行的 C语言爬虫不受待见 C语言爬虫有哪些可用的库和工具 C语言爬虫示例 总结 在当今的编程世界中&#xff0c;C语言相比于一些主流编程语言如Python、JavaScript等&#xff0c;使用范围相对较窄。然而&#xff0c;尽管C语言在爬虫领域的应用并不常见&…

Redis持久化、主从与哨兵架构详解

Redis持久化 RDB快照&#xff08;snapshot&#xff09; 在默认情况下&#xff0c; Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中。 你可以对 Redis 进行设置&#xff0c; 让它在“ N 秒内数据集至少有 M 个改动”这一条件被满足时&#xff0c; 自动保存一次数…

MIT6.824 Spring2021 Lab 1: MapReduce

文章目录 0x00 准备0x01 MapReduce简介0x02 RPC0x03 调试0x04 代码coordinator.gorpc.goworker.go 0x00 准备 阅读MapReduce论文配置GO环境 因为之前没用过GO,所以 先在网上学了一下语法A Tour of Go 感觉Go的接口和方法的语法和C挺不一样, 并发编程也挺有意思 0x01 MapRed…

OCR多语言识别模型构建资料收集

OCR多语言识别模型构建 构建多语言识别模型方案 合合&#xff0c;百度&#xff0c;腾讯&#xff0c;阿里这四家的不错 调研多家&#xff0c;发现有两种方案&#xff0c;但是大多数厂商都是将多语言放在一个字典里&#xff0c;构建1w~2W的字典&#xff0c;训练一个可识别多种语…

解决报错之org.aspectj.lang不存在

一、IDEA在使用时&#xff0c;可能会遇到maven依赖包明明存在&#xff0c;但是build或者启动时&#xff0c;报找不存在。 解决办法&#xff1a;第一时间检查Setting->Maven-Runner红圈中的√有没有选上。 二、有时候&#xff0c;明明依赖包存在&#xff0c;但是Maven页签中…