当前位置: 首页 > news >正文

负载均衡式在线OJ

文章目录

  • 项目介绍
  • 所用技术与开发环境
    • 所用技术
    • 开发环境
  • 项目框架
  • compiler_server模块
    • compiler编译功能
      • comm/util.hpp 编译时的临时文件
      • comm/log.hpp 日志
      • comm/util.hpp 时间戳
      • comm/util.hpp 检查文件是否存在
      • compile_server/compiler.hpp 编译功能总体编写
    • runner运行功能
      • 资源设置
      • comm/util.hpp 运行时的临时文件
      • compile_server/runner.hpp 运行功能编写
    • compile_server/compile_run.hpp 编译且运行
      • comm/util.hpp 生成唯一文件名
      • comm/uti.hpp 写入文件/读出文件
      • 清理临时文件
      • compiler_run模块的整体代码
      • 本地进行编译运行模块的整体测试
    • compiler_server模块(打包网络服务)
      • compiler_server/compile_server.cc
  • oj_server模块
    • oj_server.cc 路由框架
    • oj_model.hpp/oj_model2.hpp
      • 文件版本
      • 数据库版本:
    • oj_view.hpp
    • oj_control.cpp

项目介绍

项目是基于负载均衡的一个在线判题系统,用户自己编写代码,提交给后台,后台再根据负载情况选择合适的主机提供服务编译运行服务。

所用技术与开发环境

所用技术

  • C++ STL 标准库
  • Boost 准标准库(字符串切割)
  • cpp-httplib 第三方开源网络库
  • ctemplate 第三方开源前端网页渲染库
  • jsoncpp 第三方开源序列化、反序列化库
  • 负载均衡设计
  • 多进程、多线程
  • MySQL C connect
  • Ace前端在线编辑器
  • html/css/js/jquery/ajax

开发环境

  • Centos 7 云服务器
  • vscode

项目框架

在这里插入图片描述

compiler_server模块

模块结构
在这里插入图片描述

总体流程图
在这里插入图片描述

compiler编译功能

  • 在运行编译服务的时候,compiler收到来自oj_server传来的代码;我们对其进行编译
  • 在编译前,我们需要一个code.cpp形式的文件;
  • 在编译后我们会形成code.exe可执行程序,若编译失败还会形成code.error来保存错误信息;
  • 因此,我们需要对这些文件的后缀进行添加,所以我们创建temp文件夹,该文件夹用来保存code代码的各种后缀;
  • 所以在传给编译服务的时候只需要传文件名即可,拼接路径由comm公共模块下的util.hpp提供路径拼接

comm/util.hpp 编译时的临时文件

#pragma once#include <iostream>#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>namespace ns_util
{const std::string path = "./temp/";// 合并路径类class PathUtil{public:static std::string splic(const std::string &str1, const std::string &str2){return path + str1 + str2;}// cpp文件 + 后缀名// file_name -> ./temp/xxx.cppstatic std::string Src(const std::string &file_name){return splic(file_name, ".cpp");}// exe文件 + 后缀名static std::string Exe(const std::string &file_name){return splic(file_name, ".exe");}static std::string CompilerError(const std::string &file_name){return splic(file_name, ".compile_error");}};   
}

comm/log.hpp 日志

日志需要输出:等级、文件名、行数、信息、时间

#pragma once#include <string>
#include "util.hpp"namespace ns_log
{using namespace ns_util;// 日志等级enum{INFO,DEBUG,WARNING,ERROR,FATAL,};inline std::ostream& Log(const std::string& level, const std::string& file_name, int line){std::string log = "[";log += level;log += "]";log += "[";log += file_name;log += "]";log += "[";log += std::to_string(line);log += "]";  log += "[";log += TimeUtil::GetTimeStamp();log += "]";  std::cout << log;return std::cout;}#define LOG(level) Log(#level, __FILE__, __LINE__)
} 

获取时间利用的是时间戳,在util工具类中编写获取时间戳的代码。利用操作系统接口:gettimeofday

comm/util.hpp 时间戳

class TimeUtil
{
public:static std::string GetTimeStamp(){struct timeval _t;gettimeofday(&_t, nullptr);return std::to_string(_t.tv_sec);}
};    

进行编译服务的编写,根据传入的源程序文件名,子进程对stderr进行重定向到文件compile_error中,使用execlp进行程序替换,父进程在外面等待子进程结果,等待成功后根据是否生成可执行程序决定是否编译成功;

判断可执行程序是否生成,我们利用系统调用stat来查看文件属性,如果有,则说明生成,否则失败;

comm/util.hpp 检查文件是否存在

class FileUtil
{
public:static bool IsFileExists(const std::string path_name){// 系统调用 stat 查看文件属性// 获取属性成功返回 0struct stat st;if (stat(path_name.c_str(), &st) == 0){return true;}return false;}
};

compile_server/compiler.hpp 编译功能总体编写

#pragma once#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>#include "../comm/util.hpp"
#include "../comm/log.hpp"// 只负责代码的编译
namespace ns_compiler
{// 引入路径拼接using namespace ns_util;using namespace ns_log;class Compiler{Compiler() {}~Compiler() {}public:// 返回值:是否编译成功// file_name :  xxx// file_name -> ./temp/xxx.cpp// file_name -> ./temp/xxx.exe// file_name -> ./temp/xxx.errorstatic bool Compile(const std::string &file_name){pid_t id = fork();if (id < 0){LOG(ERROR) << "内部错误,当前子进程无法创建" << "\n";return false;}else if (id == 0) // 子进程 编译程序{int _error = open(PathUtil::Error(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_error < 0){LOG(WARNING) << "没有成功形成 error 文件" << "\n";exit(1);}// 重定向标准错误到 _errordup2(_error, 2);// g++ -o target src -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), "-std=c++11", nullptr);LOG(ERROR) << "g++执行失败,检查参数是否传递正确" << "\n";exit(2);}else // 父进程 判断编译是否成功{waitpid(id, nullptr, 0);if (FileUtil::IsFileExists(PathUtil::Exe(file_name))){LOG(INFO) << PathUtil::Exe(file_name) << "编译成功!" << "\n";return true;}LOG(ERROR) << "编译失败!" << "\n";return false;}}};}

runner运行功能

编译完成后,我们就可以执行可执行程序了,执行前,首先打开三个文件xxx.stdin,xxx.stdout,xxx.stderr并将标准输入、标准输出和标准错误分别重定向到三个文件中。创建子进程来进行程序替换执行程序;每道题的代码运行时间和内存大小都有限制,所以在执行可执行程序之前我们对内存和时间进行限制。

资源设置

利用setrlimit系统调用来实现

int setrlimit(int resource, const struct rlimit *rlim);
        static void SetProcLimit(int cpu_limit, int mem_limit){struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur = cpu_limit;cpu_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU, &cpu_rlimit);struct rlimit mem_rlimit;mem_rlimit.rlim_cur = mem_limit * 1024;mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mem_rlimit);}

comm/util.hpp 运行时的临时文件

        static std::string Stdin(const std::string &file_name){return splic(file_name, ".stdin");}static std::string Stdout(const std::string &file_name){return splic(file_name, ".stdout");}// error文件 + 后缀名static std::string Stderr(const std::string &file_name){return splic(file_name, ".stderr");}

compile_server/runner.hpp 运行功能编写

#pragma once
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/resource.h>#include "../comm/util.hpp"
#include "../comm/log.hpp"namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner() {}~Runner() {}static void SetProcLimit(int cpu_limit, int mem_limit){struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur = cpu_limit;cpu_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU, &cpu_rlimit);struct rlimit mem_rlimit;mem_rlimit.rlim_cur = mem_limit * 1024;mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mem_rlimit);}// 指明文件名即可,无后缀、无路径// 返回值: // < 0 内部错误 // = 0运行成功,成功写入stdout等文件 // > 0运行中断,用户代码存在问题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 id =  fork();if(id < 0){ close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);LOG(ERROR) << "内部错误, 创建子进程失败" << "\n";return -2;}else if(id == 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(id, &status, 0);LOG(INFO) << "运行完毕!退出码为: " << (status & 0x7F) << "\n";return status & 0x7f;}}};
}

compile_server/compile_run.hpp 编译且运行

  • 用户的代码会以json串的方式传给该模块
  • 给每一份代码创建一个文件名具有唯一性的源文件
  • 调用上面的编译和运行执行该源文件
  • 再把结果构建成json串返回给上层

json串的结构

在这里插入图片描述

comm/util.hpp 生成唯一文件名

当一份用户提交代码后,我们为其生成的源文件名需要具有唯一性。名字生成唯一性我们可以利用毫秒级时间戳加上原子性的增长计数实现

获取毫秒时间戳在TimeUtil工具类中,生成唯一文件名在FileUtil工具类中

        static std::string GetTimeMs(){struct timeval _time;gettimeofday(&_time, nullptr);return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);}

comm/uti.hpp 写入文件/读出文件

因为需要填写运行成功结果和运行时报错的结果,所以我们写一个写入文件和读出文件,放在FileUtil

static bool WriteFile(const std::string &target, const std::string &content){std::ofstream out(target);if (!out.is_open()){return false;}out.write(content.c_str(), content.size());out.close();return true;}// 根据路径文件进行读出// 注意,默认每行的\\n是不进行保存的,需要保存请设置参数static bool ReadFile(const std::string &path_file, std::string *content, bool keep = false){// 利用C++的文件流进行简单的操作std::string line;std::ifstream in(path_file);if (!in.is_open())return "";while (std::getline(in, line)){(*content) += line;if (keep)(*content) += "\n";}in.close();return true;}

清理临时文件

编译还是运行都会生成临时文件,所以可以在编译运行的最后清理一下这一次服务生成的临时文件

static void RemoveTempFile(const std::string &file_name){// 因为临时文件的存在情况存在多种,删除文件采用系统接口unlink,但是需要判断std::string src_path = PathUtil::Src(file_name);if (FileUtil::IsFileExists(src_path))unlink(src_path.c_str());std::string stdout_path = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(stdout_path))unlink(stdout_path.c_str());std::string stdin_path = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(stdin_path))unlink(stdin_path.c_str());std::string stderr_path = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(stderr_path))unlink(stderr_path.c_str());std::string compilererr_path = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(compilererr_path))unlink(compilererr_path.c_str());std::string exe_path = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(exe_path))unlink(exe_path.c_str());}

提供一个Start方法让上层调用编译运行模块,参数是一个输入形式的json串和一个要给上层返回的json

使用jsoncpp反序列化,解析输入的json串。调用形成唯一文件名的方法生成一个唯一的文件名,然后使用解析出来的代码部分创建出一个源文件,把文件名交给编译模块进行编译,再把文件名和时间限制,内存限制传给运行模块运行,记录这个过程中的状态码。再最后还要序列化一个json串返还给用户,更具获得状态码含义的接口填写状态码含义,根据状态码判断是否需要填写运行成功结果和运行时报错的结果,然后把填好的结果返还给上层。

最终调用一次清理临时文件接口把这一次服务生成的所有临时文件清空即可。

两个json的具体内容
在这里插入图片描述

compiler_run模块的整体代码

#pragma once
#include <jsoncpp/json/json.h>#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/util.hpp"
#include "../comm/log.hpp"namespace ns_complie_and_run
{using namespace ns_log;using namespace ns_util;using namespace ns_compiler;using namespace ns_runner;class ComplieAndRun{public:static void RemoveTempFile(const std::string &file_name){// 因为临时文件的存在情况存在多种,删除文件采用系统接口unlink,但是需要判断std::string src_path = PathUtil::Src(file_name);if (FileUtil::IsFileExists(src_path))unlink(src_path.c_str());std::string stdout_path = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(stdout_path))unlink(stdout_path.c_str());std::string stdin_path = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(stdin_path))unlink(stdin_path.c_str());std::string stderr_path = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(stderr_path))unlink(stderr_path.c_str());std::string compilererr_path = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(compilererr_path))unlink(compilererr_path.c_str());std::string exe_path = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(exe_path))unlink(exe_path.c_str());}// > 0:进程收到信号导致异常崩溃// < 0:整个过程非运行报错// = 0:整个过程全部完成static std::string CodeToDesc(int status, const std::string &file_name){std::string desc;switch (status){case 0:desc = "运行成功!";break;case -1:desc = "代码为空";break;case -2:desc = "未知错误";break;case -3:desc = "编译报错\n";FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);break;case 6:desc = "内存超过范围";break;case 24:desc = "时间超时";break;case 8:desc = "浮点数溢出";break;case 11:desc = "野指针错误";break;default:desc = "未处理的报错-status为:" + std::to_string(status);break;}return desc;}/*输入:code: 用户提交的代码input: 用户提交的代码对应的输入cpu_limit:mem_limit:输出:必有,status: 状态码reason: 请求结果可能有,stdout: 运行完的结果stderr: 运行完的错误*/static void Start(const std::string &in_json, std::string *out_json){Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value, 1);std::string code = in_value["code"].asString();std::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;std::string file_name;if (!code.size()){status_code = -1; // 代码为空goto END;}// 毫秒级时间戳 + 原子性递增唯一值:来保证唯一性file_name = FileUtil::UniqFileName();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; // 崩溃elsestatus_code = 0;END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name);if (status_code == 0){// 整个过程全部成功std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;std::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);}};
}

本地进行编译运行模块的整体测试

自己手动构造一个json串,编译、运行、返回结果json串

#include "compile_run.hpp"using namespace ns_complie_and_run;// 编译服务会被同时请求,保证代码的唯一性
int main()
{// 客户端请求jsonstd::string in_json;Json::Value in_value;in_value["code"] = R"(  #include<iostream>int main() {std::cout << "Hello, world!" << std::endl;int *p = new int[1024 * 1024 * 20 ];return 0;})";in_value["input"] = "";       in_value["cpu_limit"] = 1;  in_value["mem_limit"] = 10240;   Json::FastWriter writer;in_json = writer.write(in_value);std::cout << "in_json: " << std::endl << in_json << std::endl;std::string out_json;ComplieAndRun::Start(in_json, &out_json);std::cout << "out_json: " << std::endl << out_json << std::endl;return 0;
}

compiler_server模块(打包网络服务)

编译运行服务已经整合在一起了,接下来将其打包成网络服务即可
我们利用httplib库将compile_run打包为一个网络编译运行服务

compiler_server/compile_server.cc

  • 使用了 httplib 库来提供 HTTP 服务
  • 实现了一个编译运行服务器
  • 通过命令行参数接收端口号
  • 一个POST /compile_and_run主要的编译运行接口
  • 接收JSON格式的请求体,包含:代码内容、输入数据、CPU 限制、内存限制
#include "compile_run.hpp"
#include "../comm/httplib.h"using namespace ns_compile_and_run;
using namespace httplib;void Usage(std::string proc)
{std::cerr << "Usage : " << "\n\t" << proc << "prot" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);return 1;}Server svr;svr.Post("/compile_and_run", [](const Request &req, Response &resp){std::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])); // 启动 http 服务return 0;
}

oj_server模块

oj_server.cc 路由框架

步骤:

  • 服务器初始化:
  1. 创建 HTTP 服务器实例
  2. 初始化控制器
  3. 设置信号处理函数
  • 请求处理:
  1. 接收 HTTP 请求
  2. 根据 URL 路由到对应处理函数
  3. 调用控制器相应方法
  4. 返回处理结果
  • 判题流程:
  1. 接收用户提交的代码
  2. 通过控制器进行判题
  3. 返回判题结果

  • 创建一个服务器对象
int main()
{Server svr;  // 服务器对象
}
  • 获取所有题目列表
    返回所有题目的HTML页面
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){std::string html;ctrl.AllQuestions(&html);resp.set_content(html, "text/html; charset=utf-8");
});
  • 获取单个题目
    返回单个题目的详细信息页面
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){std::string number = req.matches[1];std::string html;ctrl.Question(number, &html);resp.set_content(html, "text/html; charset=utf-8");
});
  • 提交代码判题
    处理用户提交的代码
    返回 JSON 格式的判题结果
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){std::string number = req.matches[1];std::string result_json;ctrl.Judge(number, req.body, &result_json);resp.set_content(result_json, "application/json;charset=utf-8");
});
  • 服务器配置和启动
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
  • 维护一个全局控制器指针
    Recovery 函数处理 SIGQUIT 信号,用于服务器恢复
static Control *ctrl_ptr = nullptr;void Recovery(int signo)
{ctrl_ptr->RecoveryMachine();
}

oj_model.hpp/oj_model2.hpp

整体架构为MVC模式

Model层 由oj_model.hpp文件版本和 oj_model2.hpp数据库版本构成;

负责数据的存储和访问,提供了两种实现方式

  1. 基础数据结构设计
struct Question {string number;     // 题目编号string title;      // 题目标题string star;       // 难度等级int cpu_limit;     // CPU时间限制(秒)int mem_limit;     // 内存限制(KB)string desc;       // 题目描述string header;     // 用户代码模板string tail;       // 测试用例代码
};
  1. 存储方案设计

文件版本

优势:
简单直观,易于管理
适合小规模题库
方便备份和版本控制
劣势:
并发性能较差
扩展性有限
数据一致性难保证

目录结构

./questions/├── questions.list    # 题目基本信息└── 1/                # 每个题目独立目录├── desc.txt      # 题目描述├── header.cpp    # 代码模板└── tail.cpp      # 测试用例

具体代码

#pragma once#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>// 根据题目 list 文件,加载所有题目的信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number;        // 题目编号string title;         // 题目的标题string star;          // 题目的难度int cpu_limit;        // 题目的时间要求(s)int mem_limit;        // 题目的空间要求(KB)string desc;     // 题目的描述string header;   // 题目给用户的部分代码string tail;     // 题目的测试用例,和 header 形成完整代码提交给后端编译};const string questions_list = "./questions/questions.list" ;const string question_path = "./questions/" ;class Model{private:// 【题号 < - > 题目细节】unordered_map<string, Question> questions;public:Model(){assert(LoadQuestionList(questions_list));}bool LoadQuestionList(const string &question_list){// 加载配置文件 : questions/questions.list + 题目编号文件ifstream in(question_list);if(!in.is_open()) {LOG(FATAL) << "题目加载失败!请检查是否存在题库文件" << std::endl;return false;}std::string line;while(getline(in, line)){   vector<string> tokens;StringUtil::SplitString(line, &tokens, " ");if(tokens.size() != 5){LOG(WARNING) << "加载部分题目失败!请检查文件格式" << std::endl;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());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) << "加载题目成功!" << std::endl;in.close();return true;}bool GetAllQuestions(vector<Question> *out){if(questions.size() == 0) {LOG(ERROR) << "用户获取题库失败!" << std::endl;return false;}for(const auto &q : questions)out->push_back(q.second);return true;}bool GetOneQuestion(const string &number, Question *q){const auto& iter = questions.find(number);if(iter == questions.end()) {LOG(ERROR) << "用户获取题库失败!题目编号为:" << number << std::endl;return false;}(*q) = iter->second;return true;}~Model(){}};
}

数据库版本:

优势:
更好的并发性能
事务支持,保证数据一致性

表设计

CREATE TABLE oj_questions (number VARCHAR(20) PRIMARY KEY,title VARCHAR(255) NOT NULL,star VARCHAR(20) NOT NULL,description TEXT,header TEXT,tail TEXT,cpu_limit INT,mem_limit INT
);
#pragma once#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>
#include "include/mysql.h"// 根据题目 list 文件,加载所有题目的信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number; // 题目编号string title;  // 题目的标题string star;   // 题目的难度string desc;   // 题目的描述string header; // 题目给用户的部分代码string tail;   // 题目的测试用例,和 header 形成完整代码提交给后端编译int cpu_limit; // 题目的时间要求(s)int mem_limit; // 题目的空间要求(K)};const std::string oj_question = "***";const std::string host = "***";const std::string user = "***";const std::string passwd = "***";const std::string db = "***";const int port = 3306;class Model{public:Model(){}bool QueryMysql(const std::string &sql, vector<Question> *out){       // 创建MySQL句柄MYSQL* my = mysql_init(nullptr);// 连接数据库//if(nullptr == mysql_real_connect(&my, host.c_str(), user.c_str(), db.c_str(), passwd.c_str(), port, nullptr, 0))if(nullptr == mysql_real_connect(my, host.c_str(), user.c_str(),  passwd.c_str(),db.c_str(), port, nullptr, 0)){std::cout << mysql_error(my) << std::endl;LOG(FATAL) << "连接数据库失败!!!" << "\n";return false;}LOG(INFO) << "连接数据库成功!!!" << "\n";// 设置链接的编码格式,默认是拉丁的mysql_set_character_set(my, "utf8");// 执行sql语句//if(0 != mysql_query(&my, sql.c_str()))if(0 != mysql_query(my, sql.c_str())){LOG(WARNING) << sql << " execute error!" << "\n";return false;} // 提取结果//MYSQL_RES *res = mysql_store_result(&my);MYSQL_RES *res = mysql_store_result(my);// 分析结果int rows = mysql_num_rows(res);// 获得行数int cols = mysql_num_fields(res);// 获得列数struct 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);mysql_close(my);return true;}bool GetAllQuestions(vector<Question> *out){std::string sql = "select * from oj.";sql += oj_question;return QueryMysql(sql, out);}bool GetOneQuestion(const string &number, Question *q){bool res = false;std::string sql = "select * from oj.";sql += oj_question;sql += " where number = ";sql += number;vector<Question> result;if (QueryMysql(sql, &result)){if (result.size() == 1){*q = result[0];res = true;}}return res;}~Model(){}};
}
  1. 接口设计
    返回bool表示操作成功与否
class Model {
public:// 获取所有题目bool GetAllQuestions(vector<Question> *out);// 获取单个题目bool GetOneQuestion(const string &number, Question *q);
};

oj_view.hpp

View层 由oj_view.hpp构成;
使用 ctemplate库来进行 HTML模板渲染

  • 使用 TemplateDictionary存储渲染数据
  • 使用 Template::GetTemplate加载模板
  • 使用 Expand方法进行渲染
  • 获取所有题目的渲染;
void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
  1. 设置模板文件路径 (all_questions.html)
  2. 创建模板字典
  3. 遍历所有题目,为每个题目添加:
  • 题号 (number)
  • 标题 (title)
  • 难度等级 (star)
  1. 渲染模板
  • 获取单个题目的渲染;
void OneExpandHtml(const struct Question &q, std::string *html)
  1. 设置模板文件路径 (one_question.html)
  2. 创建模板字典并设置值:
  • 题号 (number)
  • 标题 (title)
  • 难度等级 (star)
  • 题目描述 (desc)
  • 预设代码 (header)
  1. 渲染模板
#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/";class View{public:View() {};~View() {};public:void AllExpandHtml(const vector<struct Question> &questions, std::string *html){// 题目的编号 题目的标题 题目的难度// 推荐使用表格显示// 1. 形成路径std::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, std::string *html){// 1. 形成路径std::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);}};
}

oj_control.cpp

Controller层 由oj_control.hpp构成;

  • 提供服务的主机 Machine 类
    表示提供编译服务的主机
    包含 IP、端口、负载信息
    提供负载管理方法(增加、减少、重置、获取负载)
    // 提供服务的主机class Machine{public:std::string ip;int port;uint64_t load;std::mutex *mtx;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;}};
  • LoadBlance 类 (负载均衡模块)
    管理多台编译服务器
    维护在线/离线主机列表
    主要功能:
    从配置文件加载主机信息
    智能选择负载最低的主机
    处理主机上线/离线
// 负载均衡模块class LoadBlance{private:// 提供编译的主机// 每一台都有自己下标std::vector<Machine> machines;// 所有在线的主机 idstd::vector<int> online;// 所有离线的主机 idstd::vector<int> offline;// 保证 LoadBlance 的数据安全std::mutex mtx;public:LoadBlance(){assert(LoadConf(machine_path));LOG(INFO) << "加载" << machine_path << "成功" << "\n";}~LoadBlance(){}public:bool LoadConf(const std::string &machine_list){std::ifstream in(machine_list);if (!in.is_open()){LOG(FATAL) << "主机加载失败" << "\n";return false;}std::string line;while (std::getline(in, line)){std::vector<std::string> tokens;StringUtil::SplitString(line, &tokens, " ");if (tokens.size() != 2){LOG(WARNING) << "切分 " << line << "失败" << "\n";continue;}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. 随机数法// 2. 轮询 + hashint online_num = online.size();if (online_num == 0){LOG(FATAL) << "所有的主机挂掉!在线主机数量: " << online_num << ", 离线主机数量: " << offline.size() << "\n";mtx.unlock();return false;}// 找负载最小的主机*id = online[0];*m = &machines[online[0]];uint64_t min_load = machines[online[0]].Load();for (int i = 0; i < online_num; i++){uint64_t cur_load = machines[online[i]].Load();if (cur_load < min_load){min_load = cur_load;*id = online[i];*m = &machines[online[i]];}}mtx.unlock();return true;}void OfflineMachine(int id){mtx.lock();for(auto it = online.begin(); it != online.end(); it++){if(*it == id){machines[id].ResetLoad();// 离线主机已经找到online.erase(it);offline.push_back(*it);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";}void ShowMachine(){mtx.lock();std::cout << "在线主机列表: " << "\n";for(auto &it : online){std::cout << it << " ";}std::cout << std::endl;std::cout << "离线主机列表: " << "\n";for(auto &it : offline){std::cout << it << " ";}std::cout << std::endl;mtx.unlock();}};
  • Control 类 (核心控制器)
    整合 Model(数据层)和 View(视图层)
    判题
// 控制器class Control{private:Model _model; // 提供后台数据View _view;   // 提供网页渲染LoadBlance _load_blance;public:Control(){}~Control(){}public:void RecoveryMachine(){_load_blance.OnlineMachine();}// 根据题目数据构建网页// html 输出型参数bool AllQuestions(string *html){bool ret = true;vector<struct Question> all;if (_model.GetAllQuestions(&all)){sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){return q1.number < q2.number;});_view.AllExpandHtml(all, html);}else{*html = "获取题目失败,形成题目列表失败";ret = false;}return ret;}bool Question(const std::string number, string *html){bool ret = true;struct Question q;if (_model.GetOneQuestion(number, &q)){_view.OneExpandHtml(q, html);}else{*html = "指定题目:" + number + "不存在";ret = false;}return ret;}void Judge(const std::string &number, const std::string in_json, std::string *out_json){// 0.根据题目编号拿到题目细节struct 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 + "\n" + 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.选择负载最低的主机while (true){Machine *m = nullptr;int id = 0;if (!_load_blance.SmartChoice(&id, &m)){break;}LOG(INFO) << "选择主机成功, id = " << id << "详情:" << m->ip << ":" << m->port << "\n";// 4.发起 http 请求,得到结果Client cli(m->ip, m->port);m->IncLoad();if(auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")){// 5.将结果赋值给 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.ShowMachine(); // for test}}}};

首先

  • AllQuestions(): 获取并展示所有题目列表
  • Question(): 获取并展示单个题目详情

其次Judge

1. 获取题目信息
2. 解析用户提交的代码
3. 组装完整的测试代码
4. 选择负载最低的编译主机
5. 发送HTTP请求到编译主机
6. 处理编译运行结果

然后负载均衡处理使用最小负载优先算法
在这里插入图片描述

基本编译运行提交代码已经实现,后续还会增加其他功能

相关文章:

负载均衡式在线OJ

文章目录 项目介绍所用技术与开发环境所用技术开发环境 项目框架compiler_server模块compiler编译功能comm/util.hpp 编译时的临时文件comm/log.hpp 日志comm/util.hpp 时间戳comm/util.hpp 检查文件是否存在compile_server/compiler.hpp 编译功能总体编写 runner运行功能资源设…...

【3D打印机】启庞KP3S热床加热失败报错err6

最近天冷&#xff0c;打印机预热突然失败&#xff0c;热床无法加热&#xff0c;过了一段时间报错err6&#xff0c;查看另一篇资料说是天气冷原因&#xff0c;导致代码的PID控温部分达不到预期加热效果&#xff0c;从而自检报错&#xff0c;然后资料通过修改3D打印机代码的方式进…...

基于 MATLAB 的图像增强技术分享

一、引言 图像增强是数字图像处理中的重要环节&#xff0c;其目的在于改善图像的视觉效果&#xff0c;使图像更清晰、细节更丰富、对比度更高&#xff0c;以便于后续的分析、识别与理解等任务。MATLAB 作为一款功能强大的科学计算软件&#xff0c;提供了丰富的图像处理工具和函…...

前端知识补充—HTML

1. HTML 1.1 什么是HTML HTML(Hyper Text Markup Language), 超⽂本标记语⾔ 超⽂本: ⽐⽂本要强⼤. 通过链接和交互式⽅式来组织和呈现信息的⽂本形式. 不仅仅有⽂本, 还可能包含图⽚, ⾳频, 或者⾃已经审阅过它的学者所加的评注、补充或脚注等等 标记语⾔: 由标签构成的语⾔…...

安卓从Excel文件导入数据到SQLite数据库的实现

在现代的移动应用开发中&#xff0c;数据的处理和管理是至关重要的一环。有时候&#xff0c;我们需要从外部文件&#xff08;如Excel文件&#xff09;中导入数据&#xff0c;以便在应用程序中使用。本文将介绍如何在Android应用中使用Java代码从一个Excel文件中导入数据到SQLit…...

C/C++基础知识复习(44)

1) C 中多态性在实际项目中的应用场景 多态性是面向对象编程&#xff08;OOP&#xff09;中的一个重要特性&#xff0c;指的是不同的对象可以通过相同的接口来表现不同的行为。在 C 中&#xff0c;多态通常通过虚函数&#xff08;virtual&#xff09;和继承机制来实现。实际项…...

【day13】深入面向对象编程

【day12】回顾 在正文开始之前&#xff0c;先让我们回顾一下【day12】中的关键内容&#xff1a; 接口&#xff08;Interface&#xff09;&#xff1a; interface关键字用于定义接口。implements关键字用于实现接口。 接口成员&#xff1a; 抽象方法&#xff1a;需要在实现类中…...

《 火星人 》

题目描述 人类终于登上了火星的土地并且见到了神秘的火星人。人类和火星人都无法理解对方的语言&#xff0c;但是我们的科学家发明了一种用数字交流的方法。这种交流方法是这样的&#xff0c;首先&#xff0c;火星人把一个非常大的数字告诉人类科学家&#xff0c;科学家破解这…...

盒子模型(内边距的设置)

所有元素都可以设置内边距属性和外边距属性大体相同&#xff0c;可参考上一篇&#xff0c;但有区别 内边距不能设置为负值padding-方向&#xff1a;尺寸 注意&#xff1a;使用内边距padding之后元素整体会变大&#xff0c;因为他是直接加上了内边距的大小&#xff0c;不改变元素…...

CentOS7网络配置,解决不能联网、ping不通外网、主机的问题

1. 重置 关闭Centos系统 编辑->虚拟网络编辑器 还原默认设置 2. 记录基本信息 查看网关地址,并记录在小本本上 查看网段,记录下 3. 修改网卡配置 启动Centos系统 非root用户,切换root su root查看Mac地址 ifconfig 或 ip addr记录下来 修改配置文件 vim /et…...

如何测继电器是否正常

继电器是一种电控制器件&#xff0c;广泛应用于自动控制、电力保护等领域。为了确保继电器的正常工作&#xff0c;定期检测其状态是非常必要的。以下是一些常用的方法来测试继电器是否正常工作&#xff1a; 1. 视觉检查&#xff1a; - 观察继电器的外观是否有损坏、变形或烧焦…...

最优二叉搜索树【东北大学oj数据结构10-4】C++

题面 最优二叉搜索树是由 n 个键和 n1 个虚拟键构造的二叉搜索树&#xff0c;以最小化搜索操作的成本期望值。 给定一个序列 Kk1​,k2​,...,kn​&#xff0c;其中 n 个不同的键按排序顺序 &#xff0c;我们希望构造一个二叉搜索树。 对于每个关键 ki​&#xff0c;我们有一个…...

ESP32应用开发-Webserver

文章目录 库调用实例实现思路技术要点 1. 前端涉及的文件需要包装再发送2. http-GET路由3. http-POST路由 开发环境&#xff1a;Arduino 库调用 #include <WebServer.h> #include <ArduinoJson.h> //IDE没有自带&#xff0c;需自行安装实例 WebServer server…...

【IMU:视觉惯性SLAM系统】

视觉惯性SLAM系统简介 相机&#xff08;单目/双目/RGBD)与IMU结合起来就是视觉惯性&#xff0c;通常以单目/双目IMU为主。 IMU里面有个小芯片可以测量角速度与加速度&#xff0c;可分为6轴(6个自由度)和9轴&#xff08;9个自由度&#xff09;IMU&#xff0c;具体的关于IMU的介…...

前端开发 之 12个鼠标交互特效下【附完整源码】

前端开发 之 12个鼠标交互特效下【附完整源码】 文章目录 前端开发 之 12个鼠标交互特效下【附完整源码】七&#xff1a;粒子烟花绽放特效1.效果展示2.HTML完整代码 八&#xff1a;彩球释放特效1.效果展示2.HTML完整代码 九&#xff1a;雨滴掉落特效1.效果展示2.HTML完整代码 十…...

Unity文件路径访问总结:从基础到高级的资源加载方法

在Unity开发中&#xff0c;文件路径的访问和资源加载是开发者经常需要处理的任务。无论是加载纹理、模型、音频&#xff0c;还是读取配置文件&#xff0c;正确地处理路径和资源加载是确保项目顺利运行的关键。本文将以Unity文件路径访问为主线&#xff0c;详细介绍Unity中常见的…...

AWS Transfer 系列:简化文件传输与管理的云服务

在数字化转型的今天&#xff0c;企业对文件传输、存储和管理的需求日益增长。尤其是对于需要大量数据交换的行业&#xff0c;如何高效、可靠地传输数据成为了一大挑战。为了解决这一难题&#xff0c;AWS 提供了一系列的文件传输服务&#xff0c;统称为 AWS Transfer 系列。这些…...

Jenkins Api Token 访问问题

curl --location http://192.168.18.202:8080/view/ChinaFish/job/Ali/buildWithParameters?token1142be281174ee8fdf58773dedcef7ea4c&DeployTypeUpdateConfig \ --header Authorization: •••••• \ --header Cookie: JSESSIONID.824aa9a5node01ojk9yhh3imc24duwy67…...

垂起固定翼无人机大面积森林草原巡检技术详解

垂起固定翼无人机大面积森林草原巡检技术是一种高效、精准的监测手段&#xff0c;以下是对该技术的详细解析&#xff1a; 一、垂起固定翼无人机技术特点 垂起固定翼无人机结合了多旋翼和固定翼无人机的优点&#xff0c;具备垂直起降、飞行距离长、速度快、高度高等特点。这种无…...

【Leetcode 每日一题】1387. 将整数按权重排序

问题背景 我们将整数 x x x 的 权重 定义为按照下述规则将 x x x 变成 1 1 1 所需要的步数&#xff1a; 如果 x x x 是偶数&#xff0c;那么 x x / 2 x x / 2 xx/2。如果 x x x 是奇数&#xff0c;那么 x 3 x 1 x 3 \times x 1 x3x1。 比方说&#xff0c; x …...

科研笔记 KDD 2025

1 基本介绍 KDD 每年有多次投稿周期。KDD 2025 将有两个截止时间&#xff1a;分别是 2024 年 8 月 1 日和 2025 年 2 月 1 日&#xff08;全文提交截止时间在摘要提交截止后一周&#xff09;。 同时&#xff0c;KDD 会议论文集&#xff08;Proceedings&#xff09;将分两批出…...

黑马Java面试教程_P8_并发编程

系列博客目录 文章目录 系列博客目录前言1.线程的基础知识1.1 线程和进程的区别&#xff1f;难2频3面试文稿 1.2 并行和并发有什么区别&#xff1f; 难1频1面试文稿 1.3 创建线程的四种方式 难2频4面试文稿 1.4 runnable 和 callable 有什么区别 难2频3面试文稿 1.5 线程的 run…...

网络视频监控平台/安防监控/视频综合管理Liveweb视频汇聚平台解决方案

一、当前现状分析 当前视频资源面临以下问题&#xff1a; 1&#xff09;不同单位在视频平台建设中以所属领域为单位&#xff0c;设备品牌众多&#xff0c;存在的标准不一&#xff0c;各系统之间也没有统一标准&#xff1b; 2&#xff09;各单位视频平台建设分散、统筹性差&am…...

workman服务端开发模式-应用开发-后端api推送修改二

需要修改两个地方&#xff0c;第一个是总控制里面的续token延时&#xff0c;第二个是操作日志记录 一、总控续token延时方法 在根目录下app文件夹下controller文件夹下Base.php中修改isLoginAuth方法&#xff0c;具体代码如下&#xff1a; <?php /*** 总控制* User: 龙哥…...

SQL 使用带聚集函数的联结

聚集函数用于汇总数据&#xff0c;通常用于从一个表中计算统计信息&#xff0c;但也可以与联结一起使用。以下是一个例子&#xff0c;展示如何使用聚集函数统计每个顾客的订单数。 示例 1&#xff1a;使用 COUNT() 函数与 INNER JOIN 假设我们需要检索所有顾客及每个顾客所下…...

Restaurants WebAPI(三)——Serilog/FluenValidation

文章目录 项目地址一、Serilog使用1.1 安装 Serilog1.2 注册日志服务1.3 设置日志级别和详情1.4 配置到文件里1.5 给不同的环境配置日志1.5.1 配置appsettings.Development.json二、Swagger的使用三、自定义Exception中间件3.1 使用FluentValidation项目地址 教程作者:教程地址…...

概率论得学习和整理32: 用EXCEL描述正态分布,用δ求累计概率,以及已知概率求X的区间

目录 1 正态分布相关 2 正态分布的函数和曲线 2.1 正态分布的函数值&#xff0c;用norm.dist() 函数求 2.2 正态分布的pdf 和 cdf 2.3 正态分布的图形随着u 和 δ^2的变化 3 正态分布最重要的3δ原则 3.0 注意&#xff0c;这里说的概率一定是累计概率CDF&#xff0c;而…...

【原生js案例】让你的移动页面实现自定义的上拉加载和下拉刷新

目前很多前端UI都是自带有上拉加载和下拉刷新功能,按照官网配置去实现即可,比如原生小程序,vantUI等UI框架,都替我们实现了内部功能。 那如何自己来实现一个上拉加载和下拉刷新的功能? 实现效果 不用浏览器的css滚动条,自定义实现滚动效果 自定义实现滚动,添加上拉加载…...

【linux 常用命令】

1. 使用xshell 通过SSH连接到Linux服务器 ssh -p 端口号 usernameip地址2. 查看当前目录下的子文件夹的内存占用情况 du -a -h -d 1或者 du -ah -d 1-a &#xff1a;展示所有子文件夹&#xff08;包括隐藏文件夹&#xff09;&#xff0c;-h &#xff1a;以人类可读的形式&am…...

【JetPack】Room数据库笔记

Room数据库笔记 ORM框架&#xff1a;对齐数据库数据结构与面向对象数据结构之间的关系&#xff0c;使开发编程只考虑面向对象不需要考虑数据库的结构 Entity : 数据实体&#xff0c;对应数据库中的表 <完成面向对象与数据库表结构的映射> 注解&#xff1a; 类添加注解…...