# 负载均衡在线oj **Repository Path**: DaiLN/load-balancing-online-oj ## Basic Information - **Project Name**: 负载均衡在线oj - **Description**: 负载均衡在线oj项目 - **Primary Language**: C++ - **License**: Not specified - **Default Branch**: master - **Homepage**: https://gitee.com/DaiLN - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2023-03-26 - **Last Updated**: 2024-10-06 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 负载均衡在线oj ### 相关技术: * 多进程、多线程 * 系统级别文件读写、重定向 * C++ STL 标准库 * Boost库(split字符串切割) * jsoncpp第三方开源序列化、反序列化库 * cpp-httplib第三方开源网络库 * ctemplate第三方开源网页渲染库 * MySQL C connect链接库 ### 开发环境: * CentOS7云服务器 * vscode * MySQL Workbench * Postman ### 项目宏观结构及介绍 ![输入图片说明](https://foruda.gitee.com/images/1681262413058285890/7027c426_10133207.png "屏幕截图") 用户进行在线oj时,主要有三种行为: * 查看题目列表 * 进入具体题目 * 编写并提交代码 在服务器上部署了多台主机,执行compile_server服务,即编译运行服务,只负责对代码进行编译运行, 而在其中一台主机上部署oj_server服务,它是前端和后端的桥梁,为用户提供服务路由,负责接收用户请求、 统筹每台主机的任务数,负载均衡式地将用户代码转交给compile_server模块,再将结果返回给用户。 **项目共三个核心模块:** * 公共模块:提供各个模块需要的工具类,如日志、文件操作等 * 编译运行模块:只负责对客户端上传的代码进行编译运行,并将运行信息返回给上层 * oj_server: MVC为用户获取题目、提供编写能力,负载均衡、其他功能 ### 项目详细规划和API ![输入图片说明](https://foruda.gitee.com/images/1681262421366965472/7073c493_10133207.png "屏幕截图") #### 编译模块(compiler) > ![输入图片说明](https://foruda.gitee.com/images/1681262426798731774/f787e6bc_10133207.png "屏幕截图") ### 运行模块(run) 运行结果有三种: * 运行成功,结果正确 * 运行成功,结果错误 * 运行失败,程序异常 **运行模块只关心运行成功与否,结果正确与否无需关心。** 结果是否正确,**是由上层测试用例运行程序后判断结果决定的**,与运行模块无关。 ![输入图片说明](https://foruda.gitee.com/images/1681262431703576951/e2ad7f55_10133207.png "屏幕截图") ### 编译运行模块(compile_run): 直接与上层交互的模块,整合compile模块和run模块. > 上面的compile和run模块虽然都能各自实现了功能,但是还需要我们自己在对应路径下先创建好源文件/可执行文件。 > > 该模块要能够自动将用户上传数据进行序列化并生成文件,最后反序列化通过网络返回给用户。 ![输入图片说明](https://foruda.gitee.com/images/1681262443710877272/72ab3e43_10133207.png "屏幕截图") ### compile_server: ```cpp void Usage() { std::cout << "Please Enter 'port'" << std::endl; exit(-1); } int main(int args, char *argv[]) { //test(); if (args != 2) { Usage(); } // 服务端 Server srv; // 注册处理Post请求的函数 srv.Post("/compile_run", [](const Request &rqt, Response &rsp) { LOG(INFO) << "接收到编译运行请求" << std::endl; // 提取请求正文 std::string in_json = rqt.body; std::string out_json; if (!in_json.empty()) { // 调用cr模块 CompileAndRun::Start(in_json, out_json); std::cout << "运行完毕, json: " << std::endl << out_json << std::endl; // LOG(INFO) << "cr完成,正在构建响应报文并回复..." << std::endl; // 构建响应正文内容 rsp.set_content(out_json, "application/json;charset=utf-8"); } else { LOG(ERROR) << "请求正文为空" << std::endl; rsp.set_content("request is empty...", "text/plain;charset=utf8"); } }); // 绑定ip and 端口号 srv.listen("0.0.0.0", atoi(argv[1])); } ``` ### oj_model模块 ![输入图片说明](https://foruda.gitee.com/images/1681262531111321461/dca6d433_10133207.png "屏幕截图") **获取所有题目列表:** * 首先要有一个题目列表,在某个路径下,**一定需要有一个文件夹(question)存放了每个题目文件** * `./question/questionlist`记录每个文件的属性,包括: ```cpp 题目编号 题目标题 难度等级 时间限制 空间限制 例如: 1 两数之和 简单 10 300000 2 判断回文数 简单 10 200000 ``` **在question目录中,每一道题又都有自己的目录,以题目编号命名,例如:/question/1** 存放了题目的具体信息,包括: * desc:题目描述 * header.cpp:题目预设代码,如: ```cpp #include class Solution { public: static bool isPalindrome(int x) { // write... } }; ``` > 显示给用户该代码,让用户实现该接口 * tail.cpp:添加测试用例,调用head.cpp中用户填写的接口,判断是否通过用例。 **最后提交给compile_run模块的代码 = header.cpp + tail.cpp 拼接起来的代码:** ```cpp #include class Solution { public: static bool isPalindrome(int x) { // write... } }; //这三行是为了在编写的时候,包含了header.cpp中的接口,不让编译器报一堆红色波浪号,便于编写。 //在实际拼接时,这几行代码我们要去掉。 #ifndef COMPILE_ONLINE #include "header.cpp" #endif void test1() { //121 if(Solution::isPalindrome(121)) { std::cout << "通过测试用例, 测试用例: 121" << std::endl; } else { std::cout << "未通过测试用例, 测试用例: 121" << std::endl; } } void test2() { //-121 if( !Solution::isPalindrome(-121)) { std::cout << "通过测试用例, 测试用例: -121" << std::endl; } else { std::cout << "未通过测试用例, 测试用例: -121" << std::endl; } } int main() { test1(); test2(); } ``` ### oj_view模块 #### 了解ctemplate库的使用 > 渲染网页 ```cpp #include using namespace std; #include #include int main() { string in_html = "./test.html"; //创建数据字典 //dic是对象,test是字典的名字 ctemplate::TemplateDictionary dic("test"); //类比 unordered<> test; string value = "你好,世界"; //设置kv值 dic.SetValue("key", value); //类比 test.insert({pair{}}) /*** * GetTemplate: * 获取被渲染网页对象 * 参数1:要渲染的网页 * 参数2:选项,要不要去除空行、空格啥的?不要 */ ctemplate::Template *tpl = ctemplate::Template::GetTemplate(in_html, ctemplate::DO_NOT_STRIP); //添加字典数据到网页中 std::string out_html; //渲染后的网页 /** * 参数1:输出性参数,返回渲染后的网页 * 参数2:把数据字典交给被渲染的网页,共同构成渲染后的网页 */ tpl->Expand(&out_html, &dic); cout << out_html; } ``` ![输入图片说明](https://foruda.gitee.com/images/1681262547819334443/c4b969df_10133207.png "屏幕截图") * **在网页中, {{ }}两个花括号中的内容表示将来要被渲染的内容,填的是key值,最后被渲染时替换成value值** 看下列代码: ```html {{#question_list}} {{number}} {{title}} {{star}} {{/question_list}} ``` 每一个题目都需要被渲染,每一个题目就需要三列这样的属性,如果有1万道题,那么就要重复写一万个这样的渲染,这显然是很抵消的,所以有了**循环渲染** **{{#}}** :#表示这是循环渲染的起点,后面紧跟着起点名(锚点名) **{{/}}** :/表示这是循环渲染的终点,后面紧跟着锚点名(起点名 = 终点名) ![输入图片说明](https://foruda.gitee.com/images/1681262556282229128/c4b61dbe_10133207.png "屏幕截图") ```cpp //循环渲染 bool AllExpandHtml(std::vector &all, std::string &html) { if (all.size() == 0) { LOG(FATAL) << "准备渲染所有题目列表的网页,但题目列表为空" << std::endl; exit(-1); } // 待渲染网页 std::string in_html = template_path + "all_question.html"; // 创建数据字典 ctemplate::TemplateDictionary dic("all_quetion"); for (auto &elm : all) { ctemplate::TemplateDictionary* sub = dic.AddSectionDictionary("question_list"); /** * 创建子字典 * 子字典名称不能随意取,它决定最后循环渲染的是哪部分, * 当然,如果你没有子字典,说明你不需要循环渲染,只有一个主字典 * 那就像上面说的,主字典怎么取都无所谓,因为只要进行一次kv映射就结束了,整个网页一共就一个要渲染的,那就不用区分 * */ sub->SetValue("number", elm.number); sub->SetValue("title", elm.title); sub->SetValue("star", elm.star); //LOG(INFO) << "创建子字典成功" << std::endl; } ``` **循环渲染时,需要创建子字典:** * 主字典名字可以随便取,因为有待渲染的网页的路径,所以无所谓 * 子字典名称不能随意取。 > **循环渲染网页时需要指明子字典名称。所以html文件中渲染字典的名称必须和子字典名称相同。** > > 当然,如果你没有子字典,不需要循环渲染,只有一个主字典 > > 那就像上面说的,主字典怎么取都无所谓,因为只要进行一次kv映射就结束了,整个网页一共就一个要渲染的,那就不用区分。 ### oj_control模块 ![输入图片说明](https://foruda.gitee.com/images/1681262564346039076/cb4b23b1_10133207.png "屏幕截图") ### oj_server: ```cpp #include #include "../comm/httplib.h" #include "../comm/util.hpp" #include "oj_control.hpp" using namespace httplib; using namespace ns_util; using namespace ns_control; int main(int args, char* argv[]) { Server svr; Controller ctl; //控制器 /** * 注册资源路由 * 获取所有题目列表 * * */ svr.Get("/all_question", [&ctl](const Request &req, Response &resp) { std::string _html; //LOG(INFO) << "申请所有题目列表:" << std::endl; ctl.AllQuestion(_html); resp.set_content(_html, "text/html;charset=utf-8"); }); /** * 注册资源路由 * 获取指定题目 * \d 正则表达式,匹配多个数字 * */ svr.Get(R"(/question/(\d+))", [&ctl](const Request &req, Response &resp) { std::cout << "收到请求报文, url: " << req.path << std::endl; std::string number = req.matches[1]; LOG(INFO) << "申请获取题目编号:" << number << std::endl; std::string _html; ctl.Question(number, _html); resp.set_content(_html, "text/html;charset=utf-8"); }); /*** * 注册资源路由 * 提交代码, 测试用例 */ svr.Post(R"(/judge/(\d+))", [&ctl](const Request &req, Response &resp) { // LOG(INFO) << "编译运行路由" << std::endl; std::string number = req.matches[1]; std::string out_json; ctl.Judge(number, req.body, out_json); resp.set_content(out_json, "application/json;charset=utf-8"); }); svr.set_base_dir("./wwwroot"); svr.listen("0.0.0.0", 8080); } ``` ```cpp svr.Post(R"(/judge/(\d+))", [&ctl](const Request &req, Response &resp) { // LOG(INFO) << "编译运行路由" << std::endl; std::string number = req.matches[1]; std::string out_json; ctl.Judge(number, req.body, out_json); resp.set_content(out_json, "application/json;charset=utf-8"); }); ``` > (\d+):正则表达式 > > \d表示匹配数字,+表示一个或多个 > > 合起来就是匹配一个或多个数字。 因为我们的题目编号,比如像 100,101,102.... **如果写具体的数字,就把资源路由写死了,比如 /questions/100,那注册的这个Get函数,只能用于处理路径/questions/100中的资源。** 这样的话,如果有很多题,那么每一道题都需要注册一个Get函数处理,不现实。 ```cpp std::string number = req.matches[1]; ``` > matches中保存了正则表达式的内容