【專案】撰寫一個在線OJ的專案
- 1.專案目標
- 2.專案環境
- 3.模快劃分
- 1.試題模塊
- 2. 編譯模塊
- 3.http模塊
- 4.工具模塊
- 4.各模塊具體實作:
- 4.1 http模塊
- 4.1.1 回應獲取整個試題串列的請求
- 4.1.2 回應獲取單個試題的請求
- 4.1.3 回應編譯運行請求
- 4.2 試題模塊
- 4.2.0 試題保存形式及其內容
- 4.2.1 試題模塊模板
- 4.2.2 加載config檔案,獲取題目資訊
- 4.2.3 上層呼叫介面,獲取所有試題介面
- 4.2.4 上層呼叫介面,獲取單個試題介面
- 4.3 工具模板
- 4.3.1 讀取檔案內容
- 4.3.2 字串切割
- 4.3.3 決議資料包并解碼
- 4.3.4 向HTML填充資訊
- 4.3.5獲取時間戳
- 4.3.6日志記錄
- 4.4 編譯運行模塊
- 4.4.0 檢查引數
- 4.4.1 將代碼存入檔案中
- 4.4.2 編譯
- 4.4.3 運行
- 4.4.4 構造回應
- 4.4.5 洗掉臨時檔案
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.試題模塊

- 獲取所有試題資訊,回傳上層呼叫
- 獲取單個試題資訊,回傳上層呼叫
2. 編譯模塊

1.編譯運行,將結果回傳上層呼叫
3.http模塊

提供三個介面, 分別處理三個請求:
- 請求顯示所有試題:獲取整個試題串列,回復回應
- 請求顯示單個試題:獲取單個試題資訊,回復回應
- 請求顯示編譯運行:獲取編譯運行結果,回復回應
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");
});
- 呼叫試題模板的
GetAllQuestion方法獲取所有題目資訊,存盤于vector< Question > questions中 - 呼叫工具模板的
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");
});
- 呼叫試題模板的
GetOneQuestion方法獲取單個題目資訊,存盤于Question ques中 - 呼叫工具模板的
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");
});
- 根據URL的后綴名
http://192.168.21.129:17878/question/1,最后數字為題目編號,因此通過正則表達(/compile/(\d+)進行路徑匹配, - 通過
req.matches[1].str(),找到試題編號,接著呼叫試題模塊的GetOneQuestion方法獲取該試題的所有資訊 - 對資料包進行拆分
PraseBody獲取未解碼的代碼,解碼后與tail.cpp檔案組合成待編譯的檔案 - 構造
json物件,進行組織管理,交給編譯模塊運行CompileAndRun - 獲取編譯運行的回傳結果存盤在
resp_json當中 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_;
};
- 采用
Question結構體存盤一道題目的所有資訊 - 采用
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;
}
- 通過加載config檔案獲取所有試題資訊:【id】 【名字】 【難易】 【路徑】,將每一道題的資訊存盤在
Question結構體中的id_、title_、star_、path_中 - 呼叫工具模塊的ReadFile函式分別讀取【路徑】下的desc.txt,header.cpp,tail.cpp檔案,獲取題目描述,題目預定義的頭,測驗用例,存盤在
Question結構體中的desc_、header_cpp_、tail_cpp_中 - 這樣我們讀取了config檔案中的一行資訊,也就是一道題的所有資訊存盤在
Question結構體物件ques中, - 將每道題目的
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;
}
- 遍歷
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;
}
- 根據
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;
}
};
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);
}
};
- 根據字符切割字串
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;
}

- 決議資料包
code=xxx&&stdin=xxx - 根據=拆分資料包,獲得正文
- 正文進行解碼,轉化為代碼
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);
}
};
- 運用ctemplate創建根字典
- 根據每道題目創建子字典
- 依據關鍵字匹配,通過字典對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;
}
- 引數是否是錯誤的, 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;
}
- 檔案命名約定: 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;
}
- 創建子行程,子行程進行行程程式替換
- 將標準錯誤(2)重定向為 fd,將錯誤進行保存,如果編譯失敗,將其回傳,
- 如果說編譯成功了, 在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;
}
- 創建子行程,子行程進行行程程式替換
execl,進行運行編譯產生的可執行程式 - 將運行結果重定向到
stdout_fd中 - 將標準錯誤重定向到
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;
- 將所有結果放入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());
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/254789.html
標籤:其他
