负载均衡式在线OJ

news/2024/12/28 17:08:58/

文章目录

  • 项目介绍
  • 所用技术与开发环境
    • 所用技术
    • 开发环境
  • 项目框架
  • 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. 处理编译运行结果

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

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


http://www.ppmy.cn/news/1557772.html

相关文章

JavaScript文件端点提取与安全分析:两种高效实用的方法

提取JS文件中的所有端点(Endpoints) JavaScript文件中包含了大量的信息,对于安全研究人员来说,提取这些文件中的API端点是发现潜在漏洞的重要环节之一。在本篇文章中,我们将介绍两种高效提取JavaScript文件端点的方法。以下方法主要应用于渗透测试场景,尤其是针对目标域…

ISP算法之BNR降噪(Bayer域)

概述 BNR&#xff08;Bayer Noise Reduction&#xff09;即Bayer域降噪算法。对于噪声的分类如下表所示&#xff1a; 高斯噪声&#xff08; Gaussian&#xff09; 高斯噪声也被称为热噪声&#xff0c;通常是由于电路系统中自由电子的热运动&#xff0c;这种噪声幅度分布服从高…

springboot/ssm智能停车计费系统Java代码编写web停车场车位管理项目

springboot/ssm智能停车计费系统Java代码编写web停车场车位管理项目 基于springboot(可改ssm)vue项目 开发语言&#xff1a;Java 框架&#xff1a;springboot/可改ssm vue JDK版本&#xff1a;JDK1.8&#xff08;或11&#xff09; 服务器&#xff1a;tomcat 数据库&#x…

【数据库】大二数据库复习范围 (快速版)帮助你快速复习数据库

第一章 1. 信息=数据+语义 2:数据库管理系统(database management system, DBMS) 3. 数据库系统(database system, DBS)由数据库、数据库用户、计算机硬件系统和计算机软件系统等几部分组成 4. 数据模型按应用层次可分为概念模型、逻辑模型和物理模型。 5.每个二维表…

Tool之Excalidraw:Excalidraw(开源的虚拟手绘风格白板)的简介、安装和使用方法、艾米莉应用之详细攻略

Tool之Excalidraw&#xff1a;Excalidraw(开源的虚拟手绘风格白板)的简介、安装和使用方法、艾米莉应用之详细攻略 目录 Excalidraw 简介 1、Excalidraw 的主要特点&#xff1a; Excalidraw 安装和使用方法 1、Excalidraw的安装 T1、使用 npm 安装&#xff1a; T2、使用 …

virtualbox7 使用 自带的nat网络配置 解决虚机上网问题

virtualbox7 使用 自带的nat网络配置 解决虚机上网问题 单纯在虚机上设置&#xff0c;不知为啥啊nat不好使&#xff0c; 后来看了网上一篇文章&#xff0c;不在虚机上设置。 一、在virtualbox 管理器的 管理 ------>工具------> 网络管理器 ------>NAT网络 -----…

牛客--求小球落地5次后所经历的路程和第5次反弹的高度,称砝码

求小球落地5次后所经历的路程和第5次反弹的高度 描述 假设有一个小球从 hh 米高度自由落下&#xff0c;我们不考虑真实的物理模型&#xff0c;而是简洁的假定&#xff0c;该小球每次落地后会反弹回原高度的一半&#xff1b;再落下&#xff0c;再反弹&#xff1b;……。 求小球…

C语言初阶习题【14】数9的个数

1.编写程序数一下 1到 100 的所有整数中出现多少个数字9 2.思路 循环遍历1到100&#xff0c;需要判断每一位的个位数是否为9&#xff0c;十位数是否为9&#xff0c;每次符合条件就count进行计数&#xff0c;最后输出count&#xff0c;即可 3.code #define _CRT_SECURE_NO_W…