【项目设计】负载均衡式——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.mzph.cn/news/867724.shtml

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

相关文章

rkmpp移植与测试

一、mpp交叉编译 MPP(Media Process Platform )是Rockchip提供的一款硬件编解码媒体处理软件平台&#xff0c;适用于Rockchip芯片系列。它屏蔽了有关芯片的复杂底层处理&#xff0c;屏蔽了不同芯片的差异&#xff0c;为使用者提供了一组MPI统一接口。如果想达到最好的效果&…

Linux字符设备驱动

一、字符设备驱动结构 1. cdev结构体 在Linux内核中&#xff0c;使用cdev结构体来描述一个字符设备 struct cdev {struct kobject kobj; //内嵌kobject对象struct module *owner; //所属的模块const struct file_operations *ops; //该设备的文件操作结构体struct list_head…

SpringBoot新手快速入门系列教程六:基于MyBatis的一个简单Mysql读写例子

MyBatis和JPA是两种不同的Java持久层框架&#xff0c;各有其优缺点。以下是它们的比较&#xff1a; MyBatis 优点 灵活性高&#xff1a;MyBatis允许手动编写SQL查询&#xff0c;可以完全控制SQL执行过程&#xff0c;非常适合复杂的查询和需要高度优化的查询。SQL分离&#x…

用proteus软件如何设计一个基于8086微处理器的简易温度计?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

#数据结构 顺序表

线性表 顺序表 每种结构都有它存在意义 线性表的顺序存储实现指的是用一组连续的存储单元存储线性表的数据元素。 概念 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性表&#xff0c;一般情况下采用数组存储。在数组上完成数据的增查改删。 逻辑结构&#…

IDEA配Git

目录 前言 1.创建Git仓库&#xff0c;获得可提交渠道 2.选择本地提交的项目名 3.配置远程仓库的地址 4.新增远程仓库地址 5.开始进行commit操作 6.push由于邮箱问题被拒绝的解决方法&#xff1a; 后记 前言 以下操作都是基于你已经下载了Git的前提下进行的&#xff0c…

CSRF靶场通关合集

目录 前言 CSRF漏洞总结 1.PiKachu靶场 1.1CSRF(get) 1.2 CSRF(post)请求 1.3 CSRF Token 2.DVWA靶场 难度低 难度中 难度高 前言 最近系统的将从web渗透到内网渗透的知识点做一个回顾,同时结合一些实战的案例来演示,下面是对刚开始学习时对靶场的一个总结. CSRF漏洞…

OPENCV(图像入门笔记)

使用OpenCV读取图像 使用cv.imread()函数读取图像。 第一个参数为图像名称 第二个参数是一个标志&#xff0c;它指定了读取图像的方式。分别有三种 cv.IMREAD_COLOR&#xff1a; 加载彩色图像。任何图像的透明度都会被忽视。它是默认标志。 cv.IMREAD_GRAYSCALE&#xff1a;以…

【Nvidia+AI相机】涂布视觉检测方案专注提高锂电池质量把控标准

锂电池单元的质量在多个生产制造领域都至关重要&#xff0c;特别是在新能源汽车、高端消费电子等行业。这些领域的产品高度依赖锂电池提供持续、稳定的能量供应。优质的锂电池单元不仅能提升产品的性能和用户体验&#xff0c;还能确保使用安全。因此&#xff0c;保证锂电池单元…

go语言Gin框架的学习路线(六)

gin的路由器 Gin 是一个用 Go (Golang) 编写的 Web 框架&#xff0c;以其高性能和快速路由能力而闻名。在 Gin 中&#xff0c;路由器是框架的核心组件之一&#xff0c;负责处理 HTTP 请求并将其映射到相应的处理函数上。 以下是 Gin 路由器的一些关键特性和工作原理的简要解释…

昇思25天学习打卡营第19天|LSTM+CRF序列标注

概述 序列标注指给定输入序列&#xff0c;给序列中每个Token进行标注标签的过程。序列标注问题通常用于从文本中进行信息抽取&#xff0c;包括分词(Word Segmentation)、词性标注(Position Tagging)、命名实体识别(Named Entity Recognition, NER)等。 条件随机场&#xff08…

水箱高低水位浮球液位开关

水箱高低水位浮球液位开关概述 水箱高低水位浮球液位开关是一种用于监测和控制水箱中液位的自动化设备&#xff0c;它能够在水箱液位达到预设的高低限制时&#xff0c;输出开关信号&#xff0c;以控制水泵或电磁阀的开闭&#xff0c;从而维持水箱液位在一个安全的范围内。这类设…

【排序算法】插入排序(希尔排序)

一.直接插入排序 1.基本思想 直接插入排序是一种简单的插入排序法&#xff0c;其核心思想是对一个已经有序的序列插入一个数据&#xff0c;该数据依次比较有序序列中的值&#xff0c;直到插入到合适的位置。在我们玩扑克牌整理牌序的时候&#xff0c;用到的就是直接插入排序的…

Vue3.js“非原始值”响应式实现基本原理笔记(二)

如果您觉得这篇文章有帮助的话&#xff01;给个点赞和评论支持下吧&#xff0c;感谢~ 作者&#xff1a;前端小王hs 阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主 此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来 书籍&a…

28行代码完成深度学习模型——线性模型 01

在这里插入代码片## 线性模型 机器学习中的线性模型是一种预测模型&#xff0c;它基于线性关系来预测输出值。这种模型假设输入特征&#xff08;自变量&#xff09;和输出&#xff08;因变量&#xff09;之间存在线性关系。线性模型通常具有以下形式&#xff1a; y x*w b 其…

【TB作品】数码管独立按键密码锁,ATMEGA16单片机,Proteus仿真 atmega16数码管独立按键密码锁

文章目录 基于ATmega16的数码管独立按键密码锁设计实验报告实验背景硬件介绍主要元器件电路连接 设计原理硬件设计软件设计 程序原理延时函数独立按键检测密码显示主函数 资源代码 基于ATmega16的数码管独立按键密码锁设计实验报告 实验背景 本实验旨在设计并实现一个基于ATm…

数据库系统原理练习 | 作业1-第1章绪论(附答案)

整理自博主本科《数据库系统原理》专业课完成的课后作业&#xff0c;以便各位学习数据库系统概论的小伙伴们参考、学习。 *文中若存在书写不合理的地方&#xff0c;欢迎各位斧正。 专业课本&#xff1a; 目录 一、选择题 二&#xff1a;简答题 三&#xff1a;综合题 一、选择…

DAY21-力扣刷题

1.买卖股票的最佳时机 121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; class Solution {public int maxProfit(int[] prices) {int minpriceInteger.MAX_VALUE;int maxprofit0;for(int i0;i<prices.length;i){if(prices[i]<minprice){minpriceprices[…

昇思MindSpore学习笔记5-01生成式--LSTM+CRF序列标注

摘要&#xff1a; 记录昇思MindSpore AI框架使用LSTMCRF模型分词标注的步骤和方法。包括环境准备、score计算、Normalizer计算、Viterbi算法、CRF组合,以及改进的双向LSTMCRF模型。 一、概念 1.序列标注 标注标签输入序列中的每个Token 用于抽取文本信息 分词(Word Segment…

InetAddress.getLocalHost().getHostAddress()阻塞导致整个微服务崩溃

InetAddress.getLocalHost().getHostAddress()阻塞导致整个微服务崩溃 import java.net.InetAddress;public class GetHostIp {public static void main(String[] args) {try {long start System.currentTimeMillis();String ipAddress InetAddress.getLocalHost().getHostA…