基于负载均衡的在线OJ实战项目

前言:

该篇讲述了实现基于负载均衡式的在线oj,即类似在线编程做题网站一样,文章尽可能详细讲述细节即实现,便于大家了解学习。

文章将采用单篇不分段形式(ps:切着麻烦),附图文,附代码,代码部署在云服务器上

技术栈

  • C++ STL标准库

  • Boost 标准库

  • cpp-httpib 开源库

  • ctemplate 第三方开源前端网页渲染库

  • jsoncpp 第三方开源序列化、反序列化库

  • 负载均衡的设计

  • 多进程、多线程

  • MYSQL C connect

  • Ace前端在线编辑器

  • html/cdd/js/jquery/ajax

开发环境

  • vscode
  • mysql workbench
  • Centos 7云服务器

宏观结构

  • comm:公共模块
  • compile_sever:编译运行模块
  • oj_server:获取题目,负载均衡等

376d0cae8c104c84b622c10cc9de0693.png

 

 


项目演示: 


https://blog.csdn.net/Obto_/article/details/132558642?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132558642%22%2C%22source%22%3A%22Obto_%22%7Dhttps://blog.csdn.net/Obto_/article/details/132558642?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132558642%22%2C%22source%22%3A%22Obto_%22%7D


 



项目设计 -- 编译服务

工具类的准备:

供程序中各个部分调用的方法类:

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <vector>
#include <unistd.h>
#include <sys/time.h>
#include <atomic>
#include <boost/algorithm/string.hpp>
#include <fstream>
namespace ns_util
{class TimeUtil{public:static std::string GetTimeStamp(){// 获取时间戳 gettimeofdaystruct timeval _time;gettimeofday(&_time, nullptr);return std::to_string(_time.tv_sec);}// 获得毫秒时间戳static std::string GetTimeMs(){struct timeval _time;gettimeofday(&_time, nullptr);return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);}};const std::string temp_path = "./temp/";class PathUtil{public:static std::string AddSuffix(const std::string &file_name, const std::string &suffix){std::string path_name = temp_path;path_name += file_name;path_name += suffix;return path_name;}// 构建源文件路径+后缀的完整文件名// 1234 -> ./temp/1234.cppstatic std::string Src(const std::string &file_name){return AddSuffix(file_name, ".cpp");}// 构建可执行程序的完整路径 + 后缀名static std::string Exe(const std::string &file_name){return AddSuffix(file_name, ".exe");}static std::string CompilerError(const std::string &file_name){return AddSuffix(file_name, ".compile_stderr");}//-------------------------------------------------------------------// 构建该程序对应的标准错误完整的路径+后缀名static std::string Stderr(const std::string &file_name){return AddSuffix(file_name, ".stderr");}static std::string Stdin(const std::string &file_name){return AddSuffix(file_name, ".stdin");}static std::string Stdout(const std::string &file_name){return AddSuffix(file_name, ".stdout");}};class FileUtil{public:static bool IsFileExists(const std::string &path_name){struct stat st;if (stat(path_name.c_str(), &st) == 0){// 获取属性成功,文件已经存在return true;}return false;}static std::string UniqFileName(){static std::atomic_uint id(0);id++;// 毫秒时间戳std::string ms = TimeUtil::GetTimeMs();std::string uniq_id = std::to_string(id);return ms + "_" + uniq_id;}static bool WriteFile(const std::string &target, const std::string &content){// waitingstd::ofstream out(target);if (!out.is_open()){return false;}out.write(content.c_str(), content.size());out.close();return true;}static bool ReadFile(const std::string &target, std::string *content, bool keep = false){(*content).clear();std::ifstream in(target);if (!in.is_open()){return false;}std::string line;// getline是按行保存的,不保存行分隔符,自动去掉\n// 但是有些时候,需要保留行分隔符// getline内部重载了强制类型转化while (std::getline(in, line)){(*content) += line;(*content) += (keep ? "\n" : "");}in.close();return true;}};class StringUtil{public:/*str:输入型,目标要切分的字符串target:输出型,保存切分完毕的结果sep:指定的分隔符*/static void SplitString(const std::string &str,std::vector<std::string> *target,std::string sep){boost::split(*target,str,boost::is_any_of(sep),boost::algorithm::token_compress_on);//boost split}};}
  • PathUtil:路径工具
    • 形成exe完整路径
    • 形成cpp完整路径
    • 形成compile_stderr完整路径
    • 形成stderr完整路径
    • 形成stdin完整路径
    • 完整路径指的是当前代码在本地上的保存路径:即输入 1234 要形成 ./temp/1234.cpp等这里的相对路径形成依靠PathUtil工具
  • TimeUtil:时间工具
    • 获取时间戳 get time of day
    • 获得好面时间戳,用于形成文件唯一标识(名字)
  • FileUtil:文件工具
    • IsFileExits:判断某文件是否存在
    • UniqFileName:形成文件唯一名字
    • WriteFile:向指定文件写入指定字符串
    • ReadFile:读取某文件的内容
  • StringUtil:字符串工具
    • 使用boost库中的切分字符串Split()

 

compiler编译服务设计 :

目的:能够编译并运行代码,得到格式化的相关结果

bcc25d727a6e4f8f9fbb337b5612453d.png

 

#pragma once#include <iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/wait.h>
#include"../comm/util.hpp"
#include"../comm/log.hpp"
//只负责进行代码的编译
namespace ns_compiler{//引入路径拼接功能using namespace ns_util;using namespace ns_log;class Compiler{public:Compiler(){}~Compiler(){}//返回值是编译成功TRUE;else FALSE//输入参数是编译的文件名//file_name : 1234//1234 -> ./temp/1234.cpp//1234 -> ./temp/1234.exe//1234 -> ./temp/1234.stderrstatic bool Compile(const std::string &file_name){pid_t pid = fork();if(pid < 0){LOG(ERROR) << "内部错误,创建子进程失败"<<"\n";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){LOG(WARNING)<<"没有成功行成stderr文件"<<"\n";exit(1);}//重定向标准错误到_stderrdup2(_stderr,2);//程序替婚,并不影响进程的文件描述符表//子进程:调用编译器execlp("g++","g++","-o",PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(),"-std=c++11","-D","COMPILER_ONLINE",nullptr);LOG(ERROR) <<"启动编译器g++失败,可能是参数错误"<<"\n";exit(2);}else//父进程{waitpid(pid,nullptr,0);//编译是否成功,就看有没有形成对应的可执行程序if(FileUtil::IsFileExists(PathUtil::Exe(file_name).c_str())){LOG(INFO) <<PathUtil::Src(file_name)<<"编译成功!"<<"\n";return true;}}LOG(ERROR) <<"编译失败,没有形成可执行程序,return false"<<"\n";return false;}};
};

compiler编译服务只管编译传过来的代码,其他一律不管,它只关心程序是否能够编译过

LOG日志的添加:

#pragma once
#include<iostream>
#include<string>
#include"util.hpp"
namespace ns_log
{using namespace ns_util;//日志等级enum{INFO,DEBUG,WARNING,ERROR ,FATAL};//LOG() << "message"inline std::ostream &Log(const std::string &level, const std::string &file_name,int line){//添加日志等级std::string message = "[";message+=level;message+="]";//添加报错文件名称message+="[";message+=file_name;message+="]";//添加报错行message+="[";message+=std::to_string(line);message+="]";//添加日志时间戳message += "[";message += TimeUtil::GetTimeStamp();message += "]";//cout 本质内部是包含缓冲区的std::cout<<message;//不要endl刷新return std::cout;}//LOG(INFo) << "message"//开放式日志#define LOG(level) Log(#level,__FILE__,__LINE__)
}

runner运行功能设计:

#pragma once#include <iostream>
#include<sys/time.h>
#include<sys/resource.h>
#include<signal.h>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include<sys/wait.h>
#include<sys/time.h>
#include<sys/resource.h>
#include <fcntl.h>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner() {}~Runner() {}public://提供设置进程占用资源大小的接口static void SerProcLimit(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);}// 指明文件名即可,不需要带路径和后缀/*返回值如果是大于 0 :程序异常了,退出时收到了信号,返回值就是对应的信号返回值 == 0 就是正常运行完毕,结果是什么保存到了临时文件中,我不清楚返回值 < 0 属于内部错误cpu_limit:该程序运行的时候,可以使用的最大cpu的资源上限mem_limit:该程序运行的时候,可以使用的最大内存大小KB*/static int Run(const std::string &file_name,int cpu_limit,int mem_limit){/*程序运行:1.代码跑完结果争取2.代码跑完结果不正确3.代码没跑完,异常了run不需要考虑运行完后正确与否,只管跑首先我们必须知道可执行程序是谁?标准输入:不处理标准输入:程序运行完成,输出结果是什么标准错误:运行时错误信息*/std::string _execute = PathUtil::Exe(file_name);std::string _stdin = PathUtil::Stdin(file_name);std::string _stdout = PathUtil::Stdout(file_name);std::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){LOG(ERROR)<<"运行时打开标准文件失败"<<"\n";return -1; // 代表打开文件失败}pid_t pid = fork();if (pid < 0){LOG(ERROR)<<"运行时创建子进程失败"<<"\n";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);SerProcLimit(cpu_limit,mem_limit);execl(_execute.c_str()/*我要执行谁*/,_execute.c_str()/*我想在命令航商如何执行*/,nullptr);exit(1);}else{int status = 0;waitpid(pid,&status,0);//程序运行异常,一定是因为收到了信号LOG(INFO)<<"运行完毕,info:"<<(status & 0x7F)<<"\n";close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);return status&0x7F;}}};
}

compile_run:编译并运行功能:

#pragma once
#include "compiler.hpp"
#include<unistd.h>
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <jsoncpp/json/json.h>
#include <signal.h>
namespace ns_compile_and_run
{using namespace ns_log;using namespace ns_util;using namespace ns_compiler;using namespace ns_runner;class CompileAndRun{public:static void RemoveTempFile(const std::string& file_name){//清理文件的个数是不确定的,但是有哪些我们是知道的std::string _src = PathUtil::Src(file_name);if(FileUtil::IsFileExists(_src))unlink(_src.c_str());std::string _compiler_error = PathUtil::CompilerError(file_name);if(FileUtil::IsFileExists(_compiler_error))unlink(_compiler_error.c_str());std::string _execute = PathUtil::Exe(file_name);if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());std::string _stdin = PathUtil::Stdin(file_name);if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());std::string _stdout = PathUtil::Stdout(file_name);if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());std::string _stderr = PathUtil::Stderr(file_name);if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());}static std::string CodeToDesc(int code, std::string file_name) // code >0 <0 ==0{std::string desc;switch (code){case 0:desc = "编译运行成功";break;case -1:desc = "用户提交的代码是空";break;case -2:desc = "未知错误";break;case -3:// desc = "编译发生报错";FileUtil::ReadFile(PathUtil::Stderr(file_name), &desc, true);break;case -4:break;case SIGABRT:desc = "内存超过范围";break;case SIGXCPU:desc = "CPU信号超时";break;case SIGFPE:desc = "除零错误,浮点数溢出";break;default:desc = "未知:" + std::to_string(code);break;}return desc;}/*输入:code:用户提交的代码input:用户自己提交的代码,对应的输入-》不做处理cpu_limit:时间要求mem_limit:空间要求输出:必填:status:状态码reason:请求结果选填:stdout:我的的程序运行完的结果stderr:我的程序运行完的错误结构参数:in_json:{"code":"#include..."."input":"","cpu_limit":1,"mem_limit":10240}out_json:{"status":"0","reason":"","stdout":"","stderr":""};*/static void Start(const std::string &in_json, std::string *out_json){LOG(INFO)<<"启动compile_and_run"<<"\n";Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value); // 最后再处理差错问题std::string code = in_value["code"].asString();std::string input = in_value["input"].asString();int cpu_limit = in_value["cpu_limit"].asInt();int men_limit = in_value["mem_limit"].asInt();int status_code = 0;Json::Value out_value;int run_result = 0;std::string file_name; // 需要内部形成的唯一文件名if (code.size() == 0){// 说明用户一行代码都没提交status_code = -1;goto END;}// 形成的文件名只具有唯一性,没有目录没有后缀// 毫秒计时间戳+原子性递增的唯一值:来保证唯一性file_name = FileUtil::UniqFileName(); // 形成唯一文件名字LOG(DEBUG)<<"调用UniqFileName()形成唯一名字"<<file_name<<"\n";run_result = Runner::Run(file_name, cpu_limit, men_limit);if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)) // 形成临时src文件.cpp{status_code = -2; // 未知错误goto END;}if (!Compiler::Compile(file_name)){// 编译失败status_code = -3;goto END;}run_result = Runner::Run(file_name, cpu_limit, men_limit);if (run_result < 0){// 服务器的内部错误,包括不限于文件打开失败,创建子进程失败等待status_code = -2; // 未知错误goto END;}else if (run_result > 0){status_code = run_result;}else{// 运行成功status_code = 0;}END:std::cout<<"到达end语句"<<std::endl;// status_codeout_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name);LOG(DEBUG)<<CodeToDesc(status_code, file_name);if (status_code == 0){// 整个过程全部成功 , 这时候才需要运行结果std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout; }else{std::string _stderr;FileUtil::ReadFile(PathUtil::CompilerError(file_name), &_stderr, true);out_value["stderr"] = _stderr;}// 序列化Json::StyledWriter writer;*out_json = writer.write(out_value);//清理所有的临时文件RemoveTempFile(file_name);}};
}

compile_run:它的功能是接收远端传进来的json包,并反序列化得到其中的代码与输入,并调用compile进行编译

  • 编译成功:调用runner将代码运行起来->将执行结果分别保存到.exe、.stdin、.stdout 、.stderr、.compile_stderr文件中
  • 编译失败:不调用runner

最后按对应构造json 返回给上级调用,即write进out_json中,收尾清除创建的文件

 

compile_server .cc文件编写:

 

#include"compile_run.hpp"
#include<jsoncpp/json/json.h>
#include"../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;//编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性,不然影响多个用户
void Usage(std::string proc)
{std::cerr <<"Usage:"<<"\n\t"<<proc<<"port"<<std::endl;
}
// ./compiler_server port
int main(int argc,char *argv[])
{if(argc!=2){Usage(argv[0]);}Server svr;svr.Get("/hello",[](const Request &req,Response &resp){resp.set_content("hello httplib,你好httplib","content_type: text/plain");});//svr.set_base_dir("./wwwroot");svr.Post("/compile_and_run",[](const Request &req,Response &resp){  //请求服务正文是我们想要的json串LOG(DEBUG)<<"调用compile_and_run"<<"\n";std::string out_json;std::string in_json = req.body;if(!in_json.empty()){LOG(DEBUG)<<"当前的in_json"<<in_json<<"\n";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]));//启动http服务了// std::string code = "code";// Compiler::Compile(code);// Runner::Run(code);//0-----------------------测试代码-------------------//下面的工作,充当客户端请求的json串// std::string in_json;// Json::Value in_value;// //R"()" raw string 凡事在这个圆括号里面的东西,就是字符串哪怕有一些特殊的字符串// in_value["code"] =R"(#include<iostream>// int main(){//         std::cout<<"测试成功"<<std::endl;//         int a = 10;//         a /= 0;//         return 0;//     })";// in_value["input"] ="";// in_value["cpu_limit"] = 1;// in_value["mem_limit"] = 10240 * 3;// Json::FastWriter writer;// std::cout<<in_json<<std::endl;// in_json = writer.write(in_value);// //这个是将来给客户端返回的字符串// std::string out_json;// CompileAndRun::Start(in_json,&out_json);// std::cout<<out_json<<std::endl;//0-----------------------------------------------------//提供的编译服务,打包新城一个网络服务//这次直接用第三方库,cpp-httplibreturn 0;
}

直接引入的httplib库, 设置好ip和端口就可以直接监听了

  • svr.get() :就是当对该服务器发起/hello 请求的时候,我就会接受到该请求,以Get的方式返回resp

makefile:

由于当前使用的c++11的新特性,引入了json库,和多线程

compile_server:compile_server.ccg++ -o $@ $^ -std=c++11 -ljsoncpp -lpthread
.PHONY:clean
clean:rm -f compile_server

项目设计 -- 基于MVC结构的oj服务 

本质:建立一个小型网站

1.获取首页

2.编辑区域页面

3.提交判题功能(编译并运行) 

 

M:Model,通常是和数据交互的模块,比如对题库的增删改查(文件版,mysql版)

V:view,通常是拿到数据之后,要进行构建网页,渲染网页内容

C:control,控制器,也就是我们核心业务逻辑

用户的请求服务路由功能:


#include "../comm/httplib.h"
#include "login.hpp"
#include <iostream>
#include <signal.h>
#include"oj_control.hpp"
using namespace httplib;
using namespace ns_control;
const std::string login_path = "../oj_login/wwwroot/";
static Control *ctrl_ptr = nullptr;
void Recovery(int signo)
{ctrl_ptr->RecoveryMachine();
}
int main() {signal(SIGQUIT,Recovery);// 用户请求的服务路由功能Server svr;Control ctrl;Login login;ctrl_ptr = &ctrl;/*1获取所有的题目列表*/svr.Get(R"(/all_questions)", [&ctrl](const Request &req, Response &resp) {std::string html;ctrl.AllQuestions(&html);resp.set_content(html, "text/html;charset=utf-8");});//   2用户要根据题目编号来选择题目// 这里的\d是正则表达式 + 是匹配数字// R"()"保持原始字符串不会被特殊字符影响比如\d \r \n之类的不需要做相关的转义svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp) {std::string number = req.matches[1];std::string html;ctrl.OneQuestion(number,&html);resp.set_content(html,"text/html;charset=utf-8");});// 3用户提交代码,使用我们的判题功能(1.没道题目的测试用例 2.compile_and_run)svr.Post(R"(/judge/(\d+))",[&ctrl](const Request &req, Response &resp){std::string number = req.matches[1];// resp.set_content("这是指定的一道题目的判题:" + number,//                  "text/plain;charset=utf-8");std::string result_json;ctrl.Judge(number,req.body,&result_json);resp.set_content(result_json,"application/json;charset=utf-8");});svr.Post(R"(/dealregister)",[&ctrl](const Request &req, Response &resp){int status = 1;std::string in_json = req.body;std::string out_json;if(!ctrl.UserRegister(in_json,&out_json)){status = 0;}LOG(INFO)<<"用户注册status : "<<status<<"\n";Json::Value tmp;tmp["status"] = status;Json::FastWriter writer;std::string res = writer.write(tmp);resp.set_content(res,"application/json;charset=utf-8");});svr.Get(R"(/my_login)",[&login,&ctrl](const Request &req,Response &resp){//直接跳转到静态的htmlstd::string html;ctrl.Login(req.body,&html);resp.set_content(html, "text/html;charset=utf-8");});svr.Get(R"(/register)",[&login,&ctrl](const Request &req,Response &resp){//直接跳转到静态的htmlstd::string html;ctrl.Register(req.body,&html);resp.set_content(html, "text/html;charset=utf-8");});svr.set_base_dir("./wwwroot");svr.listen("0.0.0.0", 8080);return 0;
}

这样当用户通过http请求我们的oj_server服务器的时候我们可以正确的路由到合适的功能

model功能:提供对数据的操作(文件版)

#pragma once
//文件版本
/*
编号
标题
难度
描述
时间(内部),空间(内部处理)两批文件构成
1.question.list:题目列表:不需要出现题目描述
2.需要题目的描述,需要题目的预设置代码(header.cpp),测试用例代码(tail.cpp)这两个内容是通过题目的编号,产生关联的
*/
#pragma once
#include "../comm/log.hpp"#include "../comm/util.hpp"
#include <cassert>
#include <fstream>
#include <iostream>
#include <stdlib.h>
#include <string>
#include <unordered_map>
#include <vector>
// 根据题目list文件,加载所有信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model {
using namespace std;
using namespace ns_log;
using namespace ns_util;
class Question {
public:std::string number; // 题目编号std::string title;  // 题目的标题std::string star;   // 难度:简单中等困难int cpu_limit;      // 时间要求 sint mem_limit;      // 空间要求 kbstd::string desc;   // 题目的描述std::string header; // 题目预设给用户在线编辑器的代码std::string tail; // 题目的测试用例,需要和header拼接形成完整代码
};
const std::string question_list = "./questions/questions.list";
const std::string question_path = "./questions/";
class Model {
private:// 题号:题目细节unordered_map<string, Question> questions;public:Model() { assert(LoadQuestionList(question_list)); }bool LoadQuestionList(const std::string &question_list) {// 加载配置文件questions/question.list + 题目编号文件ifstream in(question_list);if (!in.is_open()) {LOG(FATEL) << "加载题库失败,请检查是否存在题库文件"<< "\n";return false;}std::string line;while (getline(in, line)) {vector<string> tokens;StringUtil::SplitString(line, &tokens, " ");if (tokens.size() != 5) {LOG(WARNING) << "加载部分题目失败,请检查文件格式"<< "\n";continue;}Question q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = atoi(tokens[3].c_str());q.mem_limit = atoi(tokens[4].c_str());std::string path = question_path;path += q.number;path += "/";FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);FileUtil::ReadFile(path + "header.cpp", &(q.header), true);FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);questions.insert({q.number, q});}LOG(INFO) << "加载题库成功!"<< "\n";in.close();return true;}bool GetAllQuestion(vector<Question> *out) {if (questions.size() == 0) {LOG(ERROR) << "用户获取题库失败"<< "\n";return false;}for (const auto &q : questions) {out->push_back(q.second); // fir是key' sec是value}return true;}bool GetOneQuestion(const std::string &number, Question *q) {const auto &iter = questions.find(number);if (iter == questions.end()) {LOG(ERROR) << "用户获取题目失败:" << number << "\n";return false;}(*q) = iter->second;return true;}~Model() {}
};
} // namespace ns_model

该设计中有一个 question的题目清单,像题库的目录一样,填写每道题目的基本信息:

对应的是:

1.题目编号 2.题目名字 3.题目难度 4.时间限制 5.空间限制

559bed10817d4f8280e61bce3787873a.png

 

 

 model功能:提供对数据的操作(数据库版)

#pragma once
//这个是mysql版本
/*
编号
标题
难度
描述
时间(内部),空间(内部处理)两批文件构成
1.question.list:题目列表:不需要出现题目描述
2.需要题目的描述,需要题目的预设置代码(header.cpp),测试用例代码(tail.cpp)这两个内容是通过题目的编号,产生关联的
*/
#pragma once
#include "../comm/log.hpp"#include "../comm/util.hpp"
#include <cassert>
#include <stdio.h>
#include <fstream>
#include <iostream>
#include <stdlib.h>
#include <string>
#include <unordered_map>
#include <vector>#include"include/mysql.h"
// 根据题目list文件,加载所有信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model {
using namespace std;
using namespace ns_log;
using namespace ns_util;
class Question {
public:std::string number; // 题目编号std::string title;  // 题目的标题std::string star;   // 难度:简单中等困难int cpu_limit;      // 时间要求 sint mem_limit;      // 空间要求 kbstd::string desc;   // 题目的描述std::string header; // 题目预设给用户在线编辑器的代码std::string tail; // 题目的测试用例,需要和header拼接形成完整代码
};const std::string oj_questions ="oj_questions"; 
const std::string oj_user = "oj_user";
const std::string host = "127.0.0.1";
const std::string user = "oj_client";
const std::string passwd = "123456";
const std::string db = "oj";
const int port = 3306;
class Model {
private:// 题号:题目细节unordered_map<string, Question> questions;
public:Model() { }bool QueryMySql(const std::string &sql,vector<Question> *out){//创建mysql句柄MYSQL *my = mysql_init(nullptr);//连接数据库if(mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0) == nullptr){LOG(FATAL)<<"连接数据库失败!"<<"\n";return false;}//一定要设置该链接的编码格式默认是拉钉的mysql_set_character_set(my,"utf8mb4");LOG(INFO)<<"连接数据库成功"<<"\n";//执行sql语句if(0 != mysql_query(my,sql.c_str())){LOG(WARNING) << sql <<"execute error!"<<"\n";return false;}MYSQL_RES *res = mysql_store_result(my);//分析结果int rows = mysql_num_rows(res); //获得行数量int cols = mysql_num_fields(res);//获得列数量Question q;for(int i = 0;i<rows;i++){MYSQL_ROW row = mysql_fetch_row(res);q.number = row[0];q.title = row[1];q.star = row[2];q.desc = row[3];q.header = row[4];q.tail = row[5];q.cpu_limit = atoi(row[6]);q.mem_limit = atoi(row[7]);out->push_back(q);}//释放结果空间free(res);//关闭mysql连接mysql_close(my);return true;}bool GetAllQuestion(vector<Question> *out) {std::string sql ="select *from ";sql += oj_questions;return QueryMySql(sql,out);}bool GetOneQuestion(const std::string &number, Question *q) {bool res = false;std::string sql = "select *from ";sql+=oj_questions;sql+= " where number=";sql+=number;vector<Question> result;if(QueryMySql(sql,&result)){if(result.size() == 1){*q = result[0];res = true;}}return res;}bool UserRegister(const std::string& in_json,std::string* out_json){//这里先对in_json反序列化Json::Reader reader;Json::Value in_value;reader.parse(in_json,in_value);std::string number = in_value["number"].asString();std::string name = in_value["name"].asString();std::string password = in_value["password"].asString();int limit = in_value["limit"].asInt();int level = in_value["level"].asInt();//判断账号密码是否可行std::string sql = " select *from ";sql+=oj_user;sql+=" where number=";sql+=number;//创建数据库MYSQL *my = mysql_init(nullptr);//连接数据库if(mysql_real_connect(my,host.c_str(),user.c_str(),passwd.c_str(),db.c_str(),port,nullptr,0) == nullptr){LOG(WARNING)<<"连接到用户数据库失败"<<"\n";return false;}//一定要记得设置该链接的编码格式mysql_set_character_set(my,"utf8");LOG(INFO)<<"连接懂啊用户数据库成功"<<"\n";if(0 != mysql_query(my,sql.c_str())){LOG(WARNING)<< sql <<"execute error!"<<"\n";return false;}MYSQL_RES *res = mysql_store_result(my);if(mysql_num_rows(res) == 0)//获得行数量{ //当前输入的数据可以创建用户MYSQL_STMT *stmt = mysql_stmt_init(my);const char* query = "insert into oj_user values (?,?,?,?,?)";if(mysql_stmt_prepare(stmt,query,strlen(query)) != 0){LOG(WARNING)<<"stmt出现错误"<<"\n";mysql_stmt_close(stmt);mysql_close(my);return false;}//下面开始绑定MYSQL_BIND bind_params[5];memset(bind_params,0,sizeof bind_params);bind_params[0].buffer_type = MYSQL_TYPE_STRING;bind_params[0].buffer = (char*)number.c_str();bind_params[0].buffer_length = number.size();bind_params[1].buffer_type = MYSQL_TYPE_STRING;bind_params[1].buffer = (char*)name.c_str();bind_params[1].buffer_length = name.size();bind_params[2].buffer_type = MYSQL_TYPE_STRING;bind_params[2].buffer = (char*)password.c_str();bind_params[2].buffer_length = password.size();bind_params[3].buffer_type = MYSQL_TYPE_LONG;bind_params[3].buffer = &limit;bind_params[3].is_unsigned = 1;bind_params[4].buffer_type = MYSQL_TYPE_LONG;bind_params[4].buffer = &level;bind_params[4].is_unsigned = 1;if(mysql_stmt_bind_param(stmt,bind_params) !=0){LOG(WARNING) <<"绑定stmt参数出错"<<"\n";mysql_stmt_close(stmt);mysql_close(my);return false;}//执行插入语句if(mysql_stmt_execute(stmt)!=0){LOG(WARNING)<<"执行stmt语句的时候出现错误..."<<"\n";mysql_stmt_close(stmt);mysql_close(my);return false;}mysql_stmt_close(stmt);mysql_close(my);return true;}else{//服务器有重复的用户num ,不允许再创建了return false;}//保存到服务器//这里out_json暂时没有用,没有要返回的值return true;}~Model() {}
};
} // namespace ns_model

control:逻辑控制模块

#pragma once#include<iostream>
#include<string>
#include<cassert>
#include<algorithm>
#include<fstream>
#include<jsoncpp/json/json.h>
#include<vector>
#include<mutex>
#include"oj_view.hpp" 
// #include"oj_model.hpp"
#include"oj_model2.hpp"
#include"../comm/log.hpp"
#include"../comm/util.hpp"
#include"../comm/httplib.h"
namespace ns_control
{using namespace std;using namespace httplib;using namespace ns_log;using namespace ns_util;using namespace ns_model; using namespace ns_view;//提供服务的主机的内容class Machine{public:std::string ip; //编译服务器的ipint port;       //编译服务器的端口uint64_t load;  //编译服务器负载std::mutex *mtx;//mutex是禁止拷贝的,使用指针来完成public:Machine():ip(""),port(0),load(0),mtx(nullptr){}~Machine(){}public:void ResetLoad(){if(mtx)mtx->lock();load = 0;LOG(DEBUG)<<"当前ip:"<<ip<<"端口:"<<port<<"的load已经清除load = "<<load<<"\n";if(mtx)mtx->unlock();}//提升主机负载void IncLoad(){if(mtx) mtx->lock();++load;if(mtx) mtx->unlock();}//减少主机负载void DecLoad(){if(mtx) mtx->lock();--load;if(mtx) mtx->unlock();}//获取主机负载,没有太大的意义,只是为了同一接口uint64_t Load(){uint64_t _load = 0;if(mtx) mtx->lock();_load = load;if(mtx) mtx->unlock();return _load;}};const std::string service_machine = "./conf/service_machine.conf";//负载均衡模块class LoadBalance{ private://可以给我们提供编译服务的所有的主机//每一台主机都有自己的下标,充当当前主机的idstd::vector<Machine> machines; //所有在线的主机std::vector<int> online;//所有离线主机的idstd::vector<int> offline;//保证选择主机上的这个东西要保证数据安全std::mutex mtx;public:LoadBalance(){assert(LoadConf(service_machine));LOG(INFO)<<"加载"<<service_machine <<"成功"<<"\n";}~LoadBalance(){}public:bool LoadConf(const std::string &machine_cof){std::ifstream in(machine_cof);if(!in.is_open())\{LOG(FATAL) <<"加载:"<<machine_cof<<"失败"<<"\n";return false;}std::string line;while (getline(in,line)){std::vector<std::string> tokens;StringUtil::SplitString(line,&tokens,":");if(tokens.size()!=2){LOG(WARNING) <<"切分"<<line<<"失败"<<"\n";std::cout<<tokens[0]<<":"<<tokens[1]<<std::endl;continue;}//LOG(INFO) <<"切分"<<tokens[0]<<tokens[1]<<"成功"<<"\n";Machine m;m.ip = tokens[0];m.port = atoi(tokens[1].c_str());m.load = 0;m.mtx = new std::mutex();online.push_back(machines.size());machines.push_back(m);}in.close();return true;}//id:是一个输出型参数//m:是一个输出型参数bool SmartChoice(int *id,Machine **m){//1.使用选择好的主机(更新该主机的负载)//2.我们需要可能离线该主机mtx.lock();//选择主机//一般的负载均衡的算法//1.随机数法 + hash//2.轮询 + hashint online_num = online.size();//在线主机的个数if(online_num == 0){mtx.unlock();LOG(FATAL) << "所有的后端编译主机已经全部离线,请后端的尽快重启"<<"\n";return false;}LOG(DEBUG)<<"online:"<<online.size()<<"\n";//通过编译,找到负载最小的机器*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;}void OfflineMachine(int which){mtx.lock();for(auto iter = online.begin();iter!=online.end();iter++){if(*iter == which){//要离线的主机找到了machines[which].ResetLoad();LOG(DEBUG)<<"当前离线主机的负载更改为:"<<machines[which].load;online.erase(iter);offline.push_back(which);break;//因为break的存在,所以暂时不考虑迭代器失效的问题}}mtx.unlock();}void OnlineMachine(){//我们统一上线,后面统一解决mtx.lock();online.insert(online.end(),offline.begin(),offline.end());offline.erase(offline.begin(),offline.end());mtx.unlock();LOG(INFO)<<"所有的主机又上线了"<<"\n";LOG(INFO)<<"online:"<<online.size()<<"offline"<<offline.size()<<"\n";}void ShowMachines(){mtx.lock();LOG(INFO)<<"online:"<<online.size()<<"offline"<<offline.size()<<"\n";mtx.unlock();}};//这是我们核心业务逻辑的控制器class Control{private:Model model_;//提供后台数据View view_; //提供网页渲染功能LoadBalance load_blance_; //核心负载均衡器public:void RecoveryMachine(){load_blance_.OnlineMachine();}//根据题目数据构建网页//html:输出型参数bool AllQuestions(string *html){bool ret = true;vector<Question> all;if(model_.GetAllQuestion(&all)){sort(all.begin(),all.end(),[](const Question &q1,const Question &q2){return atoi(q1.number.c_str()) < atoi(q2.number.c_str());});//获取题目信息 成功,将所有的题目数据构建成网页view_.AllExpandHtml(all,html);}else{*html="获取题目失败,形成题目列表失败";ret = false;}return ret;}bool OneQuestion(const string &number,string *html){Question q;bool ret = true;if(model_.GetOneQuestion(number,&q)){//获取指定信息的题目成功,构建程网页view_.OneExpandHtml(q,html);}else{*html="获取指定题目题目失败,形成题目列表失败";ret = false;}return ret;}void Login(const std::string in_json,std::string *out_json){//in_json是发送过来的请求数据,用户的账号等待//返回渲染的登录界面view_.LoginExpandHtml(out_json);}void Register(const std::string in_json,std::string *out_json){if(view_.RegisterExpandHtml(out_json)){LOG(INFO)<<"插入成功"<<"\n";}else{LOG(INFO)<<"插入失败,可能是重复的用户"<<"\n";}}bool UserRegister(const std::string in_json,std::string *out_json){return model_.UserRegister(in_json,out_json);}//id:: 100 //code:include//input:void Judge(const std::string &number,const std::string in_json,std::string *out_json){// LOG(INFO)<<"调用Judge功能"<<"\n";// LOG(DEBUG)<<in_json<<"\nnumber:"<<number<<"\n";//0.根据题目编号,拿到题目细节Question q;model_.GetOneQuestion(number,&q);//1.in_json反序列化 ,得到题目的id,得到源代码,inputJson::Reader reader;Json::Value in_value;reader.parse(in_json,in_value);std::string code = in_value["code"].asString();//2.重新拼接用户代码+测试用例代码,形成新的代码Json::Value compile_value;compile_value["input"] = in_value["input"].asString();compile_value["code"] = code + q.tail;compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;Json::FastWriter writer;std::string compile_string = writer.write(compile_value);//3.选择负载最低的主机,然后发起HTTP请求得到结果//规则:一直选择,直到主机可用,否则就是全部挂掉while(true){int id = 0;Machine *m = nullptr;if(!load_blance_.SmartChoice(&id,&m)){   break;}//4.*out_json = 将结果复制给out_jsonClient cli(m->ip,m->port);m->IncLoad();LOG(DEBUG)<<"选择主机成功,主机id:"<<id<<"\n详情:"<<m->ip<<":"<<m->port<<"当前主机负载:"<<m->Load()<<"\n";if(auto res = cli.Post("/compile_and_run",compile_string,"application/json;charset=utf-8")){//将我们的结果返回给out_jsonif(res->status == 200){*out_json = res->body;m->DecLoad();LOG(INFO)<<"请求编译和运行服务成功..."<<"\n";break;}                        m->DecLoad();}else{//请求失败LOG(ERROR)<<"选择主机失败,主机id:"<<id<<"详情:"<<m->ip<<":"<<m->port<<"可能已经离线"<<"\n";load_blance_.OfflineMachine(id);load_blance_.ShowMachines();//仅仅为了调试}//m->DecLoad();}}Control(){}~Control(){}};
}

control模块实现了 负载均衡

  • 负载均衡
    • 第一种:随机数+hash
    • 第二种:轮询+hash , 本文是在用轮询+hash
  • 为了实现负载均衡所有要把所有主机管理起来,有了Machine类
    • std::string ip :编译服务器的ip
    • int port:编译服务器的端口
    • uint64_t load :编译服务器的负载
    • std::mutex *mtx:每个机器可能会同时被多个用户访问,所以要有锁来保证临界资源,并且mutex是不允许拷贝的,所以这里直接用指针,这样在赋值构造和拷贝构造就没事了

 

 view渲染功能:将后端的代码渲染到html返回给前端

这里就要使用到ctemplate库了:

0ac4557f0c2242cab7e55958412ab2ab.png

 

 

#pragma once#include<iostream>
#include<string>
#include<ctemplate/template.h>
// #include"oj_model.hpp"
#include"oj_model2.hpp"namespace ns_view
{using namespace ns_model;const std::string template_path ="./template_html/";const std::string login_path = "./login_html/";class View{public:View(){}~View(){}bool RegisterExpandHtml(std::string *html){//新城路径std::string src_html = login_path + "register.html";//形成数据字典ctemplate::TemplateDictionary root("register");//获取渲染的网页ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);//开始渲染tpl->Expand(html,&root);return true;}void LoginExpandHtml(std::string *html){//形成路径std::string src_html = login_path + "login.html";//形成数据字典ctemplate::TemplateDictionary root("my_login");//获取渲染网页ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);//开始渲染tpl->Expand(html,&root);}void AllExpandHtml(const vector<Question> &questions,std::string *html){// 题目的编号 题目的标题 题目的难度// 推荐使用表格显示//1。形成路径std::string src_html = template_path + "all_questions.html";LOG(INFO)<<"形成路径成功:"<< src_html <<"\n";//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);LOG(INFO)<<"获取渲染网页的html成功"<<"\n";//4.开始完成渲染功能tpl->Expand(html,&root);LOG(INFO)<<"渲染成功"<<"\n";}void OneExpandHtml(const Question &q,std::string *html){//形成路径std::string src_html = template_path + "one_question.html";LOG(DEBUG)<<"one expand html :"<<src_html<<"\n";//q.desc//形成数字典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);//获取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html,ctemplate::DO_NOT_STRIP);//开始渲染功能tpl->Expand(html,&root);}};
}

 

 

总结 

1.前端的代码在博客最上端绑定的文件当中 ,篇幅太长不展示出来了

2.该项目的技术栈众多,是c++后端和前端进行交互的一个项目

3.项目的难点有:负载均衡的分配到每一台编译服务器、容错处理,能够处理多种不同的错误原因、并发处理要对临界资源的管理、以及高并发访问的话要对效率有所保证,毕竟在线oj服务是具有时效性的

4.debug困难,要在test.cc下测试成功后再进行编写,便于修改bug

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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

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

相关文章

javacv 基础04-读取mp4,avi等视频文件并截图保存图片到本地

javacv 读取mp4,avi等视频文件并截图保存图片到本地 代码如下&#xff1a; package com.example.javacvstudy;import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.Java2DFrameConverter;import javax.imageio.Im…

wangluobiancheng

UDP send: receive: TCP

朝夕光年游戏自动化测试实践

朝夕光年是面向全球用户与开发者的游戏研发与发行业务品牌&#xff0c;致力于服务全球玩家&#xff0c;帮助玩家在令人惊叹的虚拟世界中一起玩耍与创造。 在游戏的研发过程中&#xff0c;游戏自动化一直是开展难度较大的工程&#xff0c;具体包括机房机架、设备调度、软件框架、…

理解底层— —Golang的log库,二开实现自定义Logger

理解底层— —Golang的log库&#xff0c;实现自定义Logger 1 分析实现思路 基于golang中自带的log库实现&#xff1a;对日志实现设置日志级别&#xff0c;每天生成一个文件&#xff0c;同时添加上前缀以及展示文件名等 日志级别&#xff0c;通过添加prefix&#xff1a;[INFO]、…

学生信息管理系统MIS(前端)

改造HTML文件 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>学生信息管理系统MIS</title><!-- link在HTML文件中,引入外部的css文件 rel的值是固定写法,stylesheet样式表href用来指定样式表的位置--><lin…

【LeetCode】剑指 Offer <二刷>(4)

目录 题目&#xff1a;剑指 Offer 09. 用两个栈实现队列 - 力扣&#xff08;LeetCode&#xff09; 题目的接口&#xff1a; 解题思路&#xff1a; 代码&#xff1a; 过啦&#xff01;&#xff01;&#xff01; 题目&#xff1a;剑指 Offer 10- I. 斐波那契数列 - 力扣&am…

MySQL 5种索引应用

文章目录 简介一、聚集索引二、唯一索引三、聚集索引和唯一索引对比四、非唯一&#xff08;普通&#xff09;索引五、全文索引六、组合索引七、索引验证总结 简介 在本篇文章中&#xff0c;我们将学习MySQL中5种不同类型的索引及其应用场景&#xff0c;以及它们的优缺点。 一…

WIFI与BT的PCB布局布线注意事项

1、模块整体布局时&#xff0c;WIFI模组要尽量远离DDR、HDMI、USB、LCD电路以及喇叭等易干扰模块或连接座&#xff1b; 2、晶体电路布局需要优先考虑&#xff0c;布局时应与芯片在同一层并尽量靠近放置以避免打过孔&#xff0c;晶体走线尽可能的短&#xff0c;远离干扰源&…

【MetaAI】2023年MetaAI发布的开源模型和工具

MetaAI开源模型和工具 MetaAILlamaSegment AnythingDINOv2ImageBindMMSLimaVoiceboxMusicGenLlama 2AudioCraftSeamlessM4T MetaAI Meta 首席执行官扎克伯格表示&#xff0c;与其他研究者分享 Meta 公司开发的模型可以帮助该公司促进创新、发现安全漏洞和降低成本。他今年 4 月…

概念解析 | 量子机器学习:将量子力学与人工智能的奇妙融合

注1:本文系“概念解析”系列之一,致力于简洁清晰地解释、辨析复杂而专业的概念。本次辨析的概念是:量子机器学习。 量子机器学习:将量子力学与人工智能的奇妙融合 量子增强机器学习:量子经典混合卷积神经网络 量子机器学习是量子计算和机器学习的结合,它利用量子力学的特…

Opencv-C++笔记 (18) : 轮廓和凸包

文章目录 一、轮廓findContours发现轮廓drawContours绘制轮廓代码 二.几何及特性概括——凸包(Convex Hull)凸包概念凸包扫描算法介绍——Graham扫描算法 相关API介绍程序示例轮廓集合及特性性概括——轮廓周围绘制矩形框和圆形相关理论介绍轮廓周围绘制矩形 -API绘制步骤程序实…

Python数据分析案例30——中国高票房电影分析(爬虫获取数据及分析可视化全流程)

案例背景 最近总看到《消失的她》票房多少多少&#xff0c;《孤注一掷》票房又破了多少多少..... 于是我就想自己爬虫一下获取中国高票房的电影数据&#xff0c;然后分析一下。 数据来源于淘票票&#xff1a;影片总票房排行榜 (maoyan.com) 爬它就行。 代码实现 首先爬虫获…

<AMBA总线篇> AXI总线协议介绍

目录 01 AXI协议简介 AXI协议特性 AXI协议传输特性 02 AXI协议架构 AXI协议架构 write transaction(写传输) read tramsaction(读传输) Interface and interconnect 典型的AXI系统拓扑 03 文章总结 大家好&#xff0c;这里是程序员杰克。一名平平无奇的嵌入式软件工程…

Python 接口测试之Excel表格数据操作方法封装

引言 我们在做接口测试&#xff0c;经常会用到excel去管理测试数据&#xff0c;对Excel的操作比较频繁&#xff0c;那么使用python如何操作Excel文件的读与写呢&#xff1f;由于之前讲的都是大的框框&#xff0c;没有讲这么小的模块使用&#xff0c;现在就化整为0的讲解。 读…

基于OpenCV+LPR模型端对端智能车牌识别——深度学习和目标检测算法应用(含Python+Andriod全部工程源码)+CCPD数据集

目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境OpenCV环境Android环境1. 开发软件和开发包2. JDK设置3. NDK设置 模块实现1. 数据预处理2. 模型训练1&#xff09;训练级联分类器2&#xff09;训练无分割车牌字符识别模型 3. APP构建1&#xff09;导入OpenCV库…

数据结构-第一期——数组(Python)

目录 00、前言&#xff1a; 01、一维数组 一维数组的定义和初始化 一维变长数组 一维正向遍历 一维反向遍历 一维数组的区间操作 竞赛小技巧&#xff1a;不用从a[0]开始&#xff0c;从a[1]开始 蓝桥杯真题练习1 读入一维数组 例题一 例题二​ 例题三 实战训…

在iPhone 15发布之前,iPhone在智能手机出货量上占据主导地位,这对安卓来说是个坏消息

可以说这是一记重拳&#xff0c;但似乎没有一个有价值的竞争者能与苹果今年迄今为止的智能手机出货量相媲美。 事实上&#xff0c;根据Omdia智能手机型号市场跟踪机构收集的数据&#xff0c;苹果的iPhone占据了前四名。位居榜首的是iPhone 14 Pro Max&#xff0c;2023年上半年…

详细教程:Stegsolve的下载,jdk的下载、安装以及环境的配置

最近在学习隐写术&#xff0c;下载stegsolve 以及使用stegsolve倒腾了很久&#xff0c;避免朋友们和我一样倒腾了很久&#xff0c;希望此文可以帮到刚在学习隐写的朋友们(win7下使用stegsolve) 文章目录 一、下载stegsolve链接二、jdk的下载三、jdk的安装四、配置环境变量五、检…

Redis——》Pipeline

推荐链接&#xff1a; 总结——》【Java】 总结——》【Mysql】 总结——》【Redis】 总结——》【Kafka】 总结——》【Spring】 总结——》【SpringBoot】 总结——》【MyBatis、MyBatis-Plus】 总结——》【Linux】 总结——》【MongoD…

60倍!5G+卫星服务仅中国电信有,中国移动的霸主地位或被撼动

近期国内一家手机企业发布了第三代卫星手机&#xff0c;并且由于再次支持5G而获得高度关注&#xff0c;成为近期手机市场的热点&#xff0c;不过笔者查找了相关资料后却发现&#xff0c;可以支持5G卫星通信服务的仅有中国电信&#xff0c;如此中国移动的霸主地位将可能被撼动。…