RT-Thread 的环形缓冲区 ---- 镜像指示位

可以看一下这篇我写的博客,了解一下大概: 

RingBuffer 环形缓冲区----镜像指示位_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/132340883?spm=1001.2014.3001.5501

【回顾】缓冲区变满在环形缓冲区(ring buffer)中会实际发生,一般会有两种处理策略:

        🐞① 覆盖老数据

        🐞② 抛出“异常”

镜像指示位:缓冲区的长度如果是n,逻辑地址空间则为0至n-1;那么,规定n至2n-1为镜像逻辑地址空间。本策略规定读写指针的地址空间为0至2n-1,其中低半部分对应于常规的逻辑地址空间,高半部分对应于镜像逻辑地址空间。当指针值大于等于2n时,使其折返(wrapped)到ptr-2n。使用一位表示写指针或读指针是否进入了虚拟的镜像存储区:置位表示进入,不置位表示没进入还在基本存储区。

        在读写指针的值相同情况下,如果二者的指示位相同,说明缓冲区为空;如果二者的指示位不同,说明缓冲区为满。这种方法优点是测试缓冲区满/空很简单;不需要做取余数操作;读写线程可以分别设计专用算法策略,能实现精致的并发控制。缺点是读写指针各需要额外的一位作为指示位。

        如果缓冲区长度是2的幂,则本方法可以省略镜像指示位。如果读写指针的值相等,则缓冲区为空;如果读写指针相差n,则缓冲区为满,这可以用条件表达式(写指针==(读指针异或缓冲区长度))来判断。

----(来自百度百科)

一、基本步骤

1.数据结构

typedef struct ringbuffer
{uint8 *buffer_ptr;uint16 read_mirror : 1;uint16 read_index : 15;uint16 write_mirror : 1;uint16 write_index : 15;/* as we use msb of index as mirror bit, the size should be signed and* could only be positive. */uint16 size;
}ringbuff;

 2.缓冲区初始化

// 缓冲区初始化 
void rb_init(ringbuff *rb,uint8 *pool,uint16 size){/* initialize read and write index */rb->read_mirror = rb->read_index = 0;rb->write_mirror = rb->write_index = 0;/* set buffer pool and size */rb->buffer_ptr = pool;rb->size = DATA_ALIGN_DOWN(size, DATA_ALIGN_SIZE);
}

 3.创建一个ringbuffer

// 创建一个ringbuff
ringbuff* rb_create(uint16_t size) {ringbuff *rb;uint8_t *pool;size = DATA_ALIGN_DOWN(size, DATA_ALIGN_SIZE);// 大小做字节对齐rb = (ringbuff *)malloc(sizeof(ringbuff));// 申请内存if (rb == NULL)goto exit;pool = (uint8_t *)malloc(size);// 申请数据缓冲区内存if (pool == NULL) {free(rb);rb = NULL;goto exit;}rb_init(rb, pool, size);// 初始化 ringbuffexit:return rb;
}

 4.销毁环形缓冲区

// 摧毁 ringbuff
void rb_destroy(ringbuff *rb) {   cout<<"销毁ringbuff~"<<endl;    free(rb->buffer_ptr);free(rb);// 释放申请的内存
}

二、缓冲区中填充指定数据长度的数据

5. 缓冲区中填充指定数据长度的数据

举个例子:(当缓冲区空间小于待写入的数据长度事覆盖缓冲区原有数据)

图1:当环形缓冲区为空时,读索引和写索引指向相同的位置(这里初始化为0);

图2:写操作:想往(rb->buffer_size = 8)缓冲区中写入15个元素:123456789ABCDEF,但写入的数据长度(length)超过缓冲区空闲长度(space_length)了。解决方法:RT-Thread(覆盖老数据策略)就是只截取后8位数据放入缓冲区。

可知length=15,space_length=8,满足length > space_length,让ptr = &ptr[length - rb->buffer_size]

图3:由图2的操作可以得到以上的环形缓冲区的数据内容分布

图4:读操作,读取缓冲区4个元素:89AB,修改读索引

图5:写操作,写入缓冲区4个元素:1234,修改写索引

 图6:读操作,读取缓冲区4个元素:CDEF,修改读索引

🐞① 当缓冲区空间小于待写入的数据长度事覆盖缓冲区原有数据

/* 缓冲区中填充指定数据长度的数据(当缓冲区空间小于待写入的数据长度事覆盖缓冲区原有数据) */
uint16 rb_put_force(ringbuff *rb,const uint8 *ptr,uint16 length);
// 强制往 ringbuff 写入数据
uint16 rb_put_force(ringbuff *rb,const uint8 *ptr,uint16 length) {uint16 space_length = 0;space_length = rb_space_len(rb);// cout<<"ptr: "<<ptr<<endl;// cout<<"space_length: "<<space_length<<endl;// cout<<"length: "<<length<<endl;if (length > space_length) { ptr = &ptr[length - rb->size];length = rb->size;}// cout<<"ptr: "<<ptr<<endl;// cout<<"length: "<<length<<endl;if (rb->size - rb->write_index > length){// cout<<"lailailailai"<<endl;/* read_index - write_index = empty space */memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);/* this should not cause overflow because there is enough space for* length of data in current mirror */rb->write_index += length;if (length > space_length)rb->read_index = rb->write_index;return length;}memcpy(&rb->buffer_ptr[rb->write_index],&ptr[0],rb->size - rb->write_index);memcpy(&rb->buffer_ptr[0],&ptr[rb->size - rb->write_index],length - (rb->size - rb->write_index));/* we are going into the other side of the mirror */rb->write_mirror = ~rb->write_mirror;rb->write_index = length - (rb->size - rb->write_index);if (length > space_length){rb->read_mirror = ~rb->read_mirror;rb->read_index = rb->write_index;}return length;
}

🐞② 当缓冲区空间小于待写入的数据长度事不覆盖缓冲区原有数据

/* 缓冲区中填充指定数据长度的数据(当缓冲区空间小于待写入的数据长度事不覆盖缓冲区原有数据) */
uint16 rb_put(ringbuff *rb,const uint8 *ptr,uint16 length);
// 往 ringbuff 写入数据
uint16 rb_put(ringbuff *rb,const uint8 *ptr,uint16 length) {uint16 size = 0;/* whether has enough space */size = rb_space_len(rb);// 获取 ring_buff 中可用空间的大小/* no space */if (size == 0)return 0;// 如果空间不够 直接返回/* drop some data */if (size < length) // 如果缓存区的空间不够保存这一次数据, 则把能够写入的这一部分数据写进去length = size;/* One-time write */if (rb->size - rb->write_index > length){/* read_index - write_index = empty space */memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);/* this should not cause overflow because there is enough space for* length of data in current mirror */rb->write_index += length;return length;// 返回写入数据的长度}/* two-time write */memcpy(&rb->buffer_ptr[rb->write_index],&ptr[0],rb->size - rb->write_index);memcpy(&rb->buffer_ptr[0],&ptr[rb->size - rb->write_index],length - (rb->size - rb->write_index));/* we are going into the other side of the mirror */rb->write_mirror = ~rb->write_mirror;rb->write_index = length - (rb->size - rb->write_index);return length;
}

6. 缓冲区中获取指定长度的数据(返回实际获取数据的长度)

/* 缓冲区中获取指定长度的数据(返回实际获取数据的长度) */
uint16 rb_get(ringbuff *rb,uint8 *ptr,uint16 length);
// 从 ringbuff 获取数据
uint16 rb_get(ringbuff *rb,uint8 *ptr,uint16 length) {uint16 size = 0;/* The length of the existing data in the buffer */size = rb_data_len(rb);/* no data */if (size == 0)return 0;/* less data */if (size < length)length = size;cout<<"size: "<<size<<" < "<<"length: " << length<<(size < length)<<endl;if (rb->size - rb->read_index > length){/* copy all of data */memcpy(ptr, &rb->buffer_ptr[rb->read_index], length);/* this should not cause overflow because there is enough space for* length of data in current mirror */rb->read_index += length;return length;}memcpy(&ptr[0],&rb->buffer_ptr[rb->read_index],rb->size - rb->read_index);memcpy(&ptr[rb->size - rb->read_index],&rb->buffer_ptr[0],length - (rb->size - rb->read_index));/* we are going into the other side of the mirror */rb->read_mirror = ~rb->read_mirror;rb->read_index = length - (rb->size - rb->read_index);return length;
}

7.测试和打印

void readprint(ringbuff* rb,uint8 buff[]) {cout<<"读取数据:";int i = 0;while(buff[i]!='\0') {cout<<buff[i++];}print(rb);
}void writeprint(ringbuff* rb){for(int i=0;i<rb->size;i++){cout<<rb->buffer_ptr[i];}print(rb);
}void test01() {ringbuff* rb = rb_create(9);const uint8 p[] = "123456789ABCDEF";uint32_t len = sizeof(p) / sizeof(char);cout<<"写入数据:"<<p<<endl;// rb_put(rb,p,len-1);rb_put_force(rb,p,len-1); writeprint(rb);                       // 89ABCDEFuint8 saveBuff[20] = "";rb_get(rb,saveBuff,4);   // 89ABreadprint(rb,saveBuff);const uint8 p1[] = "1234";cout<<"写入数据:"<<p1<<endl;rb_put_force(rb,p1,4);    // 1234CDEFwriteprint(rb);memset(saveBuff,0,20);rb_get(rb,saveBuff,4);   // CDEFcout<<"读取数据:";readprint(rb,saveBuff);// 销毁ringbuffrb_destroy(rb);
}
写入数据:123456789ABCDEF
89ABCDEF
rb->write_index: 0
rb->read_index: 0
rb->write_mirror: 1
rb->read_mirror: 0
rb_data_len: 8
rb_space_len: 0size: 8 < length: 40
读取数据:89AB
rb->write_index: 0
rb->read_index: 4
rb->write_mirror: 1
rb->read_mirror: 0
rb_data_len: 4
rb_space_len: 4写入数据:1234
1234CDEF
rb->write_index: 4
rb->read_index: 4
rb->write_mirror: 1
rb->read_mirror: 0
rb_data_len: 8
rb_space_len: 0size: 8 < length: 40
读取数据:读取数据:CDEF
rb->write_index: 4
rb->read_index: 0
rb->write_mirror: 1
rb->read_mirror: 1
rb_data_len: 4
rb_space_len: 4销毁ringbuff~

三、缓冲区中填充一个数据

5. 缓冲区中填充一个数据

举个例子:(当缓冲区空间小于待写入的数据长度事覆盖缓冲区原有数据)

图1:依次存入1、2、3、4、5、6、7、8、A、B、C、D、E、F这些字符,直到缓冲区为满

图2:依次读出单个字符: 8、9、A、B

🐞① 当缓冲区空间小于待写入的数据长度事覆盖缓冲区原有数据

/* 缓冲区中填充一个数据(当缓冲区空间小于待写入的数据长度事覆盖缓冲区原有数据) */
uint16 rb_putchar_force(ringbuff *rb, const uint8 ch);
// 往 ringbuff 强制写入一个字符
uint16 rb_putchar_force(ringbuff *rb, const uint8 ch) {enum rb_state old_state = rb_status(rb);// 获取状态rb->buffer_ptr[rb->write_index] = ch;// 写入数据/* flip mirror */if (rb->write_index == rb->size-1) {// 检查当前镜像是不是满了rb->write_mirror = ~rb->write_mirror; // 翻转写镜像rb->write_index = 0;// 翻转之后设置下标为 0if (old_state == RINGBUFFER_FULL) {// 如果 ringbuff 的状态是满rb->read_mirror = ~rb->read_mirror; // 翻转读镜像rb->read_index = rb->write_index; // 设置读下标和写下标一致}}else{rb->write_index++; // 写下标加1if (old_state == RINGBUFFER_FULL)rb->read_index = rb->write_index;// 如果满,设置读下标等于写下标}return 1; // 写入一个字符,返回1
}

🐞② 当缓冲区空间小于待写入的数据长度事不覆盖缓冲区原有数据

/* 缓冲区中填充一个数据(当缓冲区空间小于待写入的数据长度事不覆盖缓冲区原有数据) */
uint16 rb_putchar(ringbuff *rb, const uint8 ch);
// 往 ringbuff 中写入一个字符
uint16 rb_putchar(ringbuff *rb, const uint8 ch) {/* whether has enough space */if (!rb_space_len(rb)) // 没有足够的空间就直接返回了return 0;rb->buffer_ptr[rb->write_index] = ch;// 把这个字符写入到缓冲区的指定位置/* flip mirror */if (rb->write_index == rb->size-1) {// 检查写入这个字符后,当前镜像是否写满rb->write_mirror = ~rb->write_mirror;// 翻转镜像rb->write_index = 0;// 设置下标为0}else{rb->write_index++; // 下标加1}return 1;// 写入一个字符,返回 1
}

6. 缓冲区中获取一个数据(返回实际获取数据的长度)

/* 缓冲区中获取一个数据(返回实际获取数据的长度) */
uint16 rb_getchar(ringbuff *rb, uint8 *ch);
// 从ringbuff 获取一个字符
uint16 rb_getchar(ringbuff *rb,uint8 *ch) {/* ringbuffer is empty */if (!rb_data_len(rb)) // 检查 ringbuff 是否为空return 0;/* put character */*ch = rb->buffer_ptr[rb->read_index];// 获取当前读下标的数据if (rb->read_index == rb->size-1) {// 如果当前镜像满了rb->read_mirror = ~rb->read_mirror;// 翻转镜像rb->read_index = 0; // 设置读数据的下标为0} else {rb->read_index++; // 下标加1}return 1;// 读取一个字节,返回1
}

7.测试和打印

#include "rb.h"
#include "rb.cpp"
void print(ringbuff *rb) {cout<<endl;cout<<"rb->write_index: "<<rb->write_index<<endl;cout<<"rb->read_index: "<<rb->read_index<<endl;cout<<"rb->write_mirror: "<<rb->write_mirror<<endl;cout<<"rb->read_mirror: "<<rb->read_mirror<<endl;cout<<"rb_data_len: "<<rb_data_len(rb)<<endl;cout<<"rb_space_len: "<<rb_space_len(rb)<<endl;cout<<endl;
}void writeprint(ringbuff* rb){for(int i=0;i<rb->size;i++){cout<<rb->buffer_ptr[i];}print(rb);
}void test02() {ringbuff* rb = rb_create(9);cout<<"rb->size: "<<rb->size<<endl;const uint8 p[] = "123456789ABCDEF";uint32_t len = sizeof(p) / sizeof(char);// cout<<len<<endl;cout<<"写入数据:"<<p<<endl;for(int i=0;i<len-1;i++) {// rb_putchar(rb,p[i]); rb_putchar_force(rb,p[i]); }writeprint(rb); // 9ABCDEF8                      uint8 singlechar = ' ';for(int i=0;i<4;i++) {rb_getchar(rb,&singlechar);cout<<"读单个字符: "<<singlechar<<endl;}print(rb);// 销毁ringbuffrb_destroy(rb);
}
rb->size: 8
写入数据:123456789ABCDEF
9ABCDEF8
rb->write_index: 7
rb->read_index: 7
rb->write_mirror: 1
rb->read_mirror: 0
rb_data_len: 8
rb_space_len: 0读单个字符: 8
读单个字符: 9
读单个字符: A
读单个字符: Brb->write_index: 7
rb->read_index: 3
rb->write_mirror: 1
rb->read_mirror: 1
rb_data_len: 4
rb_space_len: 4销毁ringbuff~

四、RT-Thread🦥小结

来自此文总结:ring buffer,一篇文章讲透它? - 知乎 (zhihu.com)

🦝 在多线程中,对同一个环形缓冲区进行读写操作时,需要加上锁,不然存在访问不安全问题;

🦝 当只有一个读线程和一个写线程时,用rb_put和rb_get进行读写操作缓冲区是线程安全的,无需加锁;但是rb_put_force不行,因为其可能对读写索引都进行操作的场景,这个时候再进行rb_get读操作,就是不安全访问;

🦝 读写指针已经在读写(rb_get和rb_put)过程中转换为了读写索引。所以read_index(读索引)和write_index(写索引)可以直接用来操作缓冲区,无需转换;

🦝 read_index 和write_index 的大小区间为[0,n−1],n为缓冲区大小;

🦝 RT-Thread的环形缓冲区不需要buffer大小为2的幂。

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

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

相关文章

Mock平台-08开发:项目管理(四)编辑功能和Component抽离

【Mock平台】为系列测试开发教程&#xff0c;从0到1编码带你一步步使用Spring Boot 和 Antd React框架完成搭建一个测试工具平台&#xff0c;希望作为一个实战项目对各位的测试开发学习之路有帮助&#xff0c;大奇一个专注测试技术干货原创与分享的家伙。 本篇重点&#xff1a;…

MetaMask Mobile +Chrome DevTools 调试Web3应用教程

注&#xff1a;本教程来源网络&#xff0c;根据项目做的整理 写好了WEB3应用&#xff0c;在本地调试用得好好的&#xff0c;但是用钱包软件访问就报莫名的错&#xff0c;但是又不知道是什么原因&#xff0c;排查的过程非常浪费时间 。 因此在本地同一局域网进行调试就非常有必要…

mybatis-plus--配置-(sql)日志输出-自动填充-分页-多数据源-逻辑删除

写在前面&#xff1a; 本文主要介绍mybatis-plus的配置&#xff0c;以后在有的时候在补充。欢迎交流。 文章目录 日志输出自动填充分页全局字段配置多数据源 日志输出 调试的时候需要看执行的sql&#xff0c;这时候就很需要日志来记录查看了。 mybatis-plus的日志配置在yml…

​8th参考文献:[8]许少辉.乡村振兴战略下传统村落文化旅游设计[M]北京:中国建筑出版传媒,2022.

​&#xff18;th参考文献&#xff1a;&#xff3b;&#xff18;&#xff3d;许少辉&#xff0e;乡村振兴战略下传统村落文化旅游设计&#xff3b;&#xff2d;&#xff3d;北京&#xff1a;中国建筑出版传媒&#xff0c;&#xff12;&#xff10;&#xff12;&#xff12;&…

RK3588平台开发系列讲解(AI 篇)RKNN C API 详细说明

文章目录 一、API 硬件平台支持说明二、API 函数介绍2.1、rknn_init2.2、rknn_destroy2.3、rknn_query2.4、rknn_inputs_set2.5、rknn_run2.6、rknn_outputs_get2.7、rknn_outputs_release沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要讲解 RKNN C API 详细…

springboot 项目日志配置文件详解

spring boot 项目指定 日志配置文件 在Spring Boot项目中&#xff0c;可以通过在application.properties或application.yml文件中指定日志配置文件来配置日志。 1. 使用application.properties文件&#xff1a; 在application.properties中&#xff0c;您可以使用以下属性来…

【ARM】Day4 点亮LED灯

1. 思维导图 2. 自己编写代码实现三盏灯点亮 .text .global _start _start: /**********LED1&#xff0c;LED2,LED3点灯:PE10,PF10,PE8**************/ RCC_INIT:使能GPIOE组/GPIOF组控制器,通过RXCC_MP_AHB4ENSETR设置第[5:4]位写1,地址:0x50000A28[5:4]1ldr r0,0x50000A28 …

01_Redis单线程与多线程

01——Redis单线程与多线程 一、Redis是单线程还是多线程 在谈Redis的单线程或多线程时&#xff0c;需要根据版本来区分。 在redis 3.x之前&#xff0c;redis是单线程的从redis 4.x开始&#xff0c;redis引入多线程。处理客户端请求时&#xff0c;使用单线程&#xff1b;在异…

B-树和B+树的区别

B-树和B树的区别 一、B-tree数据存储 在下图中 P 代表的是指针&#xff0c;指向的是下一个磁盘块。在第一个节点中的 16、24 就是代表我们的 key 值是什么。date 就是这个 key 值对应的这一行记录是什么。 假设寻找 key 为 33 的这条记录&#xff0c;33 在 16 和 34 中间&am…

QT TLS initialization failed问题(已解决) QT基础入门【网络编程】openssl

问题: qt.network.ssl: QSslSocket::connectToHostEncrypted: TLS initialization failed 这个问题的出现主要是使用了https请求:HTTPS ≈ HTTP + SSL,即有了加密层的HTTP 所以Qt 组件库需要OpenSSL dll 文件支持HTTPS 解决: 1.加入以下两行代码获取QT是否支持opensll以…

如何在出差期间远程访问企业ERP系统?内网穿透解决您的难题!

文章目录 概述1.查看象过河服务端端口2.内网穿透3. 异地公网连接4. 固定公网地址4.1 保留一个固定TCP地址4.2 配置固定TCP地址 5. 使用固定地址连接 概述 ERP系统对于企业来说重要性不言而喻&#xff0c;不管是财务、生产、销售还是采购&#xff0c;都需要用到ERP系统来协助。…

SharkTeam:Worldcoin运营数据及业务安全分析

Worldcoin的白皮书中声明&#xff0c;Worldcoin旨在构建一个连接全球人类的新型数字经济系统&#xff0c;由OpenAI创始人Sam Altman于2020年发起。通过区块链技术在Web3世界中实现更加公平、开放和包容的经济体系&#xff0c;并将所有权赋予每个人。并且希望让全世界每一个人都…

Web3和去中心化:互联网的下一个演化阶段

文章目录 Web3和去中心化的定义Web3&#xff1a;去中心化&#xff1a; 为什么Web3和去中心化如此重要&#xff1f;数据隐私和安全&#xff1a;去中心化的创新&#xff1a;去除中间商&#xff1a; Web3和去中心化的应用领域去中心化金融&#xff08;DeFi&#xff09;&#xff1a…

Linux驱动开发之点亮三盏小灯

头文件 #ifndef __HEAD_H__ #define __HEAD_H__//LED1和LED3的硬件地址 #define PHY_LED1_MODER 0x50006000 #define PHY_LED1_ODR 0x50006014 #define PHY_LED1_RCC 0x50000A28 //LED2的硬件地址 #define PHY_LED2_MODER 0x50007000 #define PHY_LED2_ODR 0x50007014 #define…

机器学习深度学习——NLP实战(情感分析模型——数据集)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——BERT&#xff08;来自transformer的双向编码器表示&#xff09; &#x1f4da;订阅专栏&#xff1a;机器…

Unity如何控制声音大小(包括静音功能)

一&#xff1a;UGUI制作 1. 首先在【层级】下面创建UI里面的Slider组件。设置好它对应的宽度和高度。 2.调整Slider滑动条的填充颜色。一般声音颜色我黄色&#xff0c;所以我们也调成黄色。 我们尝试滑动Slider里面的value。 a.滑动前。 b.滑动一半。 c.滑动完。 从以上滑动va…

DeFINE:用于神经序列建模的深度分解输入令牌嵌入

一、说明 DeFINE&#xff0c;是华盛顿大学和艾伦人工智能开发的自然语言处理工具&#xff0c;可以处理的范围是&#xff1a;NLP、语言模型、LM、神经机器翻译、NMT、变压器、变压器-XL等&#xff1b;本文对token-bedding进行生成。 借助DeFINE&#xff0c;Transformer-XL可以在…

汽车级36V、4A同步降压转换器MAX20404AFOD/VY、MAX20404AFOC/VY、MAX20404AFOA/VY开关稳压器

MAX20404是小型同步降压转换器&#xff0c;集成了高端和低端开关。这些IC均设计为可在3V到36V的宽输入电压范围内提供高达4A的电流。电压质量可以通过观察PGOOD信号来监测。该器件可以在99%的占空比下运行&#xff0c;非常适合汽车和工业应用。 MAX20404提供可编程输出电压或5…

Debian查询硬件状态

很早以前写过一个查询树霉派硬件状态的文章&#xff0c;用是Python写的一个小程序。里面用到了vcgencmd这个测温度的内部命令&#xff0c;但这个命令在debian里面没有&#xff0c;debian里只有lm_sensors的外部命令&#xff0c;需要安装&#xff1a;apt-get install lm_sensors…

python:tkinter + cef 模仿 mdict 界面

cefpython3 其上游是C开发的CEF&#xff08;基于webkit、V8&#xff09;&#xff0c; CEF 即 (Chromium Embedder Framework)&#xff0c; 是基于Google Chromium项目的开源 Web browser控件(WebView)。 可查看github文档&#xff1a;cefpython api pip install cefpython3 c…