模拟oj项目

项目介绍

本项目仿照leetcode的形式,自行模拟了一个在线编译系统,用户从网页上获取题目和题目的详细信息,提交代码,由本地服务器负责编译和运行,并把最终的结果返回给用户

基本框架
  1. 在线编译器
    在线编译器负责将用户在网页上提交的代码传给服务器,服务器对这一部分代码进行编译,运行,把运行后的结果返回给用户。
  2. 题目管理
    对本地的题库进行管理,使题库中所有oj的题目都能够和网页端进行交互,从网页端就能够获取到所有题目和某一题目的具体信息。
    在这里插入图片描述
项目技术
  1. 使用了第三方库httplib.h进行简单的请求和响应,库中给出了http的get,post等方法,设置监听和相应内容就搭建了简单的http服务器。
  2. 在传输请求和响应的这一部分引入了第三方库Json库,能够将请求护或者响应的正文进行序列化和反序列化,让代码更加客观,这里采用的形式为键值对的形式。
  3. 引入了ctemplate库实现了逻辑与界面分离,对界面进行渲染
    在这里插入图片描述

在线编译器模块的实现

核心功能

  1. 获取到要编译的代码并生成文件
  2. 调用g++进行编译,把编译结果也记录到临时文件中
  3. 运行可执行文件,执行测试用例代码,把运行结果也记录到临时文件中
  4. 把结果打包成最终的响应数据,并把数据返回给用户

在这里插入图片描述

准备工作

一、获取时间戳
秒级时间戳

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";
	}

二、编译和运行

  1. 将请求对象的code和测试用例的代码拼在一起,写入源文件代码中,
  2. 调用g++编译,调用fork()函数进行进程替换,生成可执行程序,如果编译出错,则把结果记录到编译失败文件中
  3. 调用可执行程序,把标准输入记录到文件中,然后把文件中内容重定向给可执行程序,可执行程序的标准输出和标准错误也要重定向到输出记录文件中
  4. 返回结果
	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;
  1. 创建子进程,父进程等待
			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);
				}
  1. 子进程进行进程替换,若出错,则将错误保存到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;
};

加载函数,文件中代码加载到内存中,

  1. 先加载oj_config.cfg文件
  2. 按行读取oj_config.cfg文件并解析
  3. 根据解析结果拼装成Question结构体
  4. 把结构体插入到哈希表中
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太麻烦,可以通过网页模板方式来解决

  1. 建立一个ctemplate对象
  2. 循环地王这个对象中添加一些子对象
  3. 每一个子对象再设置一些键值对
  4. 运行数据替换,生成最终的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次

$:行结尾

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值