🎇Linux:
- 博客主页:一起去看日落吗
- 分享博主的在Linux中学习到的知识和遇到的问题
博主的能力有限,出现错误希望大家不吝赐教
- 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。
目录
🌿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的设计模式,使用负载均衡,访问文件或数据库,调用编译模块,以及把题目列表和编辑界面展示给用户。
编写思路:
- compile_server
- oj_server
- 版本一:基于文件版本的OJ
- 前端页面设计
- 版本二:基于mysql版本的OJ
🌿4. 编译与运行服务
🍃 4.1 编译功能
🍁4.1.1 compiler.hpp
#pragmaonce#include<iostream>#include<unistd.h>#include<sys/wait.h>//waitpid#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include"../comm/util.hpp"#include"../comm/log.hpp"//只负责进行代码的编译namespace ns_compiler
{//引入ns_util工具类(路径拼接)usingnamespace ns_util;usingnamespace ns_log;classCompiler{public:Compiler(){}~Compiler(){}//返回值:编译成功:true 否则:false//输入参数:编译的文件名//flie_name:1234//1234.cpp -> ./temp/1234.cpp//1234 -> ./temp/1234.exe//1234 -> ./temp/1234.stderrstaticboolCompile(const std::string &file_name){
pid_t pid =fork();if(pid <0){LOG(ERROR)<<"内部错误,创建子进程失败"<<"\n";returnfalse;}elseif(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";returntrue;}}LOG(ERROR)<<"编译失败,没有形成可执行程序"<<"\n";returnfalse;}};}
🍁4.1.2 log.hpp
#pragmaonce#include<iostream>#include<string>#include"util.hpp"namespace ns_log
{usingnamespace 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"//开放式日志#defineLOG(level)Log(#level,__FILE__,__LINE__)}
🍁4.1.3 util.hpp
#pragmaonce#include<iostream>#include<sys/types.h>#include<sys/stat.h>#include<string>#include<unistd.h>#include<sys/time.h>namespace ns_util
{const std::string temp_path ="./temp/";classPathUtil{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){returnAddSuffix(file_name,".cpp");}//构建可执行程序的完整路径+后缀名static std::string Exe(const std::string &file_name){returnAddSuffix(file_name,".exe");}//构建该程序对应的标准错误完整路径+后缀名static std::string Stderr(const std::string &file_name){returnAddSuffix(file_name,".stderr");}};classFileUtil{public:staticboolIsFileExists(const std::string &path_name){structstat st;if(stat(path_name.c_str(),&st)==0){//获取属性成功,文件已存在returntrue;}returnfalse;}};classTimeUtil{public:static std::string GetTimeStamp(){structtimeval _time;gettimeofday(&_time,nullptr);return std::to_string(_time.tv_sec);}};}
🍁4.1.4 makefile
compile_server:compile_server.cc
g++-o $@ $^-std=c++11.PHONY:clean
clean:
rm -f compile_server
🍁4.1.5 测试
- 在temp文件下创建一个code.cpp
- 在code.cpp内写下一段代码(正确的)
- 在compile_server.cc内调用
#include<iostream>intmain(){
std::cout <<"hello byh"<< std::endl;return0;}
make生成可执行程序之后即可查看
- 成功:
- 失败:
🍃 4.2 运行功能
🍁4.2.1 runner.hpp
- 设计思路:
程序运行:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码没跑完,异常了
Run不需要考虑代码跑完,结果是否正确,测试用例决定的;我们只考虑:是否正确运行完毕
- 问题:可执行程序是谁?
一个程序在默认启动的时候
- 标准输入: 不考虑用户自测
- 标准输出:程序运行完成,输出结果是什么
- 标准错误:运行时错误信息
#pragmaonce#include<iostream>#include<string>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<wait.h>#include"../comm/log.hpp"#include"../comm/util.hpp"namespace ns_runner
{usingnamespace ns_log;usingnamespace ns_util;classRunner{public:Runner(){}~Runner(){}public://指明文件名即可,不需要代理路径,不需要带后缀//指名文件名即可,不需要带路径,带后缀//返回值>0:程序异常了,退出时收到了信号,返回值就是对应的信号编号//返回值==0:正常运行完毕,结果保存至对应的临时文件中//返回值<0:内部错误(打开文件失败,创建子进程失败)staticintRun(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;//代表创建子进程失败}elseif(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){returnAddSuffix(file_name,".stdin");}static std::string Stdout(const std::string &file_name){returnAddSuffix(file_name,".stdout");}//构建该程序对应的标准错误完整路径+后缀名static std::string Stderr(const std::string &file_name){returnAddSuffix(file_name,".stderr");}
🍁4.2.3 测试
- 成功:
- 往标准错误中输入
#include<iostream>intmain(){
std::cout <<"hello byh"<< std::endl;//写入标准错误
std::cerr <<"hello error"<< std::endl;return0;}
- 失败:
🍁4.2.4 资源限制测试
假设当用户提交的代码是恶意代码:占用大量空间,时间复杂度极高,对程序不友好,所以我们必要要对资源进行限制
- 无限循环
我们用一个while(1)模拟用户提交一个恶意程序,我们可以看到程序一直会在运行无法终止,所以我们需要增加限制使他停下来
并且我们可以查看导致退出的原因是什么信号,这里导致无限循环错误的退出的信号是24,我们可以通过kill -l来查看信号大全
- 内存问题
我们模拟程序一直申请内存,会导致程序资源严重浪费,所以同样需要对程序进行限制
我们可以看到,进行限制之后当内存申请到一定限度之后就会退出,返回的是6号信号
- 测试代码
#include<iostream>#include<sys/time.h>#include<sys/resource.h>#include<unistd.h>#include<signal.h>voidhandler(int signo){
std::cout <<"signo: "<< signo << std::endl;exit(1);}intmain(){//资源不足,导致OS终止进程,是通过信号终止//查看终止信号;for(int i =1;i <=31;i++){signal(i,handler);}//时间问题//限制累计运行时常structrlimit r;
r.rlim_cur =1;
r.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU,&r);while(1);//内存问题// 限制空间structrlimit r;
r.rlim_cur =1024*1024*20;//20M
r.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS,&r);int count =0;while(true){int*p =newint[1024*1024];
count++;
std::cout <<"size: "<< count << std::endl;sleep(1);}return0;}
- 查看信号
🍁4.2.5 运行限制
#pragmaonce#include<iostream>#include<string>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<wait.h>#include<sys/time.h>#include<sys/resource.h>#include"../comm/log.hpp"#include"../comm/util.hpp"namespace ns_runner
{usingnamespace ns_log;usingnamespace ns_util;classRunner{public:Runner(){}~Runner(){}public://提供设置进程占用资源大小的接口staticvoidSetProcLimit(int _cpu_limit,int _men_limit){//设置CPU时长structrlimit cpu_rlimit;
cpu_rlimit.rlim_max = RLIM_INFINITY;
cpu_rlimit.rlim_cur = _cpu_limit;setrlimit(RLIMIT_CPU,&cpu_rlimit);//设置内存大小structrlimit 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)staticintRun(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;//代表创建子进程失败}elseif(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,形成源文件名称的时候,要具有唯一性,要不然多个用户之间会互相影响
所以我们需要做:
- 适配用户请求,制定通信协议
- 正确调用compile 和 run
- 形成唯一的文件名
🍁4.3.2 认识json
json实际上是序列化的工作,作用是将结构化数据转化成为一个字符串,而Value是一个Json的中间类,可以填充KV值
- 按照json库
不同的json库的用法是不一样的,这里我们运用一个对于云服务器来说最简单的
sudo yum install jsoncpp-devel
安装成功之后我们就可以运用json库了
#include<iostream>#include<sys/time.h>#include<sys/resource.h>#include<unistd.h>#include<signal.h>#include<signal.h>#include<jsoncpp/json/json.h>intmain(){//序列化工作//将结构化数据转化为一个字符串//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
#pragmaonce#include"compiler.hpp"#include"runner.hpp"#include"../comm/log.hpp"#include"../comm/util.hpp"#include<signal.h>#include<unistd.h>#include<jsoncpp/json/json.h>namespace ns_compile_and_run
{usingnamespace ns_log;usingnamespace ns_util;usingnamespace ns_compiler;usingnamespace ns_runner;classCompileAndRun{public:staticvoidRemoveTempFile(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){case0:
desc ="编译运行成功";break;case-1:
desc ="提交的代码是空";break;case-2:
desc ="未知错误";break;case-3://desc = "代码编译的时候发生了错误";FileUtil::ReadFile(PathUtil::CompilerError(file_name),&desc,true);break;case SIGABRT:// 6
desc ="内存超过范围";break;case SIGXCPU:// 24
desc ="CPU使用超时";break;case SIGFPE:// 8
desc ="浮点数溢出";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":"",}
* ************************************/staticvoidStart(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;}elseif(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)的操作方法classFileUtil{public://查看文件是否存在staticboolIsFileExists(const std::string &path_name){//方法一:查看文件是否能够正常打开//方法二:stat(文件路径,文件属性(可以自己选择自己需要的属性));structstat st;if(stat(path_name.c_str(),&st)==0)returntrue;//获取文件成功->文件存在returnfalse;}//形成一个唯一的文件名(形成的文件名没有目录没有后缀)//唯一性:毫秒级别的时间戳+原子性递增的唯一值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文件staticboolwriteFile(const std::string &target,const std::string &code){
std::ofstream out(target);if(!out.is_open()){returnfalse;}
out.write(code.c_str(), code.size());
out.close();returntrue;}//将文件内容读取// target文件名,content内容保存地址,keep是否保存\nstaticboolReadFile(const std::string &target, std::string *content,bool keep =false){(*content).clear();
std::ifstream in(target);if(!in.is_open()){returnfalse;}
std::string line;// getline不保存行分隔符// getline内部重载了强制类型转化while(std::getline(in, line)){(*content)+= line;(*content)+=(keep ?"\n":"");}
in.close();returntrue;}};
🍁4.3.5 测试
- 测试
#include"compile_run.hpp"usingnamespace ns_compile_and_run;//编译服务随时可能被多个人请求,必须保证传递上来的code,形成源文件名称的时候,要具有//唯一性,要不然多个用户之间会互相影响intmain(){//提供的编译服务,打包形成一个网络服务//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 string
in_value["code"]=R"(#include<iostream>
int 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;return0;}
可以看到我们的代码已经运行成功了,同时我们也可以设置无限循环和不断申请内存来测试,最后都可以测试通过,但是后两者会返回所异常的信号量
🍁4.3.6 处理临时文件
我们每次测试代码都会产生很多的临时文件,当达到一定程度的时候就肯定会出问题,所以我们需要对临时文件进行处理,对此我们还需要设计一个函数,在CompileAndRun后面调用来清理临时文件。
staticvoidRemoveTempFile(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"usingnamespace ns_compile_and_run;#include"../comm/httplib.h"usingnamespace httplib;voidUsage(std::string proc){
std::cerr <<"Usage: "<<"\n\t"<< proc <<" port"<< std::endl;}// ./compile_server portintmain(int argc,char*argv[]){if(argc !=2){Usage(argv[0]);return1;}
Server svr;
svr.Post("/compile_and_run",[](const Request &req, Response &resp){// 用户请求的服务正文:json string
std::string in_json = req.body;
std::string out_json;if(!in_json.empty()){CompileAndRun::Start(in_json,&out_json);
resp.set_content(out_json,"application/json;charset=utf-8");}});
svr.listen("0.0.0.0",atoi(argv[1]));//启动http服务}
🌿5. 基于MVC 结构的oj 服务设计–oj_server
本质:建立一个小型网站
🍃 5.1 功能设计
- 获取首页,用题目列表充当
- 编辑区域页面
- 提交判题功能(编译并运行)
- M: Model,通常是和数据交互的模块,比如,对题库进行增删改查(文件版,MySQL)
- V: view, 通常是拿到数据之后,要进行构建网页,渲染网页内容,展示给用户的(浏览器)
- C: control, 控制器,就是我们的核心业务逻辑
🍃 5.2 用户请求的服务路由功能
oj_server.cc实现用户请求的服务路由功能
#include<iostream>#include"../comm/httplib.hpp"usingnamespace httplib;intmain(){//用户请求的服务路由功能
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);return0;}
🍃 5.3 题库设计(文件版)
🍁5.3.1 实现逻辑
- 题目的编号
- 题目的标题
- 题目的难度
- 题目的描述,题面
- 时间要求(内部处理)
- 空间要求(内部处理)
两批文件构成:
- 第一个: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<iostream>#include<vector>usingnamespace std;classSolution{public:intFindMax(vector<int>& v){returntrue;}};
- tail.hpp
这里用来做测试用例,设计测试用例是特别考验对于代码的理解能力的
#ifndefCompileOnline// 这是为了编写用例的时候有语法提示. 实际线上编译的过程中这个操作是不生效的.#include"header.cpp"#endifvoidTest1(){
vector<int> 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;}}voidTest2(){
vector<int> 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;}}intmain(){Test1();Test2();return0;}
后续如果我们想要更新题库,只需要按照一样的逻辑编写代码即可,然后赋予唯一的题目编号即可
🍃 5.4 oj_model.hpp
- 和数据进行交互,对外提供访问数据的接口
- 根据题目.list文件,加载所有的题目信息到内存中
- OJ需要的是 header.hpp+用户写的内容 + tail.cpp
#pragmaonce//文件版本#include<iostream>#include<string>#include<vector>#include<unordered_map>#include<fstream>#include<cstdlib>#include<cassert>#include"../comm/log.hpp"#include"../comm/util.hpp"// 根据题目list文件,加载所有的题目信息到内存中// model: 主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model
{usingnamespace std;usingnamespace ns_log;usingnamespace ns_util;structQuestion{
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/"classModel{private://题号:题目细节//题号 : 题目细节
unordered_map<string, Question> questions;public:Model(){assert(LoadQuestionList())}boolLoadQuestionList(const std::string &question_list){//加载配置文件:questions/questions.list + 题目编号文件
ifstream in(question_list);if(!in.is_open()){LOG(FATAL)<<" 加载题库失败,请检查是否存在题库文件"<<"\n";returnfalse;}
std::string line;while(getline(in,line)){
vector<string> 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();}voidGetAllQuestions(vector<Question>*out){if(question.size()==0){LOG(ERROR)<<"用户获取题目失败,题目编号:"<< number <<"\n";returnfalse;}for(constauto&q : questions){
out->push_back(q.second);//first:key,second:value}}voidGetOneQuestion(const std::string &number,Question *q){constauto& iter = questions.find(number);if(iter == questions.end()){returnfalse;}(*q)= iter->second;returntrue;}~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<ctemplate/template.h>#include<string>#include<iostream>intmain(){// 形成数据字典
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;return0;}
- test.html
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Document</title></head><body><!-- 渲染时,变量name会被替换成对应的值 --><h1>{{name}}</h1></body></html>
- 编译时,需要添加第三方库的依赖 -lctemplate、-lpthread。因为ctemplate库用到了pthread库
🍁5.5.4 渲染网页
#pragmaonce#include<iostream>#include<string>#include<ctemplate/template.h>#include"oj_model.hpp"namespace ns_view
{usingnamespace ns_model;const std::string template_path ="./template_html/"classView{public:view_(){}~view_(){}public:voidAllExpandHtml(const vector<structQuestion>&questions,std::string *html){// 题目的编号 题目的标题 题目的难度// 推荐使用表格显示// 1. 形成路径
std::string src_html = template_path +"all_questions.html";// 2. 形成数字典
ctemplate::TemplateDictionary root("all_questions");for(constauto& 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);}voidOneExpandHtml(conststructQuestion&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. 获取被渲染的html
ctemplate::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互斥锁保护对负载的操作。我们将主机信息和相关操作封装成一个类
// 提供服务的主机classMachine{public:
std::string ip;//编译服务的ipint port;//编译服务的portuint64_t load;//编译服务的负载
std::mutex *mtx;// mutex禁止拷贝的,使用指针public:Machine():ip(""),port(0),load(0),mtx(nullptr){}~Machine(){}public:// 提升主机负载voidIncLoad(){if(mtx)
mtx->lock();++load;if(mtx)
mtx->unlock();}// 减少主机负载voidDecLoad(){if(mtx)
mtx->lock();--load;if(mtx)
mtx->unlock();}//重置负载为0voidResetLoad(){if(mtx)
mtx->lock();
load =0;if(mtx)
mtx->unlock();}// 获取主机负载,没有太大的意义,只是为了统一接口uint64_tLoad(){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,里面存放了所有可用的主机。当请求服务到来时,只需要通过遍历的方式,找到负载最下的主机即可。
classLoadBlance{private:// 可以给我们提供编译服务的所有的主机// 每一台主机都有自己的下标,充当当前主机的id
std::vector<Machine> machines;// 所有在线的主机id
std::vector<int> online;// 所有离线的主机id
std::vector<int> offline;// 保证LoadBlance它的数据安全
std::mutex mtx;public:LoadBlance(){assert(LoadConf(service_machine));LOG(INFO)<<"加载 "<< service_machine <<" 成功"<< std::endl;}~LoadBlance(){}public:boolLoadConf(const std::string &machine_conf){
std::ifstream in(machine_conf);if(!in.is_open()){LOG(FATAL)<<" 加载: "<< machine_conf <<" 失败"<< std::endl;returnfalse;}
std::string line;while(std::getline(in, line)){
std::vector<std::string> 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();returntrue;}// id: 输出型参数// m : 输出型参数boolSmartChoice(int*id, Machine **m){// 1. 使用选择好的主机(更新该主机的负载)// 2. 我们需要可能离线该主机
mtx.lock();// 负载均衡的算法// 轮询int online_num = online.size();if(online_num ==0){
mtx.unlock();LOG(FATAL)<<" 所有的后端编译主机已经离线, 请运维的同事尽快查看"<< std::endl;returnfalse;}// 通过遍历的方式,找到所有负载最小的机器*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();returntrue;}voidOfflineMachine(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();}voidOnlineMachine(){
mtx.lock();
online.insert(online.end(), offline.begin(), offline.end());
offline.erase(offline.begin(), offline.end());
mtx.unlock();LOG(INFO)<<"所有的主机有上线啦!"<< std::endl;}// for testvoidShowMachines(){
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();}intGetOnlineMachine(){return online.size();}};
🍁5.6.3 核心业务逻辑的控制器
用户的请求有多种,包括请求所有题目列表,请求单个题目和详细内容,用户提交代码,请求判题。
- 如果是请求题目列表或者单个题目加详细信息,则需要调用oj_view模块,构建网页。
- 如果是请求判题功能,需要对用户提交的代码进行反序列化,重新拼接成新的代码,选择负载最小的主机进行编译和运行,最后将运行结果返回给oj_server
classControl{private:
Model _model;//提供后台数据
View _view;//提供html渲染功能
LoadBlance _load_blance;//核心负载均衡器public:Control(){}~Control(){}public:voidRecoveryMachine(){
_load_blance.OnlineMachine();}//根据题目数据构建网页// html: 输出型参数boolAllQuestions(string *html){bool ret =true;
vector<structQuestion> all;if(_model.GetAllQuestions(&all)){sort(all.begin(), all.end(),[](conststructQuestion&q1,conststructQuestion&q2){returnatoi(q1.number.c_str())<atoi(q2.number.c_str());});// 获取题目信息成功,将所有的题目数据构建成网页
_view.AllExpandHtml(all, html);}else{*html ="获取题目失败, 形成题目列表失败";
ret =false;}return ret;}boolQuestion(const string &number, string *html){bool ret =true;structQuestion q;if(_model.GetOneQuestion(number,&q)){// 获取指定题目信息成功,将所有的题目数据构建成网页
_view.OneExpandHtml(q, html);}else{*html ="指定题目: "+ number +" 不存在!";
ret =false;}return ret;}// code: #include...// input: ""voidJudge(const std::string &number,const std::string in_json, std::string *out_json){// LOG(DEBUG) << in_json << " \nnumber:" << number << std::endl;// 0. 根据题目编号,直接拿到对应的题目细节structQuestion q;
_model.GetOneQuestion(number,&q);// 1. in_json进行反序列化,得到题目的id,得到用户提交源代码,input
Json::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. 前端页面设计
前端页面分为三大部分:
- 首页页面
- 题目列表页面
- 指定题目的编写提交页面
由于我们的项目主要是研发后端,关于前端的代码就写的简单一点,如果后续需要美化的话再进行优化
🍃 6.1 丐版首页
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>这是我的个人OJ系统</title><style>/* 起手式, 100%保证我们的样式设置可以不受默认影响 */*{/* 消除网页的默认外边距 */
margin:0px;/* 消除网页的默认内边距 */
padding:0px;}
html,
body {
width:100%;
height:100%;}.container .navbar {
width:100%;
height:50px;
background-color: black;/* 给父级标签设置overflow,取消后续float带来的影响 */
overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素,允许你设置宽度 */
display:inline-block;/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
width:80px;/* 设置字体颜色 */
color: white;/* 设置字体的大小 */
font-size: large;/* 设置文字的高度和导航栏一样的高度 */
line-height:50px;/* 去掉a标签的下划线 */
text-decoration: none;/* 设置a标签中的文字居中 */
text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {
background-color: green;}.container .navbar .login {float: right;}.container .content {/* 设置标签的宽度 */
width:800px;/* 用来调试 *//* background-color: #ccc; *//* 整体居中 */
margin:0px auto;/* 设置文字居中 */
text-align: center;/* 设置上外边距 */
margin-top:200px;}.container .content .font_ {/* 设置标签为块级元素,独占一行,可以设置高度宽度等属性 */
display: block;/* 设置每个文字的上外边距 */
margin-top:20px;/* 去掉a标签的下划线 */
text-decoration: none;/* 设置字体大小
font-size: larger; */}</style></head><body><div class="container"><!-- 导航栏, 功能不实现--><div class="navbar"><a href="/">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><!-- 网页的内容 --><div class="content"><h1 class="font_">欢迎来到Byih的OnlineJudge平台</h1><p class="font_">这个我个人独立开发的一个在线OJ平台</p><a class="font_" href="/all_questions">点击我开始编程啦!</a></div></div></body></html>
🍃 6.2 题目列表
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>在线OJ-题目列表</title><style>/* 起手式, 100%保证我们的样式设置可以不受默认影响 */*{/* 消除网页的默认外边距 */
margin:0px;/* 消除网页的默认内边距 */
padding:0px;}
html,
body {
width:100%;
height:100%;}.container .navbar {
width:100%;
height:50px;
background-color: black;/* 给父级标签设置overflow,取消后续float带来的影响 */
overflow: hidden;}.container .navbar a {/* 设置a标签是行内块元素,允许你设置宽度 */
display:inline-block;/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */
width:80px;/* 设置字体颜色 */
color: white;/* 设置字体的大小 */
font-size: large;/* 设置文字的高度和导航栏一样的高度 */
line-height:50px;/* 去掉a标签的下划线 */
text-decoration: none;/* 设置a标签中的文字居中 */
text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover {
background-color: green;}.container .navbar .login {float: right;}.container .question_list {
padding-top:50px;
width:800px;
height:100%;
margin:0px auto;/* background-color: #ccc; */
text-align: center;}.container .question_list table {
width:100%;
font-size: large;
font-family:'Lucida Sans','Lucida Sans Regular','Lucida Grande','Lucida Sans Unicode', Geneva, Verdana, sans-serif;
margin-top:50px;
background-color:rgb(243,248,246);}.container .question_list h1 {
color: green;}.container .question_list table .item {
width:100px;
height:40px;
font-size: large;
font-family:'Times New Roman', Times, serif;}.container .question_list table .item a {
text-decoration: none;
color: black;}.container .question_list table .item a:hover {
color: blue;
text-decoration:underline;}.container .footer {
width:100%;
height:50px;
text-align: center;
line-height:50px;
color: #ccc;
margin-top:15px;}</style></head><body><div class="container"><!-- 导航栏, 功能不实现--><div class="navbar"><a href="/">首页</a><a href="/all_questions">题库</a><a href="#">竞赛</a><a href="#">讨论</a><a href="#">求职</a><a class="login" href="#">登录</a></div><div class="question_list"><h1>OnlineJuge题目列表</h1><table><tr><th class="item">编号</th><th class="item">标题</th><th class="item">难度</th></tr>{{#question_list}}<tr><td class="item">{{number}}</td><td class="item"><a href="/question/{{number}}">{{title}}</a></td><td class="item">{{star}}</td></tr>{{/question_list}}</table></div><div class="footer"><!--<hr>--><h4>@Byih</h4></div></div></body></html>
🍃 6.3 指定题目提交页面
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>{{number}}.{{title}}</title><!-- 引入ACE插件 --><!-- 官网链接:https://ace.c9.io/ --><!-- CDN链接:https://cdnjs.com/libraries/ace --><!-- 使用介绍:https://www.iteye.com/blog/ybc77107-2296261 --><!-- https://justcode.ikeepstudying.com/2016/05/ace-editor-%E5%9C%A8%E7%BA%BF%E4%BB%A3%E7%A0%81%E7%BC%96%E8%BE%91%E6%9E%81%E5%85%B6%E9%AB%98%E4%BA%AE/ --><!-- 引入ACE CDN --><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js"type="text/javascript"charset="utf-8"></script><scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ext-language_tools.js"type="text/javascript"charset="utf-8"></script><!-- 引入jquery CDN --><scriptsrc="http://code.jquery.com/jquery-2.1.1.min.js"></script><style>*{margin: 0;padding: 0;}html,
body{width: 100%;height: 100%;}.container .navbar{width: 100%;height: 50px;background-color: black;/* 给父级标签设置overflow,取消后续float带来的影响 */}.container .navbar a{/* 设置a标签是行内块元素,允许你设置宽度 */display: inline-block;/* 设置a标签的宽度,a标签默认行内元素,无法设置宽度 */width: 80px;/* 设置字体颜色 */color: white;/* 设置字体的大小 */font-size: large;/* 设置文字的高度和导航栏一样的高度 */line-height: 50px;/* 去掉a标签的下划线 */text-decoration: none;/* 设置a标签中的文字居中 */text-align: center;}/* 设置鼠标事件 */.container .navbar a:hover{background-color: green;}.container .navbar .login{float: right;}.container .part1{width: 100%;height: 600px;overflow: hidden;}.container .part1 .left_desc{width: 50%;height: 600px;float: left;overflow: scroll;}.container .part1 .left_desc h3{padding-top: 10px;padding-left: 10px;}.container .part1 .left_desc pre{padding-top: 10px;padding-left: 10px;font-size: medium;font-family:'Gill Sans','Gill Sans MT', Calibri,'Trebuchet MS', sans-serif;}.container .part1 .right_code{width: 50%;float: right;}.container .part1 .right_code .ace_editor{height: 600px;}.container .part2{width: 100%;overflow: hidden;}.container .part2 .result{width: 300px;float: left;}.container .part2 .btn-submit{width: 120px;height: 50px;font-size: large;float: right;background-color: #26bb9c;color: #FFF;/* 给按钮带上圆角 *//* border-radius: 1ch; */border: 0px;margin-top: 10px;margin-right: 10px;}.container .part2 button:hover{color:green;}.container .part2 .result{margin-top: 15px;margin-left: 15px;}.container .part2 .result pre{font-size: large;}</style></head><body><divclass="container"><!-- 导航栏, 功能不实现--><divclass="navbar"><ahref="/">首页</a><ahref="/all_questions">题库</a><ahref="#">竞赛</a><ahref="#">讨论</a><ahref="#">求职</a><aclass="login"href="#">登录</a></div><!-- 左右呈现,题目描述和预设代码 --><divclass="part1"><divclass="left_desc"><h3><spanid="number">{{number}}</span>.{{title}}_{{star}}</h3><pre>{{desc}}</pre></div><divclass="right_code"><preid="code"class="ace_editor"><textareaclass="ace_text-input">{{pre_code}}</textarea></pre></div></div><!-- 提交并且得到结果,并显示 --><divclass="part2"><divclass="result"></div><buttonclass="btn-submit"onclick="submit()">提交代码</button></div></div><script>//初始化对象
editor = ace.edit("code");//设置风格和语言(更多风格和语言,请到github上相应目录查看)// 主题大全:http://www.manongjc.com/detail/25-cfpdrwkkivkikmk.html
editor.setTheme("ace/theme/monokai");
editor.session.setMode("ace/mode/c_cpp");// 字体大小
editor.setFontSize(16);// 设置默认制表符的大小:
editor.getSession().setTabSize(4);// 设置只读(true时只读,用于展示代码)
editor.setReadOnly(false);// 启用提示菜单
ace.require("ace/ext/language_tools");
editor.setOptions({enableBasicAutocompletion:true,enableSnippets:true,enableLiveAutocompletion:true});functionsubmit(){// alert("嘿嘿!");// 1. 收集当前页面的有关数据, 1. 题号 2.代码var code = editor.getSession().getValue();// console.log(code);var number =$(".container .part1 .left_desc h3 #number").text();// console.log(number);var judge_url ="/judge/"+ number;// console.log(judge_url);// 2. 构建json,并通过ajax向后台发起基于http的json请求
$.ajax({method:'Post',// 向后端发起请求的方式url: judge_url,// 向后端指定的url发起请求dataType:'json',// 告知server,我需要什么格式contentType:'application/json;charset=utf-8',// 告知server,我给你的是什么格式data:JSON.stringify({'code':code,'input':''}),success:function(data){//成功得到结果// console.log(data);show_result(data);}});// 3. 得到结果,解析并显示到 result中functionshow_result(data){// console.log(data.status);// console.log(data.reason);// 拿到result结果标签var result_div =$(".container .part2 .result");// 清空上一次的运行结果
result_div.empty();// 首先拿到结果的状态码和原因结果var _status = data.status;var _reason = data.reason;var reason_lable =$("<p>",{text: _reason
});
reason_lable.appendTo(result_div);if(status ==0){// 请求是成功的,编译运行过程没出问题,但是结果是否通过看测试用例的结果var _stdout = data.stdout;var _stderr = data.stderr;var stdout_lable =$("<pre>",{text: _stdout
});var stderr_lable =$("<pre>",{text: _stderr
})
stdout_lable.appendTo(result_div);
stderr_lable.appendTo(result_div);}else{// 编译运行出错,do nothing}}}</script></body></html>
- 错误展示
- 正确展示
到这里我们的文件版在线OJ就已经完成了,后续我们还要进行升级,将题目变成数据库版本
🌿7. 引入mysql
此前我们已经实现了文件版的数据库,现在我们来把文件版本升级为数据库版本,这里就需要下载Mysql了,由于篇幅问题,这里就不详细说明了。可以在网上自行查找相关资源
Mysql官网
- 在数据库中设计可以远程登陆的MySQL用户,并给他赋权 oj_client
- 设计表结构 数据库:oj, 表:oj_questions
- 开始编码 连接访问数据库 有可能你的系统中,已经默认安装了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
#pragmaonce//MySQL 版本#include"../comm/util.hpp"#include"../comm/log.hpp"#include"include/mysql.h"#include<iostream>#include<string>#include<vector>#include<unordered_map>#include<fstream>#include<cstdlib>#include<cassert>// 根据题目list文件,加载所有的题目信息到内存中// model: 主要用来和数据进行交互,对外提供访问数据的接口namespace ns_model
{usingnamespace std;usingnamespace ns_log;usingnamespace ns_util;structQuestion{
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";constint port =3306;classModel{public:Model(){}boolQueryMySql(const std::string &sql, vector<Question>*out){// 创建mysql句柄
MYSQL *my =mysql_init(nullptr);// 连接数据库if(nullptr==mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(),db.c_str(),port,nullptr,0)){LOG(FATAL)<<"连接数据库失败!"<<"\n";returnfalse;}// 一定要设置该链接的编码格式, 要不然会出现乱码问题mysql_set_character_set(my,"utf8");LOG(INFO)<<"连接数据库成功!"<<"\n";// 执行sql语句if(0!=mysql_query(my, sql.c_str())){LOG(WARNING)<< sql <<" execute error!"<<"\n";returnfalse;}// 提取结果
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);returntrue;}boolGetAllQuestions(vector<Question>*out){
std::string sql ="select * from ";
sql += oj_questions;returnQueryMySql(sql, out);}boolGetOneQuestion(const std::string &number, Question *q){bool res =false;
std::string sql ="select * from ";
sql += oj_questions;
sql +=" where number=";
sql += number;
vector<Question> 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. 思维导图
版权归原作者 一起去看日落吗 所有, 如有侵权,请联系我们删除。