负载均衡式在线OJ
文章目录
- 项目介绍
- 所用技术与开发环境
- 所用技术
- 开发环境
- 项目框架
- compiler_server模块
- compiler编译功能
- comm/util.hpp 编译时的临时文件
- comm/log.hpp 日志
- comm/util.hpp 时间戳
- comm/util.hpp 检查文件是否存在
- compile_server/compiler.hpp 编译功能总体编写
- runner运行功能
- 资源设置
- comm/util.hpp 运行时的临时文件
- compile_server/runner.hpp 运行功能编写
- compile_server/compile_run.hpp 编译且运行
- comm/util.hpp 生成唯一文件名
- comm/uti.hpp 写入文件/读出文件
- 清理临时文件
- compiler_run模块的整体代码
- 本地进行编译运行模块的整体测试
- compiler_server模块(打包网络服务)
- compiler_server/compile_server.cc
- oj_server模块
- oj_server.cc 路由框架
- oj_model.hpp/oj_model2.hpp
- 文件版本
- 数据库版本:
- oj_view.hpp
- oj_control.cpp
项目介绍
项目是基于负载均衡的一个在线判题系统,用户自己编写代码,提交给后台,后台再根据负载情况选择合适的主机提供服务编译运行服务。
所用技术与开发环境
所用技术
- C++ STL 标准库
- Boost 准标准库(字符串切割)
- cpp-httplib 第三方开源网络库
- ctemplate 第三方开源前端网页渲染库
- jsoncpp 第三方开源序列化、反序列化库
- 负载均衡设计
- 多进程、多线程
- MySQL C connect
- Ace前端在线编辑器
- html/css/js/jquery/ajax
开发环境
- Centos 7 云服务器
- vscode
项目框架
compiler_server模块
模块结构
总体流程图
compiler编译功能
- 在运行编译服务的时候,
compiler
收到来自oj_server
传来的代码;我们对其进行编译 - 在编译前,我们需要一个
code.cpp
形式的文件; - 在编译后我们会形成
code.exe
可执行程序,若编译失败还会形成code.error
来保存错误信息; - 因此,我们需要对这些文件的后缀进行添加,所以我们创建
temp
文件夹,该文件夹用来保存code
代码的各种后缀; - 所以在传给编译服务的时候只需要传文件名即可,拼接路径由
comm
公共模块下的util.hpp
提供路径拼接
comm/util.hpp 编译时的临时文件
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
namespace ns_util
{
const std::string path = "./temp/";
// 合并路径类
class PathUtil
{
public:
static std::string splic(const std::string &str1, const std::string &str2)
{
return path + str1 + str2;
}
// cpp文件 + 后缀名
// file_name -> ./temp/xxx.cpp
static std::string Src(const std::string &file_name)
{
return splic(file_name, ".cpp");
}
// exe文件 + 后缀名
static std::string Exe(const std::string &file_name)
{
return splic(file_name, ".exe");
}
static std::string CompilerError(const std::string &file_name)
{
return splic(file_name, ".compile_error");
}
};
}
comm/log.hpp 日志
日志需要输出:等级、文件名、行数、信息、时间
#pragma once
#include <string>
#include "util.hpp"
namespace ns_log
{
using namespace ns_util;
// 日志等级
enum
{
INFO,
DEBUG,
WARNING,
ERROR,
FATAL,
};
inline std::ostream& Log(const std::string& level, const std::string& file_name, int line)
{
std::string log = "[";
log += level;
log += "]";
log += "[";
log += file_name;
log += "]";
log += "[";
log += std::to_string(line);
log += "]";
log += "[";
log += TimeUtil::GetTimeStamp();
log += "]";
std::cout << log;
return std::cout;
}
#define LOG(level) Log(#level, __FILE__, __LINE__)
}
获取时间利用的是时间戳,在util
工具类中编写获取时间戳的代码。利用操作系统接口:gettimeofday
comm/util.hpp 时间戳
class TimeUtil
{
public:
static std::string GetTimeStamp()
{
struct timeval _t;
gettimeofday(&_t, nullptr);
return std::to_string(_t.tv_sec);
}
};
进行编译服务的编写,根据传入的源程序文件名,子进程对stderr
进行重定向到文件compile_error
中,使用execlp
进行程序替换,父进程在外面等待子进程结果,等待成功后根据是否生成可执行程序
决定是否编译成功;
判断可执行程序是否生成,我们利用系统调用stat
来查看文件属性,如果有,则说明生成,否则失败;
comm/util.hpp 检查文件是否存在
class FileUtil
{
public:
static bool IsFileExists(const std::string path_name)
{
// 系统调用 stat 查看文件属性
// 获取属性成功返回 0
struct stat st;
if (stat(path_name.c_str(), &st) == 0)
{
return true;
}
return false;
}
};
compile_server/compiler.hpp 编译功能总体编写
#pragma once
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
// 只负责代码的编译
namespace ns_compiler
{
// 引入路径拼接
using namespace ns_util;
using namespace ns_log;
class Compiler
{
Compiler() {}
~Compiler() {}
public:
// 返回值:是否编译成功
// file_name : xxx
// file_name -> ./temp/xxx.cpp
// file_name -> ./temp/xxx.exe
// file_name -> ./temp/xxx.error
static bool Compile(const std::string &file_name)
{
pid_t id = fork();
if (id < 0)
{
LOG(ERROR) << "内部错误,当前子进程无法创建" << "\n";
return false;
}
else if (id == 0) // 子进程 编译程序
{
int _error = open(PathUtil::Error(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
if (_error < 0)
{
LOG(WARNING) << "没有成功形成 error 文件" << "\n";
exit(1);
}
// 重定向标准错误到 _error
dup2(_error, 2);
// g++ -o target src -std=c++11
execlp("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(id, nullptr, 0);
if (FileUtil::IsFileExists(PathUtil::Exe(file_name)))
{
LOG(INFO) << PathUtil::Exe(file_name) << "编译成功!" << "\n";
return true;
}
LOG(ERROR) << "编译失败!" << "\n";
return false;
}
}
};
}
runner运行功能
编译完成后,我们就可以执行可执行程序了,执行前,首先打开三个文件xxx.stdin
,xxx.stdout
,xxx.stderr
并将标准输入、标准输出和标准错误分别重定向到三个文件中。创建子进程来进行程序替换执行程序;每道题的代码运行时间和内存大小都有限制,所以在执行可执行程序之前我们对内存和时间进行限制。
资源设置
利用setrlimit
系统调用来实现
int setrlimit(int resource, const struct rlimit *rlim);
static void SetProcLimit(int cpu_limit, int mem_limit)
{
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_cur = cpu_limit;
cpu_rlimit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_CPU, &cpu_rlimit);
struct rlimit mem_rlimit;
mem_rlimit.rlim_cur = mem_limit * 1024;
mem_rlimit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS, &mem_rlimit);
}
comm/util.hpp 运行时的临时文件
static std::string Stdin(const std::string &file_name)
{
return splic(file_name, ".stdin");
}
static std::string Stdout(const std::string &file_name)
{
return splic(file_name, ".stdout");
}
// error文件 + 后缀名
static std::string Stderr(const std::string &file_name)
{
return splic(file_name, ".stderr");
}
compile_server/runner.hpp 运行功能编写
#pragma once
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/resource.h>
#include "../comm/util.hpp"
#include "../comm/log.hpp"
namespace ns_runner
{
using namespace ns_log;
using namespace ns_util;
class Runner
{
public:
Runner() {}
~Runner() {}
static void SetProcLimit(int cpu_limit, int mem_limit)
{
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_cur = cpu_limit;
cpu_rlimit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_CPU, &cpu_rlimit);
struct rlimit mem_rlimit;
mem_rlimit.rlim_cur = mem_limit * 1024;
mem_rlimit.rlim_max = RLIM_INFINITY;
setrlimit(RLIMIT_AS, &mem_rlimit);
}
// 指明文件名即可,无后缀、无路径
// 返回值:
// < 0 内部错误
// = 0运行成功,成功写入stdout等文件
// > 0运行中断,用户代码存在问题
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 || _stdout_fd < 0 || _stderr_fd < 0)
{
LOG(ERROR) << "内部错误, 标准文件打开/创建失败" << "\n";
// 文件打开失败
return -1;
}
pid_t id = fork();
if(id < 0)
{
close(_stdin_fd);
close(_stdout_fd);
close(_stderr_fd);
LOG(ERROR) << "内部错误, 创建子进程失败" << "\n";
return -2;
}
else if(id == 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(id, &status, 0);
LOG(INFO) << "运行完毕!退出码为: " << (status & 0x7F) << "\n";
return status & 0x7f;
}
}
};
}
compile_server/compile_run.hpp 编译且运行
- 用户的代码会以json串的方式传给该模块
- 给每一份代码创建一个文件名具有唯一性的源文件
- 调用上面的编译和运行执行该源文件
- 再把结果构建成json串返回给上层
json串的结构
comm/util.hpp 生成唯一文件名
当一份用户提交代码后,我们为其生成的源文件名需要具有唯一性。名字生成唯一性我们可以利用毫秒级时间戳加上原子性的增长计数实现
获取毫秒时间戳在TimeUtil
工具类中,生成唯一文件名在FileUtil
工具类中
static std::string GetTimeMs()
{
struct timeval _time;
gettimeofday(&_time, nullptr);
return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);
}
comm/uti.hpp 写入文件/读出文件
因为需要填写运行成功结果和运行时报错的结果,所以我们写一个写入文件和读出文件,放在FileUtil
中
static bool WriteFile(const std::string &target, const std::string &content)
{
std::ofstream out(target);
if (!out.is_open())
{
return false;
}
out.write(content.c_str(), content.size());
out.close();
return true;
}
// 根据路径文件进行读出
// 注意,默认每行的\\n是不进行保存的,需要保存请设置参数
static bool ReadFile(const std::string &path_file, std::string *content, bool keep = false)
{
// 利用C++的文件流进行简单的操作
std::string line;
std::ifstream in(path_file);
if (!in.is_open())
return "";
while (std::getline(in, line))
{
(*content) += line;
if (keep)
(*content) += "\n";
}
in.close();
return true;
}
清理临时文件
编译还是运行都会生成临时文件,所以可以在编译运行的最后清理一下这一次服务生成的临时文件
static void RemoveTempFile(const std::string &file_name)
{
// 因为临时文件的存在情况存在多种,删除文件采用系统接口unlink,但是需要判断
std::string src_path = PathUtil::Src(file_name);
if (FileUtil::IsFileExists(src_path))
unlink(src_path.c_str());
std::string stdout_path = PathUtil::Stdout(file_name);
if (FileUtil::IsFileExists(stdout_path))
unlink(stdout_path.c_str());
std::string stdin_path = PathUtil::Stdin(file_name);
if (FileUtil::IsFileExists(stdin_path))
unlink(stdin_path.c_str());
std::string stderr_path = PathUtil::Stderr(file_name);
if (FileUtil::IsFileExists(stderr_path))
unlink(stderr_path.c_str());
std::string compilererr_path = PathUtil::CompilerError(file_name);
if (FileUtil::IsFileExists(compilererr_path))
unlink(compilererr_path.c_str());
std::string exe_path = PathUtil::Exe(file_name);
if (FileUtil::IsFileExists(exe_path))
unlink(exe_path.c_str());
}
提供一个Start
方法让上层调用编译运行模块,参数是一个输入形式的json
串和一个要给上层返回的json
串
使用jsoncpp
反序列化,解析输入的json
串。调用形成唯一文件名的方法生成一个唯一的文件名,然后使用解析出来的代码部分创建出一个源文件,把文件名
交给编译模块进行编译,再把文件名和时间限制,内存限制传给运行模块运行,记录这个过程中的状态码。再最后还要序列化一个json
串返还给用户,更具获得状态码含义的接口填写状态码含义,根据状态码判断是否需要填写运行成功结果和运行时报错的结果,然后把填好的结果返还给上层。
最终调用一次清理临时文件接口把这一次服务生成的所有临时文件清空即可。
两个json
的具体内容
compiler_run模块的整体代码
#pragma once
#include <jsoncpp/json/json.h>
#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/util.hpp"
#include "../comm/log.hpp"
namespace ns_complie_and_run
{
using namespace ns_log;
using namespace ns_util;
using namespace ns_compiler;
using namespace ns_runner;
class ComplieAndRun
{
public:
static void RemoveTempFile(const std::string &file_name)
{
// 因为临时文件的存在情况存在多种,删除文件采用系统接口unlink,但是需要判断
std::string src_path = PathUtil::Src(file_name);
if (FileUtil::IsFileExists(src_path))
unlink(src_path.c_str());
std::string stdout_path = PathUtil::Stdout(file_name);
if (FileUtil::IsFileExists(stdout_path))
unlink(stdout_path.c_str());
std::string stdin_path = PathUtil::Stdin(file_name);
if (FileUtil::IsFileExists(stdin_path))
unlink(stdin_path.c_str());
std::string stderr_path = PathUtil::Stderr(file_name);
if (FileUtil::IsFileExists(stderr_path))
unlink(stderr_path.c_str());
std::string compilererr_path = PathUtil::CompilerError(file_name);
if (FileUtil::IsFileExists(compilererr_path))
unlink(compilererr_path.c_str());
std::string exe_path = PathUtil::Exe(file_name);
if (FileUtil::IsFileExists(exe_path))
unlink(exe_path.c_str());
}
// > 0:进程收到信号导致异常崩溃
// < 0:整个过程非运行报错
// = 0:整个过程全部完成
static std::string CodeToDesc(int status, const std::string &file_name)
{
std::string desc;
switch (status)
{
case 0:
desc = "运行成功!";
break;
case -1:
desc = "代码为空";
break;
case -2:
desc = "未知错误";
break;
case -3:
desc = "编译报错\n";
FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);
break;
case 6:
desc = "内存超过范围";
break;
case 24:
desc = "时间超时";
break;
case 8:
desc = "浮点数溢出";
break;
case 11:
desc = "野指针错误";
break;
default:
desc = "未处理的报错-status为:" + std::to_string(status);
break;
}
return desc;
}
/*
输入:
code: 用户提交的代码
input: 用户提交的代码对应的输入
cpu_limit:
mem_limit:
输出:
必有,
status: 状态码
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, 1);
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())
{
status_code = -1; // 代码为空
goto END;
}
// 毫秒级时间戳 + 原子性递增唯一值:来保证唯一性
file_name = FileUtil::UniqFileName();
if (!FileUtil::WriteFile(PathUtil::Src(file_name), code))
{
status_code = -2; // 未知错误
goto END;
}
if (!Compiler::Compile(file_name))
{
status_code = -3; // 未知错误
goto END;
}
run_result = Runner::Run(file_name, cpu_limit, mem_limit);
if (run_result < 0)
status_code = -2; // 未知错误
else if (run_result > 0)
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);
}
};
}
本地进行编译运行模块的整体测试
自己手动构造一个json串,编译、运行、返回结果json串
#include "compile_run.hpp"
using namespace ns_complie_and_run;
// 编译服务会被同时请求,保证代码的唯一性
int main()
{
// 客户端请求json
std::string in_json;
Json::Value in_value;
in_value["code"] = R"(
#include<iostream>
int main()
{
std::cout << "Hello, world!" << std::endl;
int *p = new int[1024 * 1024 * 20 ];
return 0;
}
)";
in_value["input"] = "";
in_value["cpu_limit"] = 1;
in_value["mem_limit"] = 10240;
Json::FastWriter writer;
in_json = writer.write(in_value);
std::cout << "in_json: " << std::endl << in_json << std::endl;
std::string out_json;
ComplieAndRun::Start(in_json, &out_json);
std::cout << "out_json: " << std::endl << out_json << std::endl;
return 0;
}
compiler_server模块(打包网络服务)
编译运行服务已经整合在一起了,接下来将其打包成网络服务即可
我们利用httplib
库将compile_run
打包为一个网络编译运行服务
compiler_server/compile_server.cc
- 使用了 httplib 库来提供 HTTP 服务
- 实现了一个编译运行服务器
- 通过命令行参数接收端口号
- 一个
POST /compile_and_run
主要的编译运行接口 - 接收
JSON
格式的请求体,包含:代码内容、输入数据、CPU 限制、内存限制
#include "compile_run.hpp"
#include "../comm/httplib.h"
using namespace ns_compile_and_run;
using namespace httplib;
void Usage(std::string proc)
{
std::cerr << "Usage : " << "\n\t" << proc << "prot" << std::endl;
}
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){
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 服务
return 0;
}
oj_server模块
oj_server.cc 路由框架
步骤:
- 服务器初始化:
- 创建 HTTP 服务器实例
- 初始化控制器
- 设置信号处理函数
- 请求处理:
- 接收 HTTP 请求
- 根据 URL 路由到对应处理函数
- 调用控制器相应方法
- 返回处理结果
- 判题流程:
- 接收用户提交的代码
- 通过控制器进行判题
- 返回判题结果
- 创建一个服务器对象
int main()
{
Server svr; // 服务器对象
}
- 获取所有题目列表
返回所有题目的HTML页面
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 number = req.matches[1];
std::string html;
ctrl.Question(number, &html);
resp.set_content(html, "text/html; charset=utf-8");
});
- 提交代码判题
处理用户提交的代码
返回 JSON 格式的判题结果
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){
std::string number = req.matches[1];
std::string result_json;
ctrl.Judge(number, req.body, &result_json);
resp.set_content(result_json, "application/json;charset=utf-8");
});
- 服务器配置和启动
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
- 维护一个全局控制器指针
Recovery 函数处理 SIGQUIT 信号,用于服务器恢复
static Control *ctrl_ptr = nullptr;
void Recovery(int signo)
{
ctrl_ptr->RecoveryMachine();
}
oj_model.hpp/oj_model2.hpp
整体架构为MVC模式
Model
层 由oj_model.hpp
文件版本和 oj_model2.hpp
数据库版本构成;
负责数据的存储和访问,提供了两种实现方式
- 基础数据结构设计
struct Question {
string number; // 题目编号
string title; // 题目标题
string star; // 难度等级
int cpu_limit; // CPU时间限制(秒)
int mem_limit; // 内存限制(KB)
string desc; // 题目描述
string header; // 用户代码模板
string tail; // 测试用例代码
};
- 存储方案设计
文件版本
优势:
简单直观,易于管理
适合小规模题库
方便备份和版本控制
劣势:
并发性能较差
扩展性有限
数据一致性难保证
目录结构
./questions/
├── questions.list # 题目基本信息
└── 1/ # 每个题目独立目录
├── desc.txt # 题目描述
├── header.cpp # 代码模板
└── tail.cpp # 测试用例
具体代码
#pragma once
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>
// 根据题目 list 文件,加载所有题目的信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question
{
string number; // 题目编号
string title; // 题目的标题
string star; // 题目的难度
int cpu_limit; // 题目的时间要求(s)
int mem_limit; // 题目的空间要求(KB)
string desc; // 题目的描述
string header; // 题目给用户的部分代码
string tail; // 题目的测试用例,和 header 形成完整代码提交给后端编译
};
const string questions_list = "./questions/questions.list" ;
const string question_path = "./questions/" ;
class Model
{
private:
// 【题号 < - > 题目细节】
unordered_map<string, Question> questions;
public:
Model()
{
assert(LoadQuestionList(questions_list));
}
bool LoadQuestionList(const string &question_list)
{
// 加载配置文件 : questions/questions.list + 题目编号文件
ifstream in(question_list);
if(!in.is_open())
{
LOG(FATAL) << "题目加载失败!请检查是否存在题库文件" << std::endl;
return false;
}
std::string line;
while(getline(in, line))
{
vector<string> tokens;
StringUtil::SplitString(line, &tokens, " ");
if(tokens.size() != 5)
{
LOG(WARNING) << "加载部分题目失败!请检查文件格式" << std::endl;
continue;
}
Question q;
q.number = tokens[0];
q.title = tokens[1];
q.star = tokens[2];
q.cpu_limit = atoi(tokens[3].c_str());
q.mem_limit = atoi(tokens[4].c_str());
string path = question_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.cpp", &(q.tail), true);
questions.insert({q.number, q});
}
LOG(INFO) << "加载题目成功!" << std::endl;
in.close();
return true;
}
bool GetAllQuestions(vector<Question> *out)
{
if(questions.size() == 0)
{
LOG(ERROR) << "用户获取题库失败!" << std::endl;
return false;
}
for(const auto &q : questions)
out->push_back(q.second);
return true;
}
bool GetOneQuestion(const string &number, Question *q)
{
const auto& iter = questions.find(number);
if(iter == questions.end())
{
LOG(ERROR) << "用户获取题库失败!题目编号为:" << number << std::endl;
return false;
}
(*q) = iter->second;
return true;
}
~Model()
{
}
};
}
数据库版本:
优势:
更好的并发性能
事务支持,保证数据一致性
表设计
CREATE TABLE oj_questions (
number VARCHAR(20) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
star VARCHAR(20) NOT NULL,
description TEXT,
header TEXT,
tail TEXT,
cpu_limit INT,
mem_limit INT
);
#pragma once
#include "../comm/log.hpp"
#include "../comm/util.hpp"
#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>
#include "include/mysql.h"
// 根据题目 list 文件,加载所有题目的信息到内存中
// model:主要用来和数据进行交互,对外提供访问数据的接口
namespace ns_model
{
using namespace std;
using namespace ns_log;
using namespace ns_util;
struct Question
{
string number; // 题目编号
string title; // 题目的标题
string star; // 题目的难度
string desc; // 题目的描述
string header; // 题目给用户的部分代码
string tail; // 题目的测试用例,和 header 形成完整代码提交给后端编译
int cpu_limit; // 题目的时间要求(s)
int mem_limit; // 题目的空间要求(K)
};
const std::string oj_question = "***";
const std::string host = "***";
const std::string user = "***";
const std::string passwd = "***";
const std::string db = "***";
const int port = 3306;
class Model
{
public:
Model()
{
}
bool QueryMysql(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(), db.c_str(), passwd.c_str(), port, nullptr, 0))
if(nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(),db.c_str(), port, nullptr, 0))
{
std::cout << mysql_error(my) << std::endl;
LOG(FATAL) << "连接数据库失败!!!" << "\n";
return false;
}
LOG(INFO) << "连接数据库成功!!!" << "\n";
// 设置链接的编码格式,默认是拉丁的
mysql_set_character_set(my, "utf8");
// 执行sql语句
//if(0 != mysql_query(&my, sql.c_str()))
if(0 != mysql_query(my, sql.c_str()))
{
LOG(WARNING) << sql << " execute error!" << "\n";
return false;
}
// 提取结果
//MYSQL_RES *res = mysql_store_result(&my);
MYSQL_RES *res = mysql_store_result(my);
// 分析结果
int rows = mysql_num_rows(res);// 获得行数
int cols = mysql_num_fields(res);// 获得列数
struct 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);
mysql_close(my);
return true;
}
bool GetAllQuestions(vector<Question> *out)
{
std::string sql = "select * from oj.";
sql += oj_question;
return QueryMysql(sql, out);
}
bool GetOneQuestion(const string &number, Question *q)
{
bool res = false;
std::string sql = "select * from oj.";
sql += oj_question;
sql += " where number = ";
sql += number;
vector<Question> result;
if (QueryMysql(sql, &result))
{
if (result.size() == 1)
{
*q = result[0];
res = true;
}
}
return res;
}
~Model()
{
}
};
}
- 接口设计
返回bool
表示操作成功与否
class Model {
public:
// 获取所有题目
bool GetAllQuestions(vector<Question> *out);
// 获取单个题目
bool GetOneQuestion(const string &number, Question *q);
};
oj_view.hpp
View
层 由oj_view.hpp
构成;
使用 ctemplate
库来进行 HTML
模板渲染
- 使用
TemplateDictionary
存储渲染数据- 使用
Template::GetTemplate
加载模板- 使用
Expand
方法进行渲染
- 获取所有题目的渲染;
void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
- 设置模板文件路径 (all_questions.html)
- 创建模板字典
- 遍历所有题目,为每个题目添加:
- 题号 (number)
- 标题 (title)
- 难度等级 (star)
- 渲染模板
- 获取单个题目的渲染;
void OneExpandHtml(const struct Question &q, std::string *html)
- 设置模板文件路径 (one_question.html)
- 创建模板字典并设置值:
- 题号 (number)
- 标题 (title)
- 难度等级 (star)
- 题目描述 (desc)
- 预设代码 (header)
- 渲染模板
#pragma once
#include <iostream>
#include <string>
#include <ctemplate/template.h>
// #include "oj_model.hpp"
#include "oj_model2.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<struct Question> &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. 获取被渲染的html
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
// 4. 开始完成渲染功能
tpl->Expand(html, &root);
}
void OneExpandHtml(const struct Question &q, std::string *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);
}
};
}
oj_control.cpp
Controller
层 由oj_control.hpp
构成;
- 提供服务的主机 Machine 类
表示提供编译服务的主机
包含 IP、端口、负载信息
提供负载管理方法(增加、减少、重置、获取负载)
// 提供服务的主机
class Machine
{
public:
std::string ip;
int port;
uint64_t load;
std::mutex *mtx;
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();
}
void 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;
}
};
- LoadBlance 类 (负载均衡模块)
管理多台编译服务器
维护在线/离线主机列表
主要功能:
从配置文件加载主机信息
智能选择负载最低的主机
处理主机上线/离线
// 负载均衡模块
class LoadBlance
{
private:
// 提供编译的主机
// 每一台都有自己下标
std::vector<Machine> machines;
// 所有在线的主机 id
std::vector<int> online;
// 所有离线的主机 id
std::vector<int> offline;
// 保证 LoadBlance 的数据安全
std::mutex mtx;
public:
LoadBlance()
{
assert(LoadConf(machine_path));
LOG(INFO) << "加载" << machine_path << "成功" << "\n";
}
~LoadBlance()
{
}
public:
bool LoadConf(const std::string &machine_list)
{
std::ifstream in(machine_list);
if (!in.is_open())
{
LOG(FATAL) << "主机加载失败" << "\n";
return false;
}
std::string line;
while (std::getline(in, line))
{
std::vector<std::string> tokens;
StringUtil::SplitString(line, &tokens, " ");
if (tokens.size() != 2)
{
LOG(WARNING) << "切分 " << line << "失败" << "\n";
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();
// 负载均衡的算法
// 1. 随机数法
// 2. 轮询 + hash
int online_num = online.size();
if (online_num == 0)
{
LOG(FATAL) << "所有的主机挂掉!在线主机数量: " << online_num
<< ", 离线主机数量: " << offline.size() << "\n";
mtx.unlock();
return false;
}
// 找负载最小的主机
*id = online[0];
*m = &machines[online[0]];
uint64_t min_load = machines[online[0]].Load();
for (int i = 0; i < online_num; i++)
{
uint64_t cur_load = machines[online[i]].Load();
if (cur_load < min_load)
{
min_load = cur_load;
*id = online[i];
*m = &machines[online[i]];
}
}
mtx.unlock();
return true;
}
void OfflineMachine(int id)
{
mtx.lock();
for(auto it = online.begin(); it != online.end(); it++)
{
if(*it == id)
{
machines[id].ResetLoad();
// 离线主机已经找到
online.erase(it);
offline.push_back(*it);
break;
}
}
mtx.unlock();
}
void OnlineMachine()
{
// 当所有主机离线后,统一上线
mtx.lock();
online.insert(online.end(), offline.begin(), offline.end());
offline.erase(offline.begin(), offline.end());
mtx.unlock();
LOG(INFO) << "所有的主机已上线" << "\n";
}
void ShowMachine()
{
mtx.lock();
std::cout << "在线主机列表: " << "\n";
for(auto &it : online)
{
std::cout << it << " ";
}
std::cout << std::endl;
std::cout << "离线主机列表: " << "\n";
for(auto &it : offline)
{
std::cout << it << " ";
}
std::cout << std::endl;
mtx.unlock();
}
};
- Control 类 (核心控制器)
整合 Model(数据层)和 View(视图层)
判题
// 控制器
class Control
{
private:
Model _model; // 提供后台数据
View _view; // 提供网页渲染
LoadBlance _load_blance;
public:
Control()
{
}
~Control()
{
}
public:
void RecoveryMachine()
{
_load_blance.OnlineMachine();
}
// 根据题目数据构建网页
// html 输出型参数
bool AllQuestions(string *html)
{
bool ret = true;
vector<struct Question> all;
if (_model.GetAllQuestions(&all))
{
sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){
return q1.number < q2.number;
});
_view.AllExpandHtml(all, html);
}
else
{
*html = "获取题目失败,形成题目列表失败";
ret = false;
}
return ret;
}
bool Question(const std::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;
}
void Judge(const std::string &number, const std::string in_json, std::string *out_json)
{
// 0.根据题目编号拿到题目细节
struct Question 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.重新拼接用户的代码 + 测试用例,形成新的代码
Json::Value compile_value;
compile_value["input"] = in_value["input"].asString();
compile_value["code"] = 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)
{
Machine *m = nullptr;
int id = 0;
if (!_load_blance.SmartChoice(&id, &m))
{
break;
}
LOG(INFO) << "选择主机成功, id = " << id << "详情:" << m->ip << ":" << m->port << "\n";
// 4.发起 http 请求,得到结果
Client cli(m->ip, m->port);
m->IncLoad();
if(auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8"))
{
// 5.将结果赋值给 out_json
if(res->status == 200)
{
*out_json = res->body;
m->DecLoad();
LOG(INFO) << "请求编译、运行成功" << "\n";
break;
}
m->DecLoad();
}
else
{
// 请求失败
LOG(ERROR) << "当前请求主机id = " << id << "详情:" << m->ip << ":" << m->port << " 该主机可能已经离线" << "\n";
_load_blance.OfflineMachine(id);
_load_blance.ShowMachine(); // for test
}
}
}
};
首先
- AllQuestions(): 获取并展示所有题目列表
- Question(): 获取并展示单个题目详情
其次Judge
1. 获取题目信息
2. 解析用户提交的代码
3. 组装完整的测试代码
4. 选择负载最低的编译主机
5. 发送HTTP请求到编译主机
6. 处理编译运行结果
然后负载均衡处理使用最小负载优先算法
基本编译运行提交代码已经实现,后续还会增加其他功能