各種類型網站建設售后完善長沙網站建站模板
文章目錄
- 項目介紹
- 所用技術與開發(fā)環(huán)境
- 所用技術
- 開發(fā)環(huán)境
- 項目框架
- compiler_server模塊
- compiler編譯功能
- comm/util.hpp 編譯時的臨時文件
- comm/log.hpp 日志
- comm/util.hpp 時間戳
- comm/util.hpp 檢查文件是否存在
- compile_server/compiler.hpp 編譯功能總體編寫
- runner運行功能
- 資源設置
- comm/util.hpp 運行時的臨時文件
- compile_server/runner.hpp 運行功能編寫
- compile_server/compile_run.hpp 編譯且運行
- comm/util.hpp 生成唯一文件名
- comm/uti.hpp 寫入文件/讀出文件
- 清理臨時文件
- compiler_run模塊的整體代碼
- 本地進行編譯運行模塊的整體測試
- compiler_server模塊(打包網絡服務)
- compiler_server/compile_server.cc
- oj_server模塊
- oj_server.cc 路由框架
- oj_model.hpp/oj_model2.hpp
- 文件版本
- 數據庫版本:
- oj_view.hpp
- oj_control.cpp
項目介紹
項目是基于負載均衡的一個在線判題系統(tǒng),用戶自己編寫代碼,提交給后臺,后臺再根據負載情況選擇合適的主機提供服務編譯運行服務。
所用技術與開發(fā)環(huán)境
所用技術
- C++ STL 標準庫
- Boost 準標準庫(字符串切割)
- cpp-httplib 第三方開源網絡庫
- ctemplate 第三方開源前端網頁渲染庫
- jsoncpp 第三方開源序列化、反序列化庫
- 負載均衡設計
- 多進程、多線程
- MySQL C connect
- Ace前端在線編輯器
- html/css/js/jquery/ajax
開發(fā)環(huán)境
- Centos 7 云服務器
- vscode
項目框架
compiler_server模塊
模塊結構
總體流程圖
compiler編譯功能
- 在運行編譯服務的時候,
compiler
收到來自oj_server
傳來的代碼;我們對其進行編譯 - 在編譯前,我們需要一個
code.cpp
形式的文件; - 在編譯后我們會形成
code.exe
可執(zhí)行程序,若編譯失敗還會形成code.error
來保存錯誤信息; - 因此,我們需要對這些文件的后綴進行添加,所以我們創(chuàng)建
temp
文件夾,該文件夾用來保存code
代碼的各種后綴; - 所以在傳給編譯服務的時候只需要傳文件名即可,拼接路徑由
comm
公共模塊下的util.hpp
提供路徑拼接
comm/util.hpp 編譯時的臨時文件
#pragma once#include <iostream>#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>namespace ns_util
{const std::string path = "./temp/";// 合并路徑類class PathUtil{public:static std::string splic(const std::string &str1, const std::string &str2){return path + str1 + str2;}// cpp文件 + 后綴名// file_name -> ./temp/xxx.cppstatic std::string Src(const std::string &file_name){return splic(file_name, ".cpp");}// exe文件 + 后綴名static std::string Exe(const std::string &file_name){return splic(file_name, ".exe");}static std::string CompilerError(const std::string &file_name){return splic(file_name, ".compile_error");}};
}
comm/log.hpp 日志
日志需要輸出:等級、文件名、行數、信息、時間
#pragma once#include <string>
#include "util.hpp"namespace ns_log
{using namespace ns_util;// 日志等級enum{INFO,DEBUG,WARNING,ERROR,FATAL,};inline std::ostream& Log(const std::string& level, const std::string& file_name, int line){std::string log = "[";log += level;log += "]";log += "[";log += file_name;log += "]";log += "[";log += std::to_string(line);log += "]"; log += "[";log += TimeUtil::GetTimeStamp();log += "]"; std::cout << log;return std::cout;}#define LOG(level) Log(#level, __FILE__, __LINE__)
}
獲取時間利用的是時間戳,在util
工具類中編寫獲取時間戳的代碼。利用操作系統(tǒng)接口:gettimeofday
comm/util.hpp 時間戳
class TimeUtil
{
public:static std::string GetTimeStamp(){struct timeval _t;gettimeofday(&_t, nullptr);return std::to_string(_t.tv_sec);}
};
進行編譯服務的編寫,根據傳入的源程序文件名,子進程對stderr
進行重定向到文件compile_error
中,使用execlp
進行程序替換,父進程在外面等待子進程結果,等待成功后根據是否生成可執(zhí)行程序
決定是否編譯成功;
判斷可執(zhí)行程序是否生成,我們利用系統(tǒng)調用stat
來查看文件屬性,如果有,則說明生成,否則失敗;
comm/util.hpp 檢查文件是否存在
class FileUtil
{
public:static bool IsFileExists(const std::string path_name){// 系統(tǒng)調用 stat 查看文件屬性// 獲取屬性成功返回 0struct stat st;if (stat(path_name.c_str(), &st) == 0){return true;}return false;}
};
compile_server/compiler.hpp 編譯功能總體編寫
#pragma once#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>#include "../comm/util.hpp"
#include "../comm/log.hpp"// 只負責代碼的編譯
namespace ns_compiler
{// 引入路徑拼接using namespace ns_util;using namespace ns_log;class Compiler{Compiler() {}~Compiler() {}public:// 返回值:是否編譯成功// file_name : xxx// file_name -> ./temp/xxx.cpp// file_name -> ./temp/xxx.exe// file_name -> ./temp/xxx.errorstatic bool Compile(const std::string &file_name){pid_t id = fork();if (id < 0){LOG(ERROR) << "內部錯誤,當前子進程無法創(chuàng)建" << "\n";return false;}else if (id == 0) // 子進程 編譯程序{int _error = open(PathUtil::Error(file_name).c_str(), O_CREAT | O_WRONLY, 0644);if (_error < 0){LOG(WARNING) << "沒有成功形成 error 文件" << "\n";exit(1);}// 重定向標準錯誤到 _errordup2(_error, 2);// g++ -o target src -std=c++11execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),PathUtil::Src(file_name).c_str(), "-std=c++11", nullptr);LOG(ERROR) << "g++執(zhí)行失敗,檢查參數是否傳遞正確" << "\n";exit(2);}else // 父進程 判斷編譯是否成功{waitpid(id, nullptr, 0);if (FileUtil::IsFileExists(PathUtil::Exe(file_name))){LOG(INFO) << PathUtil::Exe(file_name) << "編譯成功!" << "\n";return true;}LOG(ERROR) << "編譯失敗!" << "\n";return false;}}};}
runner運行功能
編譯完成后,我們就可以執(zhí)行可執(zhí)行程序了,執(zhí)行前,首先打開三個文件xxx.stdin
,xxx.stdout
,xxx.stderr
并將標準輸入、標準輸出和標準錯誤分別重定向到三個文件中。創(chuàng)建子進程來進行程序替換執(zhí)行程序;每道題的代碼運行時間和內存大小都有限制,所以在執(zhí)行可執(zhí)行程序之前我們對內存和時間進行限制。
資源設置
利用setrlimit
系統(tǒng)調用來實現(xiàn)
int setrlimit(int resource, const struct rlimit *rlim);
static void SetProcLimit(int cpu_limit, int mem_limit){struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur = cpu_limit;cpu_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU, &cpu_rlimit);struct rlimit mem_rlimit;mem_rlimit.rlim_cur = mem_limit * 1024;mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mem_rlimit);}
comm/util.hpp 運行時的臨時文件
static std::string Stdin(const std::string &file_name){return splic(file_name, ".stdin");}static std::string Stdout(const std::string &file_name){return splic(file_name, ".stdout");}// error文件 + 后綴名static std::string Stderr(const std::string &file_name){return splic(file_name, ".stderr");}
compile_server/runner.hpp 運行功能編寫
#pragma once
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <wait.h>
#include <fcntl.h>
#include <sys/resource.h>#include "../comm/util.hpp"
#include "../comm/log.hpp"namespace ns_runner
{using namespace ns_log;using namespace ns_util;class Runner{public:Runner() {}~Runner() {}static void SetProcLimit(int cpu_limit, int mem_limit){struct rlimit cpu_rlimit;cpu_rlimit.rlim_cur = cpu_limit;cpu_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_CPU, &cpu_rlimit);struct rlimit mem_rlimit;mem_rlimit.rlim_cur = mem_limit * 1024;mem_rlimit.rlim_max = RLIM_INFINITY;setrlimit(RLIMIT_AS, &mem_rlimit);}// 指明文件名即可,無后綴、無路徑// 返回值: // < 0 內部錯誤 // = 0運行成功,成功寫入stdout等文件 // > 0運行中斷,用戶代碼存在問題static int Run(const std::string& file_name, int cpu_limit, int mem_limit){// 運行程序會有三種結果:/* 1. 代碼跑完,結果正確2. 代碼跑完,結果錯誤3. 代碼異常Run 不考慮結果正確與否,只在意是否運行完畢;結果正確與否是有測試用例決定程序在啟動的時候默認生成以下三個文件標準輸入:標準輸出:標準錯誤:*/std::string _execute = PathUtil::Exe(file_name);std::string _stdin = PathUtil::Stdin(file_name); std::string _stdout = PathUtil::Stdout(file_name);std::string _stderr = PathUtil::Stderr(file_name);umask(0);int _stdin_fd = open(_stdin.c_str(), O_CREAT | O_RDONLY, 0644);int _stdout_fd = open(_stdout.c_str(), O_CREAT | O_WRONLY, 0644);int _stderr_fd = open(_stderr.c_str(), O_CREAT | O_WRONLY, 0644);if(_stdin_fd < 0 || _stdout_fd < 0 || _stderr_fd < 0){LOG(ERROR) << "內部錯誤, 標準文件打開/創(chuàng)建失敗" << "\n";// 文件打開失敗return -1;}pid_t id = fork();if(id < 0){ close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);LOG(ERROR) << "內部錯誤, 創(chuàng)建子進程失敗" << "\n";return -2;}else if(id == 0) // 子進程{dup2(_stdin_fd, 0);dup2(_stdout_fd, 1);dup2(_stderr_fd, 2); SetProcLimit(cpu_limit, mem_limit);execl(_execute.c_str(), /*要執(zhí)行誰*/ _execute.c_str(), /*命令行如何執(zhí)行*/ nullptr);exit(1);}else // 父進程{close(_stdin_fd);close(_stdout_fd);close(_stderr_fd);int status = 0;waitpid(id, &status, 0);LOG(INFO) << "運行完畢!退出碼為: " << (status & 0x7F) << "\n";return status & 0x7f;}}};
}
compile_server/compile_run.hpp 編譯且運行
- 用戶的代碼會以json串的方式傳給該模塊
- 給每一份代碼創(chuàng)建一個文件名具有唯一性的源文件
- 調用上面的編譯和運行執(zhí)行該源文件
- 再把結果構建成json串返回給上層
json串的結構
comm/util.hpp 生成唯一文件名
當一份用戶提交代碼后,我們?yōu)槠渖傻脑次募枰哂形ㄒ恍?。名字生成唯一性我們可以利用毫秒級時間戳加上原子性的增長計數實現(xiàn)
獲取毫秒時間戳在TimeUtil
工具類中,生成唯一文件名在FileUtil
工具類中
static std::string GetTimeMs(){struct timeval _time;gettimeofday(&_time, nullptr);return std::to_string(_time.tv_sec * 1000 + _time.tv_usec / 1000);}
comm/uti.hpp 寫入文件/讀出文件
因為需要填寫運行成功結果和運行時報錯的結果,所以我們寫一個寫入文件和讀出文件,放在FileUtil
中
static bool WriteFile(const std::string &target, const std::string &content){std::ofstream out(target);if (!out.is_open()){return false;}out.write(content.c_str(), content.size());out.close();return true;}// 根據路徑文件進行讀出// 注意,默認每行的\\n是不進行保存的,需要保存請設置參數static bool ReadFile(const std::string &path_file, std::string *content, bool keep = false){// 利用C++的文件流進行簡單的操作std::string line;std::ifstream in(path_file);if (!in.is_open())return "";while (std::getline(in, line)){(*content) += line;if (keep)(*content) += "\n";}in.close();return true;}
清理臨時文件
編譯還是運行都會生成臨時文件,所以可以在編譯運行的最后清理一下這一次服務生成的臨時文件
static void RemoveTempFile(const std::string &file_name){// 因為臨時文件的存在情況存在多種,刪除文件采用系統(tǒng)接口unlink,但是需要判斷std::string src_path = PathUtil::Src(file_name);if (FileUtil::IsFileExists(src_path))unlink(src_path.c_str());std::string stdout_path = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(stdout_path))unlink(stdout_path.c_str());std::string stdin_path = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(stdin_path))unlink(stdin_path.c_str());std::string stderr_path = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(stderr_path))unlink(stderr_path.c_str());std::string compilererr_path = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(compilererr_path))unlink(compilererr_path.c_str());std::string exe_path = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(exe_path))unlink(exe_path.c_str());}
提供一個Start
方法讓上層調用編譯運行模塊,參數是一個輸入形式的json
串和一個要給上層返回的json
串
使用jsoncpp
反序列化,解析輸入的json
串。調用形成唯一文件名的方法生成一個唯一的文件名,然后使用解析出來的代碼部分創(chuàng)建出一個源文件,把文件名
交給編譯模塊進行編譯,再把文件名和時間限制,內存限制傳給運行模塊運行,記錄這個過程中的狀態(tài)碼。再最后還要序列化一個json
串返還給用戶,更具獲得狀態(tài)碼含義的接口填寫狀態(tài)碼含義,根據狀態(tài)碼判斷是否需要填寫運行成功結果和運行時報錯的結果,然后把填好的結果返還給上層。
最終調用一次清理臨時文件接口把這一次服務生成的所有臨時文件清空即可。
兩個json
的具體內容
compiler_run模塊的整體代碼
#pragma once
#include <jsoncpp/json/json.h>#include "compiler.hpp"
#include "runner.hpp"
#include "../comm/util.hpp"
#include "../comm/log.hpp"namespace ns_complie_and_run
{using namespace ns_log;using namespace ns_util;using namespace ns_compiler;using namespace ns_runner;class ComplieAndRun{public:static void RemoveTempFile(const std::string &file_name){// 因為臨時文件的存在情況存在多種,刪除文件采用系統(tǒng)接口unlink,但是需要判斷std::string src_path = PathUtil::Src(file_name);if (FileUtil::IsFileExists(src_path))unlink(src_path.c_str());std::string stdout_path = PathUtil::Stdout(file_name);if (FileUtil::IsFileExists(stdout_path))unlink(stdout_path.c_str());std::string stdin_path = PathUtil::Stdin(file_name);if (FileUtil::IsFileExists(stdin_path))unlink(stdin_path.c_str());std::string stderr_path = PathUtil::Stderr(file_name);if (FileUtil::IsFileExists(stderr_path))unlink(stderr_path.c_str());std::string compilererr_path = PathUtil::CompilerError(file_name);if (FileUtil::IsFileExists(compilererr_path))unlink(compilererr_path.c_str());std::string exe_path = PathUtil::Exe(file_name);if (FileUtil::IsFileExists(exe_path))unlink(exe_path.c_str());}// > 0:進程收到信號導致異常崩潰// < 0:整個過程非運行報錯// = 0:整個過程全部完成static std::string CodeToDesc(int status, const std::string &file_name){std::string desc;switch (status){case 0:desc = "運行成功!";break;case -1:desc = "代碼為空";break;case -2:desc = "未知錯誤";break;case -3:desc = "編譯報錯\n";FileUtil::ReadFile(PathUtil::CompilerError(file_name), &desc, true);break;case 6:desc = "內存超過范圍";break;case 24:desc = "時間超時";break;case 8:desc = "浮點數溢出";break;case 11:desc = "野指針錯誤";break;default:desc = "未處理的報錯-status為:" + std::to_string(status);break;}return desc;}/*輸入:code: 用戶提交的代碼input: 用戶提交的代碼對應的輸入cpu_limit:mem_limit:輸出:必有,status: 狀態(tài)碼reason: 請求結果可能有,stdout: 運行完的結果stderr: 運行完的錯誤*/static void Start(const std::string &in_json, std::string *out_json){Json::Value in_value;Json::Reader reader;reader.parse(in_json, in_value, 1);std::string code = in_value["code"].asString();std::string input = in_value["input"].asString();int cpu_limit = in_value["cpu_limit"].asInt();int mem_limit = in_value["mem_limit"].asInt();int status_code = 0;Json::Value out_value;int run_result = 0;std::string file_name;if (!code.size()){status_code = -1; // 代碼為空goto END;}// 毫秒級時間戳 + 原子性遞增唯一值:來保證唯一性file_name = FileUtil::UniqFileName();if (!FileUtil::WriteFile(PathUtil::Src(file_name), code)){status_code = -2; // 未知錯誤goto END;}if (!Compiler::Compile(file_name)){status_code = -3; // 未知錯誤goto END;}run_result = Runner::Run(file_name, cpu_limit, mem_limit);if (run_result < 0)status_code = -2; // 未知錯誤else if (run_result > 0)status_code = run_result; // 崩潰elsestatus_code = 0;END:out_value["status"] = status_code;out_value["reason"] = CodeToDesc(status_code, file_name);if (status_code == 0){// 整個過程全部成功std::string _stdout;FileUtil::ReadFile(PathUtil::Stdout(file_name), &_stdout, true);out_value["stdout"] = _stdout;std::string _stderr;FileUtil::ReadFile(PathUtil::Stderr(file_name), &_stderr, true);out_value["stderr"] = _stderr;}Json::StyledWriter writer;*out_json = writer.write(out_value);RemoveTempFile(file_name);}};
}
本地進行編譯運行模塊的整體測試
自己手動構造一個json串,編譯、運行、返回結果json串
#include "compile_run.hpp"using namespace ns_complie_and_run;// 編譯服務會被同時請求,保證代碼的唯一性
int main()
{// 客戶端請求jsonstd::string in_json;Json::Value in_value;in_value["code"] = R"( #include<iostream>int main() {std::cout << "Hello, world!" << std::endl;int *p = new int[1024 * 1024 * 20 ];return 0;})";in_value["input"] = ""; in_value["cpu_limit"] = 1; in_value["mem_limit"] = 10240; Json::FastWriter writer;in_json = writer.write(in_value);std::cout << "in_json: " << std::endl << in_json << std::endl;std::string out_json;ComplieAndRun::Start(in_json, &out_json);std::cout << "out_json: " << std::endl << out_json << std::endl;return 0;
}
compiler_server模塊(打包網絡服務)
編譯運行服務已經整合在一起了,接下來將其打包成網絡服務即可
我們利用httplib
庫將compile_run
打包為一個網絡編譯運行服務
compiler_server/compile_server.cc
- 使用了 httplib 庫來提供 HTTP 服務
- 實現(xiàn)了一個編譯運行服務器
- 通過命令行參數接收端口號
- 一個
POST /compile_and_run
主要的編譯運行接口 - 接收
JSON
格式的請求體,包含:代碼內容、輸入數據、CPU 限制、內存限制
#include "compile_run.hpp"
#include "../comm/httplib.h"using namespace ns_compile_and_run;
using namespace httplib;void Usage(std::string proc)
{std::cerr << "Usage : " << "\n\t" << proc << "prot" << std::endl;
}int main(int argc, char* argv[])
{if(argc != 2){Usage(argv[0]);return 1;}Server svr;svr.Post("/compile_and_run", [](const Request &req, Response &resp){std::string in_json = req.body;std::string out_json;if(!in_json.empty()){CompileAndRun::Start(in_json, &out_json);resp.set_content(out_json, "application/json;charset=utf-8");}});svr.listen("0.0.0.0", atoi(argv[1])); // 啟動 http 服務return 0;
}
oj_server模塊
oj_server.cc 路由框架
步驟:
- 服務器初始化:
- 創(chuàng)建 HTTP 服務器實例
- 初始化控制器
- 設置信號處理函數
- 請求處理:
- 接收 HTTP 請求
- 根據 URL 路由到對應處理函數
- 調用控制器相應方法
- 返回處理結果
- 判題流程:
- 接收用戶提交的代碼
- 通過控制器進行判題
- 返回判題結果
- 創(chuàng)建一個服務器對象
int main()
{Server svr; // 服務器對象
}
- 獲取所有題目列表
返回所有題目的HTML頁面
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp){std::string html;ctrl.AllQuestions(&html);resp.set_content(html, "text/html; charset=utf-8");
});
- 獲取單個題目
返回單個題目的詳細信息頁面
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp){std::string number = req.matches[1];std::string html;ctrl.Question(number, &html);resp.set_content(html, "text/html; charset=utf-8");
});
- 提交代碼判題
處理用戶提交的代碼
返回 JSON 格式的判題結果
svr.Post(R"(/judge/(\d+))", [&ctrl](const Request &req, Response &resp){std::string number = req.matches[1];std::string result_json;ctrl.Judge(number, req.body, &result_json);resp.set_content(result_json, "application/json;charset=utf-8");
});
- 服務器配置和啟動
svr.set_base_dir("./wwwroot");
svr.listen("0.0.0.0", 8080);
- 維護一個全局控制器指針
Recovery 函數處理 SIGQUIT 信號,用于服務器恢復
static Control *ctrl_ptr = nullptr;void Recovery(int signo)
{ctrl_ptr->RecoveryMachine();
}
oj_model.hpp/oj_model2.hpp
整體架構為MVC模式
Model
層 由oj_model.hpp
文件版本和 oj_model2.hpp
數據庫版本構成;
負責數據的存儲和訪問,提供了兩種實現(xiàn)方式
- 基礎數據結構設計
struct Question {string number; // 題目編號string title; // 題目標題string star; // 難度等級int cpu_limit; // CPU時間限制(秒)int mem_limit; // 內存限制(KB)string desc; // 題目描述string header; // 用戶代碼模板string tail; // 測試用例代碼
};
- 存儲方案設計
文件版本
優(yōu)勢:
簡單直觀,易于管理
適合小規(guī)模題庫
方便備份和版本控制
劣勢:
并發(fā)性能較差
擴展性有限
數據一致性難保證
目錄結構
./questions/├── questions.list # 題目基本信息└── 1/ # 每個題目獨立目錄├── desc.txt # 題目描述├── header.cpp # 代碼模板└── tail.cpp # 測試用例
具體代碼
#pragma once#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>// 根據題目 list 文件,加載所有題目的信息到內存中
// model:主要用來和數據進行交互,對外提供訪問數據的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number; // 題目編號string title; // 題目的標題string star; // 題目的難度int cpu_limit; // 題目的時間要求(s)int mem_limit; // 題目的空間要求(KB)string desc; // 題目的描述string header; // 題目給用戶的部分代碼string tail; // 題目的測試用例,和 header 形成完整代碼提交給后端編譯};const string questions_list = "./questions/questions.list" ;const string question_path = "./questions/" ;class Model{private:// 【題號 < - > 題目細節(jié)】unordered_map<string, Question> questions;public:Model(){assert(LoadQuestionList(questions_list));}bool LoadQuestionList(const string &question_list){// 加載配置文件 : questions/questions.list + 題目編號文件ifstream in(question_list);if(!in.is_open()) {LOG(FATAL) << "題目加載失敗!請檢查是否存在題庫文件" << std::endl;return false;}std::string line;while(getline(in, line)){ vector<string> tokens;StringUtil::SplitString(line, &tokens, " ");if(tokens.size() != 5){LOG(WARNING) << "加載部分題目失敗!請檢查文件格式" << std::endl;continue;}Question q;q.number = tokens[0];q.title = tokens[1];q.star = tokens[2];q.cpu_limit = atoi(tokens[3].c_str());q.mem_limit = atoi(tokens[4].c_str());string path = question_path;path += q.number;path += "/";FileUtil::ReadFile(path+"desc.txt", &(q.desc), true);FileUtil::ReadFile(path+"header.cpp", &(q.header), true);FileUtil::ReadFile(path+"tail.cpp", &(q.tail), true);questions.insert({q.number, q});} LOG(INFO) << "加載題目成功!" << std::endl;in.close();return true;}bool GetAllQuestions(vector<Question> *out){if(questions.size() == 0) {LOG(ERROR) << "用戶獲取題庫失敗!" << std::endl;return false;}for(const auto &q : questions)out->push_back(q.second);return true;}bool GetOneQuestion(const string &number, Question *q){const auto& iter = questions.find(number);if(iter == questions.end()) {LOG(ERROR) << "用戶獲取題庫失敗!題目編號為:" << number << std::endl;return false;}(*q) = iter->second;return true;}~Model(){}};
}
數據庫版本:
優(yōu)勢:
更好的并發(fā)性能
事務支持,保證數據一致性
表設計
CREATE TABLE oj_questions (number VARCHAR(20) PRIMARY KEY,title VARCHAR(255) NOT NULL,star VARCHAR(20) NOT NULL,description TEXT,header TEXT,tail TEXT,cpu_limit INT,mem_limit INT
);
#pragma once#include "../comm/log.hpp"
#include "../comm/util.hpp"#include <iostream>
#include <string>
#include <unordered_map>
#include <cassert>
#include <vector>
#include <fstream>
#include <cstdlib>
#include <boost/algorithm/string.hpp>
#include "include/mysql.h"// 根據題目 list 文件,加載所有題目的信息到內存中
// model:主要用來和數據進行交互,對外提供訪問數據的接口namespace ns_model
{using namespace std;using namespace ns_log;using namespace ns_util;struct Question{string number; // 題目編號string title; // 題目的標題string star; // 題目的難度string desc; // 題目的描述string header; // 題目給用戶的部分代碼string tail; // 題目的測試用例,和 header 形成完整代碼提交給后端編譯int cpu_limit; // 題目的時間要求(s)int mem_limit; // 題目的空間要求(K)};const std::string oj_question = "***";const std::string host = "***";const std::string user = "***";const std::string passwd = "***";const std::string db = "***";const int port = 3306;class Model{public:Model(){}bool QueryMysql(const std::string &sql, vector<Question> *out){ // 創(chuàng)建MySQL句柄MYSQL* my = mysql_init(nullptr);// 連接數據庫//if(nullptr == mysql_real_connect(&my, host.c_str(), user.c_str(), db.c_str(), passwd.c_str(), port, nullptr, 0))if(nullptr == mysql_real_connect(my, host.c_str(), user.c_str(), passwd.c_str(),db.c_str(), port, nullptr, 0)){std::cout << mysql_error(my) << std::endl;LOG(FATAL) << "連接數據庫失敗!!!" << "\n";return false;}LOG(INFO) << "連接數據庫成功!!!" << "\n";// 設置鏈接的編碼格式,默認是拉丁的mysql_set_character_set(my, "utf8");// 執(zhí)行sql語句//if(0 != mysql_query(&my, sql.c_str()))if(0 != mysql_query(my, sql.c_str())){LOG(WARNING) << sql << " execute error!" << "\n";return false;} // 提取結果//MYSQL_RES *res = mysql_store_result(&my);MYSQL_RES *res = mysql_store_result(my);// 分析結果int rows = mysql_num_rows(res);// 獲得行數int cols = mysql_num_fields(res);// 獲得列數struct Question q;for(int i = 0; i < rows; i++){MYSQL_ROW row = mysql_fetch_row(res);q.number = row[0];q.title = row[1];q.star = row[2];q.desc = row[3];q.header = row[4];q.tail = row[5];q.cpu_limit = atoi(row[6]);q.mem_limit = atoi(row[7]);out->push_back(q);}// 釋放結果空間free(res);// 關閉MySQL連接//mysql_close(&my);mysql_close(my);return true;}bool GetAllQuestions(vector<Question> *out){std::string sql = "select * from oj.";sql += oj_question;return QueryMysql(sql, out);}bool GetOneQuestion(const string &number, Question *q){bool res = false;std::string sql = "select * from oj.";sql += oj_question;sql += " where number = ";sql += number;vector<Question> result;if (QueryMysql(sql, &result)){if (result.size() == 1){*q = result[0];res = true;}}return res;}~Model(){}};
}
- 接口設計
返回bool
表示操作成功與否
class Model {
public:// 獲取所有題目bool GetAllQuestions(vector<Question> *out);// 獲取單個題目bool GetOneQuestion(const string &number, Question *q);
};
oj_view.hpp
View
層 由oj_view.hpp
構成;
使用 ctemplate
庫來進行 HTML
模板渲染
- 使用
TemplateDictionary
存儲渲染數據- 使用
Template::GetTemplate
加載模板- 使用
Expand
方法進行渲染
- 獲取所有題目的渲染;
void AllExpandHtml(const vector<struct Question> &questions, std::string *html)
- 設置模板文件路徑 (all_questions.html)
- 創(chuàng)建模板字典
- 遍歷所有題目,為每個題目添加:
- 題號 (number)
- 標題 (title)
- 難度等級 (star)
- 渲染模板
- 獲取單個題目的渲染;
void OneExpandHtml(const struct Question &q, std::string *html)
- 設置模板文件路徑 (one_question.html)
- 創(chuàng)建模板字典并設置值:
- 題號 (number)
- 標題 (title)
- 難度等級 (star)
- 題目描述 (desc)
- 預設代碼 (header)
- 渲染模板
#pragma once#include <iostream>
#include <string>
#include <ctemplate/template.h>// #include "oj_model.hpp"
#include "oj_model2.hpp"namespace ns_view
{using namespace ns_model;const std::string template_path = "./template_html/";class View{public:View() {};~View() {};public:void AllExpandHtml(const vector<struct Question> &questions, std::string *html){// 題目的編號 題目的標題 題目的難度// 推薦使用表格顯示// 1. 形成路徑std::string src_html = template_path + "all_questions.html";// 2. 形成數據字典ctemplate::TemplateDictionary root("all_questions");for (const auto &q : questions){ctemplate::TemplateDictionary *sub = root.AddSectionDictionary("question_list");sub->SetValue("number", q.number);sub->SetValue("title", q.title);sub->SetValue("star", q.star);}// 3. 獲取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);// 4. 開始完成渲染功能tpl->Expand(html, &root);}void OneExpandHtml(const struct Question &q, std::string *html){// 1. 形成路徑std::string src_html = template_path + "one_question.html";// 2. 形成數字典ctemplate::TemplateDictionary root("one_question");root.SetValue("number", q.number);root.SetValue("title", q.title);root.SetValue("star", q.star);root.SetValue("desc", q.desc);root.SetValue("pre_code", q.header);//3. 獲取被渲染的htmlctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);//4. 開始完成渲染功能tpl->Expand(html, &root);}};
}
oj_control.cpp
Controller
層 由oj_control.hpp
構成;
- 提供服務的主機 Machine 類
表示提供編譯服務的主機
包含 IP、端口、負載信息
提供負載管理方法(增加、減少、重置、獲取負載)
// 提供服務的主機class Machine{public:std::string ip;int port;uint64_t load;std::mutex *mtx;public:Machine() : ip(""), port(0), load(0), mtx(nullptr){}~Machine(){}public:// 提升主機負載void IncLoad(){if (mtx)mtx->lock();load++;if (mtx)mtx->unlock();}// 減少主機負載void DecLoad(){if (mtx)mtx->lock();load--;if (mtx)mtx->unlock();}void ResetLoad(){ if (mtx)mtx->lock();load = 0;if (mtx)mtx->unlock();}// 獲取主機負載uint64_t Load(){uint64_t _load = 0;if (mtx)mtx->lock();_load = load;if (mtx)mtx->unlock();return _load;}};
- LoadBlance 類 (負載均衡模塊)
管理多臺編譯服務器
維護在線/離線主機列表
主要功能:
從配置文件加載主機信息
智能選擇負載最低的主機
處理主機上線/離線
// 負載均衡模塊class LoadBlance{private:// 提供編譯的主機// 每一臺都有自己下標std::vector<Machine> machines;// 所有在線的主機 idstd::vector<int> online;// 所有離線的主機 idstd::vector<int> offline;// 保證 LoadBlance 的數據安全std::mutex mtx;public:LoadBlance(){assert(LoadConf(machine_path));LOG(INFO) << "加載" << machine_path << "成功" << "\n";}~LoadBlance(){}public:bool LoadConf(const std::string &machine_list){std::ifstream in(machine_list);if (!in.is_open()){LOG(FATAL) << "主機加載失敗" << "\n";return false;}std::string line;while (std::getline(in, line)){std::vector<std::string> tokens;StringUtil::SplitString(line, &tokens, " ");if (tokens.size() != 2){LOG(WARNING) << "切分 " << line << "失敗" << "\n";continue;}Machine m;m.ip = tokens[0];m.port = atoi(tokens[1].c_str());m.load = 0;m.mtx = new std::mutex();online.push_back(machines.size());machines.push_back(m);}in.close();return true;}// id: 輸出型參數// m: 輸出型參數bool SmartChoice(int *id, Machine **m){// 1. 使用選擇好的主機(更新負載)// 2. 我們可能需要離線該主機mtx.lock();// 負載均衡的算法// 1. 隨機數法// 2. 輪詢 + hashint online_num = online.size();if (online_num == 0){LOG(FATAL) << "所有的主機掛掉!在線主機數量: " << online_num << ", 離線主機數量: " << offline.size() << "\n";mtx.unlock();return false;}// 找負載最小的主機*id = online[0];*m = &machines[online[0]];uint64_t min_load = machines[online[0]].Load();for (int i = 0; i < online_num; i++){uint64_t cur_load = machines[online[i]].Load();if (cur_load < min_load){min_load = cur_load;*id = online[i];*m = &machines[online[i]];}}mtx.unlock();return true;}void OfflineMachine(int id){mtx.lock();for(auto it = online.begin(); it != online.end(); it++){if(*it == id){machines[id].ResetLoad();// 離線主機已經找到online.erase(it);offline.push_back(*it);break;}}mtx.unlock();}void OnlineMachine(){// 當所有主機離線后,統(tǒng)一上線mtx.lock();online.insert(online.end(), offline.begin(), offline.end()); offline.erase(offline.begin(), offline.end());mtx.unlock();LOG(INFO) << "所有的主機已上線" << "\n";}void ShowMachine(){mtx.lock();std::cout << "在線主機列表: " << "\n";for(auto &it : online){std::cout << it << " ";}std::cout << std::endl;std::cout << "離線主機列表: " << "\n";for(auto &it : offline){std::cout << it << " ";}std::cout << std::endl;mtx.unlock();}};
- Control 類 (核心控制器)
整合 Model(數據層)和 View(視圖層)
判題
// 控制器class Control{private:Model _model; // 提供后臺數據View _view; // 提供網頁渲染LoadBlance _load_blance;public:Control(){}~Control(){}public:void RecoveryMachine(){_load_blance.OnlineMachine();}// 根據題目數據構建網頁// html 輸出型參數bool AllQuestions(string *html){bool ret = true;vector<struct Question> all;if (_model.GetAllQuestions(&all)){sort(all.begin(), all.end(), [](const struct Question &q1, const struct Question &q2){return q1.number < q2.number;});_view.AllExpandHtml(all, html);}else{*html = "獲取題目失敗,形成題目列表失敗";ret = false;}return ret;}bool Question(const std::string number, string *html){bool ret = true;struct Question q;if (_model.GetOneQuestion(number, &q)){_view.OneExpandHtml(q, html);}else{*html = "指定題目:" + number + "不存在";ret = false;}return ret;}void Judge(const std::string &number, const std::string in_json, std::string *out_json){// 0.根據題目編號拿到題目細節(jié)struct Question q;_model.GetOneQuestion(number, &q);// 1.in_json 進行反序列話,得到題目的 id ,得到用戶提交的源代碼 inputJson::Reader reader;Json::Value in_value;reader.parse(in_json, in_value);std::string code = in_value["code"].asString();// 2.重新拼接用戶的代碼 + 測試用例,形成新的代碼Json::Value compile_value;compile_value["input"] = in_value["input"].asString();compile_value["code"] = code + "\n" + q.tail;compile_value["cpu_limit"] = q.cpu_limit;compile_value["mem_limit"] = q.mem_limit;Json::FastWriter writer;std::string compile_string = writer.write(compile_value);// 3.選擇負載最低的主機while (true){Machine *m = nullptr;int id = 0;if (!_load_blance.SmartChoice(&id, &m)){break;}LOG(INFO) << "選擇主機成功, id = " << id << "詳情:" << m->ip << ":" << m->port << "\n";// 4.發(fā)起 http 請求,得到結果Client cli(m->ip, m->port);m->IncLoad();if(auto res = cli.Post("/compile_and_run", compile_string, "application/json;charset=utf-8")){// 5.將結果賦值給 out_jsonif(res->status == 200){*out_json = res->body;m->DecLoad();LOG(INFO) << "請求編譯、運行成功" << "\n";break;}m->DecLoad();}else{// 請求失敗LOG(ERROR) << "當前請求主機id = " << id << "詳情:" << m->ip << ":" << m->port << " 該主機可能已經離線" << "\n";_load_blance.OfflineMachine(id);_load_blance.ShowMachine(); // for test}}}};
首先
- AllQuestions(): 獲取并展示所有題目列表
- Question(): 獲取并展示單個題目詳情
其次Judge
1. 獲取題目信息
2. 解析用戶提交的代碼
3. 組裝完整的測試代碼
4. 選擇負載最低的編譯主機
5. 發(fā)送HTTP請求到編譯主機
6. 處理編譯運行結果
然后負載均衡處理使用最小負載優(yōu)先算法
基本編譯運行提交代碼已經實現(xiàn),后續(xù)還會增加其他功能