项目:基于httplib/消息队列负载均衡式在线OJ

文章目录

  • 写在前面
    • 关于组件
    • 开源仓库和项目上线
    • 其他文档说明
    • 项目亮点
  • 使用技术和环境
  • 项目宏观结构
  • 模块实现
    • compiler模块
    • runner模块
    • compile_run模块
    • compile_server模块
  • 基于MVC结构的OJ服务
    • 什么是MVC?
    • 用户请求服务路由功能
    • Model模块
    • view模块
    • Control模块

写在前面

关于组件

本项目使用的是一个仿RabbitMQ消息队列组件,关于该组件的细节内容在下面的链接中:

项目:仿RabbitMQ实现的消息队列组件

开源仓库和项目上线

本项目已开源到下面链接下的仓库当中

负载均衡式在线OJ

并且项目已经部署在了Linux服务器上,具体访问方式可以点击下面链接进行访问:

111.230.81.226:8080

其他文档说明

针对于日志的信息,我采用了之前写的一份利用可变参数实现日志的代码,具体链接如下

C++:可变参数实现日志系统

项目:仿RabbitMQ实现的消息队列组件

项目亮点

从技术栈的角度来讲,用的比较多,这里实现了两个版本,一个是httplib版本,在将请求从服务端发送到编译模块的部分是采用了httplib来进行发送的,而第二个版本采用的是消息队列的方式,服务端作为的是生产者,把数据推送到指定的队列,而编译模块作为消费者,来把数据进行编译后写入到Redis中,这样服务端只需要从Redis中获取数据即可

使用技术和环境

本项目使用的技术主要有:

  1. C++STL
  2. Boost标准库
  3. cpp-httplib网络库
  4. JSON序列化反序列化库
  5. Redis
  6. 消息队列

开发环境:

  1. Ubuntu22.04云服务器
  2. vscode

项目宏观结构

对于项目来说,核心的部分其实就是三个模块:

  1. common:存储的是一些公共的功能
  2. compile_server:编译运行模块
  3. oj_server:获取题目列表,查看界面,负载均衡等功能

具体的逻辑图如下所示:

对于客户端来说,会把所有的请求都发布到oj_server上,而oj_server就会把这些请求进行合理的分配,分配到各个地方进行各自的处理,其中对于请求题目和编写这样的内容不算特别吃资源,因此直接正常进行使用即可,而对于进行判定这样的操作,对于CPU的占用比较大,因此就需要使用负载均衡这样的策略,分配到各自的服务器主机上进行判定,达到一个负载均衡的效果

在这里插入图片描述

模块实现

下面进行模块的介绍实现,先说服务端,服务端主要包含下面的几个模块:

在这里插入图片描述

compiler模块

对于在线OJ来说,编译模块是必不可少的,那对于编译的具体逻辑,简单来说就是远端把代码提交到后端,此时会生成一个临时文件,而对于临时文件来说,就会进行编译的操作,编译失败,就把失败的信息生成,如果编译成功就继续执行,具体流程可以总结如下:

在这里插入图片描述
具体实现如下所示:

#pragma once
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "../common/util.hpp"
#include "../common/Log.hpp"// 只负责进行代码的编译
class Compiler
{
public:Compiler(){}~Compiler(){}static bool Compile(const string &file_name){pid_t pid = fork();if(pid < 0){lg(Error, "内部错误,创建子进程失败");return false;}else if (pid == 0){umask(0);int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if(_stderr < 0){lg(Warning, "没有生成stderr文件");exit(1);}dup2(_stderr, 2);//g++ -o target src -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),\PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE","-std=c++11",  nullptr);lg(Error, "启动编译器失败");exit(2);}else{waitpid(pid, nullptr, 0);// 编译是否成功,就看有没有形成对应的可执行程序if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){lg(Info, "%s:编译成功", PathUtil::Src(file_name).c_str());return true;}}lg(Error, "编译失败");return false;}
};

runner模块

编译了之后,就要运行了,因此下面这个模块就是运行模块

进程占用资源

对于在线OJ来说,其实是有时间复杂度和空间复杂度的需求的,因此我们也需要设置对应的接口,想要对于资源进行限制,需要用到的系统调用是setrlimit

在这里插入图片描述
对于这个接口来说,比较重要的参数就是这个结构体了:

在这里插入图片描述
这个结构体就是做出了对于软件和硬件的限制,总的来说这个系统调用还是比较简单的,我们需要限定CPU的时长和内存大小,需要用到的参数是RLIMIT_CPURLIMIT_AS

//提供设置进程占用资源大小的接口
static void SetProcLimit(int _cpu_limit, int _mem_limit)
{// 设置CPU时长struct rlimit cpu_rlimit;cpu_rlimit.rlim_max = RLIM_INFINITY;cpu_rlimit.rlim_cur = _cpu_limit;setrlimit(RLIMIT_CPU, &cpu_rlimit);// 设置内存大小struct rlimit mem_rlimit;mem_rlimit.rlim_max = RLIM_INFINITY;mem_rlimit.rlim_cur = _mem_limit * 1024; //转化成为KBsetrlimit(RLIMIT_AS, &mem_rlimit);
}

程序运行

接下来就是让程序进行运行了,程序运行主要有三种情况:

  1. 代码跑完,结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,异常了

那在运行模块,我们只管新的是程序有没有正常跑完,只要跑完了就可以,至于结果正确还是不正确,并不是这里关心的重点,因此在这个模块中,其实也需要有标准输出和标准错误两个模块,这两个模块当中记录的是到底有没有正常结束的信息

static int Run(const string &file_name, int cpu_limit, int mem_limit)
{string _execute = PathUtil::Exe(file_name);string _stdin   = PathUtil::Stdin(file_name);string _stdout  = PathUtil::Stdout(file_name);string _stderr  = PathUtil::Stderr(file_name);umask(0);int _stdin_fd = open(_stdin.c_str(), O_CREAT|O_RDONLY, 0644);int _stdout_fd = open(_stdout.c_str(), O_CREAT|O_WRONLY, 0644);int _stderr_fd = open(_stderr.c_str(), O_CREAT|O_WRONLY, 0644);if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0){lg(Error, "打开标准文件失败");return -1; //代表打开文件失败}            pid_t pid = fork();if (pid < 0){lg(Error, "创建子进程失败");close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);return -2; //代表创建子进程失败}else if (pid == 0){dup2(_stdin_fd, 0);dup2(_stdout_fd, 1);dup2(_stderr_fd, 2);SetProcLimit(cpu_limit, mem_limit);execl(_execute.c_str()/*执行谁*/, _execute.c_str()/*如何执行该程序*/, nullptr);exit(1);}else{close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);int status = 0;waitpid(pid, &status, 0);// 输出信号信息lg(Info, "运行结束,info:%d", (status & 0x7F));return status & 0x7F;}
}

compile_run模块

下面是进行compile_run模块,这个模块就是对于之前内容的整合,同时借助JSON完成序列化和反序列化的内容

对于序列化和反序列化来说,要传递的参数其实比较简单:

输入

  1. code:用户提交的代码
  2. input:用户给代码做的输入
  3. cpu_limit:时间要求
  4. mem_limit:空间要求

输出

  1. status:状态码
  2. reason:请求结果
  3. stdout:运行结果
  4. stderr:错误结果

所以在JSON中,它的组织形式更像是:

in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}

资源清理

在本项目中的策略是把创建的临时文件都放到一个temp文件夹路径下,而当运行结果结束之后,也就没有存在的必要了,因此就需要显示的把这些临时文件都清理掉,所以是需要一个垃圾回收的机制存在的

代码实现如下:

#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../common/util.hpp"
#include <signal.h>
#include <unistd.h>
#include <jsoncpp/json/json.h>
using namespace std;class CompileAndRun
{
public:static void RemoveTempFile(const string &file_name){// 清理文件的个数是不确定的,但是有哪些我们是知道的string _src = PathUtil::Src(file_name);if(FileUtil::IsFileExists(_src)) unlink(_src.c_str());string _compiler_error = PathUtil::CompilerError(file_name);if(FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str());string _execute = PathUtil::Exe(file_name);if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());string _stdin = PathUtil::Stdin(file_name);if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());string _stdout = PathUtil::Stdout(file_name);if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());string _stderr = PathUtil::Stderr(file_name);if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());}// code > 0 : 进程收到了信号导致异常奔溃// code < 0 : 整个过程非运行报错(代码为空,编译报错等)// code = 0 : 整个过程全部完成static string CodeToDesc(int code, const string &file_name){string desc;switch (code){case 0:desc = "编译运行成功";break;case -1:desc = "提交的代码是空";break;case -2:desc = "未知错误";break;case -3:// desc = "代码编译的时候发生了错误";FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);break;case SIGABRT: // 6desc = "内存超过范围";break;case SIGXCPU: // 24desc = "CPU使用超时";break;case SIGFPE: // 8desc = "浮点数溢出";break;default:desc = "未知: " + to_string(code);break;}return desc;}static void Start(const string &in_json, string *out_json){Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value);string code = in_value["code"].asString();string input = in_value["input"].asString();int cpu_limit = in_value["cpu_limit"].asInt();int mem_limit = in_value["mem_limit"].asInt();int status_code = 0;Json::Value out_value;int run_result = 0;string file_name;if (code.size() == 0){status_code = -1;goto END;}file_name = FileUtil::UniqFileName();//形成临时src文件if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){status_code = -2; //未知错误goto END;}if (!Compiler::Compile(file_name)){//编译失败status_code = -3; //代码编译的时候发生了错误goto END;}run_result = Runner::Run(file_name, cpu_limit, mem_limit);if (run_result < 0){status_code = -2; //未知错误}else if (run_result > 0){//程序运行崩溃了status_code = run_result;}else{//运行成功status_code = 0;}END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name);if (status_code == 0){// 整个过程全部成功string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}Json::StyledWriter writer;*out_json = writer.write(out_value);RemoveTempFile(file_name);}
};

compile_server模块

下面借助httplib,实现一个基础版本的服务器,httplib是一个简单的封装好的服务器库,这里直接进行使用:

#include "../common/httplib.h"
#include "compile_run.hpp"
using namespace std;void Usage(std::string proc)
{std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}//./compile_server port
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);return 1;}httplib::Server svr;svr.Post("/compile_and_run", [](const httplib::Request &req, httplib::Response &resp){// 用户请求的服务正文是我们想要的json stringstd::string in_json = req.body;std::string out_json;if(!in_json.empty()){CompileAndRun::Start(in_json, &out_json);resp.set_content(out_json, "application/json;charset=utf-8");}});svr.listen("0.0.0.0", atoi(argv[1]));return 0;
}

利用postman尝试进行测试,发现测试是可以通过的,代码正常运行:

在这里插入图片描述

基于MVC结构的OJ服务

什么是MVC?

对于MVC来说,M的意思是Model,通常是和数据进行交互的模块,比如对于题库进行增删改查,而V的意思是View,意思是利用数据进行构建网页和渲染网页内容,展示给用户对应的信息,最后C的意思是Control,控制器控制的是核心业务逻辑

下面的步骤,是要搭建一个小型网站,具体步骤如下:

  1. 获取首页信息
  2. 编辑区域页面
  3. 提交判题功能

下面就基于这些内容,进行代码的编写

用户请求服务路由功能

这个部分内容相对简单,就是借助httplib进行服务的调用

#include <iostream>
#include "../common/httplib.h"using namespace std;int main()
{httplib::Server svr;svr.Get("/all_questions", [](const httplib::Request &req, httplib::Response &resp){resp.set_content("这是所有题⽬的列表", "text/plain; charset=utf-8");});svr.Get(R"(/question/(\d+))", [](const httplib::Request &req, httplib::Response &resp){std::string number = req.matches[1];resp.set_content("这是指定的⼀道题: " + number, "text/plain; charset=utf-8");});svr.Get(R"(/judge/(\d+))", [](const httplib::Request &req, httplib::Response &resp){std::string number = req.matches[1];resp.set_content("指定题⽬的判题: " + number, "text/plain; charset=utf-8");});svr.set_base_dir("./wwwroot");svr.listen("0.0.0.0", 8080);return 0;
}

上述只是进行了一个最基础的操作,进行了一个框架的搭建,那么下面就进入MVC的模块

Model模块

Model模块,提供对于数据的一系列操作

对于这个项目的数据来说,最重要的就是题目的一些数据,这里枚举出题目的细节信息:

struct Question
{string number;string title;string star;int cpu_limit;int mem_limit;string desc;// 题目预设给用户在线编辑器的代码string header;// 题目的测试用例,需要和header拼接,形成完整代码string tail;
};

实际上,在进行OJ的过程中,基本的逻辑思路很简单,把用户提交上来的代码和测试用例进行拼接,再执行,把执行的结果和测试用例进行比较,即可得到结果,这就是项目的底层逻辑

那么有了这样的思想,实现出Model模块逻辑就很轻松了,下面进行具体的实现过程:

class Model
{
private://题号 : 题目细节unordered_map<string, Question> questions;
public:Model();bool LoadQuestionList(const string &question_list);bool GetAllQuestions(vector<Question> *out);bool GetOneQuestion(const string &number, Question *q);~Model(){}
};

view模块

view模块这里采取的是ctemplate中的渲染网页部分,它的基本思想就是实现一个key,value的值替换,达到渲染网页的目的

#pragma once
#include <iostream>
#include <string>
#include <ctemplate/template.h>
#include "oj_model.hpp"
using namespace std;const string template_path = "./template_html/";class View
{
public:View(){}~View(){}
public:void AllExpandHtml(const vector<struct Question> &questions, string *html){// 题目的编号 题目的标题 题目的难度// 推荐使用表格显示// 1. 形成路径string src_html = template_path + "all_questions.html";// 2. 形成数字典ctemplate::TemplateDictionary root("all_questions");for (const auto& q : questions){ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");sub->SetValue("number", q.number);sub->SetValue("title", q.title);sub->SetValue("star", q.star);}//3. 获取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);//4. 开始完成渲染功能tpl->Expand(html, &root);}void OneExpandHtml(const struct Question &q, string *html){// 1. 形成路径string src_html = template_path + "one_question.html";// 2. 形成数字典ctemplate::TemplateDictionary root("one_question");root.SetValue("number", q.number);root.SetValue("title", q.title);root.SetValue("star", q.star);root.SetValue("desc", q.desc);root.SetValue("pre_code", q.header);//3. 获取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);//4. 开始完成渲染功能tpl->Expand(html, &root);}
};

Control模块

最后进入Control模块

对于Control模块来说,基本的调用逻辑为:

  1. 把in_json进行反序列化,得到题目id和源代码,input等
  2. 重新拼接用户代码和测试代码,可以得到一个新的代码
  3. 把新的代码给负载最低的主机,发起http请求把结果传输过去
  4. 把结果赋值给out_json即可

选择主机

主机如何选择呢?

在本项目中,提供的方法是使用轮询方式实现的负载均衡,对比所有的主机寻找一个负载最少的进行判断即可

由于要实现负载均衡,所以在进行实际的使用过程中,要对于负载均衡有体现,在实际的运行中,理应拥有几个服务器一起进行负载均衡,那对于这些服务器的管理是有必要的,所以就要先对提供服务的主机进行管理:

// 提供服务的主机
class Machine
{
public:string ip;       //编译服务的ipint port;        //编译服务的portuint64_t load;   //编译服务的负载mutex *mtx;      // mutex禁止拷贝的,使用指针
public:Machine() : ip(""), port(0), load(0), mtx(nullptr){}~Machine(){}public:// 提升主机负载void IncLoad(){if (mtx) mtx->lock();++load;if (mtx) mtx->unlock();}// 减少主机负载void DecLoad(){if (mtx) mtx->lock();--load;if (mtx) mtx->unlock();}void ResetLoad(){if(mtx)mtx->lock();load = 0;if(mtx)mtx->unlock();}// 获取主机负载uint64_t Load(){uint64_t _load = 0;if (mtx) mtx->lock();_load = load;if (mtx) mtx->unlock();return _load;}
};

之后,实现负载均衡,由于篇幅的原因这里把最核心的展示出来:

// 选择主机和机器
bool SmartChoice(int *id, Machine **m)
{// 1. 使用选择好的主机(更新该主机的负载)// 2. 需要可能离线该主机mtx.lock();// 负载均衡的算法: 轮询int online_num = online.size();if (online_num == 0){mtx.unlock();lg(Fatal, "后端所有主机离线");return false;}// 通过遍历的方式,找到所有负载最小的机器*id = online[0];*m = &machines[online[0]];uint64_t min_load = machines[online[0]].Load();for (int i = 1; i < online_num; i++){// 获取主机的负载,如果负载小就更新uint64_t curr_load = machines[online[i]].Load();if (min_load > curr_load){min_load = curr_load;*id = online[i];*m = &machines[online[i]];}}mtx.unlock();return true;
}

有了这两个模块,就可以去实现出Control模块了,这个是整个业务最核心的模块,它用到了Model模块来提供业务数据,view实现html渲染,还有负载均衡

// 这是我们的核心业务逻辑的控制器
class Control
{void RecoveryMachine(){load_blance_.OnlineMachine();}//根据题目数据构建网页// html: 输出型参数bool AllQuestions(string *html);bool Question(const string &number, string *html);// code: #include...// input: ""void Judge(const string &number, const string in_json, string *out_json){   // 0. 根据题目编号,直接拿到对应的题目细节// 1. in_json进行反序列化,得到题目的id,得到用户提交源代码,input// 2. 重新拼接用户代码+测试用例代码,形成新的代码// 3. 选择负载最低的主机(差错处理)// 规则: 一直选择,直到主机可用,否则,就是全部挂掉// 4. 然后发起http请求,得到结果// 5. 将结果赋值给out_json}
};

这样,整个项目的最基础的业务逻辑就使用结束了

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

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

相关文章

UnityXR Interactable Toolkit如何实现Climb爬梯子

前言 在VR中,通常会有一些交互需要我们做爬梯子,爬墙的操作,之前用VRTK3时,里面是还有这个Demo的,最近看XRI,发现也除了一个爬的示例,今天我们就来讲解一下 如何在Unity中使用XR Interaction Toolkit实现爬行(Climb)操作 环境配置 步骤 1:设置XR环境 确保你的Uni…

HTML静态网页成品作业(HTML+CSS)—— 名人霍金介绍网页(6个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有6个页面。 二、作品演示 三、代…

Tomcat源码解析(八):一个请求的执行流程(附Tomcat整体总结)

Tomcat源码系列文章 Tomcat源码解析(一)&#xff1a;Tomcat整体架构 Tomcat源码解析(二)&#xff1a;Bootstrap和Catalina Tomcat源码解析(三)&#xff1a;LifeCycle生命周期管理 Tomcat源码解析(四)&#xff1a;StandardServer和StandardService Tomcat源码解析(五)&…

Seq2seq、编码器解码器神经网络

目录 一、Seq2seq 简介二、编码器三、解码器四、编码器-解码器的训练 遇到看不明白的地方&#xff0c;欢迎在评论中留言呐&#xff0c;一起讨论&#xff0c;一起进步&#xff01; 需掌握的前提知识&#xff1a; LSTM、词嵌入 本文参考&#xff1a;【官方双语】编码、解码神经网…

在Cisco Packet Tracer上配置NAT

目录 前言一、搭建网络拓扑1.1 配置PC机1.2 配置客户路由器1.3 配置ISP路由器 二、配置NAT2.1 在客户路由器中配置NAT2.2 测试是否配置成功 总结 前言 本篇文章是在了解NAT的原理基础上&#xff0c;通过使用Cisco Packet Tracer 网络模拟器实现模拟对NAT的配置&#xff0c;以加…

C语言学习系列:GCC编译器Windows版本MinGW-w64的安装教程

本文图文分享如何安装C语言编译器——MinGW-w64。 只要看到这篇文章&#xff0c;就可以按照文中步骤正确安装MinGW-w64并使用。 一、什么是 MinGW-w64 &#xff1f; 我们知道C语言是高级语言&#xff0c;必须编译为二进制文件&#xff0c;才能为计算机运行&#xff0c;MinGW…

二开版微交易系统

下载地址&#xff1a;二开版微交易系统

1035 插入与归并(测试点6)

solution 类型判断&#xff1a;插入排序中已排序的部分有序&#xff0c;未排序的和原数组元素相同&#xff1b;否则为归并排序测试点6&#xff1a;对于归并排序的子序列长度&#xff0c;不能简单视为前k个有序则子序列长度就是k 例如该测试用例的归并排序的子序列长度应该为2&…

2024海南省大数据教师培训-Hadoop集群部署

前言 本文将详细介绍Hadoop分布式计算框架的来源&#xff0c;架构和应用场景&#xff0c;并附上最详细的集群搭建教程&#xff0c;能更好的帮助各位老师和同学们迅速了解和部署Hadoop框架来进行生产力和学习方面的应用。 一、Hadoop介绍 Hadoop是一个开源的分布式计算框架&…

牛客java基础(一)

A 解析 : java源程序只允许一个public类存在 &#xff0c;且与文件名同名 ; D hashCode方法本质就是一个哈希函数&#xff0c;这是Object类的作者说明的。Object类的作者在注释的最后一段的括号中写道&#xff1a;将对象的地址值映射为integer类型的哈希值。但hashCode()并不…

SQL Chat:从SQL到SPEAKL的数据库操作新纪元

引言 SQL Chat是一款创新的、对话式的SQL客户端工具。 它采用自然语言处理技术&#xff0c;让你能够像与人交流一样&#xff0c;通过日常对话的形式对数据库执行查询、修改、创建及删除操作 极大地简化了数据库管理流程&#xff0c;提升了数据交互的直观性和效率。 在这个框…

【虚拟现实】一、AR与VR的基本原理

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 增强现实&#xff08;AR&#xff09;和虚拟现实&#xff08;VR&#xff09;技术已经从科幻小说走入现实&#xf…

UltraScale+系列模块化仪器,可以同时用作控制器、算法加速器和高速数字信号处理器

基于 XCZU7EG / XCZU4EG / XCZU2EG • 灵活的模块组合 • 易于嵌入的紧凑型外观结构 • 高性能的 ARM Cortex 处理器 • 成熟的 FPGA 可编程逻辑 &#xff0c;基于 IP 核的软件库 基于 Xilinx Zynq UltraScaleMPSoC 的 FPGA 技术&#xff0c;采用 Xilinx Zynq UltraScale&a…

Spring-Security(二)OAuth2认证详解(持续更新)

Spring Security & Oauth2系列&#xff1a; Spring Security&#xff08;一&#xff09; 源码分析及认证流程 Spring Security&#xff08;二&#xff09;OAuth2认证详解及自定义异常处理 文章目录 1、OAuth2.0 简介1.1 OAuth2.0 相关名词解释1.2 四种授权模式 1.3 、OAu…

接口(API)开发,测试工具-apifox

前言 为什么需要接口&#xff08;API&#xff09;? 因为不同的平台或系统可能使用不同的技术栈、编程语言或数据格式。API提供了一个标准化的方式&#xff0c;使得这些不同的系统可以相互交换数据和功能调用&#xff0c;实现互操作性 在开发日常的项目交互中&#xff0c;不…

内网穿透的方式有哪些——快解析的优势

外网穿透内网技术&#xff0c;即内网映射&#xff0c;是把目标本地内网地址和端口发布到互联网&#xff0c;是一种由内网开放到外网的权限操作。那么&#xff0c;内网穿透的方法有哪些呢&#xff1f;做映射外网的方法。需要结合自己本地网络环境和应用场景来实施。这里分享三种…

Python Excel 指定内容修改

需求描述 在处理Excel 自动化时,财务部门经常有一个繁琐的场景,需要读取分发的Excel文件内容复制到汇总Excel文件对应的单元格内,如下图所示: 这种需求可以延申为,财务同事制作一个模板,将模板发送给各员工,财务同事需收取邮件将员工填写的excel文件下载到本机,再类似…

[FreeRTOS 基础知识] 任务调度 与 链表

文章目录 任务并行的概念RTOS如何实现多任务调度&#xff1f; 任务并行的概念 在生活中&#xff0c;经常出现一心多用的情况。比如你需要一边吃饭一边手机回复信息&#xff0c;这里面就存在两个任务&#xff1a;任务一、吃饭。任务二、手机回复信息。 假如你无法一心多用&…

FreeRTOS实时系统 在任务中增加数组等相关操作 导致单片机起不来或者挂掉

在调试串口任务中增加如下代码&#xff0c;发现可以用keil进行仿真&#xff0c;但是烧录程序后&#xff0c;调试串口没有打印&#xff0c;状态灯也不闪烁&#xff0c;单片机完全起不来 博主就纳了闷了&#xff0c;究竟是什么原因&#xff0c;这段代码可是公司永流传的老代码了&…

平面设计神器CorelDRAW2021精简版,你值得拥有!

亲爱的设计师小伙伴们&#xff0c;今天我要为大家种草一款神奇的软件——CorelDRAW平面设计软件2021精简版&#xff01;&#x1f929;✨作为一名专业的图形设计师&#xff0c;我深知一个好工具对于我们的工作有多么重要。而这款软件简直就是我们设计师的救星&#xff01;&#…