項目功能:相似於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"); });