在线OJ项目(一)
文章目录
一、oj_server模块整体框架:
oj_server.cpp
:
#include <stdio.h>
#include <string>
#include <string.h>
#include "httplib.h"
#include "oj_model.hpp"
#include "oj_view.hpp"
#include "oj_log.hpp"
#include "compile.hpp"
int main()
{
//httplib的时候,需要使用httplib提供的命名空间
using namespace httplib;
Server svr;
//1.获取题目列表接口
//2.获取单个题目接口
//3.服务器接收用户通过浏览器提交code接口
//listen 会阻塞
svr.listen("0.0.0.0", 19999);
return 0;
}
oj_serever.cpp当中主要的就是要提供3个接口和用户进行交互,所以我们主要就是来实现这几个接口。
二、提供获取所有的题目接口
1. 把所有题目信息存储在数据结构当中
既然要展示题目,那么就需要有题目,我们在oj_model模块
当中提供展示所有的题目的接口
我们在前面约束过,题目的信息是一个四元组:题目id /题目名称/ 题目的路径 /题目的难度
。
//试题id 试题名称 试题路径 试题难度
typedef struct Question
{
std::string id_;//id
std::string name_;//题目名称
std::string path_;//题目详情所在路径
std::string star_;//题目难度
}QUES;
那么oj_model模块
我们用什么来维护这些题目信息呢?
- map底层是红黑树,有序,查询效率相对较高
- unordered_map底层是哈希表,无序但是查询效率高
- 我们使用
unordered_map
,因为服务器可能同时收到很多请求,如果为map可能会效率变低。
2. 在工具类当中封装一个字符串切割函数
还是那个原因,所有题目信息是一个四元组:题目id 、题目名称、 题目的路径、 题目的难度
,所以读到每一个题目的信息后,还需要进行切割
,分别保存在unordered_map当中。
所以在给用户提供展示所有的题目信息之前还要在工具模块封装一个切割字符串的函数
tools.hpp
:
#pragma once
#include <iostream>
#include <vector>
#include <unordered_map>
#include <boost/algorithm/string.hpp>
#include <fstream>
#include "oj_log.hpp"
//实现一个切割字符串的工具函数
class StringTools
{
public:
//使用静态修饰,方便使用者进行调用
//const std::string& input:代表待切割的字符串
//const std::string& split_char:代表以什么字符进行切割
//std::vector<std::string>* output:保存切割的结果
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);
}
};
3. 把存储所题目信息配置文件当中的内容读取到unordered_map当中
oj_model.hpp
:
#pragma once
#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <fstream>
#include <algorithm>
#include "tools.hpp"
#include "oj_log.hpp"
class OjModel
{
public:
OjModel()
{
//我们人为规定,所有的题目四元组信息都在./config_oj.cfg文件中
//在ojmodel模块构造时就完成把./config_oj.cfg文件中的题目信息
//保存到unordered_map当中
LoadQuestions("./config_oj.cfg");
}
private:
//const std::string& configfile_path代表去哪里加载
//存储着题目四元组信息的文件
bool LoadQuestions(const std::string& configfile_path)
{
std::ifstream file(configfile_path.c_str());
if(!file.is_open())
{
return false;
}
std::string line;
//没次都读取一行
while(std::getline(file, line))
{
//题目id 题目名称 题目的路径 题目的难度
//1 反转链表 ./oj_data 简单
//1.需要切割字符串
std::vector<std::string> vec;
StringTools::Split(line, " ", &vec);
//如果且给完成后的vec当中不够4个,说明改行题目信息有误
//不能break掉,否则就拿不到下面的题目
if(vec.size() != 4)
{
continue;
}
//2.切割后的内容保存到unordered_map
Question ques;
ques.id_ = vec[0];//id
ques.name_ = vec[1];//name
ques.path_ = vec[2];//题目路径
ques.star_ = vec[3];//题目难度
model_map_[ques.id_] = ques;//把这道题的所有信息保存在
//unordered_map当中
}
file.close();
return true;
}
private:
//保存所有题目的信息,model_map_当中的每个元素都是Question
//Question当中存放的就是题目的四元组信息
std::unordered_map<std::string, Question> model_map_;
};
注意:
./config_oj.cfg
文件当中的题目信息必须约定格式,即:题目id /题目名称/ 题目的路径/题目的难度
共占一行,中间用空格或者table间隔
。不能是题目id一行,题目名称一行,因为如果那样一旦某一行读取错误,就会影响下面的数据。- 采用ifstream的方式读取文件
4.获取所有题目信息的函数
oj_model.hpp
:
//将保存在unordered_map当中的题目信息返回给ques,进行后续操作
bool GetAllQuestions(std::vector<Question>* ques)
{
for(const auto& kv : model_map_)
{
ques->push_back(kv.second);
}
//arr[10]
//sort(arr, arr+5)
//sort(arr, arr+10, func)
//针对内置类型进行操作的
// std::greater 降序排序
// std::less 升序进行排序
std::sort(ques->begin(), ques->end(), [](const Question& l, const Question& r){
return std::atoi(l.id_.c_str()) < std::atoi(r.id_.c_str());
});
return true;
}
三、将所有题目信息返回给浏览器
1.使用模版填充预定义的html页面
下面这种情况虽然可以完成把题目展示在浏览器,但是这种方式的代码的鲁棒性低
svr.Get("/all_questions", [&ojmodel](const Request& req, Response& resp){
//获取到所有的题目信息
std::vector<Question> ques;
ojmodel.GetAllQuestions(&ques);
//组织html页面返回给浏览器
//<html>id.name star</html>
char buf[10240] = {'\0'};
printf("%d\n", ques.size());
for(int i = 0;i < ques.size();i++)
{
snprintf(buf, sizeof(buf) - 1, "<html>%s.%s %s</html>",
ques[i].id_.c_str(), ques[i].name_.c_str(), ques[i].star_.c_str());
}
std::string html;
html.assign(buf, strlen(buf));
resp.set_content(html,"text/html; charset=UTF-8");
});
题目预定义页面:
<html>
<head>
<title>在线OJ</title>
</head>
<boday>
<div><h1>1.但链表 简单</h1></div>
</boday>
</html>
上面的写法,如果题目有多个,就需要人为的进行更改,不能动态的更新。所以使用谷歌提供的ctempalte模版库进行预定义填充
,把整个题目预定义页面放在./template/all_questions.html
当中:
all_questions.html
:
<html>
<head>
<!--title标签定义浏览器标题栏信息-->
<title>在线oj</title>
</head>
<body>
{{#question}}
<div>
<a href="/question/{{id}}"><h2>{{id}}.{{name}} {{star}}</h2><a>
</div>
{{/question}}
</body>
</html>
{{id}}
当中的内容相当于是待填充的数据{{#question}}和{{/question}}
可以理解为构成一个循环,动态更新预定义页面- 注意{{#…}}其中的名字是可以是任意的,但是在进行填充渲染的时候必须和这个名字相同,所以请那个小本本记住这个模版当中的
question
,后面会用到。
2. 用template对预定义页面进行渲染
oj_view.hpp
:
#pragma once
#include <ctemplate/template.h>
#include <string>
#include <vector>
#include "oj_model.hpp"
class OjView
{
public:
//渲染html页面,并且将该页面返还给调用
//std::string* html:填充渲染完成后的html页面
//std::vector<Question>& ques:填充的内容,即拿什么进行填充
static void ExpandAllQuestionshtml(std::string* html, std::vector<Question>& ques)
{
//1.获取数据字典-->将拿到的试题数据按照一定顺序保存到内存当中
//all_questions:没有任何作用
ctemplate::TemplateDictionary dict("all_questions");
for(const auto& que:ques)
{
//question就是html预定义页面当中的{{#question}},必须一样
ctemplate::TemplateDictionary* section_dict = dict.AddSectionDictionary("question");
section_dict->SetValue("id", que.id_);
section_dict->SetValue("id", que.id_);
section_dict->SetValue("name", que.name_);
section_dict->SetValue("star", que.star_);
}
//2.获取模板类指针,加载预定义的html页面到内存当中
//./template/all_questions.html:代表预定义html页面的路径
//ctemplate::DO_NOT_STRIP:代表逐字输出到模版当中
ctemplate::Template* tl = ctemplate::Template::GetTemplate("./template/all_questions.html", ctemplate::DO_NOT_STRIP);
//3.渲染 拿着模板类的指针,将数据字典当中的数据更新到html页面的内存中
tl->Expand(html, &dict);
}
};
到这里已经完成所有题目的获取渲染并返回给浏览器
3. 完整获取所有题目并经过渲染返回给浏览器的代码
svr.Get("/all_questions", [&ojmodel](const Request& req, Response& resp){
//1.定义存储所有题目信息的容器
std::vector<Question> ques;
//2.调用oj_model当中的获取所有题目的接口并放在ques当中
ojmodel.GetAllQuestions(&ques);
//3.调用oj_view当中的函数去填充html页面
std::string html;
OjView::ExpandAllQuestionshtml(&html, ques);
//4.把填充完成后的页面返回给浏览器
resp.set_content(html,"text/html; charset=UTF-8");
});