实战项目:负载均衡式在线OJ
博主主页:桑榆非晚ᴷ
博主能力有限,如果有出错的地方希望大家不吝赐教
给自己打气:成功没有快车道,幸福没有高速路。所有的成功,都来自不倦地努力和奔跑,所有的幸福都来自平凡的奋斗和坚持✨
oj_server
模块oj_server
子模块oj_server
子模块介绍主要完成对用户所请求资源的路由功能,分别为获取所有所有题目、获取单个指定题目和对指定题目运行结果的判题。
#include
#include "../comm/httplib.h"
#include "oj_control.hpp"
using std::string;
using namespace httplib;
using namespace ns_control;
int main()
{
// 用户请求服务器路由的功能
Server svr;
Control ctrl;
svr.set_base_dir("./wwwroot");
// 1.功能:获取所有的题目列表
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp)
{
// 用于测试Get /all_questions资源是否可以成功
resp.set_content("这是所有题目的列表", "text/plain; charset=utf-8");
// 返回一张包含所有题目的html网页
// string html;
// ctrl.AllQuestions(&html);
// resp.set_content(html, "text/html; charset=utf-8");
});
// 2.功能:用户根据题目编号,获取题目的内容
// (\d+)正则表达式:\d表示一个数字,+表式多个数字
// R"()"保持字符串原貌
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp)
{
string number = req.matches[1];
// 用于测试Get /question/(\d+)资源是否可以成功
resp.set_content("这是指定的一道题:" + number, "text/plain; charset=utf-8");
// 返回用户要获取指定题目的html网页
// string html;
// ctrl.OneQuestion(number, &html);
// resp.set_content(html, "text/html; charset=utf-8");
});
// 3.功能:用户提交代码,使用测试用例进行判题
svr.Get(R"(/judge/(\d+))", [](const Request &req, Response &resp)
{
string number = req.matches[1];
// 用于测试Get /judge/(\d+)资源是否可以成功
resp.set_content("指定题目的判题:" + number, "text/plain; charset=utf-8");
});
svr.listen("0.0.0.0", 8080);
return 0;
}
oj_server
子模块测试测试用例为上面代码,由于相关功能还没有实现,所以简单模拟返回三个路由结果来测试路由功能是否正常。
观察上面测试结果,满足我们预期想要的结果,所以目前路由功能是没有问题的。
oj_model
子模块oj_model
子模块介绍oj_model
子模块主要用来服务器和浏览器进行数据交互,对外提供访问数据的接口。例如浏览器获取所有的oj题目链表和获取指定oj题目的具体内容。
#pragma once
// 文件版本
#include
#include
#include
#include
#include
#include
#include
#include "../comm/util.hpp"
#include "../comm/log.hpp"
// 根据题目list文件,加载所有的题目信息到内存中
// model: 主要用来和数据进行交互,对外提供访问数据的接口
namespace ns_model
{
using std::string;
using std::vector;
using std::unordered_map;
using namespace ns_log;
using namespace ns_util;
const std::string questins_list = "./questions/questions.list";
const std::string questins_path = "./questions/";
struct Question
{
string number; // 题目编号,唯一
string title; // 题目的标题
string star; // 难度: 简单 中等 困难
int cpu_limit; // 题目的时间要求(S)
int mem_limit; // 题目的空间要去(KB)
string desc; // 题目的描述
string header; // 题目预设给用户在线编辑器的代码
string tail; // 题目的测试用例,需要和header拼接,形成完整代码
};
class Model
{
public:
Model()
{
assert(LoadQuestionList(questins_list));
}
// 加载配置文件: questions/questions.list + 题目编号文件
// 形成题目编号和题目内容的映射关系保存到unordered_map当中
bool LoadQuestionList(const string &question_list)
{
std::ifstream in(question_list);
if (!in.is_open())
{
LOG(FATAL) << " 加载题库失败,请检查是否存在题库文件"
<< "\n";
return false;
}
string line;
// 按行读取不保留\n
while (std::getline(in, line))
{
vector<string> tokens;
// 将获取的行数据 {1 判断回文数 简单 1 30000} 按照空格切割,
// 切割产生的五个字符串切割产生的五个字符串存放在tokens当中
StringUtil::SplitString(line, &tokens, " ");
if (tokens.size() != 5)
{
LOG(WARNING) << "加载部分题目失败, 请检查文件格式"
<< "\n";
continue;
}
Question question;
question.number = tokens[0];
question.title = tokens[1];
question.star = tokens[2];
question.cpu_limit = atoi(tokens[3].c_str());
question.mem_limit = atoi(tokens[4].c_str());
string path = questins_path;
path += question.number;
path += "/";
// ./questions/ + question.number + desc.txt or header.cpp or tail.cpp
FileUtil::ReadFile(path + "desc.txt", &(question.desc));
FileUtil::ReadFile(path + "header.cpp", &(question.header));
FileUtil::ReadFile(path + "tail.cpp", &(question.tail));
// 形成题目编号和题目内容的映射关系保存到unordered_map当中
_questions.insert({question.number, question});
}
LOG(INFO) << "加载题库...成功!" << "\n";
in.close();
return true;
}
// 从编号到题目的映射表unordered_map中获取所有题目
bool GetAllQuestions(vector<Question> *out)
{
if (_questions.size() == 0)
{
LOG(ERROR) << "用户获取题库失败"
<< "\n";
return false;
}
for (const auto &q : _questions)
{
out->push_back(q.second); // first: key, second: value
}
return true;
}
// 从编号到题目的映射表unordered_map中获取指定题目
bool GetOneQuestion(const std::string &number, Question *q)
{
const auto &iter = _questions.find(number);
if (iter == _questions.end())
{
LOG(ERROR) << "用户获取题目失败, 题目编号: " << number << "\n";
return false;
}
(*q) = iter->second;
return true;
}
private:
// 题号 : 题目细节
unordered_map<string, Question> _questions;
};
}
oj_model
子模块测试在当前工作路径下,创建questions目录,在questions目录下创建questions.list文件,并录入一些题目:
一下文题目编号为1题目的相关内容:
接着在questions目录下以题目编号为目录名创建目录,并在目录下创建desc.txt、header.cpp和tail.cpp
以编号为1的题目为例:
当用户要获取1号题目时,服务器会把header.cpp文件中的内容发送给用户浏览器,用户在其中进行编写代码,最后把编写好的代码提交给服务器,服务器把提交上来的代码与tail.cpp拼接,把拼接好的代码负载均衡式的分配给后台负责编译运行的服务器进行处理。
测试代码:
#include "oj_model.hpp"
#include
using namespace ns_model;
using namespace std;
int main()
{
Model model;
vector<Question> all_questions;
model.GetAllQuestions(&all_questions);
if (all_questions.size() == 2)
{
cout << "*********************************" << endl;
for (auto &question : all_questions)
{
cout << question.number << endl;
cout << question.title << endl;
cout << question.star << endl;
cout << question.cpu_limit << endl;
cout << question.mem_limit << endl;
cout << question.mem_limit << endl;
cout << question.desc << endl;
cout << question.header << endl;
cout << question.tail << endl;
}
cout << "*********************************" << endl;
}
else
{
cout << "GetAllQuestion error" << endl;
}
Question question;
if (model.GetOneQuestion("1", &question))
{
cout << "*********************************" << endl;
cout << question.number << endl;
cout << question.title << endl;
cout << question.star << endl;
cout << question.cpu_limit << endl;
cout << question.mem_limit << endl;
cout << question.mem_limit << endl;
cout << question.desc << endl;
cout << question.header << endl;
cout << question.tail << endl;
cout << "*********************************" << endl;
}
else
{
cout << "GetOneQuestion error" << endl;
}
return 0;
}
测试结果:
[lk@VM-8-15-centos oj_server]$ g++ test.cc -std=c++11
[lk@VM-8-15-centos oj_server]$ ./a.out
[INFO][oj_model.hpp][94][1682948237]加载题库...成功!
*********************************
2
求最大值
简单
1
30000
30000
求最大值,比如:vector v ={1,2,3,4,5,6,12,3,4,-1};
求最大值, 比如:输出 12
#include
#include
#include
using namespace std;
class Solution
{
public:
int Max(const vector &v)
{
//将你的代码写在下面
return 0;
}
};
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
void Test1()
{
vector v = {1, 2, 3, 4, 5, 6};
int max = Solution().Max(v);
if (max == 6)
{
std::cout << "Test 1 .... OK" << std::endl;
}
else
{
std::cout << "Test 1 .... Failed" << std::endl;
}
}
void Test2()
{
vector v = {-1, -2, -3, -4, -5, -6};
int max = Solution().Max(v);
if (max == -1)
{
std::cout << "Test 2 .... OK" << std::endl;
}
else
{
std::cout << "Test 2 .... Failed" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}
1
判断回文数
简单
1
30000
30000
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
#include
#include
#include
#include
#include
using namespace std;
class Solution{
public:
bool isPalindrome(int x)
{
//将你的代码写在下面
return true;
}
};
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
void Test1()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(121);
if(ret){
std::cout << "通过用例1, 测试121通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例1, 测试的值是: 121" << std::endl;
}
}
void Test2()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(-10);
if(!ret){
std::cout << "通过用例2, 测试-10通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例2, 测试的值是: -10" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}
*********************************
*********************************
1
判断回文数
简单
1
30000
30000
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:
输入: -121
输出: false
解释: 从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
#include
#include
#include
#include
#include
using namespace std;
class Solution{
public:
bool isPalindrome(int x)
{
//将你的代码写在下面
return true;
}
};
#ifndef COMPILER_ONLINE
#include "header.cpp"
#endif
void Test1()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(121);
if(ret){
std::cout << "通过用例1, 测试121通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例1, 测试的值是: 121" << std::endl;
}
}
void Test2()
{
// 通过定义临时对象,来完成方法的调用
bool ret = Solution().isPalindrome(-10);
if(!ret){
std::cout << "通过用例2, 测试-10通过 ... OK!" << std::endl;
}
else{
std::cout << "没有通过用例2, 测试的值是: -10" << std::endl;
}
}
int main()
{
Test1();
Test2();
return 0;
}
*********************************
阅读测试结果,与预期想得到的结果一致,所以目前model
子模块没有问题。
oj_view
子模块oj_view
子模块介绍oj_view
子模块的功能是对数据进行渲染形成html文件。
#pragma once
#include
#include
#include
#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:
void AllExpandHtml(const vector<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. 获取被渲染的html
ctemplate::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("header", q.header);
// 3. 获取被渲染的html
// ctemplate::DO_NOT_STRIP 保持代码原样
ctemplate::Template *tpl = ctemplate::Template::GetTemplate(src_html, ctemplate::DO_NOT_STRIP);
// 4. 开始完成渲染功能
tpl->Expand(html, &root);
}
};
}
oj_view
子模块测试这里先不做测试,稍后与oj_control
联合测试。
oj_control
子模块oj_control
子模块介绍#pragma once
#include
#include
#include
#include "oj_model.hpp"
#include "oj_view.hpp"
#include "../comm/log.hpp"
#include "../comm/util.hpp"
namespace ns_control
{
using std::cout;
using std::string;
using std::vector;
using namespace ns_log;
using namespace ns_util;
using namespace ns_model;
using namespace ns_view;
class Control
{
public:
bool AllQuestions(string *html)
{
vector<Question> all_questions;
if (_model.GetAllQuestions(&all_questions))
{
LOG(INFO) << "获取所有题目成功" << "\n";
// 构建网页返回
_view.AllExpandHtml(all_questions, html);
return true;
}
else
{
LOG(ERROE) << "获取所有题目失败" << "\n";
*html = "获取题目失败, 形成题目列表失败";
return false;
}
}
bool OneQuestion(const string &number, string *html)
{
Question question;
if (_model.GetOneQuestion(number, &question))
{
LOG(INFO) << "获取单个题目成功" << "\n";
// 构建网页返回
_view.OneExpandHtml(question, html);
return true;
}
else
{
LOG(ERROE) << "获取单个题目失败" << "\n";
*html = "指定题目: " + number + " 不存在!";
return false;
}
}
private:
Model _model;
View _view;
};
}
oj_contral
子模块测试这里对oj_contral
子模块测试采用前后端测试,即oj_server
子模块联动测试
#include
#include "../comm/httplib.h"
#include "oj_control.hpp"
using std::string;
using namespace httplib;
using namespace ns_control;
int main()
{
// 用户请求服务器路由的功能
Server svr;
Control ctrl;
svr.set_base_dir("./wwwroot");
// 1.功能:获取所有的题目列表
svr.Get("/all_questions", [&ctrl](const Request &req, Response &resp)
{
// 返回一张包含所有题目的html网页
string html;
ctrl.AllQuestions(&html);
resp.set_content(html, "text/html; charset=utf-8");
});
// 2.功能:用户根据题目编号,获取题目的内容
// (\d+)正则表达式:\d表示一个数字,+表式多个数字
// R"()"保持字符串原貌
svr.Get(R"(/question/(\d+))", [&ctrl](const Request &req, Response &resp)
{
string number = req.matches[1];
// 返回用户要获取指定题目的html网页
string html;
ctrl.OneQuestion(number, &html);
resp.set_content(html, "text/html; charset=utf-8");
});
// 3.功能:用户提交代码,使用测试用例进行判题
svr.Get(R"(/judge/(\d+))", [](const Request &req, Response &resp)
{
string number = req.matches[1];
// 用于测试Get /judge/(\d+)资源是否可以成功
resp.set_content("指定题目的判题:" + number, "text/plain; charset=utf-8");
});
svr.listen("0.0.0.0", 8080);
return 0;
}
在第二幅图中我们发现题目没有按照升序进行排列,其他结果都是预期的,所以我们只需要解决这个bug就好。我们只需要在获取到所有题目后对题目进行排序就好。
经过这样的测试,我们可以确定写到目前为止,项目还没有出现问题,接下来就是对oj_view
子模块的完善了,优化一下前端页面。前端页面的优化就不进行讲解了,这个项目的主要任务是后端的内容。
最终前端显示的内容: