分布式在线评测系统
OnlineJudge
- 前言
- 所用技术
- 开发环境
- 1. 需求分析
- 2. 项目宏观结构
- 3. compile_server服务设计
- 3.1 compiler服务设计
- 3.2 runner服务设计
- 3.3 compile_run
- 3.4 compile_server.cpp
- 4. oj_server服务设计
- 4.1 model设计
- 4.2 view设计
- 4.3 control设计
- 4.3.1 获取题目列表功能
- 4.3.2 获取单个题目详情页
- 4.3.3 判题功能
- oj_server.cpp
- 5. 项目扩展
前言
此项目是仿leetcode
实现在线OJ功能的,只实现类似leetcode
的题目列表+在线编程功能
主要聚焦于后端设计,前端仅仅实现其功能即可
所用技术
- C++ STL 标准库
- Boost 准标准库(字符串切割)
- cpp-httplib 第三方开源网络库
- ctemplate 第三方开源前端网页渲染库
- jsoncpp 第三方开源序列化、反序列化库
- 负载均衡设计
- 多进程、多线程
- MySQL C connect
- Ace前端在线编辑器(了解)
- html/css/js/jquery/ajax (了解)
开发环境
Ubuntu0.22.04.1
云服务器
vscode
Mysql Workbench
1. 需求分析
- 用户能够查看题目列表
- 用户能够看到题目详细信息
- 用户能够编写代码并提交测试,测试结果返回给用户
2. 项目宏观结构
我们的项目核心是三个模块
- comm : 公共模块
- compile_server : 编译与运行模块
- oj_server : 获取题目列表,查看题目编写题目界面,负载均衡,其他功能
采用BS模式,浏览器访问后端服务器
用户的请求将通过oj_server
来进行处理,如果是访问题目的请求,会访问文件或者数据库,如果是编译与运行服务会下放到负责此功能的主机,实现功能解耦。
3. compile_server服务设计
compile_server
模块分为:compiler
、runner
和compile_run
compiler
:负责代码的编译服务
runner
:负责代码的运行服务
compile_run
:负责接收要处理的服务并将编译运行的结果处理成格式化结果返回
3.1 compiler服务设计
提供的服务:编译代码,得到编译的结果
- 对于接收的代码,创建代码的临时文件,以供编译
- 代码编译后,如果编译错误,将错误信息存入一个文件,如果编译正确,则会生成可执行文件
- 可预见的:在运行时也需要生成很多临时文件存储标准输入、标准输出、标准错误
所以,我们需要一个临时目录来存放这些临时文件,且临时文件名不能重复
我们统一命名这些临时文件:时间戳_num.xxx
在创建临时文件时,需要先获取时间戳和num,num是一个原子性的计数器,线程安全,这样就能保证所有的文件都是不同名的
于是:
- 源文件名:
时间戳_num.src
- 可执行文件名:
时间戳_num.exe
- 编译错误文件名:
时间戳_num.compile_err
在下一个模块中,也是同样的命名规范
#pragma once
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <iostream>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
namespace ns_compiler
{
using namespace ns_util;
using namespace ns_log;
class Compiler{
public:
//file_name: 不包含文件后缀和路径,只是文件名
static bool Compile(const std::string& file_name)
{
pid_t pid = fork();
if(pid < 0)
{
LOG(ERROR) << "创建子进程失败" << std::endl;
return false;
}
if(pid == 0)
{
//子进程
std::string compile_err_name = PathUtil::CompileError(file_name);
//设置权限mask,消除平台差异
umask(0);
//以写方式打开文件,若是新文件,权限为rw|r|r
int file_compile_error = open(compile_err_name.c_str(),O_CREAT | O_WRONLY, 0644);
if(file_compile_error == -1)
{
LOG(ERROR) << "打开compile_error文件失败" << std::endl;
exit(1);
}
//标准错误重定向到compile_err_name文件中
//如果oldfd打开且合法,就不会出错
dup2(file_compile_error,2);
//子进程替换为g++,编译源文件
execlp("g++","g++", "-o", PathUtil::Exec(file_name).c_str(),\
PathUtil::Src(file_name).c_str(),"-D", "ONLINE_COMPILE" ,"--std=c++11", nullptr);
LOG(ERROR) << "进程替换失败" << std::endl;
exit(2);
}
else
{
//父进程
waitpid(pid,nullptr,0);
//如果没有形成可执行文件,表示编译出错
if( !FileUtil::IsExistPathName(PathUtil::Exec(file_name)) )
{
LOG(INFO) << "代码编译错误" << std::endl;
return false;
}
}
LOG(INFO) << "代码编译成功" << std::endl;
return true;
}
};
} // namespace ns_compile
开放式日志方法LOG
#pragma once
#include <iostream>
#include <string>
#include "util.hpp"
namespace ns_log{
using namespace ns_util;
enum{
INFO, //提示信息
DEBUG, //调试信息
WARNING, //警告,不影响系统
ERROR, //错误,影响系统但是系统依旧能提供服务
FATAL // 致命错误,系统崩溃,无法提供服务
};
//开发式日志:[level][file][line]+ 其他信息
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 += std::to_string(TimeUtil::GetTimeStamp());
message += "]";
std::cout << message;
return std::cout;
}
//在预处理中,#是字符串化操作符,可以直接将宏参数转换为字符串字面量
//__FILE__宏,在编译时直接替换为文件名
//__LINE__宏,在编译时直接替换为代码行数
#define LOG(level) Log(#level,__FILE__,__LINE__)
}
3.2 runner服务设计
提供的服务:运行编译好的可执行文件,得到程序的结果
临时文件:
- 标准输入文件名:
时间戳_num.stdin
- 标准输出文件名:
时间戳_num.stdout
- 标准错误文件名:
时间戳_num.stderr
运行可执行文件有三种结果:
- 运行失败
- 运行成功,结果正确
- 运行成功,结果错误
对于runner
模块,我们并不考虑程序结果正确与否,因为要达到功能解耦,我们只关心程序是否运行成功
所以运行是否成功也有三种情况:
- 运行失败,系统或者其他原因 – 不需要让用户知道,例如:创建进程失败,代码为空等
- 运行失败,代码出错 – 需要让用户知道,例如:野指针、时间复杂度过大等
- 运行成功
进程运行时崩溃一定是被信号杀掉的,所以我们获取进程退出的状态码中的信号标识位,可得到运行失败原因
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_runner{
using namespace ns_log;
using namespace ns_util;
//只考虑程序能否运行成功,不考虑结果
class Runner{
public:
/*****
* 程序运行情况:
* 1.系统自身发生错误---返回负数
* 2.程序被信号杀掉了---返回信号
* 3.程序运行成功--- 返回0
*******/
//设置时间空间限制
static void SetProcLimit(int _cpu_limit,int _mem_limit)
{
struct rlimit cpu_limit;
cpu_limit.rlim_cur = _cpu_limit;
cpu_limit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_CPU, &cpu_limit);
struct rlimit mem_limit;
mem_limit.rlim_max = RLIM_INFINITY;
mem_limit.rlim_cur = _mem_limit*1024;
setrlimit(RLIMIT_AS,&mem_limit);
}
//cpu单位:s, memory单位:kb
static int Run(const std::string& file_name,int cpu,int memory)
{
std::string _exe_name = PathUtil::Exec(file_name);
std::string _stdin_name = PathUtil::Stdin(file_name);
std::string _stdout_name = PathUtil::Stdout(file_name);
std::string _stderr_name = PathUtil::Stderr(file_name);
umask(0);
int _stdin_fd = open(_stdin_name.c_str(),O_CREAT | O_RDONLY,0644);
int _stdout_fd = open(_stdout_name.c_str(),O_CREAT | O_WRONLY ,0644);
int _stderr_fd = open(_stderr_name.c_str(),O_CREAT | O_WRONLY, 0644);
if(_stdin_fd == -1 || _stdout_fd == -1 || _stderr_fd == -1)
{
LOG(ERROR) << "打开标准文件错误" << std::endl;
return -1; //代表打开文件失败
}
pid_t pid = fork();
if(pid < 0)
{
LOG(ERROR) << "创建子进程失败" << std::endl;
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
return -2;
}
if(pid == 0)
{
//子进程
dup2(_stdin_fd,0);
dup2(_stdout_fd,1);
dup2(_stderr_fd,2);
SetProcLimit(cpu,memory);
execl(_exe_name.c_str(),_exe_name.c_str(),nullptr);
exit(1);
}
else{
//父进程
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
int st;
//程序运行异常一定是收到了信号
waitpid(pid,&st,0);
LOG(INFO) << "运行完毕, info: " << (st & 0x7F) << "\n";
return 0x7f & st;
}
}
};
}
3.3 compile_run
功能:获取输入,编译运行,提供格式化输出
此处输入输出需要序列化与反序列化,我们采用jsoncpp
第三方库来完成
输入格式:
{
"code" : "", //需要编译运行代码
"input" : "", //用户直接提供的测试代码
"cpu_limit" : *, //时间限制,单位s
"mem_limit" : * //空间限制,单位kb
}
输出格式
{
"reason" : "", //状态码对应的信息
"status" : *, //状态码,0标识运行成功,>0代码运行异常,<0系统或者其他导致运行失败
//如果状态码为0,运行成功才有stdout和stderr
"stdout" : "",
"stderr" :
}
compile_run
#pragma once
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <jsoncpp/json/json.h>
namespace compile_run{
using namespace ns_log;
using namespace ns_util;
using namespace ns_compiler;
using namespace ns_runner;
class CompileAndRun{
public:
static void RemoveFile(const std::string& file_name)
{
std::string exe_path = PathUtil::Exec(file_name);
if(FileUtil::IsExistPathName(exe_path))
unlink(exe_path.c_str());
std::string src_path = PathUtil::Src(file_name);
if(FileUtil::IsExistPathName(src_path))
unlink(src_path.c_str());
std::string compile_err_path = PathUtil::CompileError(file_name);
if(FileUtil::IsExistPathName(compile_err_path))
unlink(compile_err_path.c_str());
std::string stdin_path = PathUtil::Stdin(file_name);
if(FileUtil::IsExistPathName(stdin_path))
unlink(stdin_path.c_str());
std::string stderr_path = PathUtil::Stderr(file_name);
if(FileUtil::IsExistPathName(stderr_path))
unlink(stderr_path.c_str());
std::string stdout_path = PathUtil::Stdout(file_name);
if(FileUtil::IsExistPathName(stdout_path))
unlink(stdout_path.c_str());
}
static std::string GetReason(int status,const std::string& file_name)
{
std::string message;
switch(status)
{
case 0:
message = "运行成功!";
break;
case -1:
message = "代码为空";
break;
case -2:
message = "未知错误";
break;
case -3:
FileUtil::ReadFromFile(PathUtil::CompileError(file_name),&message);
break;
case SIGFPE:
message = "浮点数溢出";
break;
case SIGXCPU:
message = "运行超时";
break;
case SIGABRT:
message = "内存超过范围";
break;
default:
message = "未能识别此错误:[";
message += std::to_string(status);
message += ']';
break;
}
return message;
}
/*************************
* 接受的json的格式:
* code:代码
* input:输入
* cpu_limit:cpu限制 s
* mem_limit:内存限制 kb
*
* 发送的json格式:
* 必填:
* status:状态码
* reason:请求结果
* 选填:
* stdout:程序输出结果
* stderr:程序运行完的错误信息
*/
static void Start(const std::string& in_json,std::string* out_json)
{
Json::Value in_root;
Json::Reader read;
read.parse(in_json,in_root);
//获取输入
std::string code = in_root["code"].asString();
std::string input = in_root["input"].asString();
int cpu_limit = in_root["cpu_limit"].asInt();
int mem_limit = in_root["mem_limit"].asInt();
int status = 0;//运行编译的总状态码
int run_st = 0; //程序运行返回的状态码
std::string file_name;
if(code.size() == 0)
{
status = -1; //代码为空
goto END;
}
//得到一个唯一的文件名
file_name = FileUtil::GetUniqeFileName();
if(!FileUtil::WriteToFile(PathUtil::Src(file_name),code)
|| !FileUtil::WriteToFile(PathUtil::Stdin(file_name),input))
{
status = -2; //未知错误
goto END;
}
if(!Compiler::Compile(file_name))
{
status = -3; //编译错误
goto END;
}
run_st = Runner::Run(file_name,cpu_limit,mem_limit);
if(run_st < 0)
{
status = -2; //未知错误
}
else if(run_st > 0)
{
// 程序运行崩溃
status = run_st;
}
else
{
//程序运行成功
status = 0;
}
END:
Json::Value out_root;
std::string reason = GetReason(status,file_name);
out_root["reason"] = reason;
out_root["status"] = status;
if(status == 0)
{
std::string stdout_mes;
std::string stderr_mes;
FileUtil::ReadFromFile(PathUtil::Stdout(file_name),&stdout_mes);
FileUtil::ReadFromFile(PathUtil::Stderr(file_name),&stderr_mes);
out_root["stdout"] = stdout_mes;
out_root["stderr"] = stderr_mes;
}
Json::StyledWriter writer;
if(out_json)
{
*out_json = writer.write(out_root);
}
//移除临时文件
RemoveFile(file_name);
}
};
}
3.4 compile_server.cpp
主要用来提供网络服务,接收http请求,并响应结果返回
采用了cpp-httplib
第三方库
#include "compile_run.hpp"
#include "../comm/httplib.h"
using namespace compile_run;
using namespace httplib;
void Usage(const std::string& proc)
{
std::cout << "Usage:" << proc << ' ' << "port" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
Server svr;
svr.Post("/compile_and_run",[](const Request& req,Response& resp){
std::string in_json = req.body;
std::string out_json;
if(in_json.empty())
return;
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]));
return 0;
}
4. oj_server服务设计
oj_server
采用MVC模式,分为:model
、view
、control
model
:用来与底层数据交互view
:用来处理用户视图,即前端页面control
:统筹model
与view
实现业务逻辑
4.1 model设计
对于在线OJ平台,最重要的数据就是题目
题目的设计:
struct Question{
std::string _id; //题目编号
std::string _title; //题目标题
std::string _difficulty; //题目难度
std::string _desc; //题目描述
std::string _prev_code; //预设给用户的代码
std::string _test_code; //测试用例
int _cpu_limit; //时间限制
int _mem_limit; //空间限制
};
对于题目的存储,我们可以采用文件版,也可以采用数据库版,这里我们就采用数据库版的
采用第三方库mysql C API
,需要去mysql
官网下载
model2代码
#pragma once
#include <string>
#include <vector>
#include <iostream>
#include <mysql/mysql.h>
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_model{
using namespace ns_log;
using namespace ns_util;
struct Question{
std::string _id;
std::string _title;
std::string _difficulty;
std::string _desc;
std::string _prev_code;
std::string _test_code;
int _cpu_limit;
int _mem_limit;
};
std::string table_name = "题目表名";
std::string ip = "mysql服务端ip";
std::string user_name = "用户名";
std::string password = "密码";
std::string database = "数据库名字";
uint32_t port = 3306;/*数据库端口号*/
class Model{
public:
bool QueryMysql(const std::string& sql,std::vector<Question>* questions)
{
//创建与mysql的连接
MYSQL* mysql = mysql_init(nullptr);
if(!mysql_real_connect(mysql,ip.c_str(),user_name.c_str(),password.c_str(),database.c_str(),port,nullptr,0))
{
LOG(DEBUG) << "连接数据库失败" << "\n";
return false;
}
mysql_set_character_set(mysql,"utf8");
//执行sql语句
if(mysql_query(mysql,sql.c_str()))
{
LOG(DEBUG) << "查询数据库失败" << "\n";
mysql_close(mysql);
return false;
}
//判断是否有结果
MYSQL_RES* result = mysql_store_result(mysql);
if(!result)
{
if (mysql_field_count(mysql) == 0) {
LOG(DEBUG) << "查询执行成功,但无返回结果(可能是非 SELECT 查询)" << "\n";
} else {
LOG(DEBUG) << "查询数据库失败:" << mysql_error(mysql) << "\n";
}
mysql_close(mysql);
return false;
}
//逐行获取结果
MYSQL_ROW fields;
while((fields = mysql_fetch_row(result)) != nullptr)
{
Question q;
q._id = fields[0] ? fields[0] : "";
q._title = fields[1] ? fields[1] : "";
q._difficulty = fields[2] ? fields[2] : "";
q._desc = fields[3] ? fields[3] : "";
q._prev_code = fields[4] ? fields[4] : "";
q._test_code = fields[5] ? fields[5] : "";
q._cpu_limit = fields[6] ? atoi(fields[6]) : 0;
q._mem_limit = fields[7] ? atoi(fields[7]) : 0;
questions->push_back(q);
}
//记得关闭句柄和释放结果
mysql_free_result(result);
mysql_close(mysql);
return true;
}
bool GetAllQuestions(std::vector<Question>* questions)
{
std::string sql = "select id,title,difficulty,`desc`,prev_code,test_code,cpu_limit,mem_limit from ";
sql += table_name;
if(!QueryMysql(sql,questions))
return false;
return true;
}
bool GetOneQuestion(const std::string& id,Question* quest)
{
std::string sql = "select id,title,difficulty,`desc`,prev_code,test_code,cpu_limit,mem_limit from ";
sql += table_name;
sql += " where id = ";
sql += id;
std::vector<Question> vq;
if(!QueryMysql(sql,&vq) || vq.size() != 1)
return false;
*quest = vq[0];
return true;
}
};
}
4.2 view设计
#pragma once
#include <vector>
#include <ctemplate/template.h>
#include "oj_model2.hpp"
namespace ns_view{
using namespace ns_model;
using namespace ctemplate;
static const std::string html_path = "./template_html/";
static const std::string all_questions_html = "all_questions.html";
static const std::string one_question_html = "one_question.html";
class View{
public:
void ShowAllQuestion(const std::vector<Question>& questions,std::string* html)
{
if(!html) return;
TemplateDictionary root("all_questions");
for(const auto& q : questions)
{
TemplateDictionary* row_dict = root.AddSectionDictionary("question_list");
row_dict->SetValue("id",q._id);
row_dict->SetValue("title",q._title);
row_dict->SetValue("difficulty",q._difficulty);
}
Template* tpl = Template::GetTemplate(html_path+all_questions_html,DO_NOT_STRIP);
tpl->Expand(html,&root);
}
void ShowOneQuestion(const Question& quest,std::string* html)
{
if(!html) return;
TemplateDictionary root("one_question");
root.SetValue("id",quest._id);
root.SetValue("prev_code",quest._prev_code);
root.SetValue("title",quest._title);
root.SetValue("difficulty",quest._difficulty);
root.SetValue("desc",quest._desc);
Template* tpl = Template::GetTemplate(html_path+one_question_html,DO_NOT_STRIP);
tpl->Expand(html,&root);
}
};
}
总共分为三个页面呈现给用户:
- OJ主页
- 题目列表
- 题目详情页即代码编辑区
4.3 control设计
4.3.1 获取题目列表功能
bool AllQuestions(std::string* html)
{
if(!html) return false;
std::vector<Question> questions;
if(!_model.GetAllQuestions(&questions))
{
LOG(ERROR) << "用户读取所有题目失败" << '\n';
return false;
}
std::sort(questions.begin(),questions.end(),[](const Question& q1,const Question& q2)
{
return stoi(q1._id) < stoi(q2._id);
});
_view.ShowAllQuestion(questions,html);
return true;
}
4.3.2 获取单个题目详情页
bool OneQuestion(const std::string& id,std::string* html)
{
if(!html) return false;
Question quest;
if(!_model.GetOneQuestion(id,&quest))
{
LOG(WARNING) << "用户读取题目[" << id << "]失败" << '\n';
return false;
}
_view.ShowOneQuestion(quest,html);
return true;
}
4.3.3 判题功能
判题功能设计到的内容较多,包括负载均衡选择负载较少的主机、主机的下线等
主机类:
//这个主机类要注意,在loadblance里会进行拷贝操作,如果实现了析构函数释放锁空间会出现问题,即二次释放
struct Machine{
std::string _ip;
int _port;
uint64_t _load;
std::mutex* _mtx;
Machine(const std::string& ip,int port)
:_ip(ip),
_port(port),
_load(0),
_mtx(new std::mutex)
{}
void IncLoad()
{
_mtx->lock();
_load++;
_mtx->unlock();
}
void DecLoad()
{
_mtx->lock();
_load--;
_mtx->unlock();
}
size_t Load()
{
uint64_t load;
_mtx->lock();
load = _load;
_mtx->unlock();
return load;
}
void ResetLoad()
{
_mtx->lock();
_load =0;
_mtx->unlock();
}
};
对于负载的修改,必须要保证线程安全
由于c++
标准库中的mutex
是不允许拷贝的,后序有涉及到主机的拷贝,所以存储锁的指针,而不是锁本身
负载均衡类
const std::string machines_conf = "./cnf/service_machine.conf";
class LoadBlance{
bool LoginMachines()
{
std::ifstream in(machines_conf);
if(!in.is_open())
{
LOG(FATAL) << "未能读取判题服务器配置文件" << "\n";
return false;
}
std::string line;
while(getline(in,line))
{
std::vector<std::string> tokens;
StringUtil::SplitString(line,&tokens,":");
if(tokens.size() != 2)
{
LOG(WARNING) << "某个判题服务器配置出错" << "\n";
continue;
}
Machine mac(tokens[0],stoi(tokens[1]));
_onlines.push_back(_machines.size());
_machines.push_back(mac);
}
in.close();
return true;
}
public:
LoadBlance()
{
assert(LoginMachines());
LOG(INFO) << "加载主机成功" << "\n";
}
//将下线主机全部上线策略
void OnlineMachines()
{
_mtx.lock();
_onlines.insert(_onlines.end(),_offlines.begin(),_offlines.end());
_offlines.clear();
_mtx.unlock();
LOG(INFO) << "所有主机上线成功" << std::endl;
}
void OfflineMachine(int which)
{
_mtx.lock();
std::vector<int>::iterator it = _onlines.begin();
while(it != _onlines.end())
{
if(*it == which)
{
_machines[which].ResetLoad();
_offlines.push_back(which);
_onlines.erase(it);
break;
}
it++;
}
_mtx.unlock();
}
bool SmartChoice(int* pnumber,Machine** ppmac)
{
//轮询+hash
_mtx.lock();
if(_onlines.size() == 0)
{
_mtx.unlock();
LOG(FATAL) << "没有在线主机,请尽快修复" << "\n";
return false;
}
std::vector<int>::iterator it = _onlines.begin();
int min_machine_index = 0;
while(it != _onlines.end())
{
if(_machines[*it].Load() < _machines[min_machine_index].Load())
{
min_machine_index = *it;
}
it++;
}
*pnumber = min_machine_index;
*ppmac = &_machines[min_machine_index];
_mtx.unlock();
return true;
}
//仅仅为了调试
void ShowMachines()
{
_mtx.lock();
std::cout << "当前在线主机列表: ";
for(auto &id : _onlines)
{
std::cout << id << " ";
}
std::cout << std::endl;
std::cout << "当前离线主机列表: ";
for(auto &id : _offlines)
{
std::cout << id << " ";
}
std::cout << std::endl;
_mtx.unlock();
}
private:
std::vector<Machine> _machines;
std::vector<int> _onlines;
std::vector<int> _offlines;
std::mutex _mtx;
};
判题功能:
void Judge(const std::string& question_id,const std::string& in_json,std::string* out_json)
{
if(!out_json) return;
//先得到此题信息
Question quest;
if(!_model.GetOneQuestion(question_id,&quest)) return;
Json::Reader reader;
Json::Value root;
reader.parse(in_json,root);
std::string prev_code = root["code"].asString();
std::string input = root["input"].asString();
//构建编译运行的json串
Json::Value compile_root;
//一定要加\n,如果不加会导致test_code.cpp里的条件编译和prev_code.cpp的代码连在一起,以至于无法消除条件编译
compile_root["code"] = prev_code + "\n" +quest._test_code;
compile_root["input"] = input;
compile_root["cpu_limit"] = quest._cpu_limit;
compile_root["mem_limit"] = quest._mem_limit;
Json::StyledWriter writer;
std::string judge_json = writer.write(compile_root);
//负载均衡的选择主机进行判题任务
int id;
Machine* m;
while(true)
{
if(!_load_blance.SmartChoice(&id,&m))
{
break;
}
m->IncLoad();
httplib::Client client(m->_ip,m->_port);
LOG(INFO) << "选择主机成功,主机id: " << id << " 详情: " << m->_ip << ":" << m->_port << " 当前主机的负载是: " << m->Load() << "\n";
if(auto res = client.Post("/compile_and_run",judge_json,"application/json;charset=utf-8"))
{
if(res->status = 200)
{
*out_json = res->body;
LOG(INFO) << "请求编译运行服务成功" << '\n';
m->DecLoad();
break;
}
m->DecLoad();
}
else
{
LOG(WARNING) << "请求主机[" << id << "]" << "可能已下线" << '\n';
_load_blance.OfflineMachine(id);
_load_blance.ShowMachines();
}
}
}
oj_server.cpp
主要完成网络服务,路由功能
#include <iostream>
#include <jsoncpp/json/json.h>
#include "../comm/httplib.h"
#include "oj_control.hpp"
using namespace httplib;
void Usage(const std::string& proc)
{
std::cout << "Usage:" << proc << ' ' << "port" << std::endl;
}
using namespace ns_control;
Control* ptr_ctrl = nullptr;
void Recovery(int signo)
{
ptr_ctrl->Recovery();
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(1);
}
//设置某个信号,当对oj_server发出这个信号时,负载均衡将重启所有主机
signal(SIGQUIT, Recovery);
Server svr;
Control ctrl;
ptr_ctrl = &ctrl;
//路由功能
svr.Get("/all_questions", [&ctrl](const Request& req,Response& resp){
//获取所有题目
std::string html;
ctrl.AllQuestions(&html);
resp.set_content(html,"text/html;charset=utf-8");
});
svr.Get(R"(/question/(\d+))", [&ctrl](const Request& req,Response& resp){
std::string html;
std::string id = req.matches[1];
ctrl.OneQuestion(id,&html);
resp.set_content(html,"text/html;charset=utf-8");
});
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request& req,Response& resp){
std::string id = req.matches[1];
std::string in_json = req.body;
std::string out_json;
ctrl.Judge(id,in_json,&out_json);
resp.set_content(out_json,"application/json;charset=utf-8");
});
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", atoi(argv[1]));
return 0;
}
5. 项目扩展
- 基于注册和登陆的录题功能
- 业务扩展,自己写一个论坛,接入到在线OJ中
- 即便是编译服务在其他机器上,也其实是不太安全的,可以将编译服务部署在docker
- 目前后端compiler的服务我们使用的是http方式请求(仅仅是因为简单),但是也可以将我们的compiler服务,设计成为远程过程调用,推荐:rest_rpc,替换我们的httplib
- 功能上更完善一下,判断一道题目正确之后,自动下一道题目