目录
View 使用后端数据对前端页面进行渲染,获取渲染之后的html
Control 逻辑控制模块,oj_server.cc直接调用Control提供的方法
总览
简述
此项目,旨在设计出一款类似于leetcode的在线OJ平台,通过浏览器客户端获取服务端提供的http服务。主要提供的功能有两个: 1. 获取题目列表 2. 获取指定题目的详细信息,并获取一个在线编辑器,用于提交代码,提交后显示出此题的提交结果: 编译错误 / 运行时出错(程序崩溃) / 编译运行成功但没有通过测试用例 / 编译运行成功且通过测试用例
项目核心的三个模块
1. common 公共模块 : 用于提供一些第三方库文件,一些工具类,工具方法。
2. compile_server 编译与运行模块 : 通过提供网络服务的方式,获取通过网络请求发送来的源代码,仅提供编译运行功能,并将编译运行的结果通过网络返回回去。
3. oj_server 在线OJ模块 : 提供http服务,如获取题目列表,进入指定题目的OJ界面,负载均衡。
综上,common就是提供一些工具方法,用于另外两个模块使用。而oj_server相当于是一个在线oj平台的后端服务器,提供http服务,当用户获取列表,编写指定题目之后,将前端代码通过http请求发送给oj_server时,oj_server通过网络请求,负载均衡式地使用compile_server提供的编译运行服务,获取编译运行结果,再返回给用户的前端界面。
项目宏观结构
compile_server 编译与运行服务
总览分析
compile_server模块旨在实现出一个通过网络请求的方式,提供编译并运行服务的后端。在此项目中此功能用于服务oj_server编译并运行用户提交的oj代码。注意:此模块,仅提供编译运行功能,提交的代码是否通过测试用例,此模块不负责。
compiler.hpp提供编译服务,runner.hpp提供运行服务。compile_run.hpp整合下方两个功能,提供一个编译并运行功能的接口。而compile_server.cc通过网络请求,获取代码,调用compile_run.hpp提供的的接口。
这里有一个注意点:将来,oj_server模块,将用户提交的代码通过网络请求给compile_server,大体思路是:此模块创建临时文件,将代码写入文件中,也就成为了源文件。下方对源文件进行编译运行,此处文件名等并不重要,我们的目的是编译运行获取结果。那么,如何让compiler.hpp和runner.hpp对正确的源文件进行编译,和对正确的可执行程序进行运行呢?只要统一他们的文件名即可(不包含后缀)
compiler.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "./../common/util.hpp"
#include "../common/log.hpp"
// 只负责进行代码的编译
namespace ns_compiler
{
using namespace ns_util;
using namespace ns_log;
class Compiler
{
public:
// 返回值: 编译成功true,编译失败false
// 输入参数: 编译的文件名(不带路径,不带后缀)
static bool compile(const std::string &file_name)
{
// 程序替换为g++编译,成功则没有输出,可执行程序生成
// 失败则g++会向标准错误中输出错误信息,即编译失败的原因
pid_t pid = fork();
if (pid < 0)
{
LOG(ERROR) << "内部错误,编译时创建子进程失败" << std::endl;
return false;
}
else if (pid == 0)
{
// 子进程,进行程序替换g++,编译指定文件
umask(0);
int _compile_error_fd = open(PathUtil::CompileError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
if (_compile_error_fd < 0)
{
LOG(WARNING) << "内部错误, 编译时没有生成stderr文件" << std::endl;
exit(1); // 其实父进程不关心
}
// 程序替换,并不影响进程的文件描述符表
dup2(_compile_error_fd, 2);
execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),
PathUtil::Src(file_name).c_str(), "-D", "COMPILE_ONLINE", "-std=c++11", nullptr);
exit(2); // 其实父进程不关心
}
else
{
// 父进程
waitpid(pid, nullptr, 0); // 不关心子进程的退出结果,只关心是否编译成功(exe是否生成)
// std::cout << "flag" << std::endl;
if (FileUtil::IsFileExists(PathUtil::Exe(file_name)) == true)
{
// 可执行程序已生成,编译成功
// std::cout << "flag 2" << std::endl;
LOG(INFO) << PathUtil::Src(file_name) << "编译成功!" << std::endl;
return true;
}
}
// 可执行程序没有生成,g++错误信息已打印到CompileError文件中。
return false;
}
};
}
Compiler类提供编译功能接口,对参数传来的指定的文件(不包含后缀,路径),进行编译。编译成功返回true,对应的可执行生成。失败返回false,编译错误原因在对应的CompileError文件中。
思路:要编译,肯定要程序替换为g++,思路是让子进程进行程序替换,替换为g++,父进程通过对应的可执行是否生成来判断是否编译成功。编译失败时,g++会向标准错误中输出错误信息,标准错误原本为显示器,现通过dup2系统调用,将其重定向至CompileError文件中,则若编译失败,失败原因会在对应的CompileError文件中保存。
注意: 在编译运行模块中,会出现很多临时文件,也就是对应某一次编译运行请求所生成的源文件,编译错误文件,可执行等等。此处的策略为,在compile_server编译运行模块下下创建一个temp目录,用于存储临时文件。
runner.hpp
#pragma once
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <fcntl.h>
#include "../common/util.hpp"
#include "../common/log.hpp"
namespace ns_runner
{
using namespace ns_util;
using namespace ns_log;
// 只负责运行编译好的可执行
class Runner
{
public:
/* 返回值 > 0,oj程序运行异常,收到了信号,返回值为信号编号
* 返回值 = 0,oj程序运行成功,标准输出和标准错误信息在对应的文件中
* 返回值 < 0,内部错误。如打开文件失败,创建子进程执行oj程序时失败。
* cpu_limit: file_name程序运行时,可以使用的CPU资源上限(时间,秒)
* mem_limit: file_name程序运行时,可以使用的内存资源上限(KB)
*/
static int run(const std::string file_name, int cpu_limit, int mem_limit)
{
std::string _excute = PathUtil::Exe(file_name);
std::string _stdin = PathUtil::Stdin(file_name);
std::string _stdout = PathUtil::Stdout(file_name);
std::string _stderror = PathUtil::Stderror(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); // OJ程序的输出结果
int _stderror_fd = open(_stderror.c_str(), O_CREAT | O_WRONLY, 0644); // OJ程序的运行时错误信息
if (_stdin_fd < 0 || _stdout_fd < 0 || _stderror_fd < 0)
{
LOG(ERROR) << "内部错误,运行时打开文件失败" << std::endl;
return -1; // 代表打开文件失败
}
pid_t pid = fork();
if (pid < 0)
{
LOG(ERROR) << "运行时创建子进程失败" << std::endl;
close(_stdin_fd);
close(_stdout_fd);
close(_stderror_fd);
return -2; // 代表创建子进程失败
}
else if (pid == 0)
{
// 子进程进行程序替换,执行可执行程序
dup2(_stdin_fd, 0);
dup2(_stdout_fd, 1);
dup2(_stderror_fd, 2);
SetProcLimit(cpu_limit, mem_limit);
execl(_excute.c_str(), _excute.c_str(), nullptr);
exit(1);
}
else
{
close(_stdin_fd);
close(_stdout_fd);
close(_stderror_fd);
// 父进程获知程序的执行情况,仅关心成功执行or异常终止
// 对于成功执行之后的执行结果并不关心,是上层的任务,需要根据测试用例判断
int status = 0;
waitpid(pid, &status, 0);
LOG(INFO) << "OJ题运行完毕, 退出信号: " << (status & 0x7F) << std::endl;
return status & 0x7F; // 将子进程的退出信号返回(并非退出码)
}
}
// 设置进程占用资源大小的接口(CPU资源,内存资源)mem_limit的单位为KB
static void SetProcLimit(int _cpu_limit, int _mem_limit)
{
// 设置进程占用CPU时长限制
struct rlimit cpu_rlimit;
cpu_rlimit.rlim_max = RLIM_INFINITY;
cpu_rlimit.rlim_cur = _cpu_limit;
setrlimit(RLIMIT_CPU, &cpu_rlimit);
// 设置进程占用内存资源限制
struct rlimit mem_limit;
mem_limit.rlim_max = RLIM_INFINITY;
mem_limit.rlim_cur = _mem_limit * 1024; // 转化为KB
setrlimit(RLIMIT_AS, &mem_limit);
}
};
}
参数:要执行的可执行程序的文件名(不包含路径,后缀),cpu限制,mem限制(OJ题一般会有时间空间限制)。程序执行结果,通过返回值进行判断,大于0,为程序异常终止(收到了信号),等于0,为程序运行成功,此时,标准输出和标准错误信息都在对应的文件中。小于0,为内部错误,如打开文件失败,创建子进程失败等等...
这里思路和compiler.hpp相似,让子进程进行程序替换,执行可执行(在temp目录下,也就是编译模块编译好的可执行),父进程通过waitpid,阻塞式等待子进程退出结果,通过信号判断其运行情况。若崩溃,则信号大于0,此时不考虑标准输出和标准错误。若等于0,则信号为0,表示程序运行成功,此时标准输出和标准错误在对应文件中。这里的关键其实还是,重定向,也就是子进程程序替换为可执行前要dup2,将标准输入输出错误进行重定向(此处不考虑输入),对应的临时文件在temp目录下。
compile_run.hpp
// 需定制通信协议
#pragma once
#include "../compile_server/compiler.hpp"
#include "../compile_server/runner.hpp"
#include "../common/log.hpp"
#include "../common/util.hpp"
#include "jsoncpp/json/json.h"
namespace ns_compile_run
{
using namespace ns_compiler;
using namespace ns_runner;
using namespace ns_log;
using namespace ns_util;
class CompileAndRun
{
public:
/***************************************
* 应用层协议定制:
* 输入json串:
* code: 用户提交的OJ代码
* input: 用户提交的代码对应的输入(不做处理)
* cpu_limit: OJ程序的时间要求
* mem_limit: OJ程序的空间要求
*
* 输出json串:
* 必填:
* status: 状态码
* reason: 请求结果(状态码描述)
* 选填:(当OJ程序编译且运行成功时)
* stdout: OJ程序运行完的标准输出结果
* stderr: OJ程序运行完的标准错误结果
* (若编译且运行成功out_json中才会有stdout和stderr字段)
* 参数:
* in_json: {"code": "#include...", "input": "","cpu_limit":1, "mem_limit":10240}
* out_json: {"status":"0", "reason":"","stdout":"","stderr":"",}
* ************************************/
static void execute(const std::string &in_json, std::string *out_json)
{
// 随便写写:输入的json串中有代码,输入,时间空间限制等等
// 提取出代码,写入到源文件中。
// 编译源文件,看编译结果
// 若编译成功,则运行可执行
// 若一切顺利则status状态码为0,对应的输出结果也写入
// 若某一步出现了错误,则status设置为对应的数字
// reason也写好
// 对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 = 0; // 状态码,最终要写入到out_json中
std::string file_name;
int run_result = 0;
if (code.empty())
{
status = -1; // 用户输入的OJ代码为空(非服务端问题)
goto END;
}
file_name = FileUtil::UniqueFileName();
// 形成临时源文件,代码为传来的code,这个文件名不重要,唯一即可
// 我们的目的是编译并运行这份代码
if (FileUtil::WriteFile(PathUtil::Src(file_name), code) == false)
{
status = -2; // 未知错误
goto END;
}
if (Compiler::compile(file_name) == false)
{
status = -3; // 编译失败,跳过后面的运行
goto END;
}
run_result = Runner::run(file_name, cpu_limit, mem_limit);
if (run_result < 0)
{
status = -2; // 未知错误(不管run内部是打开文件失败还是创建子进程失败,统一称之为内部错误,即服务端错误)
}
else if (run_result > 0)
{
status = run_result; // 运行错误,程序崩溃,此时status为程序退出信号
}
else
{
status = 0; // 运行成功(且编译成功)
}
END:
Json::Value out_value;
out_value["status"] = status; // 状态码
out_value["reason"] = StatusToDesc(status, file_name); // 状态码描述
if (status == 0)
{
// 只有当编译且运行成功时,才有stdout stderr字段
std::string _stdout;
FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true); // 读取OJ程序的标准输出结果
out_value["stdout"] = _stdout;
std::string _stderr;
FileUtil::ReadFile(PathUtil::Stderror(file_name), &_stderr, true); // 读取OJ程序的标准错误结果
out_value["stderr"] = _stderr;
}
Json::StyledWriter writer;
*out_json = writer.write(out_value); // 将out_value进行序列化
// RemoveTempFile