项目介绍
本项目仿照leetcode的形式,自行模拟了一个在线编译系统,用户从网页上获取题目和题目的详细信息,提交代码,由本地服务器负责编译和运行,并把最终的结果返回给用户
基本框架
- 在线编译器
在线编译器负责将用户在网页上提交的代码传给服务器,服务器对这一部分代码进行编译,运行,把运行后的结果返回给用户。 - 题目管理
对本地的题库进行管理,使题库中所有oj的题目都能够和网页端进行交互,从网页端就能够获取到所有题目和某一题目的具体信息。
项目技术
- 使用了第三方库httplib.h进行简单的请求和响应,库中给出了http的get,post等方法,设置监听和相应内容就搭建了简单的http服务器。
- 在传输请求和响应的这一部分引入了第三方库Json库,能够将请求护或者响应的正文进行序列化和反序列化,让代码更加客观,这里采用的形式为键值对的形式。
- 引入了ctemplate库实现了逻辑与界面分离,对界面进行渲染
在线编译器模块的实现
核心功能
- 获取到要编译的代码并生成文件
- 调用g++进行编译,把编译结果也记录到临时文件中
- 运行可执行文件,执行测试用例代码,把运行结果也记录到临时文件中
- 把结果打包成最终的响应数据,并把数据返回给用户
准备工作
一、获取时间戳
秒级时间戳
class TimeStamp{
static int64_t TimeStamp(){
//秒级时间戳
struct timeval tv;
::gettimeofday(&tv,NULL);
return tv.tv_sec;
};
微秒级时间戳
static int64_t TimeStampMS(){
//微秒级时间戳
struct timeval tv;
::gettimeofday(&tv,NULL);
return tv.tv_sec*1000+tv.tv_usec/1000;
};
};
二、打印日志
日志等级划分
enum Level{
INFO,
WARNING;
ERROR,
FATAL,
};
打印日志
inline std::osream& Log(Level level,const std::string& file_name,int line_num){
//打印日志
std::string prefix = "[";
if(level==I){
prefix += "I";
}else if(level==W){
prefix+="W";
}else if(level==E){
prefix += "E";
}else if(level==F){
prefix += "F";
}
prefix += std::to_string(TimeUtil::TimeStampMS());
prefix += " ";
prefix += file_name;
prefix += ":";
prefix += "]";
std::out << prefix;
return std::out;
}
三、文件操作
对文件的读操作
class FileUtil{
//对文件的读写操作
public:
static bool Read(const std::string& file_path,std::string* content){
content->clear();
std::ifstream file(file_path.c_str());
if(!file_isopen()){
return false;
}
std::string line;
while(std::getline(file,line)){
*content += line +"\n";
}
file.close();
return true;
}
对文件的写操作
static bool Write(std::string& file_path,std::string* content){
std::ofstream file(file_path.c_str());
if(!file_isopen){
return false;
}
file.write(content.c_str(),content.size());
file.close();
return close;
}
};
四、切分字符
class StringUtil{
public:
static void Split(const std::string& input,const std::string&
split_char,std::vector<std::string>*output){ boost::split(*output,input,boost::is_any_of(split_char),
boost::token_compress_on);
}
};
五、解码操作
将从网页上获取的请求进行解码
class UrlUtil{
//解析模块
public:
static void ParseBody(const std::string& body,std::unordered_map
<std::string,std::string>*params){
1.对body字符进行切分,切分成键值对的形式
std::vector<string> kvs;
//a.先按&符号切分
StringUtil::Split(&body,"&",&kvs);
for(size_t i=0;i<kvs.size();++i){
std::vector<std::string> kv;
//b.再按=进行切分
StringUtil::Split(kv[i],"=",&kv);
if(kv.size()!=2){
continue;
}
//2.对键值对进行解码
(*params) static std::string UrlDecode
(const std::string& str);
[kv[0]] =UrlDecode(kv[1]);
}
static std::string UrlDecode(const std::string& str)
{
std::string strTemp = "";
size_t length = str.length();
for (size_t i = 0; i < length; i++)
{
if (str[i] == '+') strTemp += ' ';
else if (str[i] == '%')
{
assert(i + 2 < length);
unsigned char high = FromHex((unsigned
char)str[++i]);
unsigned char low = FromHex((unsigned
char)str[++i]);
strTemp += high*16 + low;
}
else strTemp += str[i];
}
return strTemp;
}
}
};
编译模块
一、文件路径处理
源代码文件
class Compiler{
//编译和运行模块
public:
//文件路径处理
static std::string& SrcPath(const std::string& name){
//源代码文件
return "/temp_files"+name+".cpp";
}
编译错误文件
static std::string& CompileErrorPath(const std::string& name){
//编译失败文件
return "/temp_files"+name+".compile_error";
}
可执行程序文件
static std::string& ExePath(const std::string& name){
//可执行程序文件
return "/temp_files"+name+".exe";
}
标准输入文件
static std::string & StdinPath(const std::string& name){
//标准输入文件
return "/temp_files"+name +".stdin";
}
标准输出文件
static std::string& StdoutPath(const std::string& name){
//标准输出文件
return "/temp_files" + name +".stdout";
}
标准错误文件
static std::string& StderrPath(const std::string& name){
//标准错误文件
return "/temp_files" +name +".stderr";
}
二、编译和运行
- 将请求对象的code和测试用例的代码拼在一起,写入源文件代码中,
- 调用g++编译,调用fork()函数进行进程替换,生成可执行程序,如果编译出错,则把结果记录到编译失败文件中
- 调用可执行程序,把标准输入记录到文件中,然后把文件中内容重定向给可执行程序,可执行程序的标准输出和标准错误也要重定向到输出记录文件中
- 返回结果
static bool ComepileAndRun(const Json::Value& req,Json::Value& resp){
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();
std::string file_name =
WriteTmpFile(code,req["stdin"].asString());
bool ret = Compile(file_name);
if(!ret){
(*resp)["error"] = 1;
std::string reason;
FileUtil::Read(CompileErrorPath(file_name),&reason);
(*resp)["reason"] = reason;
LOG(ERROR)<<"Compile Failed" << std::endl;
return false;
}
int sig = Run(file_name);
if(sig!=0){
(*resp)["error"] = 2;
(*resp)["reason"]="Program exit by signo:"
+ std::to_string(sig);
LOG(INFO)<<"Program exit by signo"<<std::endl;
return false;
}
(*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;
}
把用户提交代码和测试用例代码写入文件中,给这次请求分配一个唯一的名字,用过返回值返回,分配名字形如:
tmp_1550976161.1
tmp_1550976161.2
这是为了解决一旦出现同一时间请求的情况无法区分对应的请求,采用了计数器的方法,来记录所有的请求,具有原子性,这里没有使用到锁的原因是因为锁的开销太大。
private:
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;
}
编译代码:
5. 构造出编译指令
g++ oj_server.cpp -o oj_server.exe -std=c++11
static bool Compile(const std::string& file_name){
char* commond[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",SrdPath(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_CEART,0666);
if(fd<0){
LOG(ERROR)<<"open Compile file error"<< std::endl;
exit(0);
}
- 子进程进行进程替换,若出错,则将错误保存到stderr文件中
dup(fd,2);
execvp(command[0],command);
exit(0);
}
如何知道是否编译成功?
通过star()函数来判断编译文件是否存在,若ret<0说明编译成功,否则编译失败
struct stat st;
ret = stat(ExePath(file_name).c_str(),&st);
if(ret<0){
LOG(INFO)<<"compile failed"<<std::endl;
return false;
}
LOG(INFO)<<"compile"<<std::endl;
return true;
}
运行代码,
创建子进程,父进程进行等待,将标准输入,标准输出,标准错误进行重定向,子进程进行进程替换
static int Run(std::string& file_name){
int ret = fork();
if(ret>0){
int status=0;
waitpid(ret,status,0);
return status & 0x7f;
}
else{
int fd_stdin = open(StdinPath(file_name).c_str(),
O_RDONLY);
dup2(fd_stdin,0);
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),NULL);
exit(0);
}
}
};
题目管理模块
管理当前系统上所有oj题目,能够和网页端进行交互,获取到所有的题目列表以及某个题目详情。
本项目是以文件形式来加载题目的,在目录中约定好题目的格式,详细信息,以及所存放的目录,形如:
1 回文数 简单 /oj_data/1
题目的框架,详情描述,测试用例则存储在/oj_data/1这个目录中,
一、题目描述
题目id,名字,难度,题目详情,题目描述,代码框架,测试用例,
class Question{
std::string id;
std::string name;
std::string dir;
std::string star;
std::string desc;
std::string header_cpp;
std::string tail_cpp;
};
加载函数,文件中代码加载到内存中,
- 先加载oj_config.cfg文件
- 按行读取oj_config.cfg文件并解析
- 根据解析结果拼装成Question结构体
- 把结构体插入到哈希表中
class OjModel{
private:
map<std::string,Question> model_;
public:
bool Load(){
std::ifstream file("./oj_data/oj_config.cfg");
if(!file_open()){
return false;
}
std::string line;
while(std::getline(file,line)){
std::vector<std::string> tokens;
StringUitl::Split(line,"&",&tokens);
if(tokens.size()==4){
LOG(ERROR)<<"config file format error\n";
continue;
}
Question q;
q.id=tokens[0];
q.name = tokens[1];
q.star = token[2];
q.dir = tokens[3];
FileUtil::Read(q.dir+"/desc.txt",q.desc);
FileUtil::Read(q.dir+"/header.cpp",q.desc);
FileUtil::Read(q.dir+"/tail.cpp",q.desc);
model_[q.id]=q;
}
file.close();
LOG(INFO)<<"Load"<< model.size()<<"question\n";
return true;
}
获得所有题目
bool GetAllQuestion(std::vector<Question>* question){
question->clear();
for(const auto& kv:model_){
question.push_back(kv.second);
}
return true;
}
获得某个题目
bool GetQuestion(){
auto pos = model_.find(id);
if(pos==model_.end()){
return false;
}
*q=pos->second;
return true;
}
};
页面渲染模块
把所有题目数据以html的形式转换成题目列表
在c++中直接通过拼接字符方式造html太麻烦,可以通过网页模板方式来解决
- 建立一个ctemplate对象
- 循环地王这个对象中添加一些子对象
- 每一个子对象再设置一些键值对
- 运行数据替换,生成最终的html
class OjView{
public:
static void RenderAllQuestion(const std::vector<Question>& all_question,std::string* html){
ctemplate::TemplateDictionary dict("all_question");
for(const auto& question:all_question){
ctemplate::TemplateDictionary* table_dict=dict.AddSectionDictionary("question");
table_dict->SetValue("id",question.id);
table_dict->SetValue("name",question.name);
table_dict->SetValue("star",question.star);
}
ctemplate::Template* tpl;
tpl = ctemplate::Template::GetTemplate("./template/all_question.html",
ctemplate::DO_NOT_STRIP);
tpl->Expand(html,&dict);
}
static void RenderQuestion(const Question& question,std::string* html){
ctemplate::TemplateDictionary dict("question");
dict.SetValue("id",question.id);
dict.SetValue("name",question.name);
dict.SetValue("star",question.star);
dict.SetValue("desc",question.desc);
dict.SetValue("header",question.header_cpp);
ctemplate::Template* tpl;
tpl = ctemplate::Template::GetTemplate("./template/question.html",
ctemplate::DO_NOT_STRIP);
tpl->Expand(html,&dict);
}
};
oj_server.cc模块
#include"httplib.h"
#include"util.hpp"
#include"oj_model.hpp"
#include"oj_view.hpp"
#include<jsoncpp/json/json.h>
#include"compile.hpp"
int main(){
OjModel model;
model.Load();
using namespace httplib;
Server server;
server.Get("/all_question",[&model](const Request& req,Response& resp){
(void)req;
std::vector<Question> all_questions;
model.GetAllQuestions(&all_questions);
std::string html;
OjView::RenderAllQuestion(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("/compile",[](const Request& req,Response& resp){
//1.根据id获取到题目信息
Question question;
model.GetQuestion(req.matches[1].str(),&question);
//2.解析body,获取到用户提交的代码
std::unordered_map<std::string,std::string> body_kv;
UrlUtil::ParseBody(req.body,&body_kv);
const std::string& user_code = body_kv["code"];
//3.构造JSON结构的参数
Json::Value req_json;
//用户提交的代码+测试用例的代码
req_json["code"] = user_code + question.tail_cpp;
Json::Value resp_json;
//4.调用编译模块进行编译
Compiler::CompileAndRun(req_json,&resp_json);
//5.根据编译结果构造成最终的网页
std::string html;
OjView::RenderResult(resp_json["stdout"].asString(),
resp_json["reason"].asString(),&html);
});
server.set_base_dir("./wwwroot");
server.listen("0.0.0.0",9092);
return 0;
}
R"()",c++引入的语法,原始字符串,忽略字符串中的转移字符,
/d+,正则表达式,用特殊符号来表示字符串满足的条件,指在用户输入题目id的时候,不止输入一个数字
表示行开头、至少出现一bai次数字du、(任意字符和至少出现一次数字)出现1次或0次、行结尾。
^:行开头
\d:数字
+:出现至少1次
.:任意字符,除换行和回车之外
?:出现0或1次
(.\d+)?:括号里内出现0或1次
$:行结尾