[linux]IO多路复用机制:select、poll、epoll

为什么需要IO多路复用

首先我要向大家输出一个IO的概念:IO在我看来就是 等 + 拷贝(简化IO模型),等就是等待系统资源(设备。数据等)就绪(比如等待文件描述符就绪,等待数据就绪),拷贝就是拷贝数据资源(比如将你写的把内容将它从写缓冲区拷贝到读缓冲区,让用户可以正常读取)。如果想要IO速度快,减少等待时间是提高IO性能的关键。等待时间通常包括等待硬件资源(如磁盘、网络)就绪的时间以及等待操作系统调度的时间。在IO密集型应用中,多个IO操作可能同时进行。减少每个操作的等待时间可以显著提高整体吞吐量,因为更多的操作可以在相同的时间内完成。相比之下,拷贝时间通常是固定的,且受限于系统硬件和架构的约束,其改进空间相对较小。使用更高效的IO多路复用机制是减少等待时间的有效方式:操作系统为我们提供了select、poll、epoll三个函数,这三个函数都是用于IO多路复用的机制,它们允许单个进程或线程监视多个文件描述符,并在一个或多个文件描述符准备就绪时通知程序。从左到右(select、poll、epoll),它们在性能和功能上确实逐渐增强。(除了使用更高效的IO多路复用机制外,还有其他策略可以减少等待时间,如使用异步IO、非阻塞IO、多线程/多进程、以及优化数据结构和算法以减少CPU负载等。对于异步IO、非阻塞IO不了解的可以看我这篇博客:http://t.csdnimg.cn/WfwzE,它介绍了五种IO模型)。

select

select的作用

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

select函数模型:

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
          fd_set *exceptfds, struct timeval *timeout);

参数解释:

  • 参数nfds是需要监视的最大的文件描述符值+1;
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描
  • 述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间

参数timeout取值:

  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。
     

函数返回值:

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

错误值可能为:

  • EBADF 文件描述词为无效的或该文件已关闭
  • EINTR 此调用被信号所中断
  • EINVAL 参数n 为负值。
  • ENOMEM 核心内存不足

操作fd_set的接口

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真

  • 如果指定的文件描述符 fd 在集合 set 中被设置(即,对应的位为真),则函数返回非零值(通常是1)。
  • 如果指定的文件描述符 fd 在集合 set 中未被设置(即,对应的位为假),则函数返回零。

void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

 fd_set结构:

其实这个结构体就是一个整数数组,更准确的来说是一个“位图”,长整型的每个位的位置代表的是文件描述符的值,这个位的值(1或者0)代表是否关心(值为1代表关心反之则是不关心)这个文件描述符的状态。 

timeval结构:

struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};

直接初始化

struct timeval tv = {5, 0};//5秒0毫秒


单独赋值

#include <sys/time.h>  
  
struct timeval tv;  
  
tv.tv_sec = 10;  // 10秒  
tv.tv_usec = 500000;  // 500,000微秒,即0.5秒

select的缺点

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

 

select使用示例

检测标准输入

#include <iostream>
#include <unistd.h>
#include <sys/select.h>
using namespace std;

int main()
{
    fd_set read_fd;
    FD_ZERO(&read_fd);   // read_fd的全部位置零
    FD_SET(0, &read_fd); // 将文件描述符0(表中输入)加入read_fd"位图"表示要关心文件描述符0的状态
    while (1)
    {
        cout << ">";
        fflush(stdout);//刷新标准输出缓冲区
        int ret = select(1, &read_fd, nullptr, nullptr, nullptr);

        if (ret < 0)
        {
            cerr << "select fail" << endl;
            return 1;
        }
        if(FD_ISSET(0, &read_fd))//判断0号文件描述符是否就绪
        {
            char buff[1024];
            int n = read(0, buff, sizeof(buff) - 1);
            if(n > 0)
            {
                buff[n] = 0;
                cout << "input:" << buff << endl;
            }
            else
            {
                cerr << "read fail" << endl;
            }
            
        }
        else
        {
            cout << "is not ready" << endl;
            continue;
        }

    }
    return 0;
}

结果:

 
 

poll

poll函数接口

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);


// pollfd结构
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events */
    short revents; /* returned events */
}

参数说明:

  • fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合(就绪的文件描述符).
  • nfds表示fds数组的长度.
  • timeout表示poll函数的超时时间, 单位是毫秒(ms).

events和revents的取值

返回结果

  • 返回值小于0, 表示出错;
  • 返回值等于0, 表示poll函数等待超时;
  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回

poll的优缺点

优点:

  • pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便.
  • poll并没有最大数量限制 (但是数量过大后性能也是会下降).
     

缺点:

poll中监听的文件描述符数目增多时

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

 

poll使用示例

#include <poll.h>
#include <unistd.h>
#include <iostream>
using namespace std;
int main()
{
    struct pollfd poll_fd;
    poll_fd.fd = 0;
    poll_fd.events = POLLIN;
    for (;;)
    {
        int ret = poll(&poll_fd, 1, 1000);
        if (ret < 0)
        {
            perror("poll");
            continue;
        }
        if (ret == 0)
        {
            cout << "poll timeout" << endl;
            continue;
        }
        if (poll_fd.revents == POLLIN)
        {
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            cout << "stdin: " << buf << endl;
        }
    }
    return 0;
}

 结果:

epoll

epoll_create

函数原型

#include <sys/epoll.h>  
  
int epoll_create(int size);

功能

epoll_create通过调用内核接口,在内核中创建一个新的epoll实例,并返回一个非负的文件描述符(句柄),该描述符用于后续对epoll实例的操作。

参数

size参数在Linux 2.6.8及以后的版本中已被忽略,但调用时仍需传入一个大于0的值。这个参数原本用来告诉内核这个监听的数目一共有多大,即事件表中可以关注的最大文件描述符数。

返回值:

成功时,epoll_create返回一个非负的文件描述符,即epoll句柄;失败时,返回-1,并设置errno以指示错误。

注意:epoll句柄是一个重要的系统资源,使用完毕后应使用close系统调用进行关闭,以避免资源泄漏。

epoll_ctl

函数原型

#include <sys/epoll.h>  
  
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

函数功能

epoll_ctl是Linux系统下的一个系统调用,用于控制epoll实例中文件描述符的事件。它允许程序向epoll实例中添加、修改或删除文件描述符及其相关的事件,从而实现对多个文件描述符上I/O事件的高效管理。

参数说明

  • epfd:由epoll_create系统调用返回的文件描述符(epoll句柄),代表要操作的epoll实例。
  • op:表示要执行的操作类型,可以是以下三个宏之一:
    • EPOLL_CTL_ADD:向epoll实例中添加一个新的文件描述符。
    • EPOLL_CTL_MOD:修改epoll实例中已存在的文件描述符的事件。
    • EPOLL_CTL_DEL:从epoll实例中删除一个文件描述符。
  • fd:要操作的文件描述符,即要添加到epoll实例中、要修改其事件或要从epoll实例中删除的文件描述符。
  • event:指向epoll_event结构体的指针,用于指定要添加或修改的文件描述符的事件类型和其他相关信息。events可以是以下几个宏的集合:
    • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
    • EPOLLOUT : 表示对应的文件描述符可以写;
    • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
    • EPOLLERR : 表示对应的文件描述符发生错误;
    • EPOLLHUP : 表示对应的文件描述符被挂断;
    • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
    • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.

epoll_event结构体 

typedef union epoll_data {  
    void        *ptr;  
    int          fd;  
    __uint32_t   u32;  
    __uint64_t   u64;  
} epoll_data_t;  
  
struct epoll_event {  
    __uint32_t     events;     /* Epoll events */  
    epoll_data_t   data;       /* User data variable */  
};

返回值

  • 成功:返回0。
  • 失败:返回-1,并设置errno以指示错误原因。可能的错误码包括EBADF(无效的文件描述符)、EEXIST(尝试添加已存在的文件描述符)、EINVAL(无效的参数)等。


epoll_wait

函数原型
 

#include <sys/epoll.h>  
  
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

函数功能

监听I/O事件

参数说明

  • epfd:这是由epoll_createepoll_create1创建并返回的epoll实例的文件描述符。通过这个描述符,可以向epoll实例中添加、删除或修改要监视的文件描述符。
  • events:这是一个指向epoll_event结构数组的指针,用于接收准备就绪的事件。epoll_event结构包含与事件相关的文件描述符和数据。在调用epoll_wait后,events数组会被填充为准备就绪的文件描述符和它们关联的事件(例如读就绪、写就绪等),即它是一个输出型参数。
  • maxevents:这个参数指定了events数组可以容纳的最大事件数。epoll_wait最多会返回这个数目的准备就绪事件。如果少于这个数目的事件准备就绪,那么实际返回的事件数会少于maxevents。如果这个参数设置为0,那么epoll_wait将立即返回,不等待任何事件发生。
  • timeout:这个参数指定了epoll_wait在没有事件准备就绪时应等待的最长时间(以毫秒为单位)。如果设置为-1,epoll_wait将无限期地等待,直到至少有一个事件准备就绪。如果设置为0,epoll_wait将立即返回,不等待任何事件发生。如果设置为一个正整数N,epoll_wait将等待最多N毫秒。

返回值

  • 成功:返回值为ssize_t类型,表示实际ready的文件描述符的数量。如果返回值为0,表示在指定的超时时间内没有事件发生。
  • 失败:返回值为-1,并设置全局变量errno以指示错误类型。可能的错误码包括EBADF(epfd不是有效的文件描述符)、EFAULT(具有写许可权不能访问事件指向的存储区)、EINTR(信号处理程序中断了该调用)、EINVAL(epfd不是epoll文件描述符,或者maxevents小于或等于零)等。

使用顺序

  • 调用epoll_create创建一个epoll句柄;
  • 调用epoll_ctl, 将要监控的文件描述符进行注册;
  • 调用epoll_wait, 等待文件描述符就绪

epoll的两种工作方式

水平触发(LT:Level Triggered)

epoll默认状态下就是LT工作模式.

特点

  • 持续触发:一旦有事件发生,内核会持续通知应用程序,直到应用程序处理了所有的数据或文件描述符状态发生变化。
  • 编程简洁:由于内核会持续通知,编程时不需要担心错过事件,因此编程模型相对简洁。
  • 支持阻塞和非阻塞socket:LT模式既可以在阻塞socket上使用,也可以在非阻塞socket上使用。
边缘触发(ET:Edge Triggered)

如果我们在epoll_ctl将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式
特点

  • 一次触发:一次事件只触发一次通知,如果事件(如可读事件)发生后,应用程序没有及时处理(即没有清空缓冲区),那么内核不会再次发送通知,直到有新的数据到达或文件描述符状态再次改变。
  • 高效性:由于减少了不必要的通知,ET模式在处理大量并发连接时更为高效。
  • 要求非阻塞socket:使用ET模式时,通常需要将socket设置为非阻塞模式,以避免在处理完数据前阻塞进程
例子说明

假如有这样一个例子:
我们已经把一个tcp socket添加到epoll描述符
这个时候socket的另一端被写入了2KB的数据
调用epoll_wait,并且它会返回. 说明它已经准备好读取操作
然后调用read, 只读取了1KB的数据
继续调用epoll_wait......

LT会这样做:

由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait
仍然会立刻返回并通知socket读事件就绪,直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回

ET会这样做:

虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候,
epoll_wait 不会再返回了.也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会

 

LT VS ET 

LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到
每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
另一方面, ET 的代码复杂程度更高了

epoll使用例子

监视标准输入流读读事件

代码

#include <sys/epoll.h>  
#include <unistd.h>  
#include <fcntl.h>  
#include <iostream>  
#include <cstring>  
  
using namespace std;  
  
int main()  
{  
    int epoll_fd = epoll_create1(0); // 创建一个 epoll 实例  
    if (epoll_fd == -1) {  
        perror("epoll_create1");  
        return 1;  
    }  
  
    struct epoll_event event;  
    struct epoll_event events[10]; // 存放就绪的文件描述符  
  
    // 监听标准输入(stdin)  
    event.data.fd = 0; // 文件描述符为 0,即 stdin  
    event.events = EPOLLIN; // 监听可读事件  
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, 0, &event) == -1) {  
        perror("epoll_ctl: add");  
        return 1;  
    }  
  
    for (;;) {  
        int nfds = epoll_wait(epoll_fd, events, 10, 1000); // 等待事件发生  
        if (nfds == -1) {  
            perror("epoll_wait");  
            continue;  
        }  
  
        if (nfds == 0) {  
            cout << "epoll timeout" << endl;  
            continue;  
        }  
  
        for (int i = 0; i < nfds; ++i) {  
            if (events[i].data.fd == 0 && (events[i].events & EPOLLIN)) {  
                char buf[1024] = {0};  
                ssize_t count = read(0, buf, sizeof(buf) - 1);  
                if (count > 0) {  
                    buf[count] = '\0'; // 确保字符串正确结束  
                    cout << "stdin: " << buf << endl;  
                }  
            }  
        }  
    }  
  
    close(epoll_fd); // 关闭 epoll 实例  
    return 0;  
}

结果

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux开发:Fuse介绍

Fuse(filesystem in userspace),是一个用户空间的文件系统。通过fuse内核模块的支持&#xff0c;开发者只需要根据fuse提供的接口实现具体的文件操作时所对应的回调函数&#xff0c;就可以实现一个文件系统。由于其主要实现代码位于用户空间中&#xff0c;因此不需要重新编译内…

springboot+vue 开发记录(九)后端打包部署运行

本篇文章主要内容是后端项目写好了&#xff0c;怎么打包部署到服务器上运行。 文章目录 1. 在服务器上安装Docker2. 在Docker中装MySQL3. 在Docker中设置网桥&#xff0c;实现容器间的网络通信4. 修改后端配置文件5. 修改pom.xml文件6. 打包7. 编写DockerFile文件8. 上传文件到…

【调试笔记-20240713-Windows-Tauri 多个HTML页面支持】

调试笔记-系列文章目录 调试笔记-20240713-Windows-Tauri 多个HTML页面支持 文章目录 调试笔记-系列文章目录调试笔记-20240713-Windows-Tauri 多个HTML页面支持 前言一、调试环境操作系统&#xff1a;Windows 10 专业版调试环境调试目标 二、调试步骤搜索相似问题 三、应用场…

Python中的数据容器及其在大数据开发中的应用

在Python编程中&#xff0c;数据容器是存储和组织数据的基本工具。作为大数据开发者&#xff0c;了解并灵活运用各种容器类型对于高效处理大规模数据至关重要。今天&#xff0c;我们将从Set出发&#xff0c;探讨Python中的各种数据容器&#xff0c;以及它们在大数据处理中的应用…

Leetcode3200. 三角形的最大高度

Every day a Leetcode 题目来源&#xff1a;3200. 三角形的最大高度 解法1&#xff1a;模拟 枚举第一行是红色还是蓝色&#xff0c;再按题意模拟即可。 代码&#xff1a; /** lc appleetcode.cn id3200 langcpp** [3200] 三角形的最大高度*/// lc codestart class Solutio…

【 香橙派 AIpro评测】烧系统到运行并使用Jupyter Lab 界面体验 AI 应用样例(新手福音)

文章目录 ⭐前言⭐初始化开发板⭐下载镜像烧系统⭐开发板初始化系统&#x1f496; 远程ssh&#x1f496;查看ubuntu桌面&#x1f496; 远程向日葵 ⭐体验 AI 应用样例&#x1f496; 运行 jupyterLab&#x1f496; 打开Jupyter Lab页面&#x1f496; 释放内存&#x1f496; 运行…

AI Native时代:重塑人机交互与创作流程

随着2024年上海世界人工智能大会的圆满落幕&#xff0c;业界领袖们纷纷就AI应用的新机遇展开深入讨论。结合a16z播客中的观点&#xff0c;本文将探讨AI原生&#xff08;AI Native&#xff09;应用的几个关键特征&#xff0c;这些特征正在重新定义我们的工作方式和创作过程。 一…

排序-java(详解)

一&#xff0c;分类 主要的排序大致分为以下几类&#xff1a; 1&#xff0c;插入排序&#xff0c;又分为直接插入排序和希尔排序 2&#xff0c;选择排序&#xff0c;又分为选择排序和堆排序 3&#xff0c;交换排序&#xff0c;又分为冒泡排序和快速排序 4&#xff0c;归并…

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(三)-机上无线电接入节点无人机

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…

大模型高效参数微调技术

文章目录 一、Fine-Tuning&#xff1a;微调二、Prompt-Tuning&#xff1a;提示调优2.1 工作原理2.2 PET (Pattern-Exploiting Training)2.3 Prompt-Tuning集成2.4 模板构建方式 三、Prefix Tuning&#xff1a;连续提示模板3.1 提出动机3.2 工作原理 四、P-Tuning V1/V24.1 P-Tu…

【Qt课设】基于Qt实现的中国象棋

一、摘 要 本报告讨论了中国象棋程序设计的关键技术和方法。首先介绍了中国象棋的棋盘制作&#xff0c;利用Qt中的一些绘画类的函数来进行绘制。在创作中国象棋棋子方面&#xff0c;首先&#xff0c;我们先定义一下棋子类&#xff0c;将棋子中相同的部分进行打包&#xff0c;使…

redisTemplate报错为nil,通过redis-cli查看前缀有乱码

public void set(String key, String value, long timeout) {redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);} 改完之后 public void set(String key, String value, long timeout) {redisTemplate.setKeySerializer(new StringRedisSerializer()…

前端工程化10-webpack静态的模块化打包工具之各种loader处理器

9.1、案例编写 我们创建一个component.js 通过JavaScript创建了一个元素&#xff0c;并且希望给它设置一些样式&#xff1b; 我们自己写的css,要把他加入到Webpack的图结构当中&#xff0c;这样才能被webpack检测到进行打包&#xff0c; style.css–>div_cn.js–>main…

【架构】分布式与微服务架构解析

分布式与微服务架构解析 一、分布式1、什么是分布式架构2、为什么需要分布式架构3、分布式架构有哪些优势&#xff1f;4、分布式架构有什么劣势&#xff1f;5、分布式架构有哪些关键技术&#xff1f;6、基于分布式架构如何提高其高性能&#xff1f;7、如何基于架构提高系统的稳…

LabVIEW中modbusTCP怎样才能和profibusDP通信?

在LabVIEW中&#xff0c;Modbus TCP和Profibus DP是两种不同的工业通信协议&#xff0c;要实现这两者之间的通信&#xff0c;可以采用网关设备进行协议转换&#xff0c;或者通过一个中间设备&#xff08;如PLC&#xff09;进行数据桥接。以下是实现此通信的一些方法&#xff1a…

客家菜餐馆点菜小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;菜系管理&#xff0c;菜品信息管理&#xff0c;我的订单管理&#xff0c;桌号管理&#xff0c;退款信息管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;菜品信息&#…

220V降5V芯片输出电压电流封装选型WT

220V降5V芯片输出电压电流封装选型WT 220V降5V恒压推荐&#xff1a;非隔离芯片选型及其应用方案 在考虑220V转低压应用方案时&#xff0c;以下非隔离芯片型号及其封装形式提供了不同的电压电流输出能力&#xff1a; 1. WT5101A&#xff08;SOT23-3封装&#xff09;适用于将2…

证件照制作神器

软件详情 一款功能强大的证件照制作神器&#xff0c;提供换底色背景处理、AI智能美颜调整编辑功能&#xff0c;让你的证件照真实而又美丽&#xff01; 随着科技的快速发展&#xff0c;越来越多的软件应用于各个方面&#xff0c;为人们的生活和工作带来便利。今天&#xff0c;…

[RuoYi-Vue] - 2. 入门案例

文章目录 &#x1f9c0;1. 步骤分析&#x1f958;2. 代码生成导入sql系统导入配置代码生成代码 &#x1f369;3. 代码导入导入课程菜单导入后端代码导入前端代码 &#x1f36f;4. 访问测试 &#x1f9c0;1. 步骤分析 1、准备课程表结构和数据sql文件&#xff0c;导入到数据库中…

Tomcat组件概念和请求流程

Tomcat:是一个Servlet容器(实现了Container接口)&#xff0c;容器分层架构从上到下分为。Engine(List<Host>)->Host(List<Context>)->Context(List<Wrapper>)->Wrapper(List<Servlet>); Engine:引擎&#xff0c;Servlet 的顶层容器&#xff0…