【项目设计】负载均衡式——Online Judge

负载均衡式——Online Judge😎

  • 前言🙌
  • Online Judge 项目
    • 一、项目介绍
    • 二、项目技术栈
    • 三、项目使用环境
    • 四、项目宏观框架
    • 五、项目后端服务实现过程
      • 1、comm模块设计
        • 1.1 Log.hpp实现
        • 1.2 Util.hpp实现
      • 2、compiler_server 模块设计
        • 2.1compile.hpp文件代码编写
        • 2.2 runner.hpp文件代码编写
        • 2.3 compile_runing.hpp文件代码编写
        • 2.4 compile_server.cc文件代码编写
      • 3、设计文件版题库
      • 4、oj_server 模块设计——基于MVC
        • 4.1 初步使用一下cpp-httplib。
        • 4.2 oj_model.hpp文件代码编写
        • 4.3 oj_control.hpp文件代码编写
        • 4.4 oj_view.hpp文件代码编写
        • 4.5 oj_server.cc文件代码编写
    • 六、项目前端页面实现过程
    • 七、 项目演示过程
    • 八、项目的扩展方向
  • 总结撒花💞

追梦之旅,你我同行

   
😎博客昵称:博客小梦
😊最喜欢的座右铭:全神贯注的上吧!!!
😊作者简介:一名热爱C/C++,算法等技术、喜爱运动、热爱K歌、敢于追梦的小博主!

😘博主小留言:哈喽!😄各位CSDN的uu们,我是你的博客好友小梦,希望我的文章可以给您带来一定的帮助,话不多说,文章推上!欢迎大家在评论区唠嗑指正,觉得好的话别忘了一键三连哦!😘
在这里插入图片描述

前言🙌

    哈喽各位友友们😊,我今天又学到了很多有趣的知识现在迫不及待的想和大家分享一下!😘我仅已此文,手把手带领大家实现负载均衡式——Online Judge 项目~ 都是精华内容,可不要错过哟!!!😍😍😍

Online Judge 项目

一、项目介绍

    在线OJ系统,是一个能够自动对用户提交的程序源代码进行编译、执行和评测的在线平台。Online Judge项目实现的主要意义是为了帮助我们更好地适应工作和学习中的编程需求。Online Judge系统具有丰富的题库和实时反馈功能,可以帮助我们更好地进行编程练习和测试。 此外,负载均衡在线oj系统还可以进行项目的扩展,具有方便的使用和活跃的社区互动等优势,可以提高我们的学习效果和团队协作能力。
    学习编程技术的小伙伴,我相信一定是做过在线OJ的。而本项目就是借鉴于LeetCode和牛客网等各大知名的刷题网站提供的在线OJ功能,实现自己的一个在线OJ系统。

二、项目技术栈

所⽤技术栈:

  • C++ STL 标准库
  • Boost 准标准库,应用于本项目实现字符串切割的功能需求。
  • cpp-httplib 第三⽅开源⽹络库
  • ctemplate 第三⽅开源前端⽹⻚渲染库
  • jsoncpp 第三⽅开源序列化、反序列化库
  • 负载均衡设计
  • 多进程、多线程
  • Ace前端在线编辑器(了解)
  • html/css/js/jquery/ajax (了解)

三、项目使用环境

  • Centos7云服务器
  • vscode

四、项目宏观框架

我们的项⽬核⼼是三个模块

  1. comm :公共模块
  2. compile_server :编译与运⾏模块
  3. oj_server :获取题⽬列表,查看题⽬编写题⽬界⾯,负载均衡,其他功能
    leetcode 结构:只实现类似 leetcode 的题⽬列表+在线编程功能
    在这里插入图片描述

五、项目后端服务实现过程

1、comm模块设计

我们将所有功能模块,所要使用的共性功能封装成comm模块,从而实现复用,提高项目整体的开发效率。

1.1 Log.hpp实现

Log.hpp实现的核心点分析:

  1. 在C语言和C++中,__ FILE __和 __ LINE __ 是两个非常有用的预定义宏(predefined macros)。它们主要用于调试目的,帮助开发者追踪错误发生的具体位置。 __ FILE __ 是一个字符串常量,表示当前源代码文件的名称。这个宏在编译时被自动替换为包含它的源文件名。 __ LINE __ 是一个十进制整型常量,表示当前源代码中的行号。这个宏在编译时自动替换为它在源代码中的行号。
  2. 单 # 作用:表示将该变量转换成对应的字符串表示。例如我们的enum菜单中,1 对应 DEBUG,则会将1 转化为DEBUG。
  3. #define 设计的一个LOG宏函数,当调用宏函数时,会在调用的地方进行替换成Log函数。

功能代码的整体实现:


namespace ns_log
{using namespace ns_util;// 日志等级enum{INFOR = 0,DEBUG,WARNING,ERROR,FATAL};inline std::ostream &Log(const std::string& level, const std::string& file, int line){// 日志等级信息std::string logmessage = "[";logmessage += level;logmessage += "]";// 日志出错在哪个文件名的信息logmessage += "[";logmessage += file;logmessage += "]";// 日志出错在哪一行的信息logmessage += "[";logmessage += std::to_string(line);logmessage += "]";// 日志打印的时间信息logmessage += "[";logmessage += TimeUtil::GetTimeStamp();logmessage += "]";//将上述信息加载到cout内部的缓冲区中std::cout << logmessage;return std::cout;}
// 单#:表示将该变量转换成对应的字符串表示
#define LOG(level) Log(#level, __FILE__, __LINE__)
}
1.2 Util.hpp实现

Util.hpp 实现的核心点分析:

  1. 获取时间信息方法:本项目使用了系统调用gettimeofday函数,来获取对应的时间信息。
    在这里插入图片描述
    参数类型的解析:
    在这里插入图片描述

  2. 路径 + 具体文件名字形成方法:编译时需要有的临时文件,构建源文件路径+后缀的完整文件名,例如,1234 -> . . /temp/1234.cpp

  3. 对于文件一些操作方法 :这里设计了判断一个可执行文件是否存在、形成唯一文件名、读文件、写文件方法。
    其中判断可执行文件是否存在,使用了系统调用接口stat。该接口可以获得一个文件的属性。我们都知道文件 = 内容 + 属性。如果属性存在,那么这个文件也是一定存在的。
    在这里插入图片描述
    形成唯一文件名,我们采用的是毫秒级时间戳 + 唯一的递增id相结合形成一个相对唯一的文件名。由于存在多个执行流进入该函数,造成多执行流并发访问的问题。我们使用atomic_uint 类型进行唯一的递增id的设计,保证它是唯一的。

  4. 字符串切割方法: 我们使用了Boost准标准库中的split函数接口实现了字符串切割。可以将字符串按照指定的分隔符拆分成多个子字符串,并将这些子字符串存储在指定的容器中。第一个参数是用于保存分割后结果的容器,通常使用 std::vector < std::string > 类型。第二个参数是需要被分割的原始字符串。第三个参数是分隔符,可以是一个字符或一组字符。这个参数决定了如何分割源字符串。boost::split 还提供了一个可选的第四个参数 boost::token_compress_on,当连续出现多个分隔符时,可以用来合并这些分隔符

功能代码的整体实现:


namespace ns_util
{class TimeUtil{public:// 获取1970年1月1日至今的秒数 --- 时间戳static std::string GetTimeStamp(){struct timeval Tvl;gettimeofday(&Tvl, nullptr);return std::to_string(Tvl.tv_sec);}static std::string GetMsLevelTimestamp(){struct timeval Tvl;gettimeofday(&Tvl, nullptr);return std::to_string(Tvl.tv_sec * 1000 + Tvl.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 pathname = temp_path;pathname += file_name;pathname += suffix;return pathname;}static 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_error");}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");}static std::string Stderr(const std::string &file_name){return AddSuffix(file_name, ".stderr");}};class FileUtil{public:static bool IsFileExists(const std::string &path_name){struct stat st;// stat:获得一个文件的属性  如果文件存在 ---- 属性也存在if (stat(path_name.c_str(), &st) == 0)return true;elsereturn false;}// 形成唯一的文件名static std::string UniqFileName(){static std::atomic_uint id(0);id++;std::string ms = TimeUtil::GetMsLevelTimestamp();std::string uniq_id = std::to_string(id);return ms + "_" + uniq_id;}static bool WriteFile(const std::string &path_name, const std::string &content){std::ofstream out(path_name);if (!out.is_open()){return false;}out.write(content.c_str(), content.size());out.close();return true;}static bool ReadFile(const std::string &path_name, std::string *content, bool keep = false){content->clear();std::ifstream in(path_name);if (!in.is_open()){return false;}std::string line;while (std::getline(in, line)){*(content) += line;*(content) += keep ? "\n" : "";}in.close();return true;}};class StringUtil{public:static void SpileString(const std::string &target, std::vector<std::string> *out, const std::string &sep){boost::split((*out), target, boost::is_any_of(sep), boost::algorithm::token_compress_on);}};
}

2、compiler_server 模块设计

在这里插入图片描述

提供的服务:编译并运⾏代码,得到格式化的相关的结果

2.1compile.hpp文件代码编写

compile.hpp代码编写重难点分析:

  1. 采用多进程系统编程技术:利用系统调用fork函数进行创建子进程,帮助我们完成编译服务,父进程等待子进程的退出并回收子进程的相关资源,并继续执行主程序判断子进程编译执行的结果。
    fork()函数的接口信息:
    在这里插入图片描述
  2. 由于子进程的pcb会继承父进程许多的信息,文件描述符也会被拷贝。因此我们可以在子进程中使用系统调用dup2接口,将stderr(2)进行重定向到我们指定的的临时文件中,这样就可以实现将编译错误信息记录在相应的临时文件中。
    在这里插入图片描述
  3. 如何让我们的子进程去进行编译程序呢?这里使用到了进程程序替换。本项目使用的是exelp接口。在这里插入图片描述
  4. 父进程使用waitpid方法,等待子进程的退出,并将他回收。这是必须要做的。如果父进程不等待子进程,会系统会产生僵尸进程,导致系统资源不断地变少。父进程调用Util.hpp中的文件操作方法,判断子进程编译运行的情况。
  5. 调用Log.hpp的LOG宏函数,打印日志信息,有助于查看程序运行的信息,排查错误。

功能代码的整体实现:


// 实现编译功能
namespace ns_compile
{using namespace ns_util;using namespace ns_log;class Compiler{public:Compiler(){}// 根据给的文件名进行编译static bool Compile(const std::string file_name){pid_t id = fork();if (id < 0){LOG(ERROR) << "内部错误,创建⼦进程失败" << "\n";return false;}else if (id == 0){// 创建编译错误文件,并进行重定向umask(0);int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_stderr < 0){LOG(WARNING) << "没有成功形成CompilerError⽂件" << "\n";exit(1);}LOG(INFOR) << "成功形成CompilerError⽂件" << "\n";dup2(_stderr, 2);// 子进程去进行完成编译工作// 使用exec进行进程程序替换,调用编译器// 编译的时候,我们会产生很多临时文件// 需要以nullptr结尾// 注意有些函数接口是需要c语言风格的字符串,string 进行转换。!!!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(-1);}else{// 父进程pid_t rid = waitpid(id, nullptr, 0);// 对于子进程是否完成编译成功进行判断。if (FileUtil::IsFileExists(PathUtil::Exe(file_name))){// 编译成功LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";return true;}}// 编译不成功LOG(ERROR) << "编译失败,没有形成可执⾏程序" << "\n";return false;}~Compiler(){}private:};}
2.2 runner.hpp文件代码编写

runner.hpp代码编写重难点分析:

1:程序运行

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

2:一个程序在默认启动的时候

  1. 标准输入: 我们这里不处理
  2. 标准输出: 程序运行完成,输出结果是什么
  3. 标准错误: 运行时错误信息

3:资源条件的约束
防止用户上传恶意代码,从而形成恶意可执行。我们需要对这个进行排查。

  1. 我们再刷LeetCode时,会发现每一道题目都有时间和空间的约束。
  2. 我们通过下面代码就能够对我们的运行程序进行资源的限制

使用setrlimit系统调用接口实现。
在这里插入图片描述
结构体的解析和内部属性。rlimit_cur:表示当前设置的资源数量,rlimit_max表示最大设置的资源数量。第二个参数是设置软限制和硬限制。非特权进程,os需要对其进行受限,我们在该项目编写时都是非特权的,设置rlim_max为无穷即可,rlim_cur设置为我们想要的值。

在这里插入图片描述
资源限制的代码设计:

		static void SetProcLimit(int _cpu_limit, int _mem_limit){// 设置CPU时长struct rlimit cpu_rlimit;cpu_rlimit.rlim_max = RLIM_INFINITY;cpu_rlimit.rlim_cur = _cpu_limit;setrlimit(RLIMIT_CPU, &cpu_rlimit);// 设置内存大小struct rlimit mem_rlimit;mem_rlimit.rlim_max = RLIM_INFINITY;mem_rlimit.rlim_cur = _mem_limit * 1024; //转化成为KBsetrlimit(RLIMIT_AS, &mem_rlimit);}

    我们这里只是从时间和内存这两个方面进行排查,也可以进行扩展,只是比较复杂。好的在线oj,因为每一个不同的题目要求不一样,因此,无法进行统一。

    Run需要考虑代码跑完,结果正确与否吗??我们无需考虑!结果正确与否:是由我们的测试用例决定的!我们只考虑:是否正确运行完毕

功能代码的整体实现:


namespace ns_runner
{using namespace ns_util;using namespace ns_log;class Runner{public:Runner() {}~Runner() {}public://提供设置进程占用资源大小的接口static void SetProcLimit(int _cpu_limit, int _mem_limit){// 设置CPU时长struct rlimit cpu_rlimit;cpu_rlimit.rlim_max = RLIM_INFINITY;cpu_rlimit.rlim_cur = _cpu_limit;setrlimit(RLIMIT_CPU, &cpu_rlimit);// 设置内存大小struct rlimit mem_rlimit;mem_rlimit.rlim_max = RLIM_INFINITY;mem_rlimit.rlim_cur = _mem_limit * 1024; //转化成为KBsetrlimit(RLIMIT_AS, &mem_rlimit);}static int Run(const std::string &file_name, int cpu_limit, int mem_limit){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);SetProcLimit(cpu_limit, mem_limit);execl(_execute.c_str(), _execute.c_str(), nullptr);exit(1);}else{close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);int status = 0;waitpid(pid, &status, 0);// 程序运行异常,一定是因为因为收到了信号!LOG(INFO) << "运行完毕, info: " << (status & 0x7F) << "\n"; return status & 0x7F;}}};
}
2.3 compile_runing.hpp文件代码编写

compile_runing.hpp代码编写重难点分析:

  1. 先做一个宏观上的设计:
    在这里插入图片描述
  2. 通过compile_runing.hpp模块的编写,使我们的编译和运行模块相结合起来。
  3. 通过传入的json字符串进行反序列化,初始化我们的json对象,然后获取需要的信息。形成源文件、对源文件编译形成可执行、对可执行文件运行。再把运行得到的信息填充到json对象中,经过序列化形成json字符串,然后通过out_json带出去。
  4. 每一次运行后成功后,都会产生很多的临时文件,我们可以设计一个函数,对这些生成的临时文件进行一个清理。

功能代码的整体实现:


namespace ns_compile_run
{using namespace ns_compile;using namespace ns_runner;class compile_run{public:compile_run(){};~compile_run(){};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 _exe = PathUtil::Exe(file_name);if(FileUtil::IsFileExists(_exe)) unlink(_exe.c_str());std::string _compileError = PathUtil::CompilerError(file_name);if(FileUtil::IsFileExists(_compileError)) unlink(_compileError.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 StatusToStr(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:FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);break;case SIGABRT: // 6desc = "内存超过范围";break;case SIGXCPU: // 24desc = "CPU使用超时";break;case SIGFPE: // 8desc = "浮点数溢出";break;default:desc = "未知: " + std::to_string(status);break;}return desc;}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);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 cr_status = 1;int c_status = 1;int r_status = 1;std::string file_name;int run_result;if (code.size() == 0){// 代码为空cr_status = -1;goto END;}file_name = FileUtil::UniqFileName();// 将获得的代码形成源文件Srcif (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){// 写入失败了,发生未知错误cr_status = -2;goto END;}// 写入成功// 对Src文件进行编译if (!Compiler::Compile(file_name)){// 编译失败cr_status = -3;goto END;}// 编译成功,形成可执行// 运行run_result = Runner::Run(file_name, cpu_limit, mem_limit);if (run_result > 0){// run失败(异常)cr_status = run_result;}else if (run_result < 0){// run发送内部错误,未知错误到导致cr_status = -2;}else{// run完毕cr_status = 0;}END:// 对每一个编译运行情况进行统一的差错处理!Json::Value out_value;out_value["status"] = cr_status;out_value["reason"] = StatusToStr(cr_status, file_name);if (cr_status == 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::FastWriter Write;*out_json = Write.write(out_value);RemoveTempFile(file_name);}};
}
2.4 compile_server.cc文件代码编写

各个文件间的调用逻辑:
在这里插入图片描述
compile_server.cc代码编写重难点分析:

  • 接⼊cpp-httplib:header-only,只需要将.h拷⻉到项⽬中,即可直接使⽤
  • cpp-httplib:需要使⽤⾼版本的gcc,建议是gcc 7,8,9 [如果没有升级,cpp-httplib:要么就是编译报错,要么就是运⾏出错]
  • cpp-httplib: 阻塞式多线程的⼀个⽹络http库
  • 测试的时候,可以采⽤postman进⾏测试
  • 我们使用的Post方法,将编译链接服务打包形成网络服务。我们使用lambda表达式。这样使得代码更加简洁。我们获取客户端的数据,并将运行的结果返回给客户。让我们的服务器一直处于监听状态,一旦访问,便可以进行服务。

功能代码的整体实现:

#include "compile_runing.hpp"
#include "../comm/httplib.h"
using namespace ns_compile_run;
using namespace httplib;
void Usage(std::string proc)
{std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);return 0;}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()){compile_run::Start(in_json, &out_json);resp.set_content(out_json, "application/json;charset=utf-8");}});svr.listen("0.0.0.0", atoi(argv[1]));
}

3、设计文件版题库

  1. 题⽬的编号
  2. 题⽬的标题
  3. 题⽬的难度
  4. 题⽬的描述,题⾯
  5. 时间要求(内部处理)
  6. 空间要求(内部处理)

两批⽂件构成

  1. 第⼀个:questions.list : 题⽬列表(不需要题⽬的内容)
  2. 第⼆个:题⽬的描述,题⽬的预设置代码(header.cpp), 测试⽤例代码(tail.cpp)

这两个内容是通过题⽬的编号,产⽣关联的。
当⽤⼾提交⾃⼰的代码的时候:header.cpp

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Solution{
public:bool isPalindrome(int x){//将你的代码写在下⾯//code//code//code//code//codereturn true;}
};

OJ不是只把上⾯的代码提交给compile_and_run, ⽽是要拼接上该题号对应的测试⽤例 : tail.cpp。

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Solution{
public:bool isPalindrome(int x){//将你的代码写在下⾯//code//code//code//code//codereturn true;}
};

最终提交给编译服务主机的代码是:

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
class Solution{
public:bool isPalindrome(int x){//将你的代码写在下⾯//code//code//code//code//codereturn true;}
};
// 下⾯的代码,仅仅是为了让我们设计测试⽤例的时候,不要报错。
// 我们不想让编译器编译的时候,保留它,⽽是裁剪掉(g++ -D COMPILER_ONLINE)
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endifvoid Test1()
{bool ret = Solution().PalindromeNumber(121);if (ret){cout << "通过用例1, 测试121通过 ... OK!" << endl;}else{std::cout << "没有通过用例1, 测试的值是: 121" << std::endl;}
}void Test2()
{bool ret = Solution().PalindromeNumber(-10);if (!ret){cout << "通过用例2, 测试-10通过 ... OK!" << endl;}else{std::cout << "没有通过用例2, 测试的值是: -10" << std::endl;}
}int main()
{Test1();Test2();
}

4、oj_server 模块设计——基于MVC

4.1 初步使用一下cpp-httplib。

功能代码的整体实现:

#include <iostream>
#include "../comm/httplib.h"
//进行服务路由功能
using namespace httplib;
int main()
{Server svr;//获取所有题目svr.Get("/all_questions",[](const Request& Req, Response& Resp){Resp.set_content("这是所有题目列表","text/plain;charset=utf-8");});//用户根据题号获取特定的一道题svr.Get(R"(/question/(\d+))",[](const Request& Req, Response& Resp){std::string number = Req.matches[1];Resp.set_content("这是指定的一道题: " + number,"text/plain;charset=utf-8");});//用户提交代码,使用我们的判题功能。svr.Get(R"(/judge/(\d+))",[](const Request& Req, Response& Resp){std::string number = Req.matches[1];Resp.set_content("这是指定题目的判题功能: " + number, "text/plain;charset=utf-8");});svr.set_base_dir("./wwwroot");svr.listen("0.0.0.0",8080);return 0;
}

同级目录下,创建wwwroot文件夹。里面存放首页网页。

功能代码的整体实现:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>这是题目列表</title>
</head>
<body></body>
</html>

效果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.2 oj_model.hpp文件代码编写

    compile_server.cc代码编写重难点分析:

  • oj_model.hpp文件主要是用于数据交互,我们的数据就是题目list的相关信息。
  • 首先就是将我们的题目列表的信息从文件加载到内存中,和读取每一道题目的题目描述、预设值函数头代码、对应的测试用例代码。形成每一个question。
    question的设计:
    在这里插入图片描述
  • 提供获取一个题目的问题细节方法和获取所有题目问题细节的方法。

功能代码的整体实现:

class Model{public:Model(){assert(LoadQuestionsList(questions_list));};~Model(){};bool LoadQuestionsList(const string& QuestionsList){//首先打开题目信息文件ifstream in(QuestionsList);if(!in.is_open()){//打开文件失败LOG(FATAL) << " 加载题库失败,请检查是否存在题库文件"  << "\n";return false;}//打开成功string line;while(getline(in,line)){vector<string> lines;// 1 判断回⽂数 简单 1 30000//对于获得每一行进行切割StringUtil::SpileString(line,&lines," ");if(lines.size() != 5){LOG(WARNING) << "加载部分题目失败,请检查文件格式" << "\n";}Question q;q.number = lines[0];q.title = lines[1];q.dif_level = lines[2];q.cpu_limit = stoi(lines[3]);q.mem_limit = stoi(lines[4]);std::string path = question_path;path += q.number;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(INFOR) << "加载题库...成功!" << "\n";in.close();return true;}bool GetAllQuestions(vector<Question>* out){if(questions.size() == 0){//没有题目数据/或者加载题目数据失败LOG(ERROR) << "用户获取题库失败" << "\n";   return false;}//有题目数据信息for(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 << "\n";return false;}*q = iter->second;return true;}private://题目:问题细节unordered_map<string, Question> questions;};
4.3 oj_control.hpp文件代码编写

oj_control.hpp代码编写重难点分析:

  1. 先描述在组织,我们对于每一台进行编译服务的主机进行描述。
  		string _ip;     // 提供编译服务主机的ipint _port;      // 提供编译服务主机的端口号uint64_t _load; // 主机的负载均衡系数mutex *_mtx;    // C++的mutex是禁止拷贝的,所以这里定义指针

    具体的方法实现:我们使用的网络库是基于多线程的,因此,当很多网络服务到来时,可能有多个执行流进入同一台主机,从而对于该主机的负载情况进行修改,因此必须加锁进行保护。

		// 获取主机的负载情况uint64_t Load(){uint64_t load = 0;if (_mtx)_mtx->lock();load = _load;if (_mtx)_mtx->unlock();return load;}// 增加主机的负载void IncLoad(){if (_mtx)_mtx->lock();_load++;if (_mtx)_mtx->unlock();}// 清除主机的负载void ClearLoad(){if (_mtx)_mtx->lock();_load = 0;if (_mtx)_mtx->unlock();}// 减少主机的负载void DecLoad(){if (_mtx)_mtx->lock();_load--;if (_mtx)_mtx->unlock();}
  1. 负载均衡设计:
    1. 首先先将主机列表配置文件进行加载,并将他添加至在线主机列表和主机列表中。
      具体代码实现:
 		bool LoadConf(const string &machine_conf){ifstream in(machine_conf);if (!in.is_open()){// 没有成功打开文件LOG(FATAL) << " 打开machine_conf出错" << "\n";return false;}// 打开文件成功string line;while (getline(in, line)){vector<string> tokens;StringUtil::SpileString(line, &tokens, ":");if (tokens.size() != 2){LOG(WARNING) << "machine_conf文件,格式有误" << "\n";continue;}// 成功切分Machine m;m._ip = tokens[0];m._port = stoi(tokens[1]);m._load = 0;m._mtx = new mutex();Online.push_back(Machines.size());Machines.push_back(m);}LOG(INFOR) << "成功加载machine_conf文件" << "\n";in.close(); // 记得关闭文件return true;}
  1. 智能选择主机方法设计,选择一个在线列表中,负载系数最低的主机进行处理当前的网络服务。需要加锁保护。
    具体实现代码:
		bool SmartChoice(int *id, Machine **m){_mtx.lock();int OnlineNum = Online.size();if (OnlineNum == 0){LOG(FATAL) << " 所有主机都离线了,请运维的老铁过来看看 " << "\n";_mtx.unlock();return false;}*id = Online[0];*m = &Machines[Online[0]];uint64_t min_load = Machines[Online[0]].Load();for (int i = 1; i < OnlineNum; 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;}
  1. 当一台主机离线后,我们需要对它的负载系数进行一个清0操作。如果不清零,可能他离线后,它的负载系数还是一个比较高的值,当让它再次上线后,就无法调用到这台主机了。需要加锁保护
    具体实现代码:
		// 将指定的一台主机进行离线void OfflineMachines(int witch){_mtx.lock();for (auto iter = Online.begin(); iter != Online.end(); iter++){if (*iter == witch){// 清除主机的负载系数Machines[witch].ClearLoad();Online.erase(iter);Offline.push_back(witch);break; //   直接break,迭代器失效不影响}}_mtx.unlock();}
  1. 一键上线功能设计。当我们所有的主机都挂掉了,我们需要将主机重新上线,进行编译服务。需要加锁保护。
    主要实现代码:
 		void OnlineMachines(){_mtx.lock();Online.insert(Online.end(), Offline.begin(), Offline.end());cout << "所有主机都上线啦!" << endl;_mtx.unlock();}
  1. Control结构体的设计:
 		Model _model;              // 提供后台数据View _view;                // 提供html渲染功能LoadBalance _load_balance; // 核心负载均衡器
  • 调用获取所有题目列表的信息方法,并调用oj_view模块中的形成网页的方法,形成一个题目列表的网页。
    具体实现代码:
		bool AllQuestions(string *html){vector<Question> all;bool ret = true;if (_model.GetAllQuestions(&all)){// 对题目进行排序sort(all.begin(), all.end(), [](const Question &q1, const Question &q2){ return q1.number < q2.number; });// 获取题目数据成功// 形成网页_view.AllExpandHtml(all, html);}else{*html = "获取题⽬失败, 形成题⽬列表失败";ret = false;// 获取题目信息失败}return ret;}
  • 调用获取所有题目列表的信息方法,并调用oj_view模块中的形成网页的方法,形成指定题目的网页。
    具体实现代码:
		bool Onequestion(const string &number, string *html){Question q;bool ret = true;if (_model.GetOneQuestion(number, &q)){// 获取指定题目信息成功// 形成网页_view.OneExpandHtml(q, html);}else{*html = "指定题⽬: " + number + " 不存在!";ret = false;}return ret;}
  • 提供外部调用一键上线方法的接口:
 		void RecoveryMachine(){_load_balance.OnlineMachines();}
  • 设计题目的Judge服务方法。首先进行反序列化和序列化操作,形成需要编译运行的json字符串。
  • in_json进⾏反序列化,得到用户提交的代码
  • 重新拼接⽤⼾代码+测试⽤例代码,形成新的代码
  • 选择负载最低的主机(差错处理) 规则: ⼀直选择,直到主机可⽤,否则,就是全部挂掉
  • 然后发起http请求,得到结果。http的状态码是200,才表明是成功的
  • 将结果赋值给out_json

具体代码实现:

		void Judge(const string &number, const string in_json, string *out_json){// 0. 根据题⽬编号,直接拿到对应的题⽬细节Question q;_model.GetOneQuestion(number, &q);Json::Reader reader;Json::Value in_value;reader.parse(in_json, in_value);string code = in_value["code"].asString();Json::Value compile_value;compile_value["input"] = in_value["input"].asString(); // 注意asString()和asCString()接口。compile_value["code"] = code + q.tail;compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;Json::FastWriter writer;string compile_string = writer.write(compile_value);while (true){int id = 0;Machine *m = nullptr;if (!_load_balance.SmartChoice(&id, &m)){LOG(ERROR) << "失败" << "\n";break;}Client cli(m->_ip, m->_port);m->IncLoad();LOG(INFOR) << "选出编译服务主机,详情: " << m->_ip << ":" << m->_port << " 负载系数:" << m->Load() << "\n";if (auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")){LOG(INFOR) << "然后发起http请求" << "\n";if (res->status == 200){*out_json = res->body;m->DecLoad();LOG(INFOR) << "请求和编译服务成功..." << "\n";break;}m->DecLoad();}else // 请求失败{LOG(ERROR) << " 当前请求的主机id: " << id << "详情: " << m->_ip << ":" << m->_port << " 可能已经离线" << "\n";_load_balance.OfflineMachines(id);_load_balance.ShowMachines();}}}
4.4 oj_view.hpp文件代码编写

oj_view.hpp代码编写重难点分析:
在这里插入图片描述
本质其实就是完成四部曲的编写即可:

  • 形成路径
  • 形成数据字典
  • 获取被渲染的html的对象
  • 完成渲染

    将所有题目列表信息形成网页

		void AllExpandHtml(std::vector<Question>& all, std::string* html){std::string src_path = template_path + "all_questions.html";ctemplate::TemplateDictionary root("all_questions");for(const auto& q : all){ctemplate::TemplateDictionary* sub = root.AddSectionDictionary("question_list");sub->SetValue("number",q.number);sub->SetValue("title",q.title);sub->SetValue("dif_level",q.dif_level);}ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_path,ctemplate::DO_NOT_STRIP);tpl->Expand(html,&root);}
  1. 将特定的题目细节形成网页
void OneExpandHtml(Question& q, std::string* html){std::string src_path = template_path + "one_question.html";ctemplate::TemplateDictionary root("one_question");root.SetValue("number",q.number);root.SetValue("title",q.title);root.SetValue("dif_level",q.dif_level);root.SetValue("desc",q.desc);root.SetValue("header",q.header);ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_path,ctemplate::DO_NOT_STRIP);tpl->Expand(html,&root);}
4.5 oj_server.cc文件代码编写

调用Get方法,实现网络服务。获取所有题目、用户根据题号获取特定的一道题、用户提交代码,使用我们的判题功能。(1. 每道题的测试用例 2. compile_and_run)。


//进行服务路由功能
using namespace httplib;
using namespace ns_control;static Control* ctl_ptr = nullptr;void Recovery(int signum)
{ctl_ptr->RecoveryMachine();
}int main()
{signal(SIGQUIT,Recovery);Server svr;Control ctl;ctl_ptr = &ctl;svr.Get("/all_questions",[&ctl](const Request& Req, Response& Resp){std::string html;ctl.AllQuestions(&html);Resp.set_content(html,"text/html;charset=utf-8");});svr.Get(R"(/question/(\d+))",[&ctl](const Request& Req, Response& Resp){std::string number = Req.matches[1];std::string html;ctl.Onequestion(number,&html);//ctl.Question(number,&html);Resp.set_content(html,"text/html;charset=utf-8");});svr.Post(R"(/judge/(\d+))",[&ctl](const Request& Req, Response& Resp){std::string number = Req.matches[1];std::string in_json = Req.body;std::string out_json;ctl.Judge(number,in_json,&out_json);Resp.set_content(out_json, "application/json;charset=utf-8");});svr.set_base_dir("./wwwroot");svr.listen("0.0.0.0",8080);return 0;
}

六、项目前端页面实现过程

    我主要是做后端开发的项目,前端没有那么重要,只是了解一下前端的一些基础语法和调用简单的接口,大家了解一下即可。这里就不做详细的分析啦,大家想要前端页面的代码,可以直接到我的gitee里面copy即可~
本项目实现了一个简单版的网页前端。一共有三个网页:

  • 简单的主页网页
  • 所有题目列表信息的网页
  • 特定题目细节信息和用户在线OJ的网页。

效果展示:

  1. 前端主网页
    在这里插入图片描述

  2. 题目列表网页
    我这里只录入两道题目作为测试~
    在这里插入图片描述

  3. 指定题目细节和在线OJ网页
    在这里插入图片描述

七、 项目演示过程

  1. 首先启动我们的三台编译服务的主机
    在这里插入图片描述

  2. 启动我们的oj_server主机,负责将用户提交的代码传输到编译服务主机。
    在这里插入图片描述
    3. 前端主网页
    在这里插入图片描述

  3. 题目列表网页
    我这里只录入两道题目作为测试~
    在这里插入图片描述

  4. 指定题目细节和在线OJ网页
    在这里插入图片描述

  5. 进行编译和提交代码
    在这里插入图片描述
    我们可以看到,在线oj的基本功能都是可以跑通的。
    在这里插入图片描述

  6. 测试一下常见的错误,看能否让用户看到错误信息。
    在这里插入图片描述
    在这里插入图片描述
    可以看到,能够返回错误信息。

在这里插入图片描述

八、项目的扩展方向

    项目目前实现了查看题库,和对题目进行在线OJ的功能。本项目还有很多扩展的地方。

  1. 基于注册和登陆的录题功能
  2. 业务扩展,⾃⼰写⼀个论坛,接⼊到在线OJ中
  3. 即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在docker
  4. ⽬前后端compiler的服务我们使⽤的是http⽅式请求(仅仅是因为简单),但是也可以将我们的compiler服务,设计成为远程过程调⽤,用rest_rpc,替换我们的httplib
  5. 功能上更完善⼀下,判断⼀道题⽬正确之后,⾃动下⼀道题⽬
  6. 题库目前只实现文件版的,可以实现成用数据库来存储题库。
  7. 其他

总结撒花💞

   本篇文章旨在分享的是负载均衡式——Online Judge项目的详细设计过程。希望大家通过阅读此文有所收获
   😘如果我写的有什么不好之处,请在文章下方给出你宝贵的意见😊。如果觉得我写的好的话请点个赞赞和关注哦~😘😘😘

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

调制信号识别系列 (一):基准模型

调制信号识别系列 (一)&#xff1a;基准模型 说明&#xff1a;本文包含对CNN和CNNLSTM基准模型的复现&#xff0c;模型架构参考下述两篇文章 文章目录 调制信号识别系列 (一)&#xff1a;基准模型一、论文1、DL-PR: Generalized automatic modulation classification method b…

ThreadPoolExecutor - 管理线程池的核心类

下面是使用给定的初始参数创建一个新的 ThreadPoolExecutor &#xff08;构造方法&#xff09;。 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,…

【Python】搭建属于自己 AI 机器人

目录 前言 1 准备工作 1.1 环境搭建 1.2 获取 API KEY 2 写代码 2.1 引用库 2.2 创建用户 2.3 创建对话 2.4 输出内容 2.5 调试 2.6 全部代码 2.7 简短的总结 3 优化代码 3.1 规范代码 3.1.1 引用库 3.1.2 创建提示词 3.1.3 创建模型 3.1.4 规范输出&#xf…

Git详细安装和使用教程

文章目录 准备工作-gitee注册认识及安装GitGit配置用户信息本地初始化Git仓库记录每次更新到仓库查看及切换历史版本Git忽略文件和查看文件状态Git分支-查看及切换Git分支-创建分支Git分支-合并及删除分支Git分支-命令补充Git分支-冲突需求: 准备工作-gitee注册 传送门: gite…

HDF4文件转TIF格式

HDF4 HDF4&#xff08;Hierarchical Data Format version 4&#xff09;是一种用于存储和管理机器间数据的库和多功能文件格式。它是一种自描述的文件格式&#xff0c;用于存档和管理数据。 HDF4与HDF5是两种截然不同的技术&#xff0c;HDF5解决了HDF4的一些重要缺陷。因此&am…

[终端安全]-3 移动终端之硬件安全(TEE)

&#xff08;参考资料&#xff1a;TrustZone for V8-A. pdf&#xff0c;来源ARM DEVELOPER官网&#xff09; TEE&#xff08;Trusted Execution Environment&#xff0c;可信执行环境&#xff09;是用于执行敏感代码和处理敏感数据的独立安全区域&#xff1b;以ARM TrustZone为…

cs231n作业1——Softmax

参考文章&#xff1a;cs231n assignment1——softmax Softmax softmax其实和SVM差别不大&#xff0c;两者损失函数不同&#xff0c;softmax就是把各个类的得分转化成了概率。 损失函数&#xff1a; def softmax_loss_naive(W, X, y, reg):loss 0.0dW np.zeros_like(W)num_…

信号与系统笔记分享

文章目录 一、导论信号分类周期问题能量信号和功率信号系统的线性判断时变&#xff0c;时不变系统因果系统判断记忆性系统判断稳定性系统判断 二、信号时域分析阶跃函数冲激函数取样性质四种特性1 筛选特性2 抽样特性3 展缩特性4 卷积特性卷积作用 冲激偶函数奇函数性质公式推导…

Ubuntu 20.04下多版本CUDA的安装与切换 超详细教程

目录 前言一、安装 CUDA1.找到所需版本对应命令2.下载 .run 文件3.安装 CUDA4.配置环境变量4.1 写入环境变量4.2 软连接 5.验证安装 二、安装 cudnn1.下载 cudnn2.解压文件3.替换文件4.验证安装 三、切换 CUDA 版本1.切换版本2.检查版本 前言 当我们复现代码时&#xff0c;总会…

彻底解决Path with “WEB-INF“ or “META-INF“: [WEB-INF/views/index.jsp]

背景描述 项目使用的是springboot2jsp的架构。以前好好的项目复制了一份&#xff0c;然后就无法访问报错。百度了好久都乱七八糟的&#xff0c;还没有解决问题。错误消息如下&#xff1a; 2024-07-05 15:45:51.335 INFO [http-nio-12581-exec-1]org.springframework.web.ser…

Linux服务器使用总结-不定时更新

# 查看升级日志 cat /var/log/dpkg.log |grep nvidia|grep libnvidia-common

阶段三:项目开发---搭建项目前后端系统基础架构:任务13:实现基本的登录功能

任务描述 任务名称&#xff1a; 实现基本的登录功能 知识点&#xff1a; 了解前端Vue项目的基本执行过程 重 点&#xff1a; 构建项目的基本登陆功能 内 容&#xff1a; 通过实现项目的基本登录功能&#xff0c;来了解前端Vue项目的基本执行过程&#xff0c;并完成基…

firewalld(8) policies

简介 前面的文章中我们介绍了firewalld的一些基本配置以及NAT的相关配置。在前面的配置中&#xff0c;我们所有的策略都是与zone相关的&#xff0c;例如配置的rich rule&#xff0c;--direct,以及NAT,并且这些配置都是数据包进入zone或者从zone发出时设置的策略。 我们在介绍…

昇思25天学习打卡营第15天 | Vision Transformer图像分类

内容介绍&#xff1a; 近些年&#xff0c;随着基于自注意&#xff08;Self-Attention&#xff09;结构的模型的发展&#xff0c;特别是Transformer模型的提出&#xff0c;极大地促进了自然语言处理模型的发展。由于Transformers的计算效率和可扩展性&#xff0c;它已经能够训练…

在VMware虚拟机的创建以及安装linux操作系统

一、创建虚拟机 1.双击打开下载好的VMware Workstation软件 2.点击“创建新的虚拟机” 3.根据个人选择需要创建的虚拟机&#xff0c;点击下一步 4.直接点击下一步 5.选择稍后安装操作系统&#xff0c;点击下一步 、 6.选择需要的操作系统&#xff0c;点击下一步 7.根据…

YOLOv8改进---BiFPN特征融合

一、BiFPN原理 1.1 基本原理 BiFPN&#xff08;Bidirectional Feature Pyramid Network&#xff09;&#xff0c;双向特征金字塔网络是一种高效的多尺度特征融合网络&#xff0c;其基本原理概括分为以下几点&#xff1a; 双向特征融合&#xff1a;BiFPN允许特征在自顶向下和自…

【踩坑】修复pyinstaller报错 No module named pkg_resources.extern

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 报错如下&#xff1a; 修复方法&#xff1a; pip install --upgrade setuptools pippyinstaller -F -w main.py --hidden-importpkg_resources.py2_wa…

算法系列--分治排序|归并排序|逆序对的求解

一.基本概念与实现 归并排序(mergeSort)也是基于分治思想的一种排序方式,思路如下: 分解:根据中间下标mid将数组分解为两部分解决:不断执行上述分解过程,当分解到只有一个元素时,停止分解,此时就是有序的合并:合并两个有序的子区间,所有子区间合并的结果就是原问题的解 归并…

【TB作品】51单片机 Proteus仿真 00002仿真-智能台灯色调倒计时光强

实验报告&#xff1a;基于51单片机的智能台灯控制系统 背景 本实验旨在设计一个基于51单片机的智能台灯控制系统&#xff0c;该系统可以通过按键进行手动控制&#xff0c;并能根据环境光强度自动调节台灯亮度。此外&#xff0c;系统还具备倒计时关灯功能。 器件连接 51单片…

Xilinx FPGA:vivado关于真双端口的串口传输数据的实验

一、实验内容 用一个真双端RAM&#xff0c;端口A和端口B同时向RAM里写入数据0-99&#xff0c;A端口读出单数并存入单端口RAM1中&#xff0c;B端口读出双数并存入但端口RAM2中&#xff0c;当检测到按键1到来时将RAM1中的单数读出显示到PC端&#xff0c;当检测到按键2到来时&…