项目开始之前
需要准备的第三方库
httplib g++版本必须得是4.9以上
ctemplate
boost: yum install boost-devel.x86_64
jsoncpp: yum install jsoncpp-devel.x86_64
项目分析
  我们可以看到一个在线OJ至少有 题目ID 名字 难易度 描述 测试用例 代码框架  然后我们试做一道题,可以大概看出一个在线OJ的流程, 1 在浏览器中展示题目 2 用户可以选择题目进行作答 3 后台获取用户代码 4 针对用户代码进行编译 5 后台运行代码 6 将运行结果进行打包 7 把结果返回给客户端。 因此我将这个流程分为两大模块题目管理模块、在线编译模块
两大模块
在线编译模块
整体思路
用户发来请求 将用户请求进行切分 改成json格式 分为用户代码“code”和用户输入“stdin” 然后调用编译模块进行编译 调用运行模块进行运行 把最终结果进行返回,构造json对象
按照&和=进行切分,Split是将boost库中的split进行封装
static void ParseBody(const std::string& body,
std::unordered_map<std::string, std::string>* params){
std::vector<std::string> kvs;
Split(body, "&", &kvs);
for(size_t i = 0; i < kvs.size(); ++i){
std::vector<std::string> kv;
Split(kvs[i], "=", &kv);
if(kv.size() != 2){
continue;
}
(*params)[kv[0]] = UrlDecode(kv[1]);
}
}
//解析body,获取到用户提交的代码
std::unordered_map<std::string, std::string> body_kv;
UrlUtil::ParseBody(req.body, &body_kv);
const std::string& user_code = body_kv["code"];
//构造json结构的参数
Json::Value req_json;
req_json["code"] = user_code + question.tail_cpp;
req_json["stdin"] = user_code;
Json::Value resp_json;
//调用编译模块进行编译
Compiler::CompileAndRun(req_json, &resp_json);
编译模块
整体思路 根据所传来的请求,生成对应的源代码文件,然后调用g++来编译文件,如果编译出错,重定向到文件中,然后调用可执行程序,同时将标准输出和标准错误也重定向到文件,最后把程序的最终结果进行返回。 根据请求对象生成源码文件
if (req["code"].empty()){
(*resp)["error"] = 3;
(*resp)["reason"] = "code empty";
LOG(ERROR) << "code empty" <<std::endl;
return false;
}
const std::string& code = req["code"].asString();
const std::string&file_name=WriteTmpFile(code,req["stdin"].asString());
我们需要生成多种文件,有源代码文件 、编译错误文件 、可执行程序文件 、标准输入文件 、标准输出文件 、标准错误文件 因此将它们用不同的后缀区分开,并放到同一路径下,传入名字,就可以生成对应的临时文件,例如源代码文件
static std::string SrcPath(const std::string& name){
return "./temp_files/" + name + ".cpp";
}
把代码写到文件中,并且分配一个唯一的名字
static std::string WriteTmpFile(const std::string& code,
const std::string& str_stdin){
static std::atomic_int id(0);
++id;
std::string file_name = "tmp_" + std::to_string(TimeUtil::TimeStamp())
+ "." + std::to_string(id);
FileUtil::Write(SrcPath(file_name), code);
FileUtil::Write(StdinPath(file_name), str_stdin);
return file_name;
}
注意这里的id需要转成原子操作,因为很有可能有多个用户同时发来请求
然后调用g++进行编译,创建子进程,然后重定向标准错误到编译错误文件中,最后用程序替换的方式执行编译命令
bool ret = Compile(file_name);
static bool Compile(const std::string& file_name){
char* command[20] = {0};
char buf[20][50] = {{0}};
for(int i = 0; i < 20; ++i){
command[i] = buf[i];
}
sprintf(command[0], "%s", "g++");
sprintf(command[1], "%s", SrcPath(file_name).c_str());
sprintf(command[2], "%s", "-o");
sprintf(command[3], "%s", ExePath(file_name).c_str());
sprintf(command[4], "%s", "-std=c++11");
command[5] = NULL;
int ret = fork();
if(ret > 0){
waitpid(ret, NULL, 0);;
}
else{
int fd = open(CompileErrorPath(file_name).c_str(),
O_WRONLY | O_CREAT, 0666);
if (fd < 0){
LOG(ERROR) << "open Compile file error" << std::endl;
exit(1);
}
dup2(fd, 2);
execvp(command[0], command);
exit(0);
}
struct stat st;
ret = stat(ExePath(file_name).c_str(), &st);
if(ret < 0){
LOG(INFO) << "Compile failed!" << file_name << std::endl;
return false;
}
LOG(INFO) << "Compile " << file_name << " OK!" << std::endl;
return true;
}
调用可执行程序,思路和上面几乎一样
int sig = Run(file_name);
static int Run(const std::string& file_name){
int ret = fork();
if(ret > 0){
int status = 0;
waitpid(ret, &status, 0);
return status & 0x7f;
}
else{
int fd_stdout = open(StdoutPath(file_name).c_str(),
O_WRONLY | O_CREAT, 0666);
dup2(fd_stdout, 1);
int fd_stderr = open(StderrPath(file_name).c_str(),
O_WRONLY | O_CREAT, 0666);
dup2(fd_stderr, 2);
execl(ExePath(file_name).c_str(),
ExePath(file_name).c_str(), NULL);
exit(0);
}
}
把最终结果进行返回,构造json对象,执行到这步,那说明没有什么问题,上面编译和运行出错时都会做错误处理,
这部分我没有贴出来
(*resp)["error"] = 0;
(*resp)["reason"] = "";
std::string str_stdout;
FileUtil::Read(StdoutPath(file_name), &str_stdout);
(*resp)["stdout"] = str_stdout;
std::string str_stderr;
FileUtil::Read(StderrPath(file_name), &str_stderr);
(*resp)["stderr"] = str_stderr;
LOG(INFO) << "Program " << file_name << " Done" << std::endl;
return true;
至此在线编译模块就告一段落了
题目管理模块
总体思路 获取所有的题目列表 指定题目的详细内容 调用在线编译模块完成代码的编译和运行 根据上述思路,那么我们就需要将题目描述组织起来。 首先基于文件的方式来完成题目的组织,创建一个行文本文件作为总的目录与入口文件 ,每一个题目对应一个目录,目录的名字就是题目的id 目录里面包含以下几个文件: 1 题目的详细描述 2. 代码框架 3. 代码测试用例 用一个结构体来表示单个的题目
struct Question{
std::string id;
std::string name;
std::string dir;
std::string diff;
std::string desc;
std::string header_cpp;
std::string tail_cpp;
};
数据存储
整体思路 先打开oj_config.cfg文件(这个文件就是上述的总目录与入口文件,每一行都是一个题目,我选择用\t来作为分割符) 然后按行读取oj_config.cfg文件,并且根据\t进行切分与解析 根据解析出来的结果拼装到上述的结构体中,header.cpp文件与tail.cpp文件分别存放着代码框架中的代码与题目的测试用例代码,在编译运行的时候需要将这两个拼接起来。 最后将结构体存入哈希表中
将文件中的数据加载到结构体中
bool Load(){
std::ifstream file("./oj_data/oj_config.cfg");
if(!file.is_open()){
return false;
}
std::string line;
while(std::getline(file, line)){
std::vector<std::string> tokens;
UrlUtil::Split(line, "\t", &tokens);
if(tokens.size() != 4){
LOG(ERROR) << "config file format error!\n";
continue;
}
Question q;
q.id = tokens[0];
q.name = tokens[1];
q.diff = tokens[2];
q.dir = tokens[3];
FileUtil::Read(q.dir + "/desc.txt", &q.desc);
FileUtil::Read(q.dir + "/header.cpp", &q.header_cpp);
FileUtil::Read(q.dir + "/tail.cpp", &q.tail_cpp);
_model[q.id] = q;
}
file.close();
LOG(INFO) << "Load" << _model.size() << " questions\n";
return true;
}
然后就是读取全部的题目与读取单个题目了
bool GetAllQuestions(std::vector<Question>* questions) const{
questions->clear();
for(const auto& kv : _model){
questions->push_back(kv.second);
}
return true;
}
bool GetQuestion(const std::string& id, Question* q) const{
const auto pos = _model.find(id);
if(pos == _model.end()){
return false;
}
*q = pos->second;
return true;
}
页面显示
这里我们根据数据生成对应的html文件,我们这里使用ctemplate来帮助我们完成。 这里我们需要三个html,一个是所有的题目的html,一个是单个题目的html,还有一个是返回的结果的html。 ctemplate可以使我们所要填充的数据和界面分离,相当于把我们需要计算填入的位置挖出一个空,然后在编写代码的时候,我们将其替换。
将所有题目页面进行渲染
static void RenderAllQuestions(const std::vector<Question>& all_questions,
std::string* html) {
ctemplate::TemplateDictionary dict("all_question");
for(const auto& question : all_questions){
ctemplate::TemplateDictionary* table_dict
= dict.AddSectionDictionary("question");
table_dict->SetValue("id", question.id);
table_dict->SetValue("name", question.name);
table_dict->SetValue("diff", question.diff);
}
ctemplate::Template* tpl;
tpl = ctemplate::Template::GetTemplate(
"./template/all_questions.html",
ctemplate::DO_NOT_STRIP);
tpl->Expand(html, &dict);
}
另外两个的渲染原理相同。
服务器
这里构建服务器,使用了第三方库httplib,同时用C++11中的正则表达式,来忽略转义字符,并且获取题号。最终完成三种页面的创建。
int main(){
OjModel model;
model.Load();
using namespace httplib;
Server server;
server.Get("/",[&model](const Request& req, Response& resp){
(void) req;
std::vector<Question> all_questions;
model.GetAllQuestions(&all_questions);
std::string html;
OjView::RenderAllQuestions(all_questions,&html);
resp.set_content(html,"text/html");
});
server.Get(R"(/question/(\d+))",[&model](const Request& req, Response& resp){
Question question;
model.GetQuestion(req.matches[1].str(),&question);
std::string html;
OjView::RenderQuestion(question,&html);
resp.set_content(html,"text/html");
});
server.Post(R"(/compile/(\d+))",[&model](const Request& req, Response& resp){
Question question;
model.GetQuestion(req.matches[1].str(),&question);
std::unordered_map<std::string,std::string> body_kv;
UrlUtil::ParseBody(req.body,&body_kv);
const std::string& user_code = body_kv["code"];
Json::Value req_json;
req_json["code"] = user_code + question.tail_cpp;
req_json["stdin"] = user_code;
Json::Value resp_json;
Compiler::CompileAndRun(req_json,&resp_json);
std::string html;
OjView::RenderResult(resp_json["stdout"].asString(),
resp_json["reason"].asString(),&html);
resp.set_content(html,"text/html");
});
以上就是全部内容啦 GitHub地址 
|