003 仿muduo实现高性能服务器组件_前置知识

news2025/1/8 4:36:57

​🌈个人主页:Fan_558
🔥 系列专栏:仿muduo
🌹关注我💪🏻带你学更多知识

文章目录

  • 前言
    • 时间轮timewheel设计
    • 正则表达式介绍(了解知道怎么使用)
    • 通用型any容器的实现
  • 小结

前言

在正式讲解模块设计前,将会介绍模块当中的一些前置知识以及组件设计(timewheel时间轮,正则表达式、通用型容器any)
在这里插入图片描述

时间轮timewheel设计

由于服务器的资源是有限的,为了避免某些客户端连接上来之后一直不通信而平白浪费服务器资源的情况,我们需要对非活跃连接设置定时销毁,而实现这个功能的前提是得有一个定时器。
这个小组件将会运用到TimerQueue子模块当中
Linux当中提供给我们了一个定时器,如下:

#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
 clockid: CLOCK_REALTIME-系统实时时间,如果修改了系统时间就会出问题; 
 CLOCK_MONOTONIC-从开机到现在的时间是⼀种相对时间;
 flags: 0-默认阻塞属性

int timerfd_settime(int fd, int flags, struct itimerspec *new, struct
itimerspec *old);
 fd: timerfd_create返回的⽂件描述符
 flags: 0-相对时间, 1-绝对时间;默认设置为0即可.
 new: ⽤于设置定时器的新超时时间
 old: ⽤于接收原来的超时时间
 
 struct timespec {
 time_t tv_sec; /* Seconds */
 long tv_nsec; /* Nanoseconds */
 };
 
 struct itimerspec {
 struct timespec it_interval; /* 第⼀次之后的超时间隔时间 */
 struct timespec it_value; /* 第⼀次超时时间 */
 };
 定时器会在每次超时时,⾃动给fd中写⼊8字节的数据,表⽰在上⼀次读取数据到当前读取数据期间超
时了多少次。

以下是timefd的使用样例,了解即可

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/timerfd.h>

int main()
{
    //创建定时器
    int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);
    if(timerfd < 0)
    {
        perror("timefd_create error");
        return -1;
    }
    struct itimerspec itime;
    //设置首次超时时间
    itime.it_value.tv_sec = 1;  //第一次超时在1s后
    itime.it_value.tv_nsec = 0;
    //设置后续超时时间间隔
    itime.it_interval.tv_sec = 3;   //第一次超时后,每隔3秒间隔超时一次
    itime.it_interval.tv_nsec = 0;
    timerfd_settime(timerfd, 0, &itime, NULL);

    //从timefd中读取超时次数
    while(1){
        uint64_t times;
        int ret = read(timerfd, &times, 8);
        if(ret < 0)
        {
            perror("read error");
            return -1;
        }
        printf("超时,距离上次超时了%ld次\n",times);
    }
    close(timerfd);
}

在这里插入图片描述
上述的例⼦,存在⼀个很⼤的问题,每次超时都要将所有的连接遍历⼀遍,如果有上万个连接,效率⽆疑是较为低下的

针对这个问题,我们可以以连接最近一次通信的系统时间为基准建立一个小跟堆,堆中的元素按照连接最近一次通信的系统时间排序,使得堆顶元素始终是最近一次通信时间最早的连接,然后就只需要不断取出堆顶已超时的连接执行超时任务,直到没有超时连接即可。

我们也可以采用时间轮的方法,时间轮的思想来源于钟表,由此我们可以设计出一个数组,由一个指针指向起始位置,只需要让这个指针每秒钟往后走一步,及秒针(tick)走到哪里执行哪里的超时连接销毁任务即可
在这里插入图片描述

1、如果我们的超时时间很长应该怎么办呢?

比如我们的超时时间为一天,我们是不是要定义一个 60 * 60 * 60s 的数组?解决办法很简单,我们可以将时间轮分级,即分别定义秒级时间轮、分级时间轮以及时级时间轮,如下:
在这里插入图片描述
此时我们仅需要 3 * 60 个整形的空间就可以实现 60 小时内的定时器了 (如果使用位图来定义定时轮仅需要 4*3 个字节的空间)。

2、同一时刻的定时任务只能添加一个,需要考虑如何在同一时间时刻支持添加多个定时任务

解决方案:将时间轮的一维数组设计为二维数组(时间轮)

3、 如何实现定时任务的延时?

解决方法:类的析构函数+智能指针share_ptr,通过这两个计数可以实现定时任务 1、使用一个类,对定时任务进行封装,类实例化的每一个对象,就是一个定时任务对象,当对象被销毁的时候即是执行定时任务的时候(将定时任务的执行放到析构函数当中) 但是当一个连接建立成功后,我们给这个连接设置了一个30s后定时销毁任务,但是在10s后这个连接进行了一次通信(连接非活跃30s后则定时销毁),此时我们应该在40s时才关闭连接,那么当连接进行通信的时候,我们需要重新刷新定时任务时间,类的实例化与销毁就不太适合用于刷新定时任务了,该如何做呢? 这⾥,我们就⽤到了智能指针shared_ptr,shared_ptr有个计数器,当计数为0的时候,才会真正释放 ⼀个对象,那么如果连接在第10s进⾏了⼀次通信,则我们继续向定时任务时间轮(_wheel)中添加⼀个30s后(也就 是第40s)的任务类对象的shared_ptr,则这时候两个任务shared_ptr计数为2,则第30s的定时任务被释放的时候,计数-1,变为1,并不为0,则并不会执⾏实际的析构函数,那么就相当于这个 第30s的任务失效了,只有在第40s的时候,计数器减为0,这个任务才会被真正释放。 基于这个思想,我们可以使用share_ptr来管理定时任务对象

下面是秒级时间轮和定时任务对象类的代码实现

using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;

//定时任务类
class TimerTask{
public:
    TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb)
    :_id(id)
    ,_timeout(delay)
    ,_task_cb(cb)
    {}
    //析构的时候执行定时任务
    ~TimerTask(){
        if(_canceled == false) _task_cb(); //定时任务执行
        _release(); //释放定时器任务对象
    }
    //设置release_cb回调函数
    void SetRelease(const ReleaseFunc& cb)
    {
        if(_canceled == false)
        _release = cb;
    }
    //返回定时器任务超时时间
    uint64_t TimeOut()
    {
        return _timeout; 
    }
    //取消定时任务
    void Cancel()
    {
        _canceled = false;
    }
 private:
    uint64_t _id;   //定时器任务对象ID
    uint32_t _timeout;  //定时任务的超时时间
    TaskFunc _task_cb;  //定时器对象要执行的定时任务
    ReleaseFunc _release;   //用于删除TimerWheel中保存的定时器对象信息
    bool _canceled; //定时任务是否被取消

};
using TaskWeak = std::weak_ptr<TimerTask>;     //别名:指向TimerTask类的对象强引用,会增加引用计数
using TaskPtr = std::shared_ptr<TimerTask>;    //别名:指向TimerTask类的对象弱引用,不会增加引用计数

//时间轮类
class TimerWheel{
public:
    TimerWheel()
    :_capacity(60)
    ,_tick(0)
    ,_wheel(_capacity)
    {}
    //添加定时任务(将管理任务对象的智能指针添加到时间轮当中)
    void TimerAdd(uint64_t id, uint32_t timeout, const TaskFunc &cb)
    {
        //使用智能指针管理定时类任务
        TaskPtr tp(new TimerTask(id, timeout, cb));
        //添加WeakPtr与id的关联关系
        _timers[id] = TaskWeak(tp);
        //释放定时任务对象(这样做的好处可以添加当前任务id,及各种信息)
        tp->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));
        uint64_t pos = (timeout + _tick) % _capacity;
        //添加任务
        _wheel[pos].push_back(tp);
    }
    // 刷新/延迟定时任务(查看定时任务是否还存在,存在就刷新时间轮)
    void TimerRefresh(uint64_t id)
    {
      auto it = _timers.find(id);
      if(it == _timers.end()) return;
      //刷新定时任务
      TaskPtr tp = (it->second).lock();
      int pos = (tp->TimeOut() + _tick) % _capacity;
      _wheel[pos].push_back(tp);
    }
    //取消定时任务(如果管理任务对象的智能指针存在,则设置任务对象状态为取消状态)
    void TimerCancel(uint64_t id)
    {
        auto it = _timers.find(id);
        if(it == _timers.end()) return; //未找到定时任务
        TaskPtr tp = it->second.lock();
        if(tp) tp->Cancel();
    }
    //执行定时任务,此函数一秒被执行一次,相当于秒针向后走一步(清空时间轮当中的智能指针)
    void RunTimerTask()
    {
        _tick = (_tick + 1) % _capacity;
        _wheel[_tick].clear();  //清空数组指定的位置,将所有管理定时任务对象的share_ptr释放掉
    }

    private:
    //SetRelease回调函数,表示任务已经执行完,从unordered_map中将定时任务信息删除
    void RemoveTimer(uint64_t id)
    {
        auto it = _timers.find(id);
        if(it != _timers.end()) _timers.erase(it);
    }
    int _tick; //秒针:走到哪里,执行哪里的任务
    int _capacity; //表盘的最大数量---最大延迟时间
    std::vector<std::vector<TaskPtr>> _wheel;   //时间轮
    std::unordered_map<uint64_t, TaskWeak> _timers; //定时器任务id与管理定时任务对象的weak_ptr之间的关联关系
}; 

class Test {
public:
    Test() { std::cout << "构造" << std::endl; }
    ~Test() { std::cout << "析构" << std::endl; }
};

void Deltest(Test *t) { delete t; }

int main()
{
    Test *t = new Test();
    TimerWheel tw;
    tw.TimerAdd(1, 5, std::bind(Deltest, t));
    // 刷新定时任务
    for(int i = 0; i < 5; i++) {
        sleep(1);
        // 刷新定时任务
        tw.TimerRefresh(1);  
        // 向后移动秒针
        tw.RunTimerTask();
        std::cout << "刷新,定时任务5s后启动" << std::endl;
    }
    while(true) {
        std::cout << "滴答滴答..." << std::endl;
        tw.RunTimerTask();
        sleep(1);

    }
    return 0;
}

在这里插入图片描述

正则表达式介绍(了解知道怎么使用)

由于我们要实现的是一个带有应用层协议 (HTTP) 支持的服务器组件,因此必然会涉及到对 HTTP 请求的解析,比如我们接收到了这样的一个 HTTP 请求

那么我们需要从 HTTP 请求中提取出以下的信息:

GET – 请求方法,我们需要针对不同的请求方法执行不同的操作

/login – 请求URL,我们需要知道客户端想要访问哪里的数据

user=Fan_558&pass=123456 – 请求数据

HTTP/1.1 – 协议版本

正则表达式是基于某种字符串匹配规则来提取字符串中的特定数据。
对于简单的 regex 使用,我们只需要掌握regex_match 函数的使用即可:

bool regex_match(string src, smatch matches, regex e);
src: 用于匹配的原始字符串;
e: 字符串的匹配规则;
matches: 用于存在根据匹配规则e对原始字符串src进行匹配得到的结果;
返回值:匹配成功返回true,匹配失败返回false

常用的正则表达式的匹配规则:

\

将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符

*

匹配前面的子表达式零次或多次

+

匹配前面的子表达式一次或多次

?

匹配前面的子表达式零次或一次

.

匹配除“\n”之外的任何单个字符

x|y

匹配x或y

代码示例

bool test() {
    std::string str = "GET /login?user=zhangsan&pass=123456 HTTP/1.1\r\n";
    std::smatch matches;

    // 提取请求方法:(GET|POST|PUT|DELETE|HEAD) 
    // |表示或者   ()表示提取匹配结果   (GET|POST|PUT|DELETE|HEAD)整体表示提取其中任意一个字符串
    // 提取请求URI:_([^?]*) 
    // 我们用_表示空格  [^?]表示匹配除?号外的所有字符  *表示可以多次匹配
    // 提取数据:\\?(.*) 
    //\\?表示普通的?字符
    //(.*)表示可以多次匹配任意字符并提取
    //外边的(?:)?表示如果没有数据则匹配但不提取

    //提取协议版本:_ (HTTP/1\\.[01]) 
    // _表示空格
    //HTTP/1\\.表示匹配原字符串中的HTTP/1\\.   其中\\.表示普通的.  最后[01]表示匹配0或者1

    // \r\n处理:(?:\n|\r\n)? 
    //(?:xxx)表示匹配xxx格式字符串但不提取   最后的?表示执行前面的表达式一次或零次
    std::regex e("(GET|POST|PUT|DELETE|HEAD) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");
    if(std::regex_match(str, matches, e) == false) return false;
    for(int i = 0; i < matches.size(); i++) {
        std::cout << i << ":" << matches[i] << std::endl;
    }
    return true;
}

通用型any容器的实现

由于后续将会对socket内核接收缓冲区到来的数据进行HTTP协议格式的解析处理,我们需要拿到一个完整的HTTP请求,由于 TCP
是面向字节流的,因此服务器在接收客户端数据的时候就可能出现 socket
中的数据不足一条完整请求的情况,因此我们需要为客户端连接设置一个请求处理的上下文,用来保存请求接收,解析以及处理的状态,它决定着下一次从缓冲区中取出的数据如何进行处理,从哪里开始处理等。同时对于一条完整的请求,我们还需要对其进行解析,得到各种关键的要素,比如HTTP请求中的请求方法、请求URL、HTTP版本等,这些信息都会被保存到请求处理的上下文当中。另外地,今天我们上层使用的是HTTP协议,服务器可支持的协议可能会不断增多,不同的协议,可能都会有不同的上下文结构,所以需要有一个容器结构不同结构的数据

C++中,boost库和C++17给我们提供了⼀个通⽤类型any来灵活使⽤,如果考虑增加代码的移植性,尽量减少第三⽅库的依赖,则可以使⽤C++17特性中的any,或者⾃⼰来实现。

这里我们选择自己实现
如何设计一个可以保存任意类型数据的容器呢?我们首先能够想到的起始时模板类,模板可以接收任意类型的数据,但是当实例化对象的时候,必须传入一个指定的类型

预期设计:
不带有任何的特定的类型,但是可以保存或接收各种类型的数据
Any a; 
a = "Fan";
a = 1;

于是我们便可以在Any类中设计一个继承体系,子类placeHolder专门用于保存其它类型的数据,而父类Holder的指针去指向派生类,父类只是一个普通类不带有模板参数,再将父类Holder的指针作为Any类的成员变量,这样就可以达到我们的预期设计,不带有任何特定的类型(实例化对象的时候不需要指定类型),就可以保存任意数据类型的数据

class Any{
private:
    class Holder
    {}
    template<class T>
    class placeHolder : public Holder{
    public:
        T _val;
    };
     Holder* _content;
};

具体代码如下

class Any{
private:
    class Holder{
        public:
            virtual ~Holder() {}
            virtual const std::type_info& type() = 0;
            virtual Holder* clone() = 0;
        };

    template<class T>
    class placeHolder : public Holder{
    public:
        placeHolder(const T &val): _val(val){}
        virtual ~placeHolder() {}
        //获取子类对象保存的数据类型
        virtual const std::type_info& type()   
        {
            return typeid(T);
        }
        //针对当前的对象自身,克隆出一个新的子类对象
        virtual Holder* clone()
        {
            return new placeHolder(_val);
        }
    public:
        T _val;
    };
    
     Holder* _content;
public:
    //空的构造,直接赋值即可
    Any() :_content(nullptr) {}
    template<class T>
    Any(const T &val) :_content(new placeHolder<T>(val)) {}
    //通过其它的通用型容器构造自己的容器
    Any(const Any &other)
    : _content(other._content ? other._content->clone() : nullptr)
    {} 
    ~Any()
    {
        delete _content;
    }
    Any &swap(Any &other)
    {
        std::swap(_content, other._content);
        return *this;
    }

    template<class T>
    //返回子类对象保存的数据的指针
    T* get()
    {
        //想要获取的数据类型,必须要与保存的数据类型一致
        assert(typeid(T) == _content->type());
        //强转程子类对象类型从而能访问到子类的_val
        return &((placeHolder<T>*)_content)->_val;
    }
    template<class T>
    //赋值运算符的重载函数
    Any& operator=(const T &val)
    {
        //为val构造一个临时的通用容器,然后与当前容器自身进行指针交换,临时释放对象的时候,原先保存的书也就被释放
        Any(val).swap(*this);
        return *this;
    }
    Any& operator=(const Any &other)
    {
        //根据一个other构造一个对象
        Any(other).swap(*this);
        return *this;
    } 
};

测试

int main()
{
    Any a;
    a = 10;
    int* pa = a.get<int>();
    std::cout << *pa << std::endl;
    a = std::string("Fan_558");
    std::string* ps = a.get<std::string>();
    std::cout << *ps << std::endl;
    return 0;
}

在这里插入图片描述

小结

下一篇将会向你带来Buffer模块与Socket模块的实现

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

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

相关文章

蓝牙模块选型之蓝牙功能

蓝牙模块&#xff0c;是一种集成蓝牙功能的PCBA板&#xff0c;用于短距离无线通讯&#xff0c;蓝牙模块将芯片和外围硬件电路集成到一个PCB上&#xff0c;开发出所需的内置程序实现蓝牙功能的设备。可以通过相关接口和MCU控制设备进行数据传输、可实现蓝牙标准通信和组网。 目前…

海外仓系统要多少钱?最贵的未必是最好的,性价比高的才是

海外仓系统可以说已经是现在海外仓管理不可或缺的重要工具&#xff0c;然而&#xff0c;很多海外仓企业在选择海外仓系统时最头疼的问题就是不知道到底多少钱才合适。 确实&#xff0c;现在的海外仓系统市场价格体系非常多&#xff0c;几万几十万各种定价都有&#xff0c;让人…

揭秘编程逻辑:布瑞克(break)与坎特牛(continue)的较量

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、布瑞克与坎特牛&#xff1a;逻辑差异的探索 二、代码案例分析&#xff1a;布瑞克与坎特…

远程抄表及预付费管理系统:智能管理的新篇章

1.系统简述 远程抄表及预付费管理系统是现代能源管理方面的一项重要自主创新&#xff0c;它将传统手动式抄水表方式转变为自动化技术、智能化管理模式&#xff0c;大大提高了高效率并减少了经营成本。该系统搭载了前沿的通讯技术、数据分析技术和财务管理系统核心理念&#xf…

UVa1466/LA4849 String Phone

UVa1466/LA4849 String Phone 题目链接题意分析AC 代码 题目链接 本题是2010年icpc亚洲区域赛大田赛区的G题 题意 平面网格上有n&#xff08;n≤3000&#xff09;个单元格&#xff0c;各代表一个重要的建筑物。为了保证建筑物的安全&#xff0c;警察署给每个建筑物派了一名警察…

使用 Flask 和 Vue.js 构建 Web 应用

文章目录 入门1. 设置 Flask 后端2. 设置 Vue.js 前端 将 Flask 与 Vue.js 集成1. 配置 Flask 来提供 Vue.js 文件2. 构建 Vue.js 组件3. 运行应用程序 结论 在现代 Web 开发中&#xff0c;创建动态和响应式的应用通常涉及将后端框架如 Flask 与前端库如 Vue.js 结合起来。这种…

深度合作!博睿数据联合中国信通院开展公网服务质量评估工作!

近日&#xff0c;中国信息通信研究院&#xff08;简称“中国信通院”&#xff09;算网质量保障工作全面启动&#xff0c;博睿数据&#xff08;bonree.com&#xff0c;股票代码688229&#xff09;作为信通院算网质量测试独家技术支持单位&#xff0c;提供公网服务质量测评整体解…

新手做抖店该怎么选品?怎么选爆款?选爆品牢记这五大方法

大家好&#xff0c;我是电商花花。 不论之前还是现在&#xff0c;我们做电商想要出单&#xff0c;赚钱&#xff0c;选品对于我们店铺来说都是至关重要的&#xff0c;我们能不能在抖店上赚钱&#xff0c;就看我们的选品&#xff0c;看我们商品选的怎么样了。 如果品选的不错&a…

鸿蒙ArkUI-X跨平台开发:【资源分类与访问】

资源分类与访问 应用开发过程中&#xff0c;经常需要用到颜色、字体、间距、图片等资源&#xff0c;在不同的设备或配置中&#xff0c;这些资源的值可能不同。 应用资源&#xff1a;借助资源文件能力&#xff0c;开发者在应用中自定义资源&#xff0c;自行管理这些资源在不同…

vue/uniapp 企业微信H5使用JS-SDK

企业微信H5需要我们使用一些SDK方法如获取外部联系人userid 获取当前外部联系人userid 使用SDK前提是如何通过config接口注入权限验证配置 使用说明 - 接口文档 - 企业微信开发者中心 当前项目是vue项目&#xff0c;不好直接使用 引入JS文件&#xff0c;但我们可以安装依赖…

Codeforces Round 946 (Div.3)

C o d e f o r c e s R o u n d 946 ( D i v . 3 ) \Huge{Codeforces~Round~946~(Div.3)} Codeforces Round 946 (Div.3) 题目链接&#xff1a;Codeforces Round 946 (Div. 3) 文章目录 Problems A. Phone Desktop题意思路标程 Problems B. Symmetric Encoding题意思路标程 Pr…

Linux:IPC - System V

Linux&#xff1a;IPC - System V 共享内存 shm创建共享内存shmgetshmctlftok 挂接共享内存shmatshmdt shm特性 消息队列 msgmsggetmsgctlmsgsndmsgrcv 信号量 semSystem V 管理机制 System V IPC 是Linux系统中一种重要的进程间通信机制&#xff0c;它主要包括共享内存 shm&am…

centos下给es7.12.1设置密码

安装可参考&#xff1a; centos7下安装elasticsearch7.8.1并配置远程连接_在一台服务器centos7上安装和配置elasticsearch。-CSDN博客 1、先停掉es进程 2、设置输入密码后访问配置 cd /home/soft/elasticsearch-7.12.1/config vim elasticsearch.yml 3、启动es服务 cd /home/…

ARM鲲鹏920-oe2309-caffe

参考链接:Caffe | Installation 安装依赖包 dnf install dnf update dnf install leveldb-devel snappy-devel opencv.aarch64 boost-devel hdf5-devel gflags-devel glog-devel lmdb-devel openblas.aarch64 dnf install git wget tar gcc-g unzip automake libtool autoco…

【SQL学习进阶】从入门到高级应用(一)

文章目录 MySQL命令行基本命令数据库表的概述初始化测试数据熟悉测试数据 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01; &#x1f49d;希望您在这里可以感受到一份轻松愉快的氛围&#x…

keithely 2401 低压源表

Keithley 2401 低压源表提供精密电压和电流源以及测量功能&#xff08;1V - 20V 和 10pA - 1A&#xff09;。它既是高度稳定的直流电源&#xff0c;也是真正的仪器级 5 位万用表。电源特性包括低噪声、精度和回读。万用表功能包括高重复性和低噪声。结果是紧凑型单通道直流参数…

如何加密设计图纸|文件加密软件推荐榜单(推荐收藏)

在现代企业中&#xff0c;特别是涉及高科技研发、工程设计和制造业的公司&#xff0c;保护敏感图纸和技术资料免受信息泄露是至关重要的。图纸加密软件作为一种有效的防护手段&#xff0c;能够为企业提供全方位的安全保障。以下是几款主流的图纸加密软件以及其优势&#xff0c;…

Python零基础一天丝滑入门教程(非常详细)

目录 第1章 初识python 第1节 python介绍 1.为什么要学习Python&#xff1f; 2.python排名 3.python起源 4.python 的设计目标 第2节 软件安装 第2章 快速上手&#xff1a;基础知识 第1节 Python3 基础语法 Python 变量 字面量 数据类型转换 Python3 注释 数据类…

2024年【西式面点师(中级)】新版试题及西式面点师(中级)考试试卷

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【西式面点师&#xff08;中级&#xff09;】新版试题及西式面点师&#xff08;中级&#xff09;考试试卷&#xff0c;包含西式面点师&#xff08;中级&#xff09;新版试题答案和解析及西式面点师&#xff08;…