项目功能:相似于LeetCode及牛客网的在线答题系统,浏览器请求服务器能够得到全部试题信息,包括题目编号、题目名称、题目难度,用户能够选择某一道题进行做答,服务器返回题目描述信息以及预约义好的代码模板,用户编写完代码后浏览器将用户提交的代码返回给服务器,服务器将用户提交的代码与预约义好的题目测试用例结合编译运行,并将结果返回给浏览器告知用户经过率。
上述的功能依赖于几个模块相互配合实现,以下图
接下来就对这几个模块详细的介绍html
class Exam { public: std::string _id; std::string _name; std::string _path; std::string _star;//试题难度 };
日志模块主要负责将执行过程当中遇到的错误以及提示信息写入日志文件中前端
const char * level[]={ "INFO", "WARNING", "ERROR", "FATAL", "DEBUG" };
//提供一个获取当前时间的方法 static void GetNowTime(std::string * nowtime){ time_t tm; time(&tm);//获取到距离1970年时间的秒数 struct tm * st = localtime(&tm);//将秒数转化为年月日时间 //用st填充nowtime std::stringstream ss; ss<<st->tm_year+1900<<" "<<st->tm_mon+1<<" "<<st->tm_mday<<" "<<st->tm_hour<<":"<<st->tm_min<<":"<<st->tm_sec; *nowtime=ss.str(); }
//获取时间戳 static int64_t GetTimeStamp() { struct timeval tv; gettimeofday(&tv, NULL); return tv.tv_sec; }
日志文件以下图,记录了每一条关键信息,方便后期问题的定位node
inline static void Log(LogLevel lev, const char* file, int line, const std::string& logmsg){ //将日志信息写入日志文件中 std::string level_info = level[lev]; std::string TimeStamp; GetNowTime(&TimeStamp); //[时间 日志等级 文件:行号] 具体的日志信息 //std::cout << "[" << TimeStamp << " " << level_info << " " << file << ":" //<< line << "]" << logmsg << std::endl; //构造一个字符串直接写到文件中 std::stringstream ss; ss<<"["<<TimeStamp<<" "<<level_info<<" "<<file<<":"<<line<<"]"; ss<<logmsg; ss<<std::endl; int fd = open("./LogFile",O_RDWR|O_APPEND); write(fd,ss.str().c_str(),ss.str().size()); close(fd); }
可是在提交日志的时候不须要每一次都将文件名和行号传入,故将其实现为一个宏,调用时只须要传入日志等级以及具体描述信息便可c++
#define LOG(lev,msg) Log(lev,__FILE__,__LINE__,msg)
//经过传入的vec使用谷歌模板技术将str填充好 static void DrowAllExam(std::string * str,std::vector<Exam> & vec){ //创建一个叫作allques的字典 ctemplate::TemplateDictionary dict("all_questions"); for(const auto &e:vec){ //构建一个子字典存放每一条题目信息 //ctemplate::TemplateDictionary* section_dict = dict.AddSectionDictionary("question"); ctemplate::TemplateDictionary * section_dict = dict.AddSectionDictionary("question"); section_dict->SetValue("id",e._id); section_dict->SetValue("id",e._id); section_dict->SetValue("name",e._name); section_dict->SetValue("star",e._star); } //2.获取模板类指针,加载预约义的html页面到内存当中 ctemplate::Template* tl = ctemplate::Template::GetTemplate("./template/all_questions.html", ctemplate::DO_NOT_STRIP); //3.渲染 拿着模板类的指针,将数据字典当中的数据更新到html页面的内存中 tl->Expand(str, &dict); }
这个过程就是经过函数传入的参数,填充模板类中的预约义字段,如section_dict->SetValue("id",e._id); 而后再获取模板类指针,加载预约义的html页面到内存当中 ,此时调用这个模板类提供的渲染方法就能够完成对数据的渲染。
--->获取单个题目并渲染到浏览器的html以下,仍是以这一个为例做为说明:json
<html> <head> <title>在线OJ</title> </head> <body> <div>{{id}}.{{name}} {{star}}</div> <div>{{desc}}</div> <div> <form action="/question/{{id}}" method="POST"><!--要提交到哪里--> <textarea name="code" rows=40 cols=70>{{writ}}</textarea><!--代码框的大小和其中的内容--> <br> <input type="submit" formenctype="appliaction/json" value="提交"><!--设置提交按钮--> </form> </div> </body> </html>
{{}}内中对应的字段就是上文中提到的预约义字段,也就是将上文中提交的预约义字段中的数据填充到这个位置,构成html请求与响应的内容。数组
工具模块中实现的一些方法主要是配合各个模块工做浏览器
工具模块还提供了一些文件相关的操做,好比读取文件,这里使用的技术是利用ifstream流打开文件并用getline方法每次读取文件中的一行数据放到string串中,仍是以这个简单的函数为例服务器
static int ReadDataFromFile(const std::string& filename, std::string* content){ std::ifstream file(filename.c_str()); if(!file.is_open()){ LOG(ERROR,"File Open Faild"); return -1; } std::string line; while(std::getline(file, line)) { *content += line + "\n"; } file.close(); return 0; }
//0 : 编译运行没有错误 //1.编译错误 //2.运行错误 //3.参数错误 //4.内存错误 enum ErrorNo { OK = 0, COMPILE_ERROR, RUN_ERROR, PRAM_ERROR, INTERNAL_ERROR };
在整个过程正常结束后将产生的临时文件清理掉(unlink函数)
编译运行模块是整个项目的灵魂所在,参考代码以下并发
class Compile{ public: static void CompileAndRun(const Json::Value& req,Json::Value * resp){ if(req["code"].empty()){ (*resp)["errorno"] = PRAM_ERROR; (*resp)["reason"] = "Pram error"; LOG(ERROR,"Request Code Is Empty"); } //将代码写到文件中去 std::string code = req["code"].asString();//先将代码由Josn转化为字符串 //文件名称进行约定 tmp_时间戳.cpp std::string tmp_filename = WriteTmpFile(code); if(tmp_filename == "") { (*resp)["errorno"] = INTERNAL_ERROR; (*resp)["reason"] = "Create file failed"; LOG(ERROR, "Write Source failed"); return; } //3.编译 if(!compile(tmp_filename)) { (*resp)["Errorno"] = COMPILE_ERROR; //从错误文件中读取,构造编译错误的响应 std::string reason; FileTools::ReadDataFromFile(ErrorPath(tmp_filename), &reason); (*resp)["reason"] = reason; LOG(ERROR, "Compile Error\n"); return; } //4.运行 int sig = run(tmp_filename); if(sig != 0) { (*resp)["errorno"] = RUN_ERROR; //reason字段保存运行失败所被哪一个信号所杀 (*resp)["reason"] = "Program exit by sig " + std::to_string(sig); LOG(ERROR, "Run Error\n"); return; } //5.构造响应 //正常编译运行后响应 (*resp)["errorno"] = OK; (*resp)["reason"] = "Compile and run is okey!"; //标准输出 std::string stdout_reason; FileTools::ReadDataFromFile(StdoutPath(tmp_filename), &stdout_reason); (*resp)["stdout"] = stdout_reason; //标准错误 std::string stderr_reason; FileTools::ReadDataFromFile(StderrPath(tmp_filename), &stderr_reason); (*resp)["stderr"] = stderr_reason; //程序正常的话就清理掉临时文件 Clean(tmp_filename); return; } private: static std::string WriteTmpFile(const std::string& code) { //1.组织文件名称,组织文件的前缀名称,用来区分源码文件,可执行文件是同一组数据 std::string nowtime; std::string tmp_filename = "/tmp_" +std::to_string(GetTimeStamp()); //写文件 int ret = FileTools::WriteDataToFile(SrcPath(tmp_filename), code); if(ret < 0) { LOG(ERROR, "Write code to source failed"); return ""; } return tmp_filename; } static std::string SrcPath(const std::string& filename) { return "./tmp_files" + filename + ".cpp"; } static std::string ErrorPath(const std::string& filename) { return "./tmp_files" + filename + ".err"; } static std::string ExePath(const std::string& filename) { return "./tmp_files" + filename + ".executable"; } static std::string StdoutPath(const std::string& filename) { return "./tmp_files" + filename + ".stdout"; } static std::string StderrPath(const std::string& filename) { return "./tmp_files" + filename + ".stderr"; } static bool compile(const std::string & filename){ //构造编译命令进行文件的编译 //构造编译命令:g++ src -o des -std=c++11 //程序替换时使用execvp函数,替换g++,第二个参数是char*类型的数组,因此要构造Commond const int commondcount = 20; char buf[commondcount][50] = {{0}}; char * Commond[commondcount] = {0}; for(int i = 0 ; i < commondcount ; ++i){ Commond[i]=buf[i]; } snprintf(Commond[0],49,"%s","g++"); snprintf(Commond[1],49,"%s",SrcPath(filename).c_str()); snprintf(Commond[2],49,"%s","-o"); snprintf(Commond[3],49,"%s",ExePath(filename).c_str()); snprintf(Commond[4],49,"%s","-std=c++11"); //snprintf(Commond[5],49,"%s","-D"); //snprintf(Commond[6],49,"%s","CompileOnline"); Commond[5]=NULL; int pid = fork(); if(pid < 0){ LOG(ERROR,"Fork ERROR\n"); return false; }else if(pid == 0){ //子进程 int fd = open(ErrorPath(filename).c_str(), O_CREAT | O_RDWR, 0664); if(fd < 0){ LOG(ERROR,"Open File Faild\n"); exit(1); } //程序替换 dup2(fd, 2); execvp(Commond[0], Commond);//程序替换为g++编译 exit(0); }else{ //父进程 waitpid(pid,NULL,0); } //3.验证是否生产可执行程序 struct stat st;//stat结构体是描述文件属性的,包括inode节点等信息 int ret = stat(ExePath(filename).c_str(), &st);//这里经过返回值判断是否有这个文件 if(ret < 0) { std::stringstream ss; ss<<"Compile ERROR! Exe filename is"<<ExePath(filename)<<std::endl; LOG(ERROR, ss.str()); return false; } return true; } static int run(const std::string &filename){ //建立子进程,父进程等待,子进程执行替换后的程序 int pid = fork(); if(pid < 0){ LOG(ERROR,"Run Fork Faild\n"); return -1; }else if(pid == 0){ //子进程,要去执行filename所对应的文件 //对子进程执行的时间以及内存做出限制 alarm(1);//执行时间为1秒,超过执行时间会发出SIG_ALARM信号 struct rlimit rl; rl.rlim_cur = 1024 * 20000;//软限制,以字节为单位 rl.rlim_max = RLIM_INFINITY;//硬限制,至关于操做系统所能提供的最大资源 setrlimit(RLIMIT_AS, &rl); //子进程将标准输出和标准错误重定向到文件中 int stdout_fd = open(StdoutPath(filename).c_str(), O_CREAT | O_RDWR, 0664); if(stdout_fd < 0) { std::stringstream ss; ss<<"Open stdout file failed"<<StdoutPath(filename)<<std::endl; LOG(ERROR,ss.str()); return -1; } dup2(stdout_fd, 1); // 标准错误--》重定向到文件 int stderr_fd = open(StderrPath(filename).c_str(), O_CREAT | O_RDWR, 0664); if(stderr_fd < 0) { std::stringstream ss; ss<<"Open stderr file failed"<<StderrPath(filename)<<std::endl; LOG(ERROR,ss.str()); return -1; } dup2(stdout_fd, 2); //替换子进程去执行filename所对应文件 execl(ExePath(filename).c_str(), ExePath(filename).c_str(), NULL); exit(1); } //父进程,等待子进程 int sta = 0; waitpid(pid,&sta,0); //退出状态码是正常退出或被信号所杀,将退出状态码返回 return sta & 0x7f; } static void Clean(std::string filename) { unlink(SrcPath(filename).c_str()); unlink(ExePath(filename).c_str()); unlink(ErrorPath(filename).c_str()); unlink(StdoutPath(filename).c_str()); unlink(StderrPath(filename).c_str()); } };
服务器使用开源库httplib.h构造请求与响应方法app
using namespace httplib; Server server; Oj_Model oj_model; //要请求的内容是当前目录下的all_ques,而后组织一个响应,把全部试题返回去 server.Get("/all_questions", [&oj_model](const Request& req, Response& resp) { //(void)req; std::vector<Exam> vec; oj_model.GetAllExam(&vec); //经过模板技术将vec发送给浏览器 std::string html; Oj_View::DrowAllExam(&html,vec); //LOG(INFO, html); resp.set_content(html, "text/html; charset=UTF-8"); });