項目實戰---在線OJ

在線OJ

項目功能:相似於LeetCode及牛客網的在線答題系統,瀏覽器請求服務器能夠得到全部試題信息,包括題目編號、題目名稱、題目難度,用戶能夠選擇某一道題進行做答,服務器返回題目描述信息以及預約義好的代碼模板,用戶編寫完代碼後瀏覽器將用戶提交的代碼返回給服務器,服務器將用戶提交的代碼與預約義好的題目測試用例結合編譯運行,並將結果返回給瀏覽器告知用戶經過率。
上述的功能依賴於幾個模塊相互配合實現,以下圖
項目實戰---在線OJ
接下來就對這幾個模塊詳細的介紹html

1.試題模塊

  • 在本地建立一個目錄保存全部的試題,描述某一道試題時將試題編號、試題名稱、試題所在路徑、試題難度經過結構體組織起來,試題所在路徑中保存着這道題目的描述(desc.txt)、這道題的預約義代碼(header.cpp)以及這道題的測試代碼(tail.cpp)
    class Exam    
    {                                                                                                                      
    public:    
    std::string _id;    
    std::string _name;    
    std::string _path;    
    std::string _star;//試題難度    
    };
  • 全部試題經過unordered_map保存,經過題目編號就能夠在unordered_map中獲得該試題的全部信息,這樣也使查詢效率最高。
  • 當瀏覽器請求全部試題時,試題模塊遍歷整個unordered_map拿到全部試題信息,而後經過渲染模塊返回給瀏覽器,此時瀏覽器就能夠進行題目選擇並做答了。
    項目實戰---在線OJ
  • 當用戶選擇某一道題時瀏覽器發出「請求單個題目」的請求,服務器收到此請求,在unordered_map中查找對應的題目編號,若是有這道題那麼就將此題所在路徑下的題目描述渲染給瀏覽器。項目實戰---在線OJ
  • 用戶編輯好代碼並提交給服務器時試題模塊中還有一個函數完成了將用戶提交的代碼與測試用例代碼拼接在一塊兒,給編譯運行模塊提供支持

    2.日誌模塊

    日誌模塊主要負責將執行過程當中遇到的錯誤以及提示信息寫入日誌文件中前端

  • 經過枚舉日誌等級將日誌信息描述出來
    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;
        }

    日誌文件以下圖,記錄了每一條關鍵信息,方便後期問題的定位
    項目實戰---在線OJnode

  • 這個函數經過構造stringstream對象將用戶傳入的日至等級以及具體描述組織起來寫入日誌文件,此處要將其設置爲inline,在函數調用出展開,不然對應的文件名稱和日誌行號永遠都是日誌模塊的信息
    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)

    3.渲染模塊

  • 在構造請求與響應時須要將數據渲染到瀏覽器,這裏涉及到一點前端的知識,即構造html頁面響應給瀏覽器,這裏採用谷歌提供的模板技術
  • 以渲染全部試題信息頁面爲例
    //經過傳入的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請求與響應的內容。數組

4.工具模塊

工具模塊中實現的一些方法主要是配合各個模塊工做瀏覽器

  • 字符切割功能:在保存單個題目信息時將題目ID、題目名稱、題目路徑、題目難度保存在一行並用空格間隔,此時就須要將每一部分切割並保存到unordered_map中,採用的是boost庫提供的切割函數,將要切割的字符串和切割標誌以及切割完後的每一塊內容保存的vector傳入,就能夠完成對字符串的切割
  • 工具模塊還提供了一些文件相關的操做,好比讀取文件,這裏使用的技術是利用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;
      }
  • 此時要注意當從瀏覽器中將用戶提交的代碼獲取到時是通過URL編碼後的字符串,也就是說一些特殊字符被轉換爲%十六進制的形式,在使用時要對其進行URL解碼

    5.編譯模塊

  • 在用戶提交代碼到服務器時要通過編譯模塊對用戶提交的代碼編譯運行,並將結果響應到瀏覽器
  • 在接受用戶代碼和將編譯結果響應給瀏覽器時採用的都是Json串的格式,構造時將不一樣的字段構造不一樣的內容
  • 規定編譯時有如下幾種錯誤碼,在構造Json串返回將對應的信息填入「erronno」字段
    //0 : 編譯運行沒有錯誤
    //1.編譯錯誤                                                                                                          
    //2.運行錯誤
    //3.參數錯誤
    //4.內存錯誤
    enum ErrorNo
    {
    OK = 0,
    COMPILE_ERROR,
    RUN_ERROR,
    PRAM_ERROR,
    INTERNAL_ERROR
    };
  • 整個編譯運行模塊向外提供一個函數接口,只須要傳入從瀏覽器提交回來的Json串,從Json串中將「code」字段中的數據轉化爲字符串並以當前時間戳+後綴的形式命名臨時文件,統一放到預約義的臨時目錄下
  • 編譯時首先要構造編譯命令,構造完成就能夠建立子進程,讓子進程替換當前進程,子進程替換爲g++去執行編譯文件的過程,父進程等待子進程退出,在這個過程當中若是編譯錯誤這個錯誤信息對咱們是極其重要的,也是要返回給瀏覽器的重要部分,因此在使用時要採用dup2函數將標準錯誤重定向到統一時間戳的標準錯誤文件中,即將編譯錯誤信息寫到響應Json串中的reason字段
  • 若是編譯正常結束說明程序經過編譯,繼續向下運行即運行模塊,也就是建立子進程,子進程程序替換爲剛剛生成的可執行文件,這個過程當中要將標準輸出以及標準錯誤使用dup2函數重定向到統一時間戳的對應文件中,在構造響應Json串時依舊將其寫入reason字段。利用alarm函數以及setrlimit函數限制程序執行的時間以及最大內存,在超過這些限制後會觸發信號異常退出,因此父進程要對子進程退出碼解析,若是最後一個字節低7位不爲0則表示異常退出,反之說明程序正常結束
  • 在整個過程正常結束後將產生的臨時文件清理掉(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());
        }
    };

    6.服務器

    服務器使用開源庫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");    
        });
  • 使用Server類建立服務器對象,就能夠調用其類中提供的Get以及Post方法接收請求與構造響應,使用開源庫的好處在於其內部幫咱們解決了高併發問題,因此在程序設計時就不須要再考慮高併發的問題
  • 須要注意的是這個函數的第一個參數就是請求的資源路徑,在瀏覽器請求時要與其保持一致
相關文章
相關標籤/搜索