【項目】編寫一個在線OJ的項目,快進到自己造題刷

1.項目目標

做出一個在線oj系統,支持查看題目列表,支持點擊單個題目,支持代碼塊書寫代碼,支持提交書寫的代碼到後端,支持後端編譯+運行,支持返回結果

2.項目環境

1.利用開源庫cpp-httplib中的httplib.h頭文件鏈接如下:
https:llgitee.comliqxglcpp-httplib?_from=gitee_search

2.安裝jsoncpp:
yum install jsoncpp
yun install jsoncpp-devel

3.安裝boost環境:
sudo yum install -y snappy-devel boost-devel zlib-devel.x86_64 python-pip
sudo pip install BeautifulSoup4

git clone https://gitee.com/HGtz2222/ThirdPartLibForCpp.git
cd ./ThirdPartLibForCpp/el7.x86_64/
sh install.sh

3.模快劃分

B/S瀏覽器+服務器模式
在這裏插入圖片描述

1.試題模塊

在這裏插入圖片描述

  1. 獲取所有試題信息,返回上層調用
  2. 獲取單個試題信息,返回上層調用

2. 編譯模塊

在這裏插入圖片描述

1.編譯運行,將結果返回上層調用

3.http模塊

在這裏插入圖片描述

提供三個接口, 分別處理三個請求:

  1. 請求顯示所有試題:獲取整個試題列表,回覆響應
  2. 請求顯示單個試題:獲取單個試題信息,回覆響應
  3. 請求顯示編譯運行:獲取編譯運行結果,回覆響應

4.工具模塊

提供加載文件、字符串切割、解碼輔助、html頁面填充渲染方法等等

4.各模塊具體實現:

4.1 http模塊

#include <iostream>
#include <cstdio>
#include "httplib.h"
#include "oj_model.hpp"
#include "oj_view.hpp"

int main()
{
   
   
    using namespace httplib;
    OjModel model;
    //1.初始化httplib庫的server對象
    Server svr;

    //2.提供三個接口, 分別處理三個請求
    //2.1 獲取整個試題列表, get
    svr.Get() {
   
   };

    //2.2 獲取單個試題
    svr.Get(){
   
   };

    //2.3 編譯運行
    svr.Post(){
   
   };

    svr.listen("0.0.0.0", 17878);
    return 0;
}

4.1.1 響應獲取整個試題列表的請求

在這裏插入圖片描述

 svr.Get("/all_questions", [&model](const Request& req, Response& resp){
   
   
            //1.返回試題列表
            std::vector<Question> questions;
            model.GetAllQuestion(&questions);
            std::string html;
            OjView::DrawAllQuestions(questions, &html);

            resp.set_content(html, "text/html");
            });
  1. 調用試題模板的GetAllQuestion方法獲取所有題目信息,存儲於vector< Question > questions
  2. 調用工具模板的DrawALLQuestion方法,questions中保存的題目信息渲染到HTML頁面中,響應用戶

4.1.2 響應獲取單個試題的請求

在這裏插入圖片描述

 	//  瀏覽器提交的資源路徑是  /question/[試題編號] 
    //  \d+ : 正則表達式:表示多位數字 
    svr.Get(R"(/question/(\d+))", [&model](const Request& req, Response& resp){
   
   
            //1.獲取url當中關於試題的數字 & 獲取單個試題的信息
            std::cout << req.version << " " << req.method << std::endl;
            std::cout << req.path <<  std::endl;
            Question ques;
            model.GetOneQuestion(req.matches[1].str(), &ques);

            //2.渲染模版的html文件
            std::string html;
            OjView::DrawOneQuestion(ques, &html);
            resp.set_content(html, "text/html");
            });
  1. 調用試題模板的GetOneQuestion方法獲取單個題目信息,存儲於Question ques
  2. 調用工具模板的DrawOneQuestion方法,ques中保存的題目信息渲染到HTML頁面中,響應用戶

4.1.3 響應編譯運行請求

在這裏插入圖片描述

 svr.Post(R"(/compile/(\d+))", [&model](const Request& req, Response& resp){
   
   
            //1.獲取試題編號 & 獲取試題內容
            Question ques;
            model.GetOneQuestion(req.matches[1].str(), &ques);
            //ques.tail_cpp_ ==> main函數調用+測試用例
            //post 方法在提交代碼的時候, 是經過encode的, 要想正常獲取瀏覽器提交的內容, 需要進行
            //decode, 使用decode完成的代碼和tail.cpp進行合併, 產生待編譯的源碼
            std::unordered_map<std::string, std::string> body_kv;
            UrlUtil::PraseBody(req.body, &body_kv);

            std::string user_code = body_kv["code"];
            //2.構造json對象, 交給編譯運行模塊
            Json::Value req_json;
            req_json["code"] = user_code + ques.tail_cpp_;
            req_json["stdin"] = "";

            std::cout << req_json["code"].asString() << std::endl;

            Json::Value resp_json;
            Compiler::CompileAndRun(req_json, &resp_json);

            //獲取的返回結果都在 resp_json當中
            std::string err_no = resp_json["errorno"].asString();
            std::string case_result = resp_json["stdout"].asString();
            std::string reason = resp_json["reason"].asString();

            std::string html;
            OjView::DrawCaseResult(err_no, case_result, reason, &html);

            resp.set_content(html, "text/html");
            });
  1. 根據URL的後綴名http://192.168.21.129:17878/question/1,最後數字爲題目編號,因此通過正則表達(/compile/(\d+)進行路徑匹配。
  2. 通過req.matches[1].str(),找到試題編號。接着調用試題模塊的GetOneQuestion方法獲取該試題的所有信息
  3. 對數據包進行拆分PraseBody獲取未解碼的代碼,解碼後與tail.cpp文件組合成待編譯的文件
  4. 構造json對象,進行組織管理,交給編譯模塊運行CompileAndRun
  5. 獲取編譯運行的返回結果存儲在 resp_json當中
  6. DrawCaseResult將結果填充給html頁面,返回響應set_content

4.2 試題模塊

4.2.0 試題保存形式及其內容

1.由各個試題序號命名的文件夾 + 共用一個config文件組成:
在這裏插入圖片描述


2.config文件包含所有試題信息:題目序號、題目名字、題目難易程度、以題目序號命名的文件夾路徑(以tab鍵分隔)

在這裏插入圖片描述


3.以題目序號命名的文件夾包含:
在這裏插入圖片描述
以文件1中的三個文件爲例子:

  • header.cpp[保存文件頭部]
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;

class Solution {
   
     
  public:
    bool isPalindrome(int x) {
   
     
      return true;         
    }
};
  • desc.txt[題目的描述]
判斷一個整數是否是迴文數。迴文數是指正序(從左向右)和倒序(從右向左)讀都是一樣的整數。

示例 1:

輸入: 121
輸出: true
示例 2:

輸入: -121
輸出: false
解釋: 從左向右讀,-121 。 從右向左讀,121- 。因此它不是一個迴文數。
示例 3:

輸入: 10
輸出: false
解釋: 從右向左讀,01 。因此它不是一個迴文數。
進階:

你能不將整數轉爲字符串來解決這個問題嗎?

  • tail.cpp[文件末尾,包含測試用例以及調用邏輯]
#ifndef CompileOnline
// 這是爲了編寫用例的時候有語法提示. 實際線上編譯的過程中這個操作是不生效的.
#include "header.cpp"
#endif

///
// 此處約定:
// 1. 每個用例是一個函數
// 2. 每個用例從標準輸出輸出一行日誌
// 3. 如果用例通過, 統一打印 [TestName] ok!
// 4. 如果用例不通過, 統一打印 [TestName] failed! 並且給出合適的提示.
///

void Test1() {
   
     
  bool ret = Solution().isPalindrome(121);
  if (ret) {
   
     
    std::cout << "Test1 ok!" << std::endl;
  } else {
   
     
    std::cout << "Test1 failed! input: 121, output expected true, actual false" << std::endl;
  }
}

void Test2() {
   
     
  bool ret = Solution().isPalindrome(-10);
  if (!ret) {
   
     
    std::cout << "Test2 ok!" << std::endl;
  } else {
   
     
    std::cout << "Test2 failed! input: -10, output expected false, actual true" << std::endl;
  }
}

int main() {
   
     
  Test1();
  Test2();
  return 0;
}


4.2.1 試題模塊模板

#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <fstream>
#include <vector>
#include "tools.hpp"

struct Question
{
   
      
    std::string id_; //題目id
    std::string title_; //題目標題
    std::string star_; //題目的難易程度
    std::string path_; //題目路徑

    std::string desc_; //題目的描述
    std::string header_cpp_; //題目預定義的頭
    std::string tail_cpp_; //題目的尾, 包含測試用例以及調用邏輯
};


class OjModel
{
   
      
    public:
        OjModel()
        {
   
      
        	//加載config文件
        	Load("【config文件路徑】");
        }

        ~OjModel()
        {
   
      
        }

        //從config文件夾當中獲取題目信息
        bool Load(const std::string& filename)
        {
   
      
        }

        //提供給上層調用這一個獲取所有試題的接口
        bool GetAllQuestion(std::vector<Question>* questions)
        {
   
      
        }

        //提供給上層調用者一個獲取單個試題的接口     
        bool GetOneQuestion(const std::string& id, Question* ques)
        {
   
      
        }
    private:
        std::unordered_map<std::string, Question> ques_map_;
};

  1. 採用Question結構體存儲一道題目的所有信息
  2. 採用unordered_map將每道題目以id爲鍵, Question爲值,組織管理所有試題

4.2.2 加載config文件,獲取題目信息

 bool Load(const std::string& filename)
        {
   
      
            //fopen    open     C++      fstream
            std::ifstream file(filename.c_str());
            if(!file.is_open())
            {
   
      
                std::cout << "config file open failed" << std::endl;
                return false;
            }            
            std::string line;
            while(std::getline(file, line))
            {
   
      
                
                std::vector<std::string> vec;
                StringUtil::Split(line, "\t", &vec);
                //boost::spilt分割函數,line中字符串以tab進行分割
                Question ques;
                ques.id_ = vec[0];
                ques.title_ = vec[1];
                ques.star_ = vec[2];
                ques.path_ = vec[3];
                std::string dir = vec[3];
                FileUtil::ReadFile(dir + "/desc.txt", &ques.desc_);
                FileUtil::ReadFile(dir + "/header.cpp", &ques.header_cpp_);
                FileUtil::ReadFile(dir + "/tail.cpp", &ques.tail_cpp_);
                ques_map_[ques.id_] = ques;
            }

            file.close();
            return true;
        }
  1. 通過加載config文件獲取所有試題信息:【id】 【名字】 【難易】 【路徑】,將每一道題的信息存儲在Question結構體中的id_title_star_path_
  2. 調用工具模塊的ReadFile函數分別讀取【路徑】下的desc.txt,header.cpp,tail.cpp文件,獲取題目描述,題目預定義的頭,測試用例,存儲在Question結構體中的desc_header_cpp_tail_cpp_
  3. 這樣我們讀取了config文件中的一行信息,也就是一道題的所有信息存儲在Question結構體對象ques中。
  4. 將每道題目的id作爲鍵,ques對象作爲值,存入unordered_map<std::string, Question> ques_map_當中進行組織管理

4.2.3 上層調用接口,獲取所有試題接口

 bool GetAllQuestion(std::vector<Question>* questions)
        {
   
      
            //1.遍歷無序的map, 將試題信息返回給調用者
            //對於每一個試題, 都是一個Question結構體對象
            for(const auto& kv : ques_map_)
            {
   
      
                questions->push_back(kv.second);
            }

            //2.排序
            std::sort(questions->begin(), questions->end(), [](const Question& l, const Question& r){
   
      
                    //比較Question當中的題目編號, 按照升序規則
                    return std::stoi(l.id_) < std::stoi(r.id_);
                    });
            return true;
        }
  1. 遍歷ques_map_中的值,通過 questions作爲出參存儲所有試題信息,返還給上層調用

4.2.4 上層調用接口,獲取單個試題接口

bool GetOneQuestion(const std::string& id, Question* ques)
        {
   
      
            auto it = ques_map_.find(id);
            if(it == ques_map_.end())
            {
   
      
                return false;
            }
            *ques = it->second;
            return true;
        }
  1. 根據ques_map_中的鍵(題目id)找到對應試題信息,通過 ques存儲試題信息作爲出參,返還給上層調用

4.3 工具模板

4.3.1 讀取文件內容

class FileUtil
{
   
      
    public:
        //讀文件接口
        //file_name: 文件名稱
        //content: 讀到的內容, 輸出參數, 返還調用者
        static bool ReadFile(const std::string& file_name, std::string* content)
        {
   
      
            //1.清空content當中的內容
            content->clear();

            std::ifstream file(file_name.c_str());
            if(!file.is_open())
            {
   
      
                return false;
            }

            //文件被打開了
            std::string line;
            while(std::getline(file, line))
            {
   
      
                (*content) += line + "\n";
            }
            file.close();
            return true;
        }
};
  1. content作爲出參,保存文件內容

4.3.2 字符串切割

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_off);
        }
};
  1. 根據字符切割字符串

4.3.3 解析數據包並解碼

//body從httplib.h當中的request類的成員變量獲得
        //  body:
        //     key=value&key1=value1   ===> 並且是進行過編碼
        //  1.先切割
        //  2.在對切割之後的結果進行轉碼
        static void PraseBody(const std::string& body, std::unordered_map<std::string, std::string>* body_kv)
        {
   
      
            std::vector<std::string> kv_vec;
            StringUtil::Split(body, "&", &kv_vec);

            for(const auto& t : kv_vec)
            {
   
      
                std::vector<std::string> sig_kv;
                StringUtil::Split(t, "=", &sig_kv);

                if(sig_kv.size() != 2)
                {
   
      
                    continue;
                }

                //在保存的時候, 針對value在進行轉碼
                (*body_kv)[sig_kv[0]] = UrlDecode(sig_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;  
} 

在這裏插入圖片描述

  1. 解析數據包code=xxx&&stdin=xxx
  2. 根據=拆分數據包,獲得正文
  3. 正文進行解碼,轉化爲代碼

4.3.4 向HTML填充信息

class OjView
{
   
      
    public:
        static void DrawAllQuestions(std::vector<Question>& questions, std::string* html)
        {
   
      
            //1. 創建template字典
            ctemplate::TemplateDictionary dict("all_questions");

            //2.遍歷vector, 遍歷vector就相當於遍歷多個試題, 每一個試題構造一個子字典
            for(const auto& ques : questions)
            {
   
      
                TemplateDictionary* AddSectionDictionary(const TemplateString section_name);
                ctemplate::TemplateDictionary* sub_dict = dict.AddSectionDictionary("question");
                //void SetValue(const TemplateString variable, const TemplateString value);
                /*
                 *   variable: 指定的是在預定義的html當中的變量名稱
                 *   value: 替換的值
                 * */
                sub_dict->SetValue("id", ques.id_);
                sub_dict->SetValue("id", ques.id_);
                sub_dict->SetValue("title", ques.title_);
                sub_dict->SetValue("star", ques.star_);
            }

            //3.填充
            ctemplate::Template* tl = ctemplate::Template::GetTemplate("./template/all_questions.html", ctemplate::DO_NOT_STRIP);
            //all_questions.html內容被加載到內存中,以逐字逐句的方式,原文件未被修改
            //渲染
            tl->Expand(html, &dict);
        }

        static void DrawOneQuestion(const Question& ques, std::string* html)
        {
   
      
            ctemplate::TemplateDictionary dict("question");
            dict.SetValue("id", ques.id_);
            dict.SetValue("title", ques.title_);
            dict.SetValue("star", ques.star_);
            dict.SetValue("desc", ques.desc_);
            dict.SetValue("id", ques.id_);
            dict.SetValue("code", ques.header_cpp_);
            ctemplate::Template* tl = ctemplate::Template::GetTemplate("./template/question.html", ctemplate::DO_NOT_STRIP);
            //渲染
            tl->Expand(html, &dict);
        }
};
  1. 運用ctemplate創建根字典
  2. 根據每道題目創建子字典
  3. 依據關鍵字匹配,通過字典對html頁面進行填充渲染。

4.3.5獲取時間戳

//獲取時間戳
class TimeUtil
{
   
      
    public:
        static int64_t GetTimeStampMs()
        {
   
      
            struct timeval tv;
            gettimeofday(&tv, NULL);
            return tv.tv_sec + tv.tv_usec / 1000;
        }

        //[年月日 時分秒]
        static void GetTimeStamp(std::string* TimeStamp)
        {
   
      
            time_t st;
            time(&st);

            struct tm* t = localtime(&st);
            
            char buf[30] = {
   
       0 };
            snprintf(buf, sizeof(buf) - 1, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
            TimeStamp->assign(buf, strlen(buf));
        }
};

4.3.6日誌記錄

enum LogLevel
{
   
      
    INFO = 0,
    WARNING,
    ERROR,
    FATAL,
    DEBUG
};


const char* Level[] = 
{
   
      
    "INFO",
    "WARNING",
    "ERROR",
    "FATAL",
    "DEBUG"
};

/*
 * lev:日誌等級
 * file: 文件名稱
 * line : 行號
 * logmsg : 想要記錄的日誌內容
 * */

inline std::ostream& Log(LogLevel lev, const char* file, int line, const std::string& logmsg)
{
   
      
    std::cout << "begin log" << std::endl;
    std::string level_info = Level[lev];
    std::cout << level_info << std::endl;
    std::string TimeStamp;
    TimeUtil::GetTimeStamp(&TimeStamp);
    std::cout << TimeStamp << std::endl;

    std::cout << "[" << TimeStamp << " " << level_info << " " << file << ":" << line << "]" << " " << logmsg;
    return std::cout;
}


#define LOG(lev, msg) Log(lev, __FILE__, __LINE__, msg)

4.4 編譯運行模塊

4.4.0 檢查參數

if(Req["code"].empty())
            {
   
      
                (*Resp)["errorno"] = PRAM_ERROR;
                (*Resp)["reason"] = "Pram error";

                return;
            }
  1. 參數是否是錯誤的, json串當中的code是否爲空

4.4.1 將代碼存入文件中

std::string code = Req["code"].asString();
            std::string file_nameheader = WriteTmpFile(code);
            if(file_nameheader == "")
            {
   
      
                (*Resp)["errorno"] = INTERNAL_ERROR;
                (*Resp)["reason"] = "write file failed";
                return;
            }
  1. 文件命名約定: tmp_時間戳_src.cpp

4.4.2 編譯

        static bool Compile(const std::string& file_name)
        {
   
                  
            int pid = fork();
            if(pid > 0)
            {
   
      
                //father
                waitpid(pid, NULL, 0);
            }
            else if (pid == 0)
            {
   
      
                //child
                //進程程序替換--》 g++ SrcPath(filename) -o ExePath(filename) "-std=c++11"
                int fd = open(CompileErrorPath(file_name).c_str(), O_CREAT | O_WRONLY, 0666);
                if(fd < 0)
                {
   
      
                    return false;
                }

                //將標準錯誤(2)重定向爲 fd, 標準錯誤的輸出, 就會輸出在文件當中
                dup2(fd, 2);

                execlp("g++", "g++", SrcPath(file_name).c_str(), "-o", ExePath(file_name).c_str(), "-std=c++11", "-D", "CompileOnline", NULL);

                close(fd);
                //如果替換失敗了, 就直接讓子進程退出了,如果替換成功了, 不會走該邏輯了
                exit(0);
            }
            else
            {
   
      
                return false;
            }

            //如果說編譯成功了, 在tmp_file這個文件夾下, 一定會產生一個可執行程序, 如果
            //當前代碼走到這裏,判斷有該可執行程序, 則我們認爲g++執行成功了, 否則, 認爲執行失敗
            
            struct stat st;
            int ret = stat(ExePath(file_name).c_str(), &st);
            if(ret < 0)
            {
   
      
                return false;
            }

            return true;
        }
  1. 創建子進程,子進程進行進程程序替換
  2. 將標準錯誤(2)重定向爲 fd,將錯誤進行保存,如果編譯失敗,將其返回。
  3. 如果說編譯成功了, 在tmp_file這個文件夾下, 一定會產生一個可執行程序

4.4.3 運行

        static int Run(const std::string& file_name)
        {
   
        
            int pid = fork();
            if(pid < 0)
            {
   
      
                return -1;
            }
            else if(pid > 0)
            {
   
      
                //father
                int status = 0;
                waitpid(pid, &status, 0);
                return status & 0x7f;
            }
            else
            {
   
      
                //註冊一個定時器, alarm
                //[限制運行時間]
                alarm(1);
                //child
                //進程程序替換, 替換編譯創建出來的可執行程序
                //[限制內存] // #include <sys/resource.h>
                struct rlimit rlim;
                rlim.rlim_cur = 30000 * 1024;   //3wk
                rlim.rlim_max = RLIM_INFINITY;
                setrlimit(RLIMIT_AS, &rlim);
                int stdout_fd = open(StdoutPath(file_name).c_str(), O_CREAT | O_WRONLY, 0666);
                if(stdout_fd < 0)
                {
   
      
                    return -2;
                }
                dup2(stdout_fd, 1);

                int stderr_fd = open(StderrPath(file_name).c_str(), O_CREAT | O_WRONLY, 0666);
                if(stderr_fd < 0)
                {
   
      
                    return -2;
                }
                dup2(stderr_fd, 2);
                
                
                execl(ExePath(file_name).c_str(), ExePath(file_name).c_str(), NULL);
                exit(0);
            }

            return 0;
        }
  1. 創建子進程,子進程進行進程程序替換execl,進行運行編譯產生的可執行程序
  2. 將運行結果重定向到stdout_fd
  3. 將標準錯誤重定向到stderr_fd

4.4.4 構造響應

(*Resp)["errorno"] = OK;
            (*Resp)["reason"] = "Compile and Run ok";

            std::string stdout_str;
            FileUtil::ReadFile(StdoutPath(file_nameheader), &stdout_str);
            (*Resp)["stdout"] = stdout_str;

            std::string stderr_str;
            FileUtil::ReadFile(StderrPath(file_nameheader), &stderr_str);
            (*Resp)["stderr"] = stderr_str;
  1. 將所有結果放入json中進行組織,作爲出參傳遞給上一層

4.4.5 刪除臨時文件

static void Clean(const std::string& filename)
        {
   
      
            unlink(SrcPath(filename).c_str());
            unlink(CompileErrorPath(filename).c_str());
            unlink(ExePath(filename).c_str());
            unlink(StdoutPath(filename).c_str());
            unlink(StderrPath(filename).c_str());
        }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章