【项目设计】负载均衡在线OJ
创始人
2025-05-31 02:24:20

🎇Linux:


  • 博客主页:一起去看日落吗
  • 分享博主的在Linux中学习到的知识和遇到的问题
  • 博主的能力有限,出现错误希望大家不吝赐教
  • 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。

在这里插入图片描述


目录

  • 🌿1. 项目说明
  • 🌿2. 所用技术与开发环境
  • 🌿3. 项目宏观结构
  • 🌿4. 编译与运行服务
    • 🍃 4.1 编译功能
      • 🍁4.1.1 compiler.hpp
      • 🍁4.1.2 log.hpp
      • 🍁4.1.3 util.hpp
      • 🍁4.1.4 makefile
      • 🍁4.1.5 测试
    • 🍃 4.2 运行功能
      • 🍁4.2.1 runner.hpp
      • 🍁4.2.2 util.hpp新增
      • 🍁4.2.3 测试
      • 🍁4.2.4 资源限制测试
      • 🍁4.2.5 运行限制
    • 🍃 4.3 编译并运行功能
      • 🍁4.3.1 为什么需要单独实现
      • 🍁4.3.2 认识json
      • 🍁4.3.3 compile_run.hpp
      • 🍁4.3.4 util.hpp部分更新
      • 🍁4.3.5 测试
      • 🍁4.3.6 处理临时文件
    • 🍃 4.4 形成网络服务
      • 🍁4.4.1 可能遇见问题
      • 🍁4.4.2 实现代码
  • 🌿5. 基于MVC 结构的oj 服务设计–oj_server
    • 🍃 5.1 功能设计
    • 🍃 5.2 用户请求的服务路由功能
    • 🍃 5.3 题库设计(文件版)
      • 🍁5.3.1 实现逻辑
      • 🍁5.3.2 实现例子
    • 🍃 5.4 oj_model.hpp
    • 🍃 5.5 oj_view.hpp
      • 🍁5.5.1 ctemplate引入
      • 🍁5.5.2 ctemplate安装
      • 🍁5.5.3 ctemplate使用
      • 🍁5.5.4 渲染网页
    • 🍃 5.6 oj_control.hpp
      • 🍁5.6.1 编译主机设计
      • 🍁5.6.2 编译主机设计
      • 🍁5.6.3 核心业务逻辑的控制器
  • 🌿6. 前端页面设计
    • 🍃 6.1 丐版首页
    • 🍃 6.2 题目列表
    • 🍃 6.3 指定题目提交页面
  • 🌿7. 引入mysql
    • 🍃 7.1 建表
    • 🍃 7.2 引入第三方
    • 🍃 7.3 更改oj_model
  • 🌿8. 顶部部署makefile
  • 🌿9. 思维导图

🌿1. 项目说明

实现了一个负载均衡式的在线OJ平台,用户可以在浏览器访问各个题目,在编辑区编写代码提交,后端对代码进行编译运行,最终为用户返回结果。

OJ模块基于MVC结构,调用数据库将题目显示给用户,用户编写提交代码,OJ模块通过网络通信,负载均衡式地选择compiler模块服务器,将用户代码和测试用例组合,编译运行后将结果返回给用户。

🌿2. 所用技术与开发环境

所用技术:

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

开发环境:

  • Centos 7 云服务器
  • vscode
  • Mysql Workbench

🌿3. 项目宏观结构

代码结构:

在这里插入图片描述
项目核心是三个模块:

  • comm : 公共模块,提供一些时间戳获取,路径拼接,文件操作,字符串处理,网络请求,日志等项目里常用的功能。
  • compile_server : 编译与运行模块,让用户提交的代码与测试用例拼接后在服务器上进行编译,运行,得到结果返回给用户。
  • oj_server : 请求题目列表;请求一个具体题目,且有编辑区 ;提交判题请求。采用MVC的设计模式,使用负载均衡,访问文件或数据库,调用编译模块,以及把题目列表和编辑界面展示给用户。

在这里插入图片描述
编写思路:

  1. compile_server
  2. oj_server
  3. 版本一:基于文件版本的OJ
  4. 前端页面设计
  5. 版本二:基于mysql版本的OJ

🌿4. 编译与运行服务

🍃 4.1 编译功能

在这里插入图片描述


🍁4.1.1 compiler.hpp

#pragma once#include 
#include 
#include //waitpid
#include 
#include 
#include #include "../comm/util.hpp"#include "../comm/log.hpp"//只负责进行代码的编译namespace ns_compiler
{//引入ns_util工具类(路径拼接)using namespace ns_util;using namespace ns_log;class Compiler{public:Compiler(){}~Compiler(){}//返回值:编译成功:true 否则:false//输入参数:编译的文件名//flie_name:1234//1234.cpp -> ./temp/1234.cpp//1234 -> ./temp/1234.exe//1234 -> ./temp/1234.stderrstatic bool Compile(const std::string &file_name){pid_t pid = fork();if(pid < 0){LOG(ERROR) << "内部错误,创建子进程失败" << "\n";return false;}else if(pid == 0){int _stderr =  open(PathUtil::Stderr(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if(_stderr < 0){LOG(WARNING) << "没有成功形成stderr文件" << "\n";exit(1);}//重定向标准错误到_stderrdup2(_stderr, 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(pid, nullptr, 0);//编译是否成功,就看有没有形成对应的可执行程序if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){LOG(INFO) << PathUtil::Src(file_name) <<"编译成功" << "\n";return true;}}LOG(ERROR) << "编译失败,没有形成可执行程序" << "\n";return false;}};
}

🍁4.1.2 log.hpp

#pragma once#include 
#include 
#include "util.hpp"namespace ns_log
{using namespace ns_util;//日志等级enum{INFO,//常规的,没有任何错误信息,只是一些提示信息DEBUG,//调试时的调试日志WARNING,//告警,不影响后续使用ERROR,//错误,这个用户的请求不能继续了FATAL,//不光这个用户,整个系统都无法使用,引起系统整个出错//补充:如果正常工作中出现ERROR,FATAL那么就需要运维来解决};inline std::ostream &Log(const std::string &level, const std::string &file_name, int line){//添加日志等级std::string message = "[";message += level;message += "]";//添加报错文件message += "[";message += file_name;message += "]";//添加报错行message += "[";message += std::to_string(line);message += "]";//日志时间戳message += "[";message += TimeUtil::GetTimeStamp();message == "]";//cout 本质 内部是包含缓冲区的std::cout << message;//不要endl进行刷新return std::cout;}//LOG() << "message" << "\n"//开放式日志#define LOG(level) Log(#level,__FILE__,__LINE__)
}

🍁4.1.3 util.hpp

#pragma once#include 
#include 
#include 
#include 
#include 
#include namespace ns_util
{const std::string temp_path = "./temp/";class PathUtil {public:static std::string AddSuffix(const std::string &file_name,const std::string &suffix){std::string path_name = temp_path;path_name += file_name;path_name += suffix;return path_name;}//构建源文件路径+后缀的完整文件名//1234 -> ./temp/1234.cppstatic std::string Src(const std::string &file_name){return AddSuffix(file_name,".cpp");}//构建可执行程序的完整路径+后缀名static std::string Exe(const std::string &file_name){return AddSuffix(file_name,".exe");}//构建该程序对应的标准错误完整路径+后缀名static std::string 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;if(stat(path_name.c_str(),&st) == 0){//获取属性成功,文件已存在return true;}return false;}};class TimeUtil{public:static std::string GetTimeStamp(){struct timeval _time;gettimeofday(&_time, nullptr);return std::to_string(_time.tv_sec);}};
}

🍁4.1.4 makefile

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

🍁4.1.5 测试

  1. 在temp文件下创建一个code.cpp
  2. 在code.cpp内写下一段代码(正确的)
  3. 在compile_server.cc内调用
#include int main()
{std::cout << "hello byh" << std::endl;return 0;
}

make生成可执行程序之后即可查看

  • 成功:
    在这里插入图片描述

  • 失败:

在这里插入图片描述


🍃 4.2 运行功能

🍁4.2.1 runner.hpp

  • 设计思路:

程序运行:

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

Run不需要考虑代码跑完,结果是否正确,测试用例决定的;我们只考虑:是否正确运行完毕


  • 问题:可执行程序是谁?

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

  1. 标准输入: 不考虑用户自测
  2. 标准输出:程序运行完成,输出结果是什么
  3. 标准错误:运行时错误信息
#pragma once#include 
#include 
#include 
#include 
#include 
#include #include "../comm/log.hpp"
#include "../comm/util.hpp"namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner(){}~Runner(){}public://指明文件名即可,不需要代理路径,不需要带后缀//指名文件名即可,不需要带路径,带后缀//返回值>0:程序异常了,退出时收到了信号,返回值就是对应的信号编号//返回值==0:正常运行完毕,结果保存至对应的临时文件中//返回值<0:内部错误(打开文件失败,创建子进程失败)static int Run(const std::string &file_name){/********************************************** 程序运行:* 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 || _stderr_fd < 0 || _stdout_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);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;}}};
}

🍁4.2.2 util.hpp新增

//运行时需要有的临时文件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");}

🍁4.2.3 测试

  • 成功:

在这里插入图片描述

  • 往标准错误中输入
#include int main()
{std::cout << "hello byh" << std::endl;//写入标准错误std::cerr << "hello error" << std::endl;return 0;
}

在这里插入图片描述

  • 失败:

在这里插入图片描述

在这里插入图片描述


🍁4.2.4 资源限制测试

假设当用户提交的代码是恶意代码:占用大量空间,时间复杂度极高,对程序不友好,所以我们必要要对资源进行限制

  • 无限循环

我们用一个while(1)模拟用户提交一个恶意程序,我们可以看到程序一直会在运行无法终止,所以我们需要增加限制使他停下来
在这里插入图片描述

在这里插入图片描述
并且我们可以查看导致退出的原因是什么信号,这里导致无限循环错误的退出的信号是24,我们可以通过kill -l来查看信号大全

在这里插入图片描述

  • 内存问题

我们模拟程序一直申请内存,会导致程序资源严重浪费,所以同样需要对程序进行限制

在这里插入图片描述

我们可以看到,进行限制之后当内存申请到一定限度之后就会退出,返回的是6号信号

在这里插入图片描述

  • 测试代码
#include 
#include 
#include 
#include 
#include void handler(int signo)
{std::cout << "signo: " << signo << std::endl;exit(1);}int main()
{//资源不足,导致OS终止进程,是通过信号终止//查看终止信号;for(int i = 1;i <= 31;i++){signal(i,handler);}//时间问题//限制累计运行时常struct rlimit r;r.rlim_cur = 1;r.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU,&r);while(1);//内存问题// 限制空间struct rlimit r;r.rlim_cur = 1024 * 1024 * 20;//20Mr.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS,&r);int count = 0;while(true){int *p = new int[1024*1024];count++;std::cout << "size: " << count << std::endl;sleep(1);}return 0;
}
  • 查看信号

在这里插入图片描述


🍁4.2.5 运行限制

#pragma once#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #include "../comm/log.hpp"
#include "../comm/util.hpp"namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner(){}~Runner(){}public://提供设置进程占用资源大小的接口static void SetProcLimit(int _cpu_limit,int _men_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=_men_limit*1024;//转换为KBsetrlimit(RLIMIT_AS,&mem_rlimit);}//指明文件名即可,不需要代理路径,不需要带后缀//指名文件名即可,不需要带路径,带后缀//返回值>0:程序异常了,退出时收到了信号,返回值就是对应的信号编号//返回值==0:正常运行完毕,结果保存至对应的临时文件中//返回值<0:内部错误(打开文件失败,创建子进程失败)//cpu_limit: 该程序运行的时候,可以使用的最大cpu资源上限//mem_limit: 改程序运行的时候,可以使用的最大的内存大小(KB)static int Run(const std::string &file_name,int cpu_limit,int mem_limit){/********************************************** 程序运行:* 1. 代码跑完,结果正确* 2. 代码跑完,结果不正确* 3. 代码没跑完,异常了* Run需要考虑代码跑完,结果正确与否吗??不考虑!* 结果正确与否:是由我们的测试用例决定的!* 我们只考虑:是否正确运行完毕** 我们必须知道可执行程序是谁?* 一个程序在默认启动的时候* 标准输入: 不处理* 标准输出: 程序运行完成,输出结果是什么* 标准错误: 运行时错误信息* *******************************************/    std::string _execute = PathUtil::Exe(file_name);std::string _stdin = PathUtil::Stdin(file_name);std::string _stdout = PathUtil::Stdout(file_name);std::string _stderr = PathUtil::Stderr(file_name);umask(0);int _stdin_fd = open(_stdin.c_str(),O_CREAT|O_RDONLY,0644);int _stdout_fd = open(_stdout.c_str(),O_CREAT|O_WRONLY,0644);int _stderr_fd = open(_stderr.c_str(),O_CREAT|O_WRONLY,0644);if(_stdin_fd < 0 || _stderr_fd < 0 || _stdout_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;}}};
}

🍃 4.3 编译并运行功能

🍁4.3.1 为什么需要单独实现

编译服务随时可能被多人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性,要不然多个用户之间会互相影响

所以我们需要做:

  1. 适配用户请求,制定通信协议
  2. 正确调用compile 和 run
  3. 形成唯一的文件名

在这里插入图片描述


🍁4.3.2 认识json

json实际上是序列化的工作,作用是将结构化数据转化成为一个字符串,而Value是一个Json的中间类,可以填充KV值

  • 按照json库

不同的json库的用法是不一样的,这里我们运用一个对于云服务器来说最简单的

sudo yum install jsoncpp-devel

安装成功之后我们就可以运用json库了

#include 
#include 
#include 
#include 
#include 
#include 
#include int main()
{//序列化工作//将结构化数据转化为一个字符串//Value是一个Json的中间类,可以填充KV值Json::Value root;root["code"] = "mycode";root["user"] = "byh";root["age"] = 20;Json::StyledWriter writer;std::string str = writer.write(root);std::cout << str << std::endl;
}
  • 测试

必须得链接jsoncpp库才不会报错

在这里插入图片描述


🍁4.3.3 compile_run.hpp

#pragma once#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include 
#include #include namespace ns_compile_and_run
{using namespace ns_log;using namespace ns_util;using namespace ns_compiler;using namespace ns_runner;class CompileAndRun{public:static void RemoveTempFile(const std::string &file_name){//清理文件的个数是不确定的,但是有哪些我们是知道的std::string _src = PathUtil::Src(file_name);if(FileUtil::IsFileExists(_src)) unlink(_src.c_str());std::string _compiler_error = PathUtil::CompilerError(file_name);if(FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str());std::string _execute = PathUtil::Exe(file_name);if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());std::string _stdin = PathUtil::Stdin(file_name);if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());std::string _stdout = PathUtil::Stdout(file_name);if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());std::string _stderr = PathUtil::Stderr(file_name);if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());}// code > 0 : 进程收到了信号导致异常奔溃// code < 0 : 整个过程非运行报错(代码为空,编译报错等)// code = 0 : 整个过程全部完成//待完善static std::string CodeToDesc(int code, const std::string &file_name){std::string desc;switch(code){case 0:desc = "编译运行成功";break;case -1:desc = "提交的代码是空";break;case -2:desc = "未知错误";break;case -3://desc = "代码编译的时候发生了错误";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(code);break;}return desc;}/**************************************** 输入:* code: 用户提交的代码* input: 用户给自己提交的代码对应的输入,不做处理* cpu_limit: 时间要求* mem_limit: 空间要求** 输出:* 必填* status: 状态码* reason: 请求结果* 选填:* stdout: 我的程序运行完的结果* stderr: 我的程序运行完的错误结果** 参数:* in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}* out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}* ************************************/static void Start(const std::string &in_json,std::string *out_json){//反序列化读取信息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 status_code = 0;Json::Value out_value;int run_result = 0;std::string file_name; //需要内部形成的唯一文件名if(code.size() == 0){// out_value["status"] = -1;//代码为空// out_value["reson"] = "用户提交的代码是空的";// //序列化过程// return;status_code = -1;//代码为空goto END;}//形成的文件名只具有唯一性,没有目录没有后缀//毫秒级时间戳+原子性递增唯一值,来保证唯一性file_name = FileUtil::UniqFileName();//形成临时src文件if(!FileUtil::WriteFile(PathUtil::Src(file_name),code)){// out_value["status"] = -2;//未知错误// out_value["reson"] = "发生未知错误";// //序列化过程// return;status_code = -2;//未知错误goto END;}if(!Compiler::Compile(file_name)){// //编译失败// out_value["status"] = -3;//代码编译的时候发生错误// out_value["reson"] = FileUtil::ReadFile(PathUtil::CompilerError(file_name));// //序列化过程// return;status_code = -3;//代码编译的时候发生错误goto END;}run_result = Runner::Run(file_name,cpu_limit,mem_limit);if(run_result < 0){// out_value["status"] = -2;//未知错误// out_value["reson"] = "发生未知错误";// //序列化过程// return;status_code = -2;}else if(run_result > 0){// out_value["status"] = code;//运行报错// out_value["reson"] = SignoToDesc(code);//将信号转化为报错原因// //序列化过程// return;//程序运行崩溃了status_code = run_result;}else{//运行成功status_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);}};
}

🍁4.3.4 util.hpp部分更新

//对文件(path)的操作方法class FileUtil{public://查看文件是否存在static bool IsFileExists(const std::string &path_name){//方法一:查看文件是否能够正常打开//方法二:stat(文件路径,文件属性(可以自己选择自己需要的属性));struct stat st;if (stat(path_name.c_str(), &st) == 0)return true; //获取文件成功->文件存在return false;}//形成一个唯一的文件名(形成的文件名没有目录没有后缀)//唯一性:毫秒级别的时间戳+原子性递增的唯一值static std::string UniqFileName(){static std::atomic_uint id(0);id++;std::string ms = TimeUtil::GetTimeMS();std::string uniq_id = std::to_string(id);return ms + uniq_id;}//将code写到target中,形成临时src文件static bool writeFile(const std::string &target, const std::string &code){std::ofstream out(target);if (!out.is_open()){return false;}out.write(code.c_str(), code.size());out.close();return true;}//将文件内容读取// target文件名,content内容保存地址,keep是否保存\nstatic bool ReadFile(const std::string &target, std::string *content, bool keep = false){(*content).clear();std::ifstream in(target);if (!in.is_open()){return false;}std::string line;// getline不保存行分隔符// getline内部重载了强制类型转化while (std::getline(in, line)){(*content) += line;(*content) += (keep ? "\n" : "");}in.close();return true;}};

🍁4.3.5 测试

  • 测试
#include "compile_run.hpp"using namespace ns_compile_and_run;//编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有
//唯一性,要不然多个用户之间会互相影响int main()
{//提供的编译服务,打包形成一个网络服务//cpp-httplib// in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}// out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}// 通过http 让client 给我们 上传一个json string// 下面的工作,充当客户端请求的json串std::string in_json;Json::Value in_value;//R"()", raw stringin_value["code"] = R"(#includeint main(){std::cout << "你可以看见我了" << std::endl;return 0;})";in_value["input"] = "";in_value["cpu_limit"] = 1;in_value["mem_limit"] = 10240*3;Json::FastWriter writer;in_json = writer.write(in_value);std::cout << in_json << std::endl;//这个是将来给客户端返回的json串std::string out_json;CompileAndRun::Start(in_json, &out_json);std::cout << out_json << std::endl;return 0;
}

在这里插入图片描述

可以看到我们的代码已经运行成功了,同时我们也可以设置无限循环和不断申请内存来测试,最后都可以测试通过,但是后两者会返回所异常的信号量


🍁4.3.6 处理临时文件

我们每次测试代码都会产生很多的临时文件,当达到一定程度的时候就肯定会出问题,所以我们需要对临时文件进行处理,对此我们还需要设计一个函数,在CompileAndRun后面调用来清理临时文件。

       static void RemoveTempFile(const std::string &file_name){//清理文件的个数是不确定的,但是有哪些我们是知道的std::string _src = PathUtil::Src(file_name);if(FileUtil::IsFileExists(_src)) unlink(_src.c_str());std::string _compiler_error = PathUtil::CompilerError(file_name);if(FileUtil::IsFileExists(_compiler_error)) unlink(_compiler_error.c_str());std::string _execute = PathUtil::Exe(file_name);if(FileUtil::IsFileExists(_execute)) unlink(_execute.c_str());std::string _stdin = PathUtil::Stdin(file_name);if(FileUtil::IsFileExists(_stdin)) unlink(_stdin.c_str());std::string _stdout = PathUtil::Stdout(file_name);if(FileUtil::IsFileExists(_stdout)) unlink(_stdout.c_str());std::string _stderr = PathUtil::Stderr(file_name);if(FileUtil::IsFileExists(_stderr)) unlink(_stderr.c_str());}

🍃 4.4 形成网络服务

这里我们需要用到一个网络库,cpp-httplib,这里只需要百度一下就可以安装了,或者有个更加简单的方法,直接把httplib拷贝到项目下,引入头文件直接就可以使用了

🍁4.4.1 可能遇见问题

  • 问题一

这里我们需要用到高版本的gcc,不然就有可能报错,所以我们这里还是需要升级一下GCC,百度有很多方法,自行解决,建议升级最新版

  • 问题二

编译的时候可能会遇到线程库问题,这里我们还需要在makefile后面添加-lpthread

  • 问题三

httplib所占用空间太多,有时候导致系统运行不成功,所以这时我们需要重启一下vscode


🍁4.4.2 实现代码

#include "compile_run.hpp"
using namespace ns_compile_and_run;#include "../comm/httplib.h"
using namespace httplib;void Usage(std::string proc)
{std::cerr << "Usage: " << "\n\t" << proc << " port" << std::endl;
}//  ./compile_server port
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){// 用户请求的服务正文:json stringstd::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服务
}

🌿5. 基于MVC 结构的oj 服务设计–oj_server

本质:建立一个小型网站

🍃 5.1 功能设计

  1. 获取首页,用题目列表充当
  2. 编辑区域页面
  3. 提交判题功能(编译并运行)
  • M: Model,通常是和数据交互的模块,比如,对题库进行增删改查(文件版,MySQL)
  • V: view, 通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(浏览器)
  • C: control, 控制器,就是我们的核心业务逻辑

🍃 5.2 用户请求的服务路由功能

oj_server.cc实现用户请求的服务路由功能

#include 
#include "../comm/httplib.hpp"using namespace httplib;int main()
{//用户请求的服务路由功能Server svr;//获取所有的题目列表svr.Get("/all_questions",[](const Request &req,Response &resp){resp.set_content("这是所有题目的列表","text/plain;charset=utf-8");});//用户要根据题目编号,获取题目的内容//  /quetions/100//R"()",原始字符串raw string,保持字符串内容的原貌,不用做相关的转义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");});//用户提交代码,使用我们的判题功能(1. 每道题的测试用例 2. compile_and_run)svr.Get(R"(/jude/(\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;
}

🍃 5.3 题库设计(文件版)

🍁5.3.1 实现逻辑

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

两批文件构成:

  • 第一个:questions.list:题目列表(不需要出现题目的内容)
  • 第二个:题目的描述,预设值的代码(hander.cpp),测试用例代码(tail.cpp)通过文件的编号,产生关联的

🍁5.3.2 实现例子

  • desc.txt

这里用来写题目描述

求一个数组中最大的值示例 1:
输入: [1,2,3,4,5,6,7]
输出: 7示例 2:
输入: [-1,1,2,3,4,5,6,7,9]
输出: 9
  • header.hpp

这里用来给用户写代码

#include 
#include 
using namespace std;class Solution
{
public:int FindMax(vector& v){return true;}
};
  • tail.hpp

这里用来做测试用例,设计测试用例是特别考验对于代码的理解能力的

#ifndef CompileOnline
// 这是为了编写用例的时候有语法提示. 实际线上编译的过程中这个操作是不生效的.
#include "header.cpp"
#endifvoid Test1()
{vector v={1,2,3,4,5,6,7};int ret = Solution().FindMax(v);if (ret==7){std::cout << "Test1 ok!" << std::endl;}else{std::cout << "测试用例: {1,2,3,4,5,6,7} 未通过" << std::endl;}
}
void Test2()
{vector v={-1,1,2,3,4,5,6,7,9};int ret = Solution().FindMax(v);if (ret==9){std::cout << "Test2 ok!" << std::endl;}else{std::cout << "测试用例: {-1,1,2,3,4,5,6,7,9} 未通过" << std::endl;}
}
int main()
{Test1();Test2();return 0;
}

后续如果我们想要更新题库,只需要按照一样的逻辑编写代码即可,然后赋予唯一的题目编号即可


🍃 5.4 oj_model.hpp

  • 和数据进行交互,对外提供访问数据的接口
  • 根据题目.list文件,加载所有的题目信息到内存中
  • OJ需要的是 header.hpp+用户写的内容 + tail.cpp
#pragma once
//文件版本#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "../comm/log.hpp"
#include "../comm/util.hpp"//  根据题目list文件,加载所有的题目信息到内存中
// model: 主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{std::string number; //题目编号,唯一std::string title;  //题目的标题std::string star;   //难度: 简单 中等 困难int cpu_limit;      //题目的时间要求(S)int mem_limit;      //题目的空间要去(KB)std::string desc;   //题目的描述std::string header; //题目预设给用户在线编辑器的代码std::string tail;   //题目的测试用例,需要和header拼接,形成完整代码};const std::string questins_list = "./questions/questions.list";const std::string questins_path = "./questons/"class Model{private://题号:题目细节//题号 : 题目细节unordered_map questions;public:Model(){assert(LoadQuestionList())}bool LoadQuestionList(const std::string &question_list){//加载配置文件:questions/questions.list + 题目编号文件ifstream in(question_list);if(!in.is_open()){LOG(FATAL) << " 加载题库失败,请检查是否存在题库文件" << "\n";return false;}std::string line;while(getline(in,line)){vector tokens;StringUtil::SplitString(line,&tokens," ");if(tokens.size() != 5){LOG(WARNING) << " 加载部分题目失败,请检查文件格式" << "\n";continue;}Question q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = atoi(tokens[3].c_str());q.men_limit = atoi(tokens[4].c_str());string _path = questins_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.cc",&(q.tail),true);question.insert({q.number,q});}   LOG(INFO) << " 加载题库..成功" << "\n";in.close();}void GetAllQuestions(vector *out){if(question.size() == 0){LOG(ERROR) << "用户获取题目失败,题目编号:" << number << "\n";return false;}for(const auto &q : questions){out->push_back(q.second);//first:key,second:value}}void GetOneQuestion(const std::string &number,Question *q){const auto& iter = questions.find(number);if(iter == questions.end()){return false;}(*q) = iter->second;return true;}~Model(){}};
}

🍃 5.5 oj_view.hpp

🍁5.5.1 ctemplate引入

ctemplate最初被称为谷歌模板,因为它起源于用于谷歌搜索结果页面的模板系统。ctemplate 用于linux下的web开发,可以动态生成一个html网页,这里的 “ 动态 ” 指的是网页的数据不是固定的,可以使用变量来填充网页内容。

可以在gitee上搜索 ctemplate,选择一个下载。gitee下载链接: ctemplate下载

  • 在Linux命令行输入: git clone 复制的链接

🍁5.5.2 ctemplate安装

  • 接下来开始安装,先进入到 ctemplate 目录下
  • 第一步,因为是源码安装,需要手动运行安装程序,输入: ./autogen.sh
  • 第二步,输入:./configure
  • 第三步,输入:make
  • 如果出现了编译报错,大概率是gcc编译器版本过低的问题,可以输入gcc -v查看一下版本,此时需要更新版本。
  • 输入:sudo make install

🍁5.5.3 ctemplate使用

现在这个库已经安装到了当前系统中,我们可以在任意 .cc 文件中调用这个库

  • test.cc
#include 
#include 
#include int main(){// 形成数据字典ctemplate::TemplateDictionary dic("test");dic.SetValue("name", "张三");                // 相当于插入了一个键值对(name会在下面的网页模板中出现)// 构建空网页模板对象std::string empty_html = "./test.html";     // 空的网页模板ctemplate::Template* tp = ctemplate::Template::GetTemplate(empty_html, ctemplate::DO_NOT_STRIP);// 渲染网页模板(将网页中的变量 name 替换成 "张三")std::string filled_html;tp->Expand(&filled_html, &dic);std::cout << filled_html << std::endl;return 0;
}
  • test.html


Document

{{name}}

  • 编译时,需要添加第三方库的依赖 -lctemplate、-lpthread。因为ctemplate库用到了pthread库

在这里插入图片描述


🍁5.5.4 渲染网页

#pragma once#include 
#include 
#include #include "oj_model.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 &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. 获取被渲染的网页ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4. 开始完成渲染功能tpl->Expand(html, &root);}void OneExpandHtml(const struct Question &q,std::string1 *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);}};
}

🍃 5.6 oj_control.hpp

oj_control的主要功能是将用户提交的代码进行反序列化,得到题目的编号,通过题目编号找到对应的题目,将用户代码和对应的测试用例拼接在一起,重新组合成新的代码,再进行序列化,形成新的json串,最后再根据负载均衡算法,选择对应的编译服务器进行编译运行。

🍁5.6.1 编译主机设计

因后台可能存在多台提供编译服务的主机,为了区分不同的主机,我们就需要一个结构来保存主机的相关信息,这些信息包括

在这里插入图片描述
当用户提交代码后,编译服务的负载将增加。当代码编译完成并运行成功后,编译服务的负载将减少。如果中途服务主机突然挂了,还需要清空对应主机的负载。

因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证我对负载操作的安全性,就需要一个mutex互斥锁保护对负载的操作。我们将主机信息和相关操作封装成一个类

// 提供服务的主机
class Machine
{
public:std::string ip;  //编译服务的ipint port;        //编译服务的portuint64_t load;   //编译服务的负载std::mutex *mtx; // mutex禁止拷贝的,使用指针
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();}//重置负载为0void 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;}
};

🍁5.6.2 编译主机设计

因为后台存在多个主机提供编译服务,因此需要我们将这些主机有序的组织起来。并且我们需要为每台主机进行编号。

为此,采用vector作为存放主机的容器,因为vector的下标很好的与主机编号相匹配。在提供编译服务之前,我们需要知道有哪些主机能为我们提供服务,所以规定在当前路径下的conf文件夹下的一个.conf文件里面会存放所有的可以提供服务的主机信息,包括IP地址和端口号,中间采用":"号分割。当调用负载均衡模块时,它会自动读取该文件,并初始化vector中的主机信息。

主机存在多个,当一个服务请求主机时,选择了一个负载最低的主机,如果这个主机挂掉,那么它会选择其他负载最低的主机。同时也需要记录已经挂掉的主机。因为我们需要两个vector,一个用来存储当前可用主机,另一个用来存储已经挂掉的主机。

如果服务请求主机,然而所有主机全部挂掉,此时该请求服务就得不到任何响应,唯一能做的就是为后台开发人员提供相关的日志信息。

注意:当多个服务同时请求主机时,可能会导致负载不均衡的情况,所以也需要加锁控制

  • 负载均衡算法:

到这里我们已经有了一个vector,里面存放了所有可用的主机。当请求服务到来时,只需要通过遍历的方式,找到负载最下的主机即可

class LoadBlance
{
private:// 可以给我们提供编译服务的所有的主机// 每一台主机都有自己的下标,充当当前主机的idstd::vector machines;// 所有在线的主机idstd::vector online;// 所有离线的主机idstd::vector offline;// 保证LoadBlance它的数据安全std::mutex mtx;public:LoadBlance(){assert(LoadConf(service_machine));LOG(INFO) << "加载 " << service_machine << " 成功" << std::endl;}~LoadBlance(){}public:bool LoadConf(const std::string &machine_conf){std::ifstream in(machine_conf);if (!in.is_open()){LOG(FATAL) << " 加载: " << machine_conf << " 失败" << std::endl;return false;}std::string line;while (std::getline(in, line)){std::vector tokens;StringUtil::SplitString(line, &tokens, ":");if (tokens.size() != 2){LOG(WARNING) << " 切分 " << line << " 失败" << std::endl;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();// 负载均衡的算法// 轮询int online_num = online.size();if (online_num == 0){mtx.unlock();LOG(FATAL) << " 所有的后端编译主机已经离线, 请运维的同事尽快查看" << std::endl;return false;}// 通过遍历的方式,找到所有负载最小的机器*id = online[0];*m = &machines[online[0]];uint64_t min_load = machines[online[0]].Load();for (int i = 1; i < online_num; i++){uint64_t curr_load = machines[online[i]].Load();if (min_load > curr_load){min_load = curr_load;*id = online[i];*m = &machines[online[i]];}}mtx.unlock();return true;}void OfflineMachine(int which){mtx.lock();auto it = std::find(online.begin(), online.end(), which);if (it != online.end()){//要离线的主机已经找到啦machines[which].ResetLoad();online.erase(it);offline.push_back(which);}mtx.unlock();}void OnlineMachine(){mtx.lock();online.insert(online.end(), offline.begin(), offline.end());offline.erase(offline.begin(), offline.end());mtx.unlock();LOG(INFO) << "所有的主机有上线啦!" << std::endl;}// for testvoid ShowMachines(){mtx.lock();std::cout << "当前在线主机列表: ";for (auto &id : online){std::cout << id << " ";}std::cout << std::endl;std::cout << "当前离线主机列表: ";for (auto &id : offline){std::cout << id << " ";}std::cout << std::endl;mtx.unlock();}int GetOnlineMachine(){return online.size();}
};

🍁5.6.3 核心业务逻辑的控制器

用户的请求有多种,包括请求所有题目列表,请求单个题目和详细内容,用户提交代码,请求判题。

  • 如果是请求题目列表或者单个题目加详细信息,则需要调用oj_view模块,构建网页。
  • 如果是请求判题功能,需要对用户提交的代码进行反序列化,重新拼接成新的代码,选择负载最小的主机进行编译和运行,最后将运行结果返回给oj_server
class Control
{
private:Model _model;            //提供后台数据View _view;              //提供html渲染功能LoadBlance _load_blance; //核心负载均衡器
public:Control(){}~Control(){}
public:void RecoveryMachine(){_load_blance.OnlineMachine();}//根据题目数据构建网页// html: 输出型参数bool AllQuestions(string *html){bool ret = true;vector all;if (_model.GetAllQuestions(&all)){sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){ return atoi(q1.number.c_str()) < atoi(q2.number.c_str()); });// 获取题目信息成功,将所有的题目数据构建成网页_view.AllExpandHtml(all, html);}else{*html = "获取题目失败, 形成题目列表失败";ret = false;}return ret;}bool Question(const 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;}// code: #include...// input: ""void Judge(const std::string &number, const std::string in_json, std::string *out_json){// LOG(DEBUG) << in_json << " \nnumber:" << number << std::endl;// 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. 重新拼接用户代码+测试用例代码,形成新的代码std::string head;FileUtil::ReadFile("./questions/head.hpp", &head, true);Json::Value compile_value;compile_value["input"] = in_value["input"].asString();compile_value["code"] = head + "\n" + 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){END:int id = 0;Machine *m = nullptr;if (!_load_blance.SmartChoice(&id, &m)){break;}// 4. 然后发起http请求,得到结果Client cli(m->ip, m->port);m->IncLoad();LOG(INFO) << " 选择主机成功, 主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 当前主机的负载是: " << m->Load() << std::endl;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) << "请求编译和运行服务成功..." << std::endl;break;}m->DecLoad();}else{//请求失败LOG(ERROR) << " 当前请求的主机id: " << id << " 详情: " << m->ip << ":" << m->port << " 可能已经离线" << std::endl;_load_blance.OfflineMachine(id);if (_load_blance.GetOnlineMachine() != 0){goto END;}_load_blance.ShowMachines(); //仅仅是为了用来调试break;}}}
};

🌿6. 前端页面设计

前端页面分为三大部分:

  1. 首页页面
  2. 题目列表页面
  3. 指定题目的编写提交页面

由于我们的项目主要是研发后端,关于前端的代码就写的简单一点,如果后续需要美化的话再进行优化

🍃 6.1 丐版首页


这是我的个人OJ系统

欢迎来到Byih的OnlineJudge平台

这个我个人独立开发的一个在线OJ平台

点击我开始编程啦!

在这里插入图片描述


🍃 6.2 题目列表


在线OJ-题目列表

OnlineJuge题目列表

{{#question_list}}{{/question_list}}
编号标题难度
{{number}}{number}}">{{title}}{{star}}

@Byih

在这里插入图片描述


🍃 6.3 指定题目提交页面


{<!-- -->{number}}.{<!-- -->{title}}

{{number}}.{{title}}_{{star}}

{{desc}}

在这里插入图片描述

  • 错误展示

在这里插入图片描述

  • 正确展示

在这里插入图片描述

到这里我们的文件版在线OJ就已经完成了,后续我们还要进行升级,将题目变成数据库版本


🌿7. 引入mysql

此前我们已经实现了文件版的数据库,现在我们来把文件版本升级为数据库版本,这里就需要下载Mysql了,由于篇幅问题,这里就不详细说明了。可以在网上自行查找相关资源

Mysql官网

  1. 在数据库中设计可以远程登陆的MySQL用户,并给他赋权
    oj_client
  2. 设计表结构
    数据库:oj, 表:oj_questions
  3. 开始编码
    连接访问数据库
    有可能你的系统中,已经默认安装了mysql的开发包
    这里我们使用第三方引入的方式,不安装
    我们的oj_server基于MVC模式的,和数据打交道的只有一个oj_model模块,只需要更改该文件即可!!

🍃 7.1 建表

这里推荐使用Mysql workbench,虽然对新手不友好,但是熟练运用的话还是可以提高一定的效率

Mysql Workbench

在这里插入图片描述

下载完成之后我们就可以连接我们的mysql使用了,记得把云服务器的防火墙打开,不然是无法访问的

CREATE TABLE IF NOT EXISTS `questions`(`number` int PRIMARY KEY AUTO_INCREMENT COMMENT '题目的ID',`title` VARCHAR(64) NOT NULL COMMENT '题目的标题',`star` VARCHAR(8) NOT NULL COMMENT '题目的难度',`desc` TEXT NOT NULL COMMENT '题目描述',`header` TEXT NOT NULL COMMENT '题目头部,给用户看的代码',`tail` TEXT NOT NULL COMMENT '题目尾部,包含我们的测试用例',`time_limit` int DEFAULT 1 COMMENT '题目的时间限制',`mem_limit` int DEFAULT 5000000 COMMENT '题目的空间限制'
)ENGINE=INNODB DEFAULT CHARSET=utf8;
  • 第一步:建表

在这里插入图片描述

  • 第二步:录题

这里只需要把对应的题目内容填入表中,然后点记apply自动生成即可

在这里插入图片描述

然后我们就可以看到题目已经成功录入进去了

在这里插入图片描述


🍃 7.2 引入第三方

因为我们这里是引用外部的库,所以我们是采用建立静态连接的方式来实现的

当我们make形成可执行程序的时候,ldd一下看是否有成功找到连接的库

在这里插入图片描述

这里可以看到我是找不到这个库的,所以我们需要手动添加路径,配置环境,我们需要进到/etc/ld.so.conf.d/路径下

然后创建一个.conf文件,因为设计系统层面,所以需要sudo才能创建

在这里插入图片描述

然后我们需要把引入静态库的路径写到oj_lib_search.conf下,相当于告诉系统,你应该去哪里找这个库,我们只需要pwd就可以找到当前路径了,然后复制粘贴进来我们就可以成功运行了

在这里插入图片描述


🍃 7.3 更改oj_model

#pragma once
//MySQL 版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"
#include "include/mysql.h"#include 
#include 
#include 
#include 
#include 
#include 
#include // 根据题目list文件,加载所有的题目信息到内存中
// model: 主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{std::string number; //题目编号,唯一std::string title;  //题目的标题std::string star;   //难度: 简单 中等 困难std::string desc;   //题目的描述std::string header; //题目预设给用户在线编辑器的代码std::string tail;   //题目的测试用例,需要和header拼接,形成完整代码int cpu_limit;      //题目的时间要求(S)int mem_limit;      //题目的空间要去(KB)};const std::string oj_questions = "oj_questions";const std::string host = "127.0.0.1";const std::string user = "";//自己的数据库名称const std::string passwd = "";//数据库👩const std::string db = "oj";const int port = 3306;class Model{public:Model(){}bool QueryMySql(const std::string &sql, vector *out){// 创建mysql句柄MYSQL *my = mysql_init(nullptr);// 连接数据库if(nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(),db.c_str(),port, nullptr, 0)){LOG(FATAL) << "连接数据库失败!" << "\n";return false;}// 一定要设置该链接的编码格式, 要不然会出现乱码问题mysql_set_character_set(my, "utf8");LOG(INFO) << "连接数据库成功!" << "\n";// 执行sql语句if(0 != mysql_query(my, sql.c_str())){LOG(WARNING) << sql << " execute error!" << "\n";return false;}// 提取结果MYSQL_RES *res = mysql_store_result(my);// 分析结果int rows = mysql_num_rows(res); //获得行数量int cols = mysql_num_fields(res); //获得列数量Question q;for(int i = 0; i < rows; i++){MYSQL_ROW row = mysql_fetch_row(res);q.number = row[0];q.title = row[1];q.star = row[2];q.desc = row[3];q.header = row[4];q.tail = row[5];q.cpu_limit = atoi(row[6]);q.mem_limit = atoi(row[7]);out->push_back(q);}// 释放结果空间free(res);// 关闭mysql连接mysql_close(my);return true;}bool GetAllQuestions(vector *out){std::string sql = "select * from ";sql += oj_questions;return QueryMySql(sql, out);}bool GetOneQuestion(const std::string &number, Question *q){bool res = false;std::string sql = "select * from ";sql += oj_questions;sql += " where number=";sql += number;vector result;if(QueryMySql(sql, &result)){if(result.size() == 1){*q = result[0];res = true;}}return res;}~Model(){}};
} // namespace ns_model

🌿8. 顶部部署makefile

在顶层新建一个Makefile文件,该文件的功能就是可以make时可以同时编译CompilerServer服务和OJServer服务,当输入make submit时会自动形成一个output文件,里面包含了compiler_server和oj_server的应用程序和一些运行程序必须的文件,时间打包的功能。最后输入make clean不光会清理掉创建的可执行程序,还会清理掉output的内容。

.PHONY: all
all:@cd compile_server;\make;\cd -;\cd oj_server;\make;\cd -;.PHONY:output
output:@mkdir -p output/compile_server;\mkdir -p output/oj_server;\cp -rf compile_server/compile_server output/compile_server;\cp -rf compile_server/temp output/compile_server;\cp -rf oj_server/conf output/oj_server/;\cp -rf oj_server/lib output/oj_server/;\cp -rf oj_server/questions output/oj_server/;\cp -rf oj_server/template_html output/oj_server/;\cp -rf oj_server/wwwroot output/oj_server/;\cp -rf oj_server/oj_server output/oj_server/;.PHONY:clean
clean:@cd compile_server;\make clean;\cd -;\cd oj_server;\make clean;\cd -;\rm -rf output;

🌿9. 思维导图

在这里插入图片描述


相关内容

热门资讯

Mysql常用数据类型总结 整形 枚举类型ENUE整形       TINYINT,SMALLINT,MEDIUMINT,IN...
【flink sql】创建表 flink sql创建表语法 CREATE TABLE [IF NOT EXISTS] [catal...
python opencv 保... 👨‍💻个人简介: 深度学习图像领域工作者 dz...
Pytorch深度学习实战3-... 目录1 数据集Dataset2 数据加载DataLoader3 常用预处理方法4 模型处理5 实例&...
自定义类型的超详细讲解ᵎᵎ了解...   目录 1.结构体的声明 1.1基础知识 1.2结构体的声明 1.3结构体的特殊声明  1.4结构...
Docker等容器技术如何与移... 移动应用程序的开发面临着很多挑战,包括开发环境的设置、测试的困难、部署的复杂性等。由于...
【微服务】—— Nacos安装... 文章目录1. Windows安装1.1 下载安装包1.2 解压1.3 端口配置1.4 启动1.5 访...
【OpenGL】 为了理解这个函数我们需要先学习一些OpenGL的内容 OpenGL可视化 https://g...
hjr-详细说一下Redis集... Redis作用 缓存 一般我们用Redis做缓存,热点数据 击穿:访问到...
【蓝桥杯】 C++ 数字三角形... 文章目录题目描述输入描述输出描述实现代码解题思路注意点知识点 题目描述 上图给出了一个数字三角形。从...
VR全景展会丨探索未来,重塑现... 随着科技的不断发展,虚拟现实(VR)技术逐渐成为一个重要的...
C++数据类型 目录 C++基础数据类型 指针 指针类型 指针赋值 引用 参考:《深...
超实用!!! 三分钟将你的项目... 文章目录前言一、在项目中新增配置二、配置github page setting?三、如...
数据结构---队列 专栏:数据结构 个人主页:HaiFan. 专栏简介:这里是...
数字操作方法 系列文章目录 前端系列文章——传送门 JavaScript系列文章——传送门 文章目录系列文章目录...
Cartesi 2023 年 ... 查看 Cartesi Machine、Cartesi Rollups 和 Noether 的更新正在...
JavaWeb——jsp概述入... JSP定义:  在如下一个jsp文件里面有如下的代码  <%@ page content...
一切喜怒哀乐都来自于你的认知 01 有个学子,准备出国,父母请来清华的教授宁向东。请问教授࿱...
JAVA并发编程——synch... 引言         Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,...
git学习----3.21 未... 文章目录前言Git :一个分布式版本控制工具目标一、概述1.1 开发中的实际场景1.2...
Qt优秀开源项目之十七:QtP... QtPromise是Promises/A+规范的Qt/C++实现。该规范的译...
【前端八股文】JavaScri... 文章目录Set概念与arr的比较属性和方法并集、交集、差集Map概念属性和方法String用索引值和...
海康硬盘录像机接入RTSP/o... EasyNVR安防视频云服务平台可支持设备通过RTSP/Onvif协议接入平台,能提供...
在混合劳动力时代如何避免网络安... 在混合劳动力时代如何避免安全网络风险 三年多来,混合工作一直是工作生活中不可或缺的一...
2023还不懂Jmeter接口... 这里介绍的Jmeter接口测试的的实战,如果文章内容没遇看懂的话,我这边...
基于4G/5G弱网聚合的多链路... 基于4G/5G多卡聚合(弱网聚合)的智能融合通信设备技术亮点 增强带宽提供可靠连接 通过将多个有线和...
如何使用Synplify综合v... 文章目录使用Synplify综合的好处synplify的教程方法1(无效)...
2023年全国最新高校辅导员精... 百分百题库提供高校辅导员考试试题、辅导员考试预测题、高校辅导员考试真题、辅导员证考试题库等ÿ...
2022年18个值得期待的Le... 有数百个独特的LearnDash附加组件,您可能很难选择您的LearnDash LMS...
【java基础】Stream流... 文章目录基本介绍流的创建流的各种常见操作forEach方法filter方法map方法peek方法fl...