【項目】編寫一個在線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.試題模塊
- 獲取所有試題信息,返回上層調用
- 獲取單個試題信息,返回上層調用
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());
}