🎇Linux:
- 博客主页:一起去看日落吗
- 分享博主的在Linux中学习到的知识和遇到的问题
博主的能力有限,出现错误希望大家不吝赐教
- 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。
实现了一个负载均衡式的在线OJ平台,用户可以在浏览器访问各个题目,在编辑区编写代码提交,后端对代码进行编译运行,最终为用户返回结果。
OJ模块基于MVC结构,调用数据库将题目显示给用户,用户编写提交代码,OJ模块通过网络通信,负载均衡式地选择compiler模块服务器,将用户代码和测试用例组合,编译运行后将结果返回给用户。
所用技术:
开发环境:
代码结构:
项目核心是三个模块:
编写思路:
#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;}};
}
#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__)
}
#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);}};
}
compile_server:compile_server.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f compile_server
#include int main()
{std::cout << "hello byh" << std::endl;return 0;
}
make生成可执行程序之后即可查看
成功:
失败:
程序运行:
Run不需要考虑代码跑完,结果是否正确,测试用例决定的;我们只考虑:是否正确运行完毕
一个程序在默认启动的时候
#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;}}};
}
//运行时需要有的临时文件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");}
#include int main()
{std::cout << "hello byh" << std::endl;//写入标准错误std::cerr << "hello error" << std::endl;return 0;
}
假设当用户提交的代码是恶意代码:占用大量空间,时间复杂度极高,对程序不友好,所以我们必要要对资源进行限制
我们用一个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;
}
#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;}}};
}
编译服务随时可能被多人请求,必须保证传递上来的code,形成源文件名称的时候,要具有唯一性,要不然多个用户之间会互相影响
所以我们需要做:
json实际上是序列化的工作,作用是将结构化数据转化成为一个字符串,而Value是一个Json的中间类,可以填充KV值
不同的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库才不会报错
#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);}};
}
//对文件(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;}};
#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;
}
可以看到我们的代码已经运行成功了,同时我们也可以设置无限循环和不断申请内存来测试,最后都可以测试通过,但是后两者会返回所异常的信号量
我们每次测试代码都会产生很多的临时文件,当达到一定程度的时候就肯定会出问题,所以我们需要对临时文件进行处理,对此我们还需要设计一个函数,在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());}
这里我们需要用到一个网络库,cpp-httplib,这里只需要百度一下就可以安装了,或者有个更加简单的方法,直接把httplib拷贝到项目下,引入头文件直接就可以使用了
这里我们需要用到高版本的gcc,不然就有可能报错,所以我们这里还是需要升级一下GCC,百度有很多方法,自行解决,建议升级最新版
编译的时候可能会遇到线程库问题,这里我们还需要在makefile后面添加-lpthread
httplib所占用空间太多,有时候导致系统运行不成功,所以这时我们需要重启一下vscode
#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服务
}
本质:建立一个小型网站
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;
}
两批文件构成:
这里用来写题目描述
求一个数组中最大的值示例 1:
输入: [1,2,3,4,5,6,7]
输出: 7示例 2:
输入: [-1,1,2,3,4,5,6,7,9]
输出: 9
这里用来给用户写代码
#include
#include
using namespace std;class Solution
{
public:int FindMax(vector& v){return true;}
};
这里用来做测试用例,设计测试用例是特别考验对于代码的理解能力的
#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;
}
后续如果我们想要更新题库,只需要按照一样的逻辑编写代码即可,然后赋予唯一的题目编号即可
#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(){}};
}
ctemplate最初被称为谷歌模板,因为它起源于用于谷歌搜索结果页面的模板系统。ctemplate 用于linux下的web开发,可以动态生成一个html网页,这里的 “ 动态 ” 指的是网页的数据不是固定的,可以使用变量来填充网页内容。
可以在gitee上搜索 ctemplate,选择一个下载。gitee下载链接: ctemplate下载
git clone 复制的链接
现在这个库已经安装到了当前系统中,我们可以在任意 .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;
}
Document
{{name}}
#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);}};
}
oj_control的主要功能是将用户提交的代码进行反序列化,得到题目的编号,通过题目编号找到对应的题目,将用户代码和对应的测试用例拼接在一起,重新组合成新的代码,再进行序列化,形成新的json串,最后再根据负载均衡算法,选择对应的编译服务器进行编译运行。
因后台可能存在多台提供编译服务的主机,为了区分不同的主机,我们就需要一个结构来保存主机的相关信息,这些信息包括
当用户提交代码后,编译服务的负载将增加。当代码编译完成并运行成功后,编译服务的负载将减少。如果中途服务主机突然挂了,还需要清空对应主机的负载。
因为同一时刻可能有多个执行流在请求同一个主机,所有需要保证我对负载操作的安全性,就需要一个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;}
};
因为后台存在多个主机提供编译服务,因此需要我们将这些主机有序的组织起来。并且我们需要为每台主机进行编号。
为此,采用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();}
};
用户的请求有多种,包括请求所有题目列表,请求单个题目和详细内容,用户提交代码,请求判题。
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;}}}
};
前端页面分为三大部分:
由于我们的项目主要是研发后端,关于前端的代码就写的简单一点,如果后续需要美化的话再进行优化
这是我的个人OJ系统
在线OJ-题目列表
{{number}}.{{title}}
到这里我们的文件版在线OJ就已经完成了,后续我们还要进行升级,将题目变成数据库版本
此前我们已经实现了文件版的数据库,现在我们来把文件版本升级为数据库版本,这里就需要下载Mysql了,由于篇幅问题,这里就不详细说明了。可以在网上自行查找相关资源
Mysql官网
这里推荐使用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自动生成即可
然后我们就可以看到题目已经成功录入进去了
因为我们这里是引用外部的库,所以我们是采用建立静态连接的方式来实现的
当我们make形成可执行程序的时候,ldd一下看是否有成功找到连接的库
这里可以看到我是找不到这个库的,所以我们需要手动添加路径,配置环境,我们需要进到/etc/ld.so.conf.d/路径下
然后创建一个.conf文件,因为设计系统层面,所以需要sudo才能创建
然后我们需要把引入静态库的路径写到oj_lib_search.conf下,相当于告诉系统,你应该去哪里找这个库,我们只需要pwd就可以找到当前路径了,然后复制粘贴进来我们就可以成功运行了
#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
在顶层新建一个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;